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
69 lines
2.9 KiB
TypeScript
69 lines
2.9 KiB
TypeScript
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)}`;
|
|
}
|