From 56b0dc8a62eb45002615c1af0666efdc22c71df4 Mon Sep 17 00:00:00 2001 From: Alejandro Rosales Date: Tue, 26 Aug 2025 15:24:53 -0600 Subject: [PATCH] Refactor App component styles and remove highlights section - Updated background gradient colors for a lighter theme. - Changed button styles to improve visibility. - Removed the highlights section to streamline the layout. - Adjusted text colors for better contrast and readability. --- index.html | 2 +- public/manifest.json | 4 +- src/auth/supabase.tsx | 214 +-- src/components/planes/academic-sections.tsx | 162 ++- src/routes/__root.tsx | 4 +- .../asignatura/$asignaturaId.tsx | 262 +++- src/routes/_authenticated/asignaturas.tsx | 1071 ++++++++++----- .../_authenticated/facultad/$facultadId.tsx | 1 - src/routes/_authenticated/facultades.tsx | 287 +++- src/routes/_authenticated/plan/$planId.tsx | 175 ++- src/routes/_authenticated/usuarios.tsx | 1215 +++++++++-------- src/routes/index.tsx | 31 +- 12 files changed, 2240 insertions(+), 1188 deletions(-) diff --git a/index.html b/index.html index 88956ad..70f352a 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@ /> - Create TanStack App - tanstack-router + Genesis - La Salle
diff --git a/public/manifest.json b/public/manifest.json index 078ef50..0185032 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "TanStack App", - "name": "Create TanStack App Sample", + "short_name": "Genesis", + "name": "Genesis - La Salle", "icons": [ { "src": "favicon.ico", diff --git a/src/auth/supabase.tsx b/src/auth/supabase.tsx index d75b621..7d735a0 100644 --- a/src/auth/supabase.tsx +++ b/src/auth/supabase.tsx @@ -1,109 +1,147 @@ -import { createClient, type User } from '@supabase/supabase-js' +import { createClient, type User, type Session } from '@supabase/supabase-js' import { createContext, useContext, useEffect, useState } from 'react' export const supabase = createClient( - import.meta.env.VITE_SUPABASE_URL, - import.meta.env.VITE_SUPABASE_ANON_KEY, + import.meta.env.VITE_SUPABASE_URL, + import.meta.env.VITE_SUPABASE_ANON_KEY, ) export interface SupabaseAuthState { - isAuthenticated: boolean - user: User | null - claims: UserClaims | null - login: (email: string, password: string) => Promise - logout: () => Promise - isLoading: boolean + isAuthenticated: boolean + user: User | null + claims: UserClaims | null + login: (email: string, password: string) => Promise + logout: () => Promise + isLoading: boolean } +type Role = + | 'lci' + | 'vicerrectoria' + | 'director_facultad' // 👈 NEW + | 'secretario_academico' + | 'jefe_carrera' + | 'planeacion' + type UserClaims = { - claims_admin: boolean, - clave: string, - nombre: string, - apellidos: string, - title: string, - avatar: string | null, - carrera_id?: string, - facultad_id?: string, - role: 'lci' | 'vicerrectoria' | 'secretario_academico' | 'jefe_carrera' | 'planeacion', + claims_admin: boolean + clave: string + nombre: string + apellidos: string + title: string + avatar: string | null + carrera_id?: string | null + facultad_id?: string | null + facultad_color?: string | null // 🎨 NEW + role: Role } -const SupabaseAuthContext = createContext( - undefined, -) +const SupabaseAuthContext = createContext(undefined) -export function SupabaseAuthProvider({ - children, -}: { - children: React.ReactNode -}) { - const [user, setUser] = useState(null) - const [claims, setClaims] = useState(null) - const [isAuthenticated, setIsAuthenticated] = useState(false) - const [isLoading, setIsLoading] = useState(true) +export function SupabaseAuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null) + const [claims, setClaims] = useState(null) + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isLoading, setIsLoading] = useState(true) - useEffect(() => { - // Get initial session - supabase.auth.getSession().then(({ data: { session } }) => { - setUser(session?.user ?? null) - setClaims({ - ...(session?.user?.app_metadata as Partial ?? {}), - ...(session?.user?.user_metadata as Partial ?? {}), - } as UserClaims | null) - setIsAuthenticated(!!session?.user) - setIsLoading(false) - }) + useEffect(() => { + // Carga inicial + supabase.auth.getSession().then(async ({ data: { session } }) => { + const u = session?.user ?? null + setUser(u) + setIsAuthenticated(!!u) + setClaims(await buildClaims(session)) + setIsLoading(false) + }) - // Listen for auth changes - const { - data: { subscription }, - } = supabase.auth.onAuthStateChange((_event, session) => { - setUser(session?.user ?? null) - setClaims({ - ...(session?.user?.app_metadata as Partial ?? {}), - ...(session?.user?.user_metadata as Partial ?? {}), - } as UserClaims | null) - setIsAuthenticated(!!session?.user) - setIsLoading(false) - }) + // Suscripción a cambios de sesión + const { data: { subscription } } = supabase.auth.onAuthStateChange(async (_event, session) => { + const u = session?.user ?? null + setUser(u) + setIsAuthenticated(!!u) + setClaims(await buildClaims(session)) + setIsLoading(false) + }) - return () => subscription.unsubscribe() - }, []) + return () => subscription.unsubscribe() + }, []) - const login = async (email: string, password: string) => { - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }) - if (error?.code === 'invalid_credentials') throw new Error('Credenciales inválidas') - else if (error) throw error - } + const login = async (email: string, password: string) => { + const { error } = await supabase.auth.signInWithPassword({ email, password }) + if (error?.code === 'invalid_credentials') throw new Error('Credenciales inválidas') + else if (error) throw error + } - const logout = async () => { - const { error } = await supabase.auth.signOut() - if (error) throw error - location.href = "/login" - } + const logout = async () => { + const { error } = await supabase.auth.signOut() + if (error) throw error + location.href = "/login" + } - return ( - - {children} - - ) + return ( + + {children} + + ) } export function useSupabaseAuth() { - const context = useContext(SupabaseAuthContext) - if (context === undefined) { - throw new Error('useSupabaseAuth must be used within SupabaseAuthProvider') - } - return context -} \ No newline at end of file + const context = useContext(SupabaseAuthContext) + if (context === undefined) { + throw new Error('useSupabaseAuth must be used within SupabaseAuthProvider') + } + return context +} + +/* ===================== * + * Helpers + * ===================== */ + +// Unifica extracción de metadatos y resuelve facultad_color si hay facultad_id +async function buildClaims(session: Session | null): Promise { + const u = session?.user + if (!u) return null + + const app = (u.app_metadata ?? {}) as Partial & { role?: Role } + const meta = (u.user_metadata ?? {}) as Partial + + // Mezcla segura: app_metadata > user_metadata (para campos de claims) + const base: Partial = { + claims_admin: !!(app.claims_admin ?? (meta as any).claims_admin), + role: (app.role as Role | undefined) ?? ('lci' as Role), + facultad_id: app.facultad_id ?? meta.facultad_id ?? null, + carrera_id: app.carrera_id ?? meta.carrera_id ?? null, + clave: (meta.clave as string) ?? '', + nombre: (meta.nombre as string) ?? '', + apellidos: (meta.apellidos as string) ?? '', + title: (meta.title as string) ?? '', + avatar: (meta.avatar as string) ?? null, + } + + let facultad_color: string | null = null + if (base.facultad_id) { + // Lee color desde public.facultades + const { data, error } = await supabase + .from('facultades') + .select('color') + .eq('id', base.facultad_id) + .maybeSingle() + + if (!error && data) facultad_color = (data as any)?.color ?? null + } + + return { + claims_admin: !!base.claims_admin, + role: (base.role ?? 'lci') as Role, + clave: base.clave ?? '', + nombre: base.nombre ?? '', + apellidos: base.apellidos ?? '', + title: base.title ?? '', + avatar: base.avatar ?? null, + facultad_id: (base.facultad_id as string | null) ?? null, + carrera_id: (base.carrera_id as string | null) ?? null, + facultad_color, // 🎨 + } +} diff --git a/src/components/planes/academic-sections.tsx b/src/components/planes/academic-sections.tsx index 8a0727e..23ec8b5 100644 --- a/src/components/planes/academic-sections.tsx +++ b/src/components/planes/academic-sections.tsx @@ -1,82 +1,109 @@ import * as Icons from "lucide-react" import { useMemo, useState } from "react" -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" import { Textarea } from "@/components/ui/textarea" import { supabase } from "@/auth/supabase" -/* color helpers */ +/* ---------- helpers de color ---------- */ function hexToRgb(hex?: string | null): [number, number, number] { if (!hex) return [37, 99, 235] - const h = hex.replace("#", ""); const v = h.length === 3 ? h.split("").map(c => c + c).join("") : h - const n = parseInt(v, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255] + const h = hex.replace("#", "") + const v = h.length === 3 ? h.split("").map(c => c + c).join("") : h + const n = parseInt(v, 16) + return [(n >> 16) & 255, (n >> 8) & 255, n & 255] } const rgba = (rgb: [number, number, number], a: number) => `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${a})` -/* texto con clamp */ -function ExpandableText({ text, mono = false }: { text?: string | null; mono?: boolean }) { +/* ---------- texto expandible (acepta string o string[]) ---------- */ +function ExpandableText({ text, mono = false }: { text?: string | string[] | null; mono?: boolean }) { const [open, setOpen] = useState(false) - if (!text) return + if (!text || (Array.isArray(text) && text.length === 0)) { + return + } + const content = Array.isArray(text) ? text.join("\n• ") : text + const rendered = Array.isArray(text) ? `• ${content}` : content return (
-
{text}
- {text.length > 220 && ( +
+ {rendered} +
+ {String(rendered).length > 220 && ( )}
) } -/* panel con estilo */ +/* ---------- panel con aurora mesh ---------- */ function SectionPanel({ - title, icon: Icon, color, children, -}: { title: string; icon: any; color?: string | null; children: React.ReactNode }) { + title, icon: Icon, color, children, id, +}: { title: string; icon: any; color?: string | null; children: React.ReactNode; id: string }) { const rgb = hexToRgb(color) return ( -
-
- +
+ {/* aurora mesh sutil */} +
+
+
+
+ +
+

{title}

-
{children}
-
+
{children}
+
) } -/* ---------- TABS + EDIT DIALOG ---------- */ +/* ---------- Secciones integradas (sin tabs) ---------- */ type PlanTextFields = { - objetivo_general?: string | null; sistema_evaluacion?: string | null; - perfil_ingreso?: string | null; perfil_egreso?: string | null; - competencias_genericas?: string | null; competencias_especificas?: string | null; - indicadores_desempeno?: string | null; pertinencia?: string | null; prompt?: string | null; + objetivo_general?: string | string[] | null + sistema_evaluacion?: string | string[] | null + perfil_ingreso?: string | string[] | null + perfil_egreso?: string | string[] | null + competencias_genericas?: string | string[] | null + competencias_especificas?: string | string[] | null + indicadores_desempeno?: string | string[] | null + pertinencia?: string | string[] | null + prompt?: string | null } export function AcademicSections({ planId, plan, color, }: { planId: string; plan: PlanTextFields; color?: string | null }) { - // estado local editable const [local, setLocal] = useState({ ...plan }) const [editing, setEditing] = useState(null) const [draft, setDraft] = useState("") const [saving, setSaving] = useState(false) const sections = useMemo(() => [ - { id: "obj", title: "Objetivo general", icon: Icons.Target, key: "objetivo_general" as const, mono: false }, - { id: "eval", title: "Sistema de evaluación", icon: Icons.CheckCircle2, key: "sistema_evaluacion" as const, mono: false }, - { id: "ing", title: "Perfil de ingreso", icon: Icons.UserPlus, key: "perfil_ingreso" as const, mono: false }, - { id: "egr", title: "Perfil de egreso", icon: Icons.GraduationCap, key: "perfil_egreso" as const, mono: false }, - { id: "cg", title: "Competencias genéricas", icon: Icons.Sparkles, key: "competencias_genericas" as const, mono: false }, - { id: "ce", title: "Competencias específicas", icon: Icons.Cog, key: "competencias_especificas" as const, mono: false }, - { id: "ind", title: "Indicadores de desempeño", icon: Icons.LineChart, key: "indicadores_desempeno" as const, mono: false }, - { id: "pert", title: "Pertinencia", icon: Icons.Lightbulb, key: "pertinencia" as const, mono: false }, - { id: "prompt", title: "Prompt (origen)", icon: Icons.Code2, key: "prompt" as const, mono: true }, + { id: "sec-obj", title: "Objetivo general", icon: Icons.Target, key: "objetivo_general" as const, mono: false }, + { id: "sec-eval", title: "Sistema de evaluación", icon: Icons.CheckCircle2, key: "sistema_evaluacion" as const, mono: false }, + { id: "sec-ing", title: "Perfil de ingreso", icon: Icons.UserPlus, key: "perfil_ingreso" as const, mono: false }, + { id: "sec-egr", title: "Perfil de egreso", icon: Icons.GraduationCap, key: "perfil_egreso" as const, mono: false }, + { id: "sec-cg", title: "Competencias genéricas", icon: Icons.Sparkles, key: "competencias_genericas" as const, mono: false }, + { id: "sec-ce", title: "Competencias específicas", icon: Icons.Cog, key: "competencias_especificas" as const, mono: false }, + { id: "sec-ind", title: "Indicadores de desempeño", icon: Icons.LineChart, key: "indicadores_desempeno" as const, mono: false }, + { id: "sec-per", title: "Pertinencia", icon: Icons.Lightbulb, key: "pertinencia" as const, mono: false }, + { id: "sec-prm", title: "Prompt (origen)", icon: Icons.Code2, key: "prompt" as const, mono: true }, ], []) async function handleSave() { @@ -96,50 +123,55 @@ export function AcademicSections({ return ( <> - - {/* nav sticky con píldoras scrollables */} -
- - {sections.map(s => ( - - {s.title} - - ))} - -
- {/* contenido */} + {/* Todas las tarjetas visibles */} +
{sections.map(s => { const text = local[s.key] ?? null return ( - - - -
- - -
-
-
+ + +
+ + +
+
) })} - +
- {/* Dialog de edición */} + {/* Diálogo de edición */} { if (!o) setEditing(null) }}> - {editing ? `Editar: ${sections.find(x => x.key === editing.key)?.title}` : ""} + + {editing ? `Editar: ${sections.find(x => x.key === editing.key)?.title}` : ""} +