From 7a67ba0743350832bf118bbca2ab0394e9284d70 Mon Sep 17 00:00:00 2001 From: Cristian Felipe Cruz Buitron Date: Tue, 25 Aug 2026 14:40:52 -0500 Subject: [PATCH] feat(ecommerce): tiles promocionales intercalados en el grid del catalogo ADR 0005 seccion 3 (patron Walmart), ultimo item de la sub-fase de componentes visuales: "grid de catalogo con tiles promocionales intercalados, no solo tarjetas de producto homogeneas". `category-tiles.tsx` ya era ese mismo primitivo visual (usado en el home) -- se extrajo el tile individual a un componente exportado `PromoTile` (mismo look, sin cambio visual en el home) para reutilizarlo en `catalog-client.tsx`: un tile cada PROMO_TILE_INTERVAL=8 productos, ciclando por las categorias raiz (excluye la raiz que el usuario ya esta viendo via el breadcrumb de #40). Si no hay jerarquia de categorias disponible, la grilla queda igual que antes (solo productos) -- degradacion sin romper nada. Depende de #40 (breadcrumb + categoryNodes en CatalogClient) -- mismo archivo, se apila sobre esa rama en vez de main. --- .../components/catalog/catalog-client.tsx | 43 ++++++++++++- .../components/home/category-tiles.tsx | 62 +++++++++++-------- 2 files changed, 75 insertions(+), 30 deletions(-) diff --git a/workloads/ecommerce/components/catalog/catalog-client.tsx b/workloads/ecommerce/components/catalog/catalog-client.tsx index d5b22dd..5cbf1cd 100644 --- a/workloads/ecommerce/components/catalog/catalog-client.tsx +++ b/workloads/ecommerce/components/catalog/catalog-client.tsx @@ -3,11 +3,19 @@ import Link from "next/link"; import { ChevronRight, SlidersHorizontal, X } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; +import { PromoTile } from "@/components/home/category-tiles"; import { ProductCard } from "@/components/product/product-card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import type { Category, Product } from "@/lib/headless/types"; +// Cada cuantas tarjetas de producto se intercala un tile promocional en +// la grilla (patron Walmart, ADR 0005 seccion 3): grid con tiles +// promocionales intercalados, no solo tarjetas homogeneas. +const PROMO_TILE_INTERVAL = 8; + +type GridItem = { type: "product"; product: Product } | { type: "promo"; category: Category }; + type CatalogClientProps = { products: Product[]; categories?: Category[]; @@ -96,6 +104,11 @@ export function CatalogClient({ // trata como el nombre de categoria exacto de siempre. const allowedCategories = categoryGroups[category] ?? [category]; const breadcrumb = useMemo(() => (category === "Todos" ? [] : buildBreadcrumb(categoryNodes, category)), [categoryNodes, category]); + // No promociona la misma categoria raiz que el usuario ya esta viendo. + const promoRoots = useMemo(() => { + const excludeRootId = breadcrumb[0]?.id; + return categoryNodes.filter((node) => !node.parentId && node.id !== excludeRootId); + }, [categoryNodes, breadcrumb]); const filtered = useMemo( () => @@ -119,6 +132,26 @@ export function CatalogClient({ [age, allowedCategories, brand, category, collection, maxPrice, products, query, sort], ); + // Intercala un tile promocional cada PROMO_TILE_INTERVAL productos, + // ciclando por las categorias raiz -- si no hay jerarquia de + // categorias disponible, la grilla queda igual que antes (solo + // productos). + const gridItems = useMemo(() => { + if (promoRoots.length === 0) return filtered.map((product) => ({ type: "product", product })); + + const items: GridItem[] = []; + let promoIndex = 0; + filtered.forEach((product, index) => { + items.push({ type: "product", product }); + const isLast = index === filtered.length - 1; + if (!isLast && (index + 1) % PROMO_TILE_INTERVAL === 0) { + items.push({ type: "promo", category: promoRoots[promoIndex % promoRoots.length] }); + promoIndex += 1; + } + }); + return items; + }, [filtered, promoRoots]); + // Conteos por valor de filtro (patron Amazon: "Marca (12)"), calculados // sobre los productos que ya pasan el resto de filtros activos -- cada // dimension se cuenta excluyendose a si misma para reflejar cuantos @@ -284,9 +317,13 @@ export function CatalogClient({ {filtered.length ? (
- {filtered.map((product) => ( - - ))} + {gridItems.map((item, index) => + item.type === "product" ? ( + + ) : ( + + ), + )}
) : (
diff --git a/workloads/ecommerce/components/home/category-tiles.tsx b/workloads/ecommerce/components/home/category-tiles.tsx index d9e8fa4..cff1d07 100644 --- a/workloads/ecommerce/components/home/category-tiles.tsx +++ b/workloads/ecommerce/components/home/category-tiles.tsx @@ -2,6 +2,7 @@ import Image from "next/image"; import Link from "next/link"; import { ArrowUpRight } from "lucide-react"; import type { Category } from "@/lib/headless/types"; +import { cn } from "@/lib/utils"; type TileContent = { subtitle: string; image?: string; color: string }; @@ -20,6 +21,37 @@ const tileContent: Record = { const defaultTile: TileContent = { subtitle: "Explora esta categoría", color: "from-slate-400 to-slate-600" }; +// Tile individual reutilizado tanto por la seccion de mundos del home +// como por el grid del catalogo (tiles promocionales intercalados, +// ADR 0005 seccion 3, patron Walmart) -- mismo primitivo visual, no un +// componente nuevo. +export function PromoTile({ category, className = "" }: { category: Category; className?: string }) { + const content = tileContent[category.handle] ?? defaultTile; + + return ( + + {content.image && ( + <> + {category.name} +
+ + )} +
+
+

{category.name}

+

{content.subtitle}

+
+ + + +
+ + ); +} + export function CategoryTiles({ categories }: { categories: Category[] }) { const roots = categories.filter((category) => !category.parentId); @@ -35,33 +67,9 @@ export function CategoryTiles({ categories }: { categories: Category[] }) { Ver todo →
- {roots.map((category) => { - const content = tileContent[category.handle] ?? defaultTile; - - return ( - - {content.image && ( - <> - {category.name} -
- - )} -
-
-

{category.name}

-

{content.subtitle}

-
- - - -
- - ); - })} + {roots.map((category) => ( + + ))}
); -- 2.54.0