Reemplaza el lockup rectangular (logo-ari.svg) por un isotipo circular nuevo (logo-ari-circular.svg) con el icono mariposa + wordmark 'ARI shopping' integrados dentro del mismo circulo azul -- el formato que tenia la version previa a ADR 0008, preferido sobre el rectangulo segun mockup de referencia del usuario. El footer conserva el lockup rectangular, donde si tiene sentido como bloque de color autocontenido sobre el fondo oscuro.
347 lines
15 KiB
TypeScript
347 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname, useSearchParams } from "next/navigation";
|
|
import { ChevronDown, Heart, LayoutGrid, Menu, MessageCircle, PackageCheck, ShoppingCart, UserRound } from "lucide-react";
|
|
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
|
|
import { PredictiveSearch } from "@/components/search/predictive-search";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
|
import type { Category, Product } from "@/lib/headless/types";
|
|
import { useCartStore } from "@/store/cart-store";
|
|
import { useWishlistStore } from "@/store/wishlist-store";
|
|
|
|
type CategoryNode = Category & { children: Category[] };
|
|
|
|
function buildCategoryTree(categories: Category[]): CategoryNode[] {
|
|
return categories
|
|
.filter((category) => !category.parentId)
|
|
.map((root) => ({
|
|
...root,
|
|
children: categories.filter((category) => category.parentId === root.id),
|
|
}));
|
|
}
|
|
|
|
export function Navbar({ products, categories }: { products: Product[]; categories: Category[] }) {
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
const [accountOpen, setAccountOpen] = useState(false);
|
|
const items = useCartStore((state) => state.items);
|
|
const cartHydrated = useCartStore((state) => state.hasHydrated);
|
|
const openCart = useCartStore((state) => state.openCart);
|
|
const wishlistProducts = useWishlistStore((state) => state.products);
|
|
const wishlistHydrated = useWishlistStore((state) => state.hasHydrated);
|
|
const openWishlist = useWishlistStore((state) => state.openWishlist);
|
|
const count = cartHydrated ? items.reduce((sum, item) => sum + item.quantity, 0) : 0;
|
|
const wishlistCount = wishlistHydrated ? wishlistProducts.length : 0;
|
|
|
|
const tree = useMemo(() => buildCategoryTree(categories), [categories]);
|
|
// Derivado de los productos reales (no de un endpoint de colecciones):
|
|
// solo aparecen colecciones que de verdad tienen productos hoy.
|
|
const collections = useMemo(
|
|
() => [...new Set(products.map((product) => product.collection).filter(Boolean))],
|
|
[products],
|
|
);
|
|
|
|
return (
|
|
<header className="sticky top-0 z-40 border-b border-slate-200/80 bg-white/95 shadow-sm backdrop-blur-xl">
|
|
<div className="bg-ari-marino px-4 py-2 text-center text-xs font-black text-white sm:text-sm">
|
|
Paga tus compras a cuotas · Envíos nacionales · <span className="underline decoration-ari-coral-claro decoration-2 underline-offset-2">Compra segura con ARI</span>
|
|
</div>
|
|
|
|
<div className="relative overflow-visible bg-white">
|
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-1 bg-ari-coral-claro" />
|
|
<div className="mx-auto grid max-w-7xl grid-cols-[auto_1fr_auto] items-center gap-4 px-4 py-4 lg:gap-8">
|
|
{/* Isotipo circular con icono + wordmark integrados (como en la
|
|
version previa a ADR 0008), pedido explicito del usuario en
|
|
vez del lockup rectangular. logo-ari.svg (rectangular) se
|
|
mantiene para el footer. */}
|
|
<Link href="/" aria-label="ARI shopping" className="shrink-0">
|
|
{/* eslint-disable-next-line @next/next/no-img-element -- vector propio, next/image no optimiza SVG sin habilitar dangerouslyAllowSVG */}
|
|
<img src="/images/brand/logo-ari-circular.svg" alt="ARI shopping" className="size-16 lg:size-20" width={200} height={200} />
|
|
</Link>
|
|
<PredictiveSearch />
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="outline" size="icon" aria-label="Mi cuenta" className="hidden border-slate-300 bg-white text-slate-900 hover:border-ari-coral hover:bg-ari-coral/10 sm:inline-flex" onClick={() => setAccountOpen(true)}><UserRound /></Button>
|
|
<Button variant="outline" size="icon" aria-label={`Favoritos: ${wishlistCount}`} className="relative hidden border-slate-300 bg-white text-slate-900 hover:border-ari-coral hover:bg-ari-coral/10 sm:inline-flex" onClick={openWishlist}>
|
|
<Heart />
|
|
{wishlistCount > 0 && <span className="absolute -right-1.5 -top-1.5 grid size-5 place-items-center rounded-full bg-ari-marino text-[10px] font-black text-white">{wishlistCount}</span>}
|
|
</Button>
|
|
<Button variant="primary" size="icon" onClick={openCart} aria-label={`Carrito con ${count} productos`} className="relative">
|
|
<ShoppingCart />
|
|
<span className="absolute -right-2 -top-2 grid size-6 place-items-center rounded-full bg-ari-marino text-[11px] font-black text-white">{count}</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Suspense fallback={<DesktopCategoryNavigationFallback tree={tree} onOpenMenu={() => setMenuOpen(true)} />}>
|
|
<DesktopCategoryNavigation tree={tree} onOpenMenu={() => setMenuOpen(true)} />
|
|
</Suspense>
|
|
|
|
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
|
|
<SheetContent side="left" className="max-w-sm">
|
|
<SheetHeader>
|
|
<SheetTitle>Explora ARI</SheetTitle>
|
|
<SheetDescription>Categorías y colecciones del catálogo.</SheetDescription>
|
|
</SheetHeader>
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
<p className="mb-3 text-xs font-black uppercase tracking-[.16em] text-ari-coral">Categorías</p>
|
|
<div className="grid gap-2">
|
|
<Suspense fallback={<MobileCategoryLinksFallback tree={tree} onNavigate={() => setMenuOpen(false)} />}>
|
|
<MobileCategoryLinks tree={tree} onNavigate={() => setMenuOpen(false)} />
|
|
</Suspense>
|
|
</div>
|
|
{collections.length > 0 && (
|
|
<>
|
|
<p className="mb-3 mt-8 text-xs font-black uppercase tracking-[.16em] text-ari-marino">Colecciones</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{collections.map((collection) => <Link key={collection} href={`/catalog?collection=${encodeURIComponent(collection)}`} onClick={() => setMenuOpen(false)} className="rounded-full bg-slate-100 px-4 py-2 text-sm font-bold hover:bg-ari-coral/10 hover:text-ari-coral">{collection}</Link>)}
|
|
</div>
|
|
</>
|
|
)}
|
|
<div className="mt-8 grid grid-cols-2 gap-3 sm:hidden">
|
|
<Button variant="outline" onClick={() => { setMenuOpen(false); setAccountOpen(true); }}><UserRound className="size-4" />Cuenta</Button>
|
|
<Button variant="outline" onClick={() => { setMenuOpen(false); openWishlist(); }}><Heart className="size-4" />Favoritos</Button>
|
|
</div>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
|
|
<Dialog open={accountOpen} onOpenChange={setAccountOpen}>
|
|
<DialogContent className="max-w-lg p-7">
|
|
<div className="pr-10">
|
|
<span className="text-xs font-black uppercase tracking-[.16em] text-ari-coral">Atención personalizada</span>
|
|
<h2 className="mt-3 text-3xl font-black">Tu cuenta ARI</h2>
|
|
<p className="mt-3 text-sm leading-6 text-slate-600">El inicio de sesión quedará conectado al backend headless. Mientras tanto, puedes gestionar pedidos y disponibilidad directamente por WhatsApp.</p>
|
|
<div className="mt-6 grid gap-3">
|
|
<Button asChild variant="primary" size="lg"><a href={process.env.NEXT_PUBLIC_WHATSAPP_URL ?? "https://wa.link/rvcqqc"} target="_blank" rel="noopener noreferrer"><MessageCircle />Hablar con ARI</a></Button>
|
|
<Button asChild variant="outline"><Link href="/catalog" onClick={() => setAccountOpen(false)}><PackageCheck />Ver mis opciones de compra</Link></Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</header>
|
|
);
|
|
}
|
|
|
|
function useCategoryNavigationState() {
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
const currentCategory = searchParams.get("category") ?? "";
|
|
const isCatalog = pathname === "/catalog";
|
|
|
|
return {
|
|
isAllActive: isCatalog && !currentCategory,
|
|
isActive: (name: string) => isCatalog && currentCategory === name,
|
|
};
|
|
}
|
|
|
|
function pillClass(active: boolean): string {
|
|
return `shrink-0 rounded-full px-4 py-2 text-sm font-extrabold transition ${
|
|
active
|
|
? "bg-white text-ari-marino shadow-lg shadow-black/10"
|
|
: "text-white/90 hover:bg-white/10 hover:text-white"
|
|
}`;
|
|
}
|
|
|
|
function mobileLinkClass(active: boolean): string {
|
|
return `rounded-2xl border px-4 py-3 font-extrabold transition ${
|
|
active
|
|
? "border-ari-coral bg-ari-coral/5 text-ari-coral"
|
|
: "border-slate-200 hover:border-ari-coral/50 hover:bg-ari-coral/5"
|
|
}`;
|
|
}
|
|
|
|
function DesktopCategoryNavigation({ tree, onOpenMenu }: { tree: CategoryNode[]; onOpenMenu: () => void }) {
|
|
const { isActive, isAllActive } = useCategoryNavigationState();
|
|
return <CategoryNavBar tree={tree} onOpenMenu={onOpenMenu} isActive={isActive} isAllActive={isAllActive} />;
|
|
}
|
|
|
|
function DesktopCategoryNavigationFallback({ tree, onOpenMenu }: { tree: CategoryNode[]; onOpenMenu: () => void }) {
|
|
return <CategoryNavBar tree={tree} onOpenMenu={onOpenMenu} isActive={() => false} isAllActive={false} />;
|
|
}
|
|
|
|
function CategoryNavBar({
|
|
tree,
|
|
onOpenMenu,
|
|
isActive,
|
|
isAllActive,
|
|
}: {
|
|
tree: CategoryNode[];
|
|
onOpenMenu: () => void;
|
|
isActive: (name: string) => boolean;
|
|
isAllActive: boolean;
|
|
}) {
|
|
return (
|
|
<nav className="bg-ari-marino text-white">
|
|
<div className="relative mx-auto flex max-w-7xl items-center gap-1 px-4 py-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={onOpenMenu}
|
|
aria-label="Abrir menú principal"
|
|
className="mr-2 shrink-0 text-white hover:bg-white/10"
|
|
>
|
|
<Menu />
|
|
</Button>
|
|
|
|
<DepartmentsMegaMenu tree={tree} isActive={isActive} />
|
|
|
|
<Link href="/catalog" prefetch={false} aria-current={isAllActive ? "page" : undefined} className={pillClass(isAllActive)}>
|
|
Todo
|
|
</Link>
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
function DepartmentsMegaMenu({ tree, isActive }: { tree: CategoryNode[]; isActive: (name: string) => boolean }) {
|
|
const [open, setOpen] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
function handlePointerDown(event: PointerEvent) {
|
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) setOpen(false);
|
|
}
|
|
function handleKeyDown(event: KeyboardEvent) {
|
|
if (event.key === "Escape") setOpen(false);
|
|
}
|
|
document.addEventListener("pointerdown", handlePointerDown);
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => {
|
|
document.removeEventListener("pointerdown", handlePointerDown);
|
|
document.removeEventListener("keydown", handleKeyDown);
|
|
};
|
|
}, [open]);
|
|
|
|
return (
|
|
<div ref={containerRef} className="shrink-0" onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((value) => !value)}
|
|
aria-expanded={open}
|
|
aria-haspopup="true"
|
|
className={`flex items-center gap-1.5 ${pillClass(open)}`}
|
|
>
|
|
<LayoutGrid className="size-4" />
|
|
Departamentos
|
|
<ChevronDown className={`size-3.5 opacity-70 transition-transform ${open ? "rotate-180" : ""}`} />
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="absolute left-0 top-full z-50 mt-1 max-h-[80vh] w-[min(92vw,64rem)] overflow-y-auto rounded-2xl border border-white/10 bg-ari-marino p-6 shadow-2xl">
|
|
<div className="grid grid-cols-2 gap-x-6 gap-y-5 sm:grid-cols-3 lg:grid-cols-6">
|
|
{tree.map((root) => (
|
|
<div key={root.id}>
|
|
<Link
|
|
href={`/catalog?category=${encodeURIComponent(root.name)}`}
|
|
prefetch={false}
|
|
aria-current={isActive(root.name) ? "page" : undefined}
|
|
onClick={() => setOpen(false)}
|
|
className={`block text-sm font-black uppercase tracking-wide transition ${
|
|
isActive(root.name) ? "text-ari-coral-claro" : "text-white hover:text-ari-coral-claro"
|
|
}`}
|
|
>
|
|
{root.name}
|
|
</Link>
|
|
{root.children.length > 0 && (
|
|
<ul className="mt-2.5 grid gap-1.5">
|
|
{root.children.map((child) => (
|
|
<li key={child.id}>
|
|
<Link
|
|
href={`/catalog?category=${encodeURIComponent(child.name)}`}
|
|
prefetch={false}
|
|
aria-current={isActive(child.name) ? "page" : undefined}
|
|
onClick={() => setOpen(false)}
|
|
className={`block text-sm transition ${isActive(child.name) ? "font-bold text-ari-coral-claro" : "text-white/70 hover:text-white"}`}
|
|
>
|
|
{child.name}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MobileCategoryLinks({ tree, onNavigate }: { tree: CategoryNode[]; onNavigate: () => void }) {
|
|
const { isActive, isAllActive } = useCategoryNavigationState();
|
|
|
|
return (
|
|
<>
|
|
<Link
|
|
href="/catalog"
|
|
prefetch={false}
|
|
aria-current={isAllActive ? "page" : undefined}
|
|
onClick={onNavigate}
|
|
className={mobileLinkClass(isAllActive)}
|
|
>
|
|
Todo el catálogo
|
|
</Link>
|
|
|
|
{tree.map((root) => (
|
|
<div key={root.id} className="contents">
|
|
<Link
|
|
href={`/catalog?category=${encodeURIComponent(root.name)}`}
|
|
prefetch={false}
|
|
aria-current={isActive(root.name) ? "page" : undefined}
|
|
onClick={onNavigate}
|
|
className={mobileLinkClass(isActive(root.name))}
|
|
>
|
|
{root.name}
|
|
</Link>
|
|
{root.children.map((child) => (
|
|
<Link
|
|
key={child.id}
|
|
href={`/catalog?category=${encodeURIComponent(child.name)}`}
|
|
prefetch={false}
|
|
aria-current={isActive(child.name) ? "page" : undefined}
|
|
onClick={onNavigate}
|
|
className={`${mobileLinkClass(isActive(child.name))} ml-4 text-sm`}
|
|
>
|
|
{child.name}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function MobileCategoryLinksFallback({ tree, onNavigate }: { tree: CategoryNode[]; onNavigate: () => void }) {
|
|
return (
|
|
<>
|
|
<Link href="/catalog" prefetch={false} onClick={onNavigate} className={mobileLinkClass(false)}>
|
|
Todo el catálogo
|
|
</Link>
|
|
|
|
{tree.map((root) => (
|
|
<div key={root.id} className="contents">
|
|
<Link href={`/catalog?category=${encodeURIComponent(root.name)}`} prefetch={false} onClick={onNavigate} className={mobileLinkClass(false)}>
|
|
{root.name}
|
|
</Link>
|
|
{root.children.map((child) => (
|
|
<Link
|
|
key={child.id}
|
|
href={`/catalog?category=${encodeURIComponent(child.name)}`}
|
|
prefetch={false}
|
|
onClick={onNavigate}
|
|
className={`${mobileLinkClass(false)} ml-4 text-sm`}
|
|
>
|
|
{child.name}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
}
|