feat: Implement faculty management routes and UI components
- Added a new route for managing faculties with a grid display of faculties. - Created a detailed view for each faculty including metrics and recent activities. - Introduced a new loader for fetching faculty data and associated plans and subjects. - Enhanced the existing plans route to include a modal for plan details. - Updated the login and index pages with improved UI and styling. - Integrated a progress ring component to visualize the quality of plans. - Applied a new font style across the application for consistency.
This commit is contained in:
@@ -24,6 +24,8 @@ import {
|
||||
FileText,
|
||||
Settings,
|
||||
LogOut,
|
||||
KeySquare,
|
||||
IdCard,
|
||||
} from "lucide-react"
|
||||
import { useSupabaseAuth } from "@/auth/supabase"
|
||||
|
||||
@@ -41,11 +43,39 @@ const nav = [
|
||||
{ to: "/materias", label: "Materias", icon: FileText },
|
||||
{ to: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
|
||||
{ to: "/ajustes", label: "Ajustes", icon: Settings },
|
||||
]
|
||||
] as const
|
||||
|
||||
function getInitials(name?: string) {
|
||||
if (!name) return "LS"
|
||||
const parts = name.trim().split(/\s+/).slice(0, 2)
|
||||
return parts.map(p => p[0]?.toUpperCase()).join("") || "LS"
|
||||
}
|
||||
|
||||
function useUserDisplay() {
|
||||
const { claims, user } = useSupabaseAuth()
|
||||
const nombre = claims?.nombre ?? ""
|
||||
const apellidos = claims?.apellidos ?? ""
|
||||
const titulo = claims?.title ?? ""
|
||||
const clave = claims?.clave ?? ""
|
||||
const fullName = [titulo, nombre, apellidos].filter(Boolean).join(" ")
|
||||
const shortName = [titulo, nombre, apellidos.split(" ")[0] ?? ""].filter(Boolean).join(" ")
|
||||
const role = claims?.role ?? ""
|
||||
return {
|
||||
fullName,
|
||||
shortName,
|
||||
clave,
|
||||
email: user?.email,
|
||||
avatar: claims?.avatar ?? null,
|
||||
initials: getInitials([nombre, apellidos].filter(Boolean).join(" ")),
|
||||
role,
|
||||
isAdmin: Boolean(claims?.claims_admin),
|
||||
}
|
||||
}
|
||||
|
||||
function Layout() {
|
||||
const { auth } = Route.useRouteContext()
|
||||
Route.useRouteContext()
|
||||
const [query, setQuery] = useState("")
|
||||
const user = useUserDisplay()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
@@ -55,19 +85,22 @@ function Layout() {
|
||||
{/* Mobile menu */}
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="md:hidden">
|
||||
<Button variant="ghost" size="icon" className="md:hidden" aria-label="Abrir menú">
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="p-0">
|
||||
<Sidebar onNavigate={() => { }} />
|
||||
<Sidebar onNavigate={() => {}} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
{/* Brand */}
|
||||
<Link to="/dashboard" className="hidden items-center gap-2 md:flex">
|
||||
<Link to={user.isAdmin ? "/dashboard" : "/planes"} className="hidden items-center gap-2 md:flex">
|
||||
<span className="inline-flex h-8 w-8 items-center justify-center rounded-xl bg-primary/10 text-primary font-bold">U</span>
|
||||
<span className="font-semibold tracking-tight">La Salle · Ingeniería</span>
|
||||
<div className="flex flex-col leading-tight">
|
||||
<span className="font-semibold tracking-tight">La Salle · Ingeniería</span>
|
||||
<span className="text-[10px] text-muted-foreground font-mono uppercase tracking-wider">Génesis</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Search */}
|
||||
@@ -79,13 +112,14 @@ function Layout() {
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Buscar…"
|
||||
className="w-64 pl-9"
|
||||
aria-label="Buscar"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" aria-label="Notificaciones">
|
||||
<Bell className="h-5 w-5" />
|
||||
</Button>
|
||||
<ModeToggle />
|
||||
<UserMenu user={{ name: auth.user?.email ?? "Usuario", avatar: auth.user?.avatar_url }} />
|
||||
<UserMenu user={user} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -99,6 +133,7 @@ function Layout() {
|
||||
|
||||
{/* Main content */}
|
||||
<main className="min-h-[calc(100dvh-4rem)] p-4 md:p-6">
|
||||
{/* Welcome strip */}
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
@@ -107,11 +142,17 @@ function Layout() {
|
||||
}
|
||||
|
||||
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||
const { claims } = useSupabaseAuth()
|
||||
const isAdmin = Boolean(claims?.claims_admin)
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="flex items-center gap-2 p-4 md:hidden">
|
||||
<span className="inline-flex h-8 w-8 items-center justify-center rounded-xl bg-primary/10 text-primary font-bold">U</span>
|
||||
<span className="font-semibold">La Salle · Ingeniería</span>
|
||||
<div className="leading-tight">
|
||||
<span className="font-semibold">La Salle · Ingeniería</span>
|
||||
<div className="text-[10px] text-muted-foreground font-mono uppercase tracking-wider">Génesis</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<ScrollArea className="h-[calc(100%-4rem)] p-2">
|
||||
@@ -120,22 +161,39 @@ function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||
<Link
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
activeOptions={{ exact: true }}
|
||||
activeProps={{ className: "bg-primary/10 text-foreground" }}
|
||||
className="group inline-flex items-center gap-3 rounded-xl px-3 py-2 text-sm text-muted-foreground hover:bg-primary/10 hover:text-foreground"
|
||||
onClick={onNavigate}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
{item.label}
|
||||
<span className="truncate">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{isAdmin && (
|
||||
<Link
|
||||
to="/facultades"
|
||||
activeOptions={{ exact: true }}
|
||||
activeProps={{ className: "bg-primary/10 text-foreground" }}
|
||||
className="group inline-flex items-center gap-3 rounded-xl px-3 py-2 text-sm text-muted-foreground hover:bg-primary/10 hover:text-foreground"
|
||||
>
|
||||
<KeySquare className="h-4 w-4" />
|
||||
<span className="truncate">Facultades</span>
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
</ScrollArea>
|
||||
<Separator className="mt-auto" />
|
||||
<div className="p-3 text-xs text-muted-foreground">© {new Date().getFullYear()} La Salle</div>
|
||||
<div className="flex items-center gap-2 p-3 text-xs text-muted-foreground">
|
||||
<IdCard className="h-3.5 w-3.5" />
|
||||
© {new Date().getFullYear()} La Salle
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function UserMenu({ user }: { user: { name?: string; avatar?: string | null } }) {
|
||||
function UserMenu({ user }: { user: ReturnType<typeof useUserDisplay> }) {
|
||||
const auth = useSupabaseAuth()
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@@ -143,20 +201,20 @@ function UserMenu({ user }: { user: { name?: string; avatar?: string | null } })
|
||||
<Button variant="ghost" className="h-9 gap-2 px-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={user.avatar ?? undefined} />
|
||||
<AvatarFallback className="text-xs">LS</AvatarFallback>
|
||||
<AvatarFallback className="text-xs">{user.initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="hidden max-w-[16ch] truncate text-sm md:inline">{user.name}</span>
|
||||
<span className="hidden max-w-[16ch] truncate text-sm md:inline">{user.shortName}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel className="truncate">{user.name}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/perfil">Perfil</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/ajustes">Ajustes</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
<DropdownMenuLabel className="truncate">
|
||||
<div className="font-mono text-sm leading-tight">
|
||||
{user.fullName}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
{`Clave: ${user.clave} · ${user.email}`}
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-destructive" onClick={auth.logout}>
|
||||
<LogOut className="mr-2 h-4 w-4" /> Cerrar sesión
|
||||
|
||||
Reference in New Issue
Block a user