feat(storefront): franquicias, sagas y personajes (Disney > Toy Story > Rex)
Taxonomía desde tags de Medusa con prefijo (franquicia:/saga:/personaje:),
muchos-a-muchos (ej. "Minnie y Mickey" tiene dos personajes). Pixar va
dentro de Disney.
- lib/taxonomy.ts: helpers + árbol franquicia > saga > personaje.
- Navbar: "Personajes" pasa a mega panel por franquicia (con totales,
sagas y personajes); el menú lateral mobile muestra franquicias y sagas.
Fallback a colecciones si el provider no trae tags.
- Catálogo: filtros "Franquicia" y "Personaje" (acotado a la franquicia),
chip removible de "Saga", ?franquicia/?saga/?personaje en la URL, título
según la taxonomía de la URL, búsqueda normalizada ("spiderman" encuentra
"Spider-Man"). Conteos client-side (no son facetas de Meilisearch).
- allowedCategories memoizado (elimina warnings exhaustive-deps).
Revisado por frontend-critic (contraste de personajes /80, H1 solo desde la
URL, Escape devuelve el foco al trigger) + Playwright: check-taxonomy
17/17 + extras, shot-navbar sin regresiones.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018r19s4H8MBGegqToqw5guP
This commit is contained in:
1 parent
271ea30f44
commit
472c12ec0a
4 files changed
+290
-33
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import type { Product } from "@/lib/headless/types";
|
||||
|
||||
// Taxonomia Franquicia > Saga > Personaje (ej. Disney > Toy Story > Rex,
|
||||
// Marvel > Spider-Man > Spider-Man). Vive en los tags de Medusa con
|
||||
// prefijo ("franquicia:Disney", "saga:Toy Story", "personaje:Rex") porque
|
||||
// la coleccion de Medusa es una sola por producto y mezclaba niveles
|
||||
// ("Disney" y "Toy Story" como pares). Los tags son muchos-a-muchos: un
|
||||
// producto puede tener varios personajes ("Minnie y Mickey").
|
||||
export type TaxonomyKind = "franquicia" | "saga" | "personaje";
|
||||
|
||||
export const TAXONOMY_KINDS: TaxonomyKind[] = ["franquicia", "saga", "personaje"];
|
||||
|
||||
export function taxonomyValues(product: Product, kind: TaxonomyKind): string[] {
|
||||
const prefix = `${kind}:`;
|
||||
return product.tags.filter((tag) => tag.startsWith(prefix)).map((tag) => tag.slice(prefix.length));
|
||||
}
|
||||
|
||||
export type TaxonomyFilter = Partial<Record<TaxonomyKind, string>>;
|
||||
|
||||
export function matchesTaxonomy(product: Product, filter: TaxonomyFilter): boolean {
|
||||
return TAXONOMY_KINDS.every((kind) => {
|
||||
const value = filter[kind];
|
||||
return !value || taxonomyValues(product, kind).includes(value);
|
||||
});
|
||||
}
|
||||
|
||||
export type SagaNode = { name: string; count: number; characters: { name: string; count: number }[] };
|
||||
export type FranchiseNode = { name: string; count: number; sagas: SagaNode[] };
|
||||
|
||||
function byCountThenName<T extends { name: string; count: number }>(a: T, b: T): number {
|
||||
return b.count - a.count || a.name.localeCompare(b.name, "es");
|
||||
}
|
||||
|
||||
export function buildFranchiseTree(products: Product[]): FranchiseNode[] {
|
||||
const franchises = new Map<string, { count: number; sagas: Map<string, { count: number; characters: Map<string, number> }> }>();
|
||||
for (const product of products) {
|
||||
const sagas = taxonomyValues(product, "saga");
|
||||
const characters = taxonomyValues(product, "personaje");
|
||||
for (const franchise of taxonomyValues(product, "franquicia")) {
|
||||
const node = franchises.get(franchise) ?? { count: 0, sagas: new Map() };
|
||||
node.count += 1;
|
||||
for (const saga of sagas) {
|
||||
const sagaNode = node.sagas.get(saga) ?? { count: 0, characters: new Map() };
|
||||
sagaNode.count += 1;
|
||||
for (const character of characters) sagaNode.characters.set(character, (sagaNode.characters.get(character) ?? 0) + 1);
|
||||
node.sagas.set(saga, sagaNode);
|
||||
}
|
||||
franchises.set(franchise, node);
|
||||
}
|
||||
}
|
||||
return [...franchises.entries()]
|
||||
.map(([name, node]) => ({
|
||||
name,
|
||||
count: node.count,
|
||||
sagas: [...node.sagas.entries()]
|
||||
.map(([sagaName, sagaNode]) => ({
|
||||
name: sagaName,
|
||||
count: sagaNode.count,
|
||||
characters: [...sagaNode.characters.entries()].map(([characterName, count]) => ({ name: characterName, count })).sort(byCountThenName),
|
||||
}))
|
||||
.sort(byCountThenName),
|
||||
}))
|
||||
.sort(byCountThenName);
|
||||
}
|
||||
|
||||
export function taxonomyHref(kind: TaxonomyKind, value: string): string {
|
||||
return `/catalog?${kind}=${encodeURIComponent(value)}`;
|
||||
}
|
||||
Reference in new issue
Block a user