socios-comerciales.skyballo.../src/components/AppShell.tsx

175 lines
6.9 KiB
TypeScript

import { Link, useRouterState } from "@tanstack/react-router";
import { CalendarCheck, Wallet, UserRound, Bell, LogOut, Menu, Sparkles, Ticket } from "lucide-react";
import { useState, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { useAppState } from "@/lib/store";
import { notificar, pedirPermisoNotificaciones } from "@/lib/notificaciones";
import { BrandLogo, BrandMark } from "@/components/BrandLogo";
import { useInstalarPWA } from "@/lib/pwa-install";
const NAV = [
{ to: "/vuelos", label: "Vuelos", icon: Sparkles },
{ to: "/reserva", label: "Reservar", icon: Ticket },
{ to: "/reservaciones", label: "Reservas", icon: CalendarCheck },
{ to: "/comisiones", label: "Comisiones", icon: Wallet },
{ to: "/perfil", label: "Perfil", icon: UserRound },
] as const;
export function AppShell({
title,
subtitle,
children,
}: {
title: string;
subtitle?: string;
children: ReactNode;
}) {
const { pathname } = useRouterState({ select: (s) => s.location });
const { socio } = useAppState();
const [sidebarAbierto, setSidebarAbierto] = useState(true);
const { instalada, instalar } = useInstalarPWA();
const activarNotificaciones = async () => {
const estado = await pedirPermisoNotificaciones();
notificar(
"Notificaciones activas",
estado === "granted"
? "Te avisaremos de reservaciones confirmadas y comisiones acreditadas."
: "Verás los avisos dentro de la app.",
);
};
return (
<div className="min-h-screen bg-background font-sans">
{/* Sidebar retráctil — solo desktop/tablet */}
<aside
className={cn(
"fixed inset-y-0 left-0 z-30 hidden flex-col bg-sidebar p-4 text-sidebar-foreground transition-all duration-300 lg:flex",
sidebarAbierto ? "w-64" : "w-20",
)}
>
<div className="px-2 py-3">
<BrandLogo soloIcono={!sidebarAbierto} invertido />
</div>
<nav className="mt-6 flex flex-1 flex-col gap-1">
{NAV.map(({ to, label, icon: Icon }) => {
const activo = pathname.startsWith(to);
return (
<Link
key={to}
to={to}
className={cn(
"flex items-center gap-3 rounded-2xl px-3 py-3 text-sm font-medium transition-colors",
activo
? "bg-brand text-brand-foreground"
: "text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground",
)}
>
<Icon className="size-5 shrink-0" />
{sidebarAbierto && label}
</Link>
);
})}
</nav>
<button
onClick={() => setSidebarAbierto((v) => !v)}
className="flex items-center gap-3 rounded-2xl px-3 py-3 text-sm text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent"
>
<Menu className="size-5" />
{sidebarAbierto && "Contraer"}
</button>
</aside>
<div className={cn("transition-all duration-300", sidebarAbierto ? "lg:pl-64" : "lg:pl-20")}>
{/* Encabezado */}
<header className="sticky top-0 z-20 border-b border-border/70 bg-background/85 backdrop-blur-xl">
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-4 sm:px-6">
<div className="min-w-0">
<h1 className="truncate text-xl font-bold tracking-tight sm:text-2xl">{title}</h1>
{subtitle && (
<p className="truncate text-sm text-muted-foreground">{subtitle}</p>
)}
</div>
<div className="flex items-center gap-2">
{!instalada && (
<button
onClick={async () => {
const ok = await instalar();
if (!ok)
notificar(
"Instalar la app",
"Usa “Agregar a pantalla de inicio” desde el menú de tu navegador.",
);
}}
className="flex items-center gap-2 rounded-2xl border border-border bg-card px-3 py-2.5 text-sm font-semibold shadow-soft transition-all hover:-translate-y-0.5 hover:shadow-lift active:scale-95"
>
<span className="grid size-6 place-items-center rounded-lg bg-ink p-0.5">
<BrandMark className="h-4" />
</span>
<span className="hidden sm:inline">Instalar</span>
</button>
)}
<button
onClick={activarNotificaciones}
aria-label="Activar notificaciones"
className="grid size-11 place-items-center rounded-2xl border border-border bg-card text-foreground shadow-soft transition-all hover:-translate-y-0.5 hover:bg-muted active:scale-95"
>
<Bell className="size-5" />
</button>
<Link
to="/perfil"
className="hidden items-center gap-2 rounded-2xl bg-ink px-3 py-2.5 text-sm font-medium text-ink-foreground sm:flex"
>
<span className="grid size-6 place-items-center rounded-full bg-brand text-[11px] font-bold text-brand-foreground">
{(socio?.nombre ?? "S").charAt(0).toUpperCase()}
</span>
<span className="max-w-28 truncate">{socio?.nombre ?? "Socio"}</span>
</Link>
</div>
</div>
</header>
<main className="mx-auto max-w-6xl px-4 pb-28 pt-5 sm:px-6 lg:pb-12">{children}</main>
</div>
{/* Bottom navigation — móvil */}
<nav className="fixed inset-x-0 bottom-0 z-30 border-t border-border bg-card/95 pb-[env(safe-area-inset-bottom)] backdrop-blur-xl lg:hidden">
<ul className="mx-auto grid max-w-md grid-cols-4">
{NAV.map(({ to, label, icon: Icon }) => {
const activo = pathname.startsWith(to);
return (
<li key={to}>
<Link
to={to}
className="flex flex-col items-center gap-1 px-2 py-3 text-[11px] font-medium"
>
<span
className={cn(
"grid size-10 place-items-center rounded-2xl transition-colors",
activo ? "bg-brand text-brand-foreground" : "text-muted-foreground",
)}
>
<Icon className="size-5" />
</span>
<span className={activo ? "text-foreground" : "text-muted-foreground"}>{label}</span>
</Link>
</li>
);
})}
</ul>
</nav>
</div>
);
}
export function SalirButton() {
return (
<span className="inline-flex items-center gap-2 text-sm">
<LogOut className="size-4" /> Cerrar sesión
</span>
);
}