all
This commit is contained in:
1 parent
90125aede9
commit
2d6c29a1f3
404 files changed
+126983
-85
No files matched your search
@@ -0,0 +1,209 @@
|
||||
"use client";
|
||||
|
||||
import { Icon } from "@iconify/react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
iconArchive,
|
||||
iconChart,
|
||||
iconCloseSquare,
|
||||
iconDashboard,
|
||||
iconDelivery,
|
||||
iconHamburgerMenu,
|
||||
iconStorefrontFill,
|
||||
iconUserPlus,
|
||||
iconUsersGroup,
|
||||
iconWalletMoney,
|
||||
} from "../../_core/icons";
|
||||
import { useSessionUser } from "../../_core/hooks/use-session-user";
|
||||
import { OperatingDayControl } from "./operating-day-control";
|
||||
import { UserMenu } from "./user-menu";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: "/admin", label: "Panel", icon: iconDashboard },
|
||||
{ href: "/admin/inventario", label: "Inventario", icon: iconArchive },
|
||||
{ href: "/admin/proveedores", label: "Proveedores", icon: iconDelivery },
|
||||
{ href: "/admin/clientes", label: "Clientes", icon: iconUserPlus },
|
||||
{ href: "/admin/reportes", label: "Reportes", icon: iconChart },
|
||||
{ href: "/admin/usuarios", label: "Usuarios", icon: iconUsersGroup },
|
||||
] as const;
|
||||
|
||||
function isActive(pathname: string, href: string) {
|
||||
if (href === "/admin") return pathname === "/admin";
|
||||
return pathname.startsWith(href);
|
||||
}
|
||||
|
||||
export function AdminShell({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const { user, logout } = useSessionUser();
|
||||
const reduceMotion = useReducedMotion();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [prevPathname, setPrevPathname] = useState(pathname);
|
||||
|
||||
if (pathname !== prevPathname) {
|
||||
setPrevPathname(pathname);
|
||||
setMenuOpen(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return;
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") setMenuOpen(false);
|
||||
}
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, [menuOpen]);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const activeItem = NAV_ITEMS.find((item) => isActive(pathname, item.href));
|
||||
|
||||
const navList = (
|
||||
<nav aria-label="Principal" className="flex flex-1 flex-col gap-1 overflow-y-auto p-3">
|
||||
<span className="px-3 pb-1.5 pt-1 text-[11px] font-semibold tracking-wider text-muted uppercase">
|
||||
Menú principal
|
||||
</span>
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const active = isActive(pathname, item.href);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={`group relative flex items-center gap-3 rounded-lg py-2.5 pl-3.5 pr-3 text-sm font-medium transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus ${
|
||||
active
|
||||
? "bg-accent-soft text-accent-soft-foreground"
|
||||
: "text-muted hover:bg-foreground/5 hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute inset-y-1.5 left-0 w-[3px] rounded-full transition-opacity ${
|
||||
active ? "bg-accent opacity-100" : "opacity-0"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
className={`flex size-8 shrink-0 items-center justify-center rounded-md transition ${
|
||||
active ? "bg-accent text-accent-foreground" : "bg-foreground/5 text-muted group-hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Icon icon={item.icon} width={17} height={17} aria-hidden="true" />
|
||||
</span>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh w-full flex-col gap-3 overflow-hidden bg-background p-3 text-foreground md:p-4">
|
||||
{/* Unified header */}
|
||||
<header className="flex h-16 shrink-0 items-center gap-3 rounded-2xl border border-border bg-surface px-3 md:px-5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMenuOpen(true)}
|
||||
aria-label="Abrir menú"
|
||||
aria-expanded={menuOpen}
|
||||
className="flex size-10 shrink-0 items-center justify-center rounded-lg text-foreground transition hover:bg-foreground/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus md:hidden"
|
||||
>
|
||||
<Icon icon={iconHamburgerMenu} width={22} height={22} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-xl bg-accent-soft">
|
||||
<Icon icon={iconStorefrontFill} width={20} height={20} className="text-accent-soft-foreground" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="hidden min-w-0 leading-tight sm:block">
|
||||
<p className="truncate text-sm font-semibold tracking-wide text-foreground">PDV</p>
|
||||
<p className="truncate text-[11px] text-muted">Panel administrativo</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-2 hidden h-8 w-px shrink-0 bg-border md:block" />
|
||||
|
||||
<h1 className="hidden min-w-0 truncate text-base font-semibold text-foreground md:block">
|
||||
{activeItem?.label ?? "Panel"}
|
||||
</h1>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<Link
|
||||
href="/pos"
|
||||
className="flex h-11 items-center gap-2 rounded-lg px-3 text-sm font-medium text-muted transition hover:bg-foreground/5 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus"
|
||||
>
|
||||
<Icon icon={iconWalletMoney} width={16} height={16} aria-hidden="true" />
|
||||
<span className="hidden sm:inline">Ir a Caja</span>
|
||||
</Link>
|
||||
<OperatingDayControl />
|
||||
<UserMenu user={user} onLogout={logout} />
|
||||
</header>
|
||||
|
||||
<div className="flex flex-1 gap-3 overflow-hidden">
|
||||
{/* Sidebar (desktop) */}
|
||||
<aside className="hidden w-64 shrink-0 flex-col rounded-2xl border border-border bg-surface md:flex">
|
||||
{navList}
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 overflow-y-auto rounded-2xl border border-border bg-surface p-4 md:p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Mobile drawer */}
|
||||
<AnimatePresence>
|
||||
{menuOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
key="backdrop"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: reduceMotion ? 0 : 0.15 }}
|
||||
className="fixed inset-0 z-40 bg-backdrop md:hidden"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<motion.div
|
||||
key="drawer"
|
||||
initial={{ x: reduceMotion ? 0 : "-100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: reduceMotion ? 0 : "-100%" }}
|
||||
transition={{ duration: reduceMotion ? 0 : 0.2, ease: "easeOut" }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Menú principal"
|
||||
className="fixed inset-y-3 left-3 z-50 flex w-72 flex-col rounded-2xl border border-border bg-surface md:hidden"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-border px-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex size-9 items-center justify-center rounded-xl bg-accent-soft">
|
||||
<Icon
|
||||
icon={iconStorefrontFill}
|
||||
width={20}
|
||||
height={20}
|
||||
className="text-accent-soft-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-semibold tracking-wide text-foreground">PDV Admin</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
aria-label="Cerrar menú"
|
||||
className="flex size-9 items-center justify-center rounded-lg text-muted transition hover:bg-foreground/10 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus"
|
||||
>
|
||||
<Icon icon={iconCloseSquare} width={20} height={20} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{navList}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user