fix(ecommerce): tiles promocionales intercalados en el grid del catalogo #41
No files matched your search
@@ -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<GridItem[]>(() => {
|
||||
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 ? (
|
||||
<div className="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{filtered.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
{gridItems.map((item, index) =>
|
||||
item.type === "product" ? (
|
||||
<ProductCard key={item.product.id} product={item.product} />
|
||||
) : (
|
||||
<PromoTile key={`promo-${index}`} category={item.category} className="min-h-[22rem]" />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid min-h-80 place-items-center rounded-3xl border border-dashed border-slate-300 text-center">
|
||||
|
||||
@@ -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,29 +21,17 @@ const tileContent: Record<string, TileContent> = {
|
||||
|
||||
const defaultTile: TileContent = { subtitle: "Explora esta categoría", color: "from-slate-400 to-slate-600" };
|
||||
|
||||
export function CategoryTiles({ categories }: { categories: Category[] }) {
|
||||
const roots = categories.filter((category) => !category.parentId);
|
||||
|
||||
if (roots.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-7xl px-4 py-14">
|
||||
<div className="mb-8 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-xs font-black uppercase tracking-[.18em] text-fuchsia-600">Explora por mundos</span>
|
||||
<h2 className="mt-2 text-4xl font-black tracking-tight sm:text-5xl">Encuentra tu próximo tesoro</h2>
|
||||
</div>
|
||||
<Link href="/catalog" className="hidden font-black text-cyan-700 sm:block">Ver todo →</Link>
|
||||
</div>
|
||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
{roots.map((category) => {
|
||||
// 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 (
|
||||
<Link
|
||||
key={category.id}
|
||||
href={`/catalog?category=${encodeURIComponent(category.name)}`}
|
||||
className={`group relative min-h-64 overflow-hidden rounded-[2rem] bg-gradient-to-br ${content.color} shadow-xl`}
|
||||
className={cn(`group relative min-h-64 overflow-hidden rounded-[2rem] bg-gradient-to-br ${content.color} shadow-xl`, className)}
|
||||
>
|
||||
{content.image && (
|
||||
<>
|
||||
@@ -61,7 +50,26 @@ export function CategoryTiles({ categories }: { categories: Category[] }) {
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
}
|
||||
|
||||
export function CategoryTiles({ categories }: { categories: Category[] }) {
|
||||
const roots = categories.filter((category) => !category.parentId);
|
||||
|
||||
if (roots.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-7xl px-4 py-14">
|
||||
<div className="mb-8 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-xs font-black uppercase tracking-[.18em] text-fuchsia-600">Explora por mundos</span>
|
||||
<h2 className="mt-2 text-4xl font-black tracking-tight sm:text-5xl">Encuentra tu próximo tesoro</h2>
|
||||
</div>
|
||||
<Link href="/catalog" className="hidden font-black text-cyan-700 sm:block">Ver todo →</Link>
|
||||
</div>
|
||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
{roots.map((category) => (
|
||||
<PromoTile key={category.id} category={category} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user