mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 16:08:47 +02:00
Repoints imports at the relocated lib/api and src/contexts modules, and refreshes Layout + Login alongside the rest of the migration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
214 lines
7.3 KiB
TypeScript
214 lines
7.3 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
|
import {
|
|
Sparkles,
|
|
Calculator,
|
|
BarChart3,
|
|
Landmark,
|
|
PiggyBank,
|
|
Droplets,
|
|
LogOut,
|
|
TrendingUp,
|
|
Wallet,
|
|
Menu,
|
|
Sun,
|
|
Moon,
|
|
Eye,
|
|
EyeOff,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
import { useTheme } from "@/contexts/theme-context";
|
|
import { usePrivacy } from "@/contexts/privacy-context";
|
|
import { useAuth } from "@/AuthContext";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
SheetClose,
|
|
} from "@/components/ui/sheet";
|
|
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" }],
|
|
},
|
|
{
|
|
label: "Finanzas",
|
|
items: [
|
|
{ to: "/budget", icon: Calculator, label: "Presupuesto" },
|
|
{ to: "/salarios", icon: Landmark, label: "Salarios" },
|
|
{ to: "/pensions", icon: PiggyBank, label: "Pensiones" },
|
|
{ to: "/proyecciones", icon: TrendingUp, label: "Proyecciones" },
|
|
{ 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}/`);
|
|
|
|
return (
|
|
<nav className="flex flex-col gap-0.5 px-3">
|
|
{navSections.map((section) => (
|
|
<div key={section.label}>
|
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-3 pt-4 pb-1">
|
|
{section.label}
|
|
</p>
|
|
{section.items.map(({ to, icon: Icon, label }) => (
|
|
<Link
|
|
key={to}
|
|
to={to}
|
|
onClick={onNavigate}
|
|
className={cn(
|
|
"flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
|
isActive(to)
|
|
? "bg-primary/10 text-primary"
|
|
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
)}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
{label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
))}
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
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 });
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background text-foreground">
|
|
<header className="border-b border-border backdrop-blur-sm sticky top-0 z-50 bg-background/90">
|
|
<div className="px-4 sm:px-6 py-3 flex items-center justify-between">
|
|
<div className="flex items-center gap-2.5">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setMobileOpen(true)}
|
|
title="Open menu"
|
|
aria-label="Open menu"
|
|
className="md:hidden"
|
|
>
|
|
<Menu className="w-5 h-5" />
|
|
</Button>
|
|
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
|
|
<Wallet className="w-4 h-4 text-primary-foreground" strokeWidth={2.5} />
|
|
</div>
|
|
<span className="text-lg font-bold tracking-tight hidden sm:inline" style={{ fontFamily: "var(--font-heading)" }}>
|
|
Wealthy<span className="text-primary">Smart</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<Button variant="ghost" size="icon" onClick={togglePrivacy} title="Toggle privacy mode" aria-label="Toggle privacy mode">
|
|
{privacyMode ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</Button>
|
|
<Button variant="ghost" size="icon" onClick={toggleTheme} title="Toggle theme" aria-label="Toggle theme">
|
|
{theme === "dark" ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={handleLogout}
|
|
title="Sign out"
|
|
aria-label="Sign out"
|
|
className="hidden md:inline-flex"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="flex">
|
|
<aside className="hidden md:flex md:flex-col md:w-56 md:flex-shrink-0 border-r border-border sticky top-[57px] h-[calc(100vh-57px)] overflow-y-auto bg-background">
|
|
<div className="flex-1">
|
|
<SidebarNav />
|
|
</div>
|
|
<div className="px-3 pb-4">
|
|
<Separator className="mb-2" />
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted w-full cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
Cerrar sesión
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
|
<SheetContent side="left" className="p-0 w-64">
|
|
<SheetHeader className="p-4">
|
|
<SheetTitle className="flex items-center gap-2.5">
|
|
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
|
|
<Wallet className="w-4 h-4 text-primary-foreground" strokeWidth={2.5} />
|
|
</div>
|
|
<span style={{ fontFamily: "var(--font-heading)" }}>
|
|
Wealthy<span className="text-primary">Smart</span>
|
|
</span>
|
|
</SheetTitle>
|
|
</SheetHeader>
|
|
<Separator />
|
|
<div className="flex flex-col h-[calc(100%-65px)]">
|
|
<div className="flex-1 overflow-y-auto">
|
|
<SidebarNav onNavigate={() => setMobileOpen(false)} />
|
|
</div>
|
|
<div className="px-3 pb-4">
|
|
<Separator className="mb-2" />
|
|
<SheetClose render={<span />}>
|
|
<button
|
|
onClick={() => {
|
|
setMobileOpen(false);
|
|
void handleLogout();
|
|
}}
|
|
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted w-full cursor-pointer"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
Cerrar sesión
|
|
</button>
|
|
</SheetClose>
|
|
</div>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
|
|
<main className="flex-1 min-w-0 px-4 sm:px-6 lg:px-8 py-6">
|
|
<div className="max-w-6xl mx-auto">
|
|
<Outlet />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|