setTaxonomy({ ...taxonomy, personaje: value === "Todos" ? undefined : value })}
+ counts={characterCounts}
+ />
+ )}
diff --git a/workloads/ecommerce/components/layout/navbar.tsx b/workloads/ecommerce/components/layout/navbar.tsx
index c77f484..9a67748 100644
--- a/workloads/ecommerce/components/layout/navbar.tsx
+++ b/workloads/ecommerce/components/layout/navbar.tsx
@@ -10,6 +10,7 @@ import { PredictiveSearch } from "@/components/search/predictive-search";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import type { Category, Product } from "@/lib/headless/types";
+import { buildFranchiseTree, taxonomyHref, type FranchiseNode, type TaxonomyKind } from "@/lib/taxonomy";
import { useCartStore } from "@/store/cart-store";
import { useCustomerStore } from "@/store/customer-store";
import { useWishlistStore } from "@/store/wishlist-store";
@@ -43,6 +44,10 @@ export function Navbar({ products, categories }: { products: Product[]; categori
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.
+ // Franquicia > saga > personaje desde los tags (lib/taxonomy.ts). Si el
+ // provider no trae tags (mock/shopify), "Personajes" cae a la lista plana
+ // de colecciones de siempre.
+ const franchises = useMemo(() => buildFranchiseTree(products), [products]);
const collections = useMemo(
() => [...new Set(products.map((product) => product.collection).filter(Boolean))],
[products],
@@ -131,8 +136,8 @@ export function Navbar({ products, categories }: { products: Product[]; categori
- setMenuOpen(true)} onOpenAccount={() => setAccountOpen(true)} />}>
- setMenuOpen(true)} onOpenAccount={() => setAccountOpen(true)} />
+ setMenuOpen(true)} onOpenAccount={() => setAccountOpen(true)} />}>
+ setMenuOpen(true)} onOpenAccount={() => setAccountOpen(true)} />
@@ -148,7 +153,28 @@ export function Navbar({ products, categories }: { products: Product[]; categori
setMenuOpen(false)} />
- {collections.length > 0 && (
+ {franchises.length > 0 && (
+ <>
+ Personajes
+
+ {franchises.map((franchise) => (
+
+
setMenuOpen(false)} className="font-black text-ari-marino hover:text-ari-coral">
+ {franchise.name}
({franchise.count})
+
+
+ {franchise.sagas.map((saga) => (
+ setMenuOpen(false)} className="rounded-full bg-slate-100 px-3 py-1.5 text-sm font-bold hover:bg-ari-coral/10 hover:text-ari-coral">
+ {saga.name}
+
+ ))}
+
+
+ ))}
+
+ >
+ )}
+ {franchises.length === 0 && collections.length > 0 && (
<>
Colecciones
@@ -174,12 +200,16 @@ function useCategoryNavigationState() {
const searchParams = useSearchParams();
const currentCategory = searchParams.get("category") ?? "";
const currentCollection = searchParams.get("collection") ?? "";
+ const currentTaxonomy = { franquicia: searchParams.get("franquicia") ?? "", saga: searchParams.get("saga") ?? "", personaje: searchParams.get("personaje") ?? "" };
+ const anyTaxonomy = Boolean(currentTaxonomy.franquicia || currentTaxonomy.saga || currentTaxonomy.personaje);
const isCatalog = pathname === "/catalog";
return {
- isAllActive: isCatalog && !currentCategory && !currentCollection,
+ isAllActive: isCatalog && !currentCategory && !currentCollection && !anyTaxonomy,
isActive: (name: string) => isCatalog && currentCategory === name,
isCollectionActive: (name: string) => isCatalog && currentCollection === name,
+ isTaxonomyActive: (kind: TaxonomyKind, name: string) => isCatalog && currentTaxonomy[kind] === name,
+ anyTaxonomyActive: isCatalog && anyTaxonomy,
};
}
@@ -189,6 +219,8 @@ const INACTIVE_NAVIGATION: NavigationState = {
isAllActive: false,
isActive: () => false,
isCollectionActive: () => false,
+ isTaxonomyActive: () => false,
+ anyTaxonomyActive: false,
};
function pillClass(active: boolean, dense = false): string {
@@ -207,7 +239,7 @@ function mobileLinkClass(active: boolean): string {
}`;
}
-type NavBarProps = { tree: CategoryNode[]; collections: string[]; onOpenMenu: () => void; onOpenAccount: () => void };
+type NavBarProps = { tree: CategoryNode[]; collections: string[]; franchises: FranchiseNode[]; onOpenMenu: () => void; onOpenAccount: () => void };
function DesktopCategoryNavigation(props: NavBarProps) {
return ;
@@ -223,9 +255,9 @@ function DesktopCategoryNavigationFallback(props: NavBarProps) {
// 1024 y 1280px no entran todas, asi que ahi se mantiene el mega menu de
// "Departamentos". "Personajes" (colecciones) va a la derecha desde lg.
// La hamburguesa solo en mobile: en desktop repetia lo mismo que la barra.
-function CategoryNavBar({ tree, collections, onOpenMenu, onOpenAccount, state }: NavBarProps & { state: NavigationState }) {
- const { isActive, isAllActive, isCollectionActive } = state;
- const anyCollectionActive = collections.some(isCollectionActive);
+function CategoryNavBar({ tree, collections, franchises, onOpenMenu, onOpenAccount, state }: NavBarProps & { state: NavigationState }) {
+ const { isActive, isAllActive, isCollectionActive, isTaxonomyActive, anyTaxonomyActive } = state;
+ const anyCollectionActive = collections.some(isCollectionActive) || anyTaxonomyActive;
return (
- {collections.length > 0 && (
+ {(franchises.length > 0 || collections.length > 0) && (
}>
- {(close) => (
+ {(close) => franchises.length > 0 ? (
+
+ ) : (
{collections.map((collection) => (
boolean;
+ close: () => void;
+}) {
+ const link = (kind: TaxonomyKind, name: string, base: string) => (
+
+ {name}
+
+ );
+ return (
+
+ {franchises.map((franchise) => (
+
+
+ {link("franquicia", franchise.name, "text-sm font-black uppercase tracking-wide text-white hover:text-ari-coral-claro")}
+ {franchise.count}
+
+
+
+ ))}
+
+ );
+}
+
const helpLinkClass = "block w-full rounded-lg px-3 py-2 text-left text-sm text-white/80 transition hover:bg-white/10 hover:text-white";
// Desplegable con la misma "intencion de hover" que DepartmentsMegaMenu
@@ -372,6 +458,7 @@ function NavDropdown({
const [open, setOpen] = useState(false);
const containerRef = useRef
(null);
const hoverOpenedAt = useRef(0);
+ const triggerRef = useRef(null);
const closeTimer = useRef | null>(null);
const cancelClose = () => {
@@ -391,7 +478,13 @@ function NavDropdown({
if (containerRef.current && !containerRef.current.contains(event.target as Node)) setOpen(false);
}
function handleKeyDown(event: KeyboardEvent) {
- if (event.key === "Escape") setOpen(false);
+ if (event.key === "Escape") {
+ setOpen(false);
+ // Devolver el foco al boton que abrio el menu: si no, queda en
+ // y quien navega con teclado pierde su posicion (reportado
+ // por frontend-critic).
+ triggerRef.current?.focus();
+ }
}
document.addEventListener("pointerdown", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
@@ -406,6 +499,7 @@ function NavDropdown({
return (
{ if (event.pointerType !== "mouse") return; cancelClose(); if (!open) hoverOpenedAt.current = Date.now(); setOpen(true); }} onPointerLeave={(event) => { if (event.pointerType === "mouse") scheduleClose(); }}>