From d9039c76b90a95d6e2f9beb1844e1d689a9f142b Mon Sep 17 00:00:00 2001 From: Carlos Escalante Date: Sat, 4 Jul 2026 10:46:34 -0600 Subject: [PATCH] Replace hand-rolled shell with shadcn sidebar + breadcrumb header AppSidebar (icon-collapsible, sync warning badge, brand header), NavUser footer (avatar + privacy/theme/logout dropdown), and a thin header with SidebarTrigger, breadcrumbs, and quick toggles. Mobile drawer and cmd+B come from the sidebar primitives. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/components/Layout.tsx | 283 ++++++------------------ frontend/src/components/app-sidebar.tsx | 100 +++++++++ frontend/src/components/nav-user.tsx | 108 +++++++++ 3 files changed, 280 insertions(+), 211 deletions(-) create mode 100644 frontend/src/components/app-sidebar.tsx create mode 100644 frontend/src/components/nav-user.tsx diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 7b86120..df22936 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,239 +1,100 @@ -import { useState, useEffect } from "react"; -import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"; -import { - Sparkles, - Telescope, - Calculator, - BarChart3, - Landmark, - PiggyBank, - Droplets, - Layers, - LogOut, - TrendingUp, - Wallet, - Menu, - RefreshCw, - Sun, - Moon, - Eye, - EyeOff, - type LucideIcon, -} from "lucide-react"; -import { useQuery } from "@tanstack/react-query"; +import { Link, Outlet, useLocation } from "react-router-dom"; +import { Eye, EyeOff, Moon, Sun } from "lucide-react"; -import { getSyncStatus } from "@/lib/api"; +import { titleFor } from "@/lib/navigation"; import { useTheme } from "@/contexts/theme-context"; import { usePrivacy } from "@/contexts/privacy-context"; -import { useAuth } from "@/AuthContext"; +import { AppSidebar } from "@/components/app-sidebar"; import { Button } from "@/components/ui/button"; import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetClose, -} from "@/components/ui/sheet"; + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; import { Separator } from "@/components/ui/separator"; -import { cn } from "@/lib/utils"; - -interface NavSection { - label: string; - items: { to: string; icon: LucideIcon; label: string }[]; -} - -const navSections: NavSection[] = [ - { - label: "General", - items: [ - { to: "/asistente", icon: Sparkles, label: "Asistente" }, - { to: "/sync", icon: RefreshCw, label: "Sincronización" }, - ], - }, - { - label: "Finanzas", - items: [ - { to: "/budget", icon: Calculator, label: "Presupuesto" }, - { to: "/salarios", icon: Landmark, label: "Salarios" }, - { to: "/financiamientos", icon: Layers, label: "Financiamientos" }, - { to: "/pensions", icon: PiggyBank, label: "Pensiones" }, - { to: "/proyecciones", icon: TrendingUp, label: "Proyecciones" }, - { to: "/planificador", icon: Telescope, label: "Planificador" }, - { to: "/analytics", icon: BarChart3, label: "Analytics" }, - ], - }, - { - label: "Servicios", - items: [ - { to: "/servicios-municipales", icon: Droplets, label: "Municipalidad" }, - ], - }, -]; - -function SidebarNav({ onNavigate }: { onNavigate?: () => void }) { - const { pathname } = useLocation(); - const isActive = (to: string) => - pathname === to || pathname.startsWith(`${to}/`); - - // Surface stalled ingestion sources as a dot on the Sincronización item. - const syncQ = useQuery({ - queryKey: ['sync-status'], - queryFn: () => getSyncStatus().then((r) => r.data), - staleTime: 5 * 60_000, - }); - const syncWarnings = syncQ.data?.warnings ?? 0; - - return ( - - ); -} +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; export default function Layout() { const { theme, toggleTheme } = useTheme(); const { privacyMode, togglePrivacy } = usePrivacy(); - const { logout } = useAuth(); - const navigate = useNavigate(); - const [mobileOpen, setMobileOpen] = useState(false); - - const handleLogout = async () => { - await logout(); - navigate("/login", { replace: true }); - }; + const { pathname } = useLocation(); + const title = titleFor(pathname); return ( -
-
-
-
+ + + +
+ + + + + + {pathname === "/" ? ( + Inicio + ) : ( + }> + Inicio + + )} + + {pathname !== "/" && ( + <> + + + {title} + + + )} + + +
-
- -
- - WealthySmart - -
- -
- -
-
-
- -
- - - - - - -
- -
- - WealthySmart - -
-
- -
-
- setMobileOpen(false)} /> -
-
- - }> - - -
-
-
-
- -
-
+ +
+
-
-
+ + ); } diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx new file mode 100644 index 0000000..310268b --- /dev/null +++ b/frontend/src/components/app-sidebar.tsx @@ -0,0 +1,100 @@ +import { Wallet } from "lucide-react"; +import { Link, useLocation } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; + +import { getSyncStatus } from "@/lib/api"; +import { isNavActive, navSections } from "@/lib/navigation"; +import { NavUser } from "@/components/nav-user"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuBadge, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from "@/components/ui/sidebar"; + +export function AppSidebar(props: React.ComponentProps) { + const { pathname } = useLocation(); + const { setOpenMobile } = useSidebar(); + + // Surface stalled ingestion sources as a dot on the Sincronización item. + const syncQ = useQuery({ + queryKey: ["sync-status"], + queryFn: () => getSyncStatus().then((r) => r.data), + staleTime: 5 * 60_000, + }); + const syncWarnings = syncQ.data?.warnings ?? 0; + + return ( + + + + + } + > +
+ +
+ + WealthySmart + +
+
+
+
+ + + {navSections.map((section) => ( + + {section.label} + + + {section.items.map(({ to, icon: Icon, label }) => ( + + setOpenMobile(false)} /> + } + > + + {label} + + {to === "/sync" && syncWarnings > 0 && ( + + + + )} + + ))} + + + + ))} + + + + + + +
+ ); +} diff --git a/frontend/src/components/nav-user.tsx b/frontend/src/components/nav-user.tsx new file mode 100644 index 0000000..32fbdc7 --- /dev/null +++ b/frontend/src/components/nav-user.tsx @@ -0,0 +1,108 @@ +import { ChevronsUpDown, Eye, EyeOff, LogOut, Moon, Sun } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; + +import { getMe } from "@/lib/api"; +import { useTheme } from "@/contexts/theme-context"; +import { usePrivacy } from "@/contexts/privacy-context"; +import { useAuth } from "@/AuthContext"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; + +export function NavUser() { + const { isMobile } = useSidebar(); + const { theme, toggleTheme } = useTheme(); + const { privacyMode, togglePrivacy } = usePrivacy(); + const { logout } = useAuth(); + const navigate = useNavigate(); + + const meQ = useQuery({ + queryKey: ["me"], + queryFn: () => getMe().then((r) => r.data), + staleTime: Infinity, + }); + const username = meQ.data?.username ?? "Usuario"; + const initials = username.slice(0, 2).toUpperCase(); + + const handleLogout = async () => { + await logout(); + navigate("/login", { replace: true }); + }; + + return ( + + + + + } + > + + {initials} + +
+ {username} + + WealthySmart + +
+ +
+ + + +
+ + {initials} + +
+ {username} + + WealthySmart + +
+
+
+
+ + + + {privacyMode ? : } + {privacyMode ? "Desactivar modo privado" : "Modo privado"} + + + {theme === "dark" ? : } + {theme === "dark" ? "Tema claro" : "Tema oscuro"} + + + + void handleLogout()}> + + Cerrar sesión + +
+
+
+
+ ); +}