fix(ecommerce): nav y tiles data-driven contra la jerarquia real de categorias
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (pull_request) Successful in 35s
Build and Push Frontend / Construir y Subir Imagen (pull_request) Skipped

Sub-fase 2 de Etapa 2 (ADR 0005 seccion 6). Navbar y CategoryTiles usaban
arrays hardcodeados (Figuras/Peluches/Mochilas/Marvel) que ya no coinciden
con los nombres reales de categoria en Medusa desde que se elimino
legacyProductMeta (PR #29) -- todos esos links de navegacion quedaron
rotos silenciosamente (0 productos al filtrar).

- lib/headless: nuevo tipo Category + metodo getCategories() en
  CatalogProvider (medusa via /store/product-categories, mock deriva de
  su catalogo plano, shopify sin implementar como el resto de sus metodos).
- Navbar: arbol real de categorias (6 raices/21 nodos), dropdown de
  hijos por raiz en desktop, lista anidada en el sheet movil. Colecciones
  del sheet ahora se derivan de los productos reales en vez de un array
  hardcodeado con 2 nombres obsoletos (Marvel/Frozen vs. los reales
  Universo Marvel/Reino de Frozen).
- CategoryTiles: tiles de home por categoria raiz real; imagen solo donde
  ya existe asset, gradiente honesto donde no (no hay fotografia para las
  6 raices todavia, deuda explicita del ADR).
- catalog-client/catalog page: cuando el query param `category` es una
  raiz con hijos, se expande a los nombres reales de sus hijos para que
  el filtro (igualdad exacta de producto.category) siga funcionando --
  sin esto, tiles/nav apuntando a una raiz mostrarian 0 productos porque
  ningun producto tiene la raiz asignada directamente.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013dunTcReWMKhWgCPkvAVHD
This commit is contained in:
devopsandClaude Sonnet 5 committed 2026-08-24 21:21:18 -05:00
1 parent 0508c6328e
commit 8742ce714c
11 files changed
+323 -130

No files matched your search

@@ -22,3 +22,7 @@ export async function getProductBySlug(slug: string) {
export async function searchProducts(query: string, limit?: number) {
return provider().searchProducts(query, limit);
}
export async function getCategories() {
return provider().getCategories();
}
@@ -1,5 +1,6 @@
import type {
CatalogProvider,
Category,
Product,
ProductFilters,
ProductVariant,
@@ -66,6 +67,18 @@ type MedusaProductsResponse = {
offset?: number;
};
type MedusaProductCategory = {
id: string;
name?: string | null;
handle?: string | null;
parent_category_id?: string | null;
rank?: number | null;
};
type MedusaCategoriesResponse = {
product_categories: MedusaProductCategory[];
};
function backendUrl(): string {
const value = process.env.MEDUSA_BACKEND_URL;
if (!value) {
@@ -339,11 +352,40 @@ async function fetchProducts(
.filter((product) => matchesFilters(product, filters));
}
async function fetchCategories(): Promise<Category[]> {
const params = new URLSearchParams({
limit: "100",
fields: "id,name,handle,rank,parent_category_id",
});
const response = await medusaFetch<MedusaCategoriesResponse>(
"/store/product-categories",
params,
);
// Ordenar por rank preserva el orden de la taxonomia tal como se creo
// (ver ADR 0005) -- los hijos de cada padre quedan en el mismo orden
// relativo porque el arbol se arma filtrando este arreglo ya ordenado.
return response.product_categories
.slice()
.sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0))
.map((category) => ({
id: category.id,
name: category.name || "",
handle: category.handle || category.id,
parentId: category.parent_category_id ?? null,
}));
}
export const medusaCatalogProvider: CatalogProvider = {
async getProducts(filters) {
return fetchProducts(filters);
},
async getCategories() {
return fetchCategories();
},
async getProductBySlug(slug) {
const params = new URLSearchParams({
handle: slug,
@@ -1,5 +1,5 @@
import catalog from "@/data/products.json";
import type { CatalogProvider, Product, ProductFilters } from "@/lib/headless/types";
import type { CatalogProvider, Category, Product, ProductFilters } from "@/lib/headless/types";
const products = catalog satisfies Product[];
@@ -39,4 +39,16 @@ export const mockCatalogProvider: CatalogProvider = {
async searchProducts(query, limit = 6) {
return products.filter((product) => matches(product, { query })).slice(0, limit);
},
async getCategories(): Promise<Category[]> {
// El catalogo mock no tiene jerarquia real (a diferencia de Medusa) --
// se devuelve una lista plana de las categorias que ya usan los
// productos mock, todas sin padre.
const names = [...new Set(products.map((product) => product.category))].filter(Boolean);
return names.map((name) => ({
id: name,
name,
handle: normalize(name).replace(/\s+/g, "-"),
parentId: null,
}));
},
};
@@ -42,4 +42,7 @@ export const shopifyCatalogProvider: CatalogProvider = {
async searchProducts(): Promise<Product[]> {
return notImplemented();
},
async getCategories() {
return notImplemented();
},
};
@@ -39,8 +39,16 @@ export interface ProductFilters {
maxPrice?: number;
}
export interface Category {
id: string;
name: string;
handle: string;
parentId: string | null;
}
export interface CatalogProvider {
getProducts(filters?: ProductFilters): Promise<Product[]>;
getProductBySlug(slug: string): Promise<Product | null>;
searchProducts(query: string, limit?: number): Promise<Product[]>;
getCategories(): Promise<Category[]>;
}