feat: add UI components and improve layout

- Introduced new UI components: Input, Label, ScrollArea, Separator, Sheet, Skeleton, Table.
- Implemented utility function for class name merging.
- Enhanced the authenticated layout with a sidebar and user menu.
- Added login functionality with improved UI and error handling.
- Integrated theme provider for consistent theming across the application.
- Updated styles with Tailwind CSS and custom properties for dark mode support.
- Refactored routes to utilize new components and improve user experience.
This commit is contained in:
2025-08-20 07:59:52 -06:00
parent ccdcfb9fa1
commit 559514d7b4
26 changed files with 1802 additions and 159 deletions

View File

@@ -1,9 +1,167 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
import { createFileRoute, Outlet, redirect, Link } from "@tanstack/react-router"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import { ModeToggle } from "@/components/mode-toggle"
import {
Menu,
Bell,
Search as SearchIcon,
LayoutDashboard,
GraduationCap,
FileText,
Settings,
LogOut,
} from "lucide-react"
import { useSupabaseAuth } from "@/auth/supabase"
export const Route = createFileRoute('/_authenticated')({
export const Route = createFileRoute("/_authenticated")({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login', search: { redirect: location.href } })
throw redirect({ to: "/login", search: { redirect: location.href } })
}
},
})
component: Layout,
})
const nav = [
{ to: "/planes", label: "Planes", icon: GraduationCap },
{ to: "/materias", label: "Materias", icon: FileText },
{ to: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
{ to: "/ajustes", label: "Ajustes", icon: Settings },
]
function Layout() {
const { auth } = Route.useRouteContext()
const [query, setQuery] = useState("")
return (
<div className="min-h-screen bg-background text-foreground">
{/* Top bar */}
<header className="sticky top-0 z-40 border-b border-border bg-card/80 backdrop-blur supports-[backdrop-filter]:bg-card/60">
<div className="mx-auto flex h-16 max-w-screen-2xl items-center gap-3 px-4">
{/* Mobile menu */}
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden">
<Menu className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="p-0">
<Sidebar onNavigate={() => { }} />
</SheetContent>
</Sheet>
{/* Brand */}
<Link to="/dashboard" 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>
</Link>
{/* Search */}
<div className="ml-auto hidden items-center gap-2 md:flex">
<div className="relative">
<SearchIcon className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar…"
className="w-64 pl-9"
/>
</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 }} />
</div>
</div>
</header>
{/* Shell */}
<div className="mx-auto grid max-w-screen-2xl grid-cols-1 md:grid-cols-[260px_1fr] gap-0">
{/* Sidebar (desktop) */}
<aside className="sticky top-16 hidden h-[calc(100dvh-4rem)] border-r border-border bg-card/40 md:block">
<Sidebar />
</aside>
{/* Main content */}
<main className="min-h-[calc(100dvh-4rem)] p-4 md:p-6">
<Outlet />
</main>
</div>
</div>
)
}
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
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>
<Separator />
<ScrollArea className="h-[calc(100%-4rem)] p-2">
<nav className="grid gap-1 p-2">
{nav.map((item) => (
<Link
key={item.to}
to={item.to}
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}
</Link>
))}
</nav>
</ScrollArea>
<Separator className="mt-auto" />
<div className="p-3 text-xs text-muted-foreground">© {new Date().getFullYear()} La Salle</div>
</div>
)
}
function UserMenu({ user }: { user: { name?: string; avatar?: string | null } }) {
const auth = useSupabaseAuth()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<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>
</Avatar>
<span className="hidden max-w-[16ch] truncate text-sm md:inline">{user.name}</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>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive" onClick={auth.logout}>
<LogOut className="mr-2 h-4 w-4" /> Cerrar sesión
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}