mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 10:28:47 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,239 +1,100 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { Link, Outlet, useLocation } from "react-router-dom";
|
||||||
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Eye, EyeOff, Moon, Sun } from "lucide-react";
|
||||||
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 { getSyncStatus } from "@/lib/api";
|
import { titleFor } from "@/lib/navigation";
|
||||||
import { useTheme } from "@/contexts/theme-context";
|
import { useTheme } from "@/contexts/theme-context";
|
||||||
import { usePrivacy } from "@/contexts/privacy-context";
|
import { usePrivacy } from "@/contexts/privacy-context";
|
||||||
import { useAuth } from "@/AuthContext";
|
import { AppSidebar } from "@/components/app-sidebar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Breadcrumb,
|
||||||
SheetContent,
|
BreadcrumbItem,
|
||||||
SheetHeader,
|
BreadcrumbLink,
|
||||||
SheetTitle,
|
BreadcrumbList,
|
||||||
SheetClose,
|
BreadcrumbPage,
|
||||||
} from "@/components/ui/sheet";
|
BreadcrumbSeparator,
|
||||||
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { cn } from "@/lib/utils";
|
import {
|
||||||
|
SidebarInset,
|
||||||
interface NavSection {
|
SidebarProvider,
|
||||||
label: string;
|
SidebarTrigger,
|
||||||
items: { to: string; icon: LucideIcon; label: string }[];
|
} from "@/components/ui/sidebar";
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<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}
|
|
||||||
{to === "/sync" && syncWarnings > 0 && (
|
|
||||||
<span
|
|
||||||
className="ml-auto w-2 h-2 rounded-full bg-amber-500"
|
|
||||||
title={`${syncWarnings} fuente(s) atrasada(s)`}
|
|
||||||
aria-label={`${syncWarnings} fuentes de datos atrasadas`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const { theme, toggleTheme } = useTheme();
|
const { theme, toggleTheme } = useTheme();
|
||||||
const { privacyMode, togglePrivacy } = usePrivacy();
|
const { privacyMode, togglePrivacy } = usePrivacy();
|
||||||
const { logout } = useAuth();
|
const { pathname } = useLocation();
|
||||||
const navigate = useNavigate();
|
const title = titleFor(pathname);
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await logout();
|
|
||||||
navigate("/login", { replace: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<SidebarProvider>
|
||||||
<header className="border-b border-border backdrop-blur-sm sticky top-0 z-50 bg-background/90">
|
<AppSidebar />
|
||||||
<div className="px-4 sm:px-6 py-3 flex items-center justify-between">
|
<SidebarInset>
|
||||||
<div className="flex items-center gap-2.5">
|
<header className="sticky top-0 z-40 flex h-14 shrink-0 items-center gap-2 border-b border-border bg-background/90 px-4 backdrop-blur-sm">
|
||||||
|
<SidebarTrigger className="-ml-1" />
|
||||||
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
className="mr-2 data-[orientation=vertical]:h-4"
|
||||||
|
/>
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
{pathname === "/" ? (
|
||||||
|
<BreadcrumbPage>Inicio</BreadcrumbPage>
|
||||||
|
) : (
|
||||||
|
<BreadcrumbLink render={<Link to="/" />}>
|
||||||
|
Inicio
|
||||||
|
</BreadcrumbLink>
|
||||||
|
)}
|
||||||
|
</BreadcrumbItem>
|
||||||
|
{pathname !== "/" && (
|
||||||
|
<>
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>{title}</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
<div className="ml-auto flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setMobileOpen(true)}
|
onClick={togglePrivacy}
|
||||||
title="Open menu"
|
title="Toggle privacy mode"
|
||||||
aria-label="Open menu"
|
aria-label="Toggle privacy mode"
|
||||||
className="md:hidden"
|
aria-pressed={privacyMode}
|
||||||
>
|
>
|
||||||
<Menu className="w-5 h-5" />
|
{privacyMode ? (
|
||||||
</Button>
|
<EyeOff className="w-4 h-4" />
|
||||||
<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} />
|
<Eye className="w-4 h-4" />
|
||||||
</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" aria-pressed={privacyMode}>
|
|
||||||
{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" aria-pressed={theme === "dark"}>
|
|
||||||
{theme === "dark" ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleLogout}
|
onClick={toggleTheme}
|
||||||
title="Sign out"
|
title="Toggle theme"
|
||||||
aria-label="Sign out"
|
aria-label="Toggle theme"
|
||||||
className="hidden md:inline-flex"
|
aria-pressed={theme === "dark"}
|
||||||
>
|
>
|
||||||
<LogOut className="w-4 h-4" />
|
{theme === "dark" ? (
|
||||||
|
<Sun className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Moon className="w-4 h-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</header>
|
<main className="flex min-h-0 flex-1 flex-col px-4 py-6 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto flex min-h-0 w-full max-w-6xl flex-1 flex-col">
|
||||||
<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 />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</SidebarInset>
|
||||||
</div>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
100
frontend/src/components/app-sidebar.tsx
Normal file
100
frontend/src/components/app-sidebar.tsx
Normal file
@@ -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<typeof Sidebar>) {
|
||||||
|
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 (
|
||||||
|
<Sidebar collapsible="icon" {...props}>
|
||||||
|
<SidebarHeader>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
render={<Link to="/" aria-label="Ir a Inicio" />}
|
||||||
|
>
|
||||||
|
<div className="bg-primary text-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||||
|
<Wallet className="size-4" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="text-base font-bold tracking-tight"
|
||||||
|
style={{ fontFamily: "var(--font-heading)" }}
|
||||||
|
>
|
||||||
|
Wealthy<span className="text-primary">Smart</span>
|
||||||
|
</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarHeader>
|
||||||
|
|
||||||
|
<SidebarContent>
|
||||||
|
{navSections.map((section) => (
|
||||||
|
<SidebarGroup key={section.label}>
|
||||||
|
<SidebarGroupLabel>{section.label}</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{section.items.map(({ to, icon: Icon, label }) => (
|
||||||
|
<SidebarMenuItem key={to}>
|
||||||
|
<SidebarMenuButton
|
||||||
|
isActive={isNavActive(to, pathname)}
|
||||||
|
tooltip={label}
|
||||||
|
render={
|
||||||
|
<Link to={to} onClick={() => setOpenMobile(false)} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
<span>{label}</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
{to === "/sync" && syncWarnings > 0 && (
|
||||||
|
<SidebarMenuBadge>
|
||||||
|
<span
|
||||||
|
className="size-2 rounded-full bg-amber-500"
|
||||||
|
title={`${syncWarnings} fuente(s) atrasada(s)`}
|
||||||
|
aria-label={`${syncWarnings} fuentes de datos atrasadas`}
|
||||||
|
/>
|
||||||
|
</SidebarMenuBadge>
|
||||||
|
)}
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
))}
|
||||||
|
</SidebarContent>
|
||||||
|
|
||||||
|
<SidebarFooter>
|
||||||
|
<NavUser />
|
||||||
|
</SidebarFooter>
|
||||||
|
<SidebarRail />
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
}
|
||||||
108
frontend/src/components/nav-user.tsx
Normal file
108
frontend/src/components/nav-user.tsx
Normal file
@@ -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 (
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
|
<SidebarMenuButton size="lg" className="aria-expanded:bg-muted" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Avatar>
|
||||||
|
<AvatarFallback>{initials}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-medium">{username}</span>
|
||||||
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
|
WealthySmart
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown className="ml-auto size-4" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-fit min-w-48"
|
||||||
|
side={isMobile ? "bottom" : "right"}
|
||||||
|
align="end"
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuLabel className="p-0 font-normal">
|
||||||
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
|
<Avatar>
|
||||||
|
<AvatarFallback>{initials}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-medium">{username}</span>
|
||||||
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
|
WealthySmart
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem onClick={togglePrivacy}>
|
||||||
|
{privacyMode ? <EyeOff /> : <Eye />}
|
||||||
|
{privacyMode ? "Desactivar modo privado" : "Modo privado"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={toggleTheme}>
|
||||||
|
{theme === "dark" ? <Sun /> : <Moon />}
|
||||||
|
{theme === "dark" ? "Tema claro" : "Tema oscuro"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => void handleLogout()}>
|
||||||
|
<LogOut />
|
||||||
|
Cerrar sesión
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user