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.
This commit is contained in:
1 parent
bc566b1fc0
commit
7a67ba0743
2 files changed
+75
-30
No files matched your search
@@ -3,11 +3,19 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ChevronRight, SlidersHorizontal, X } from "lucide-react";
|
import { ChevronRight, SlidersHorizontal, X } from "lucide-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { PromoTile } from "@/components/home/category-tiles";
|
||||||
import { ProductCard } from "@/components/product/product-card";
|
import { ProductCard } from "@/components/product/product-card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import type { Category, Product } from "@/lib/headless/types";
|
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 = {
|
type CatalogClientProps = {
|
||||||
products: Product[];
|
products: Product[];
|
||||||
categories?: Category[];
|
categories?: Category[];
|
||||||
@@ -96,6 +104,11 @@ export function CatalogClient({
|
|||||||
// trata como el nombre de categoria exacto de siempre.
|
// trata como el nombre de categoria exacto de siempre.
|
||||||
const allowedCategories = categoryGroups[category] ?? [category];
|
const allowedCategories = categoryGroups[category] ?? [category];
|
||||||
const breadcrumb = useMemo(() => (category === "Todos" ? [] : buildBreadcrumb(categoryNodes, category)), [categoryNodes, 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(
|
const filtered = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -119,6 +132,26 @@ export function CatalogClient({
|
|||||||
[age, allowedCategories, brand, category, collection, maxPrice, products, query, sort],
|
[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
|
// Conteos por valor de filtro (patron Amazon: "Marca (12)"), calculados
|
||||||
// sobre los productos que ya pasan el resto de filtros activos -- cada
|
// sobre los productos que ya pasan el resto de filtros activos -- cada
|
||||||
// dimension se cuenta excluyendose a si misma para reflejar cuantos
|
// dimension se cuenta excluyendose a si misma para reflejar cuantos
|
||||||
@@ -284,9 +317,13 @@ export function CatalogClient({
|
|||||||
|
|
||||||
{filtered.length ? (
|
{filtered.length ? (
|
||||||
<div className="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
<div className="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{filtered.map((product) => (
|
{gridItems.map((item, index) =>
|
||||||
<ProductCard key={product.id} product={product} />
|
item.type === "product" ? (
|
||||||
))}
|
<ProductCard key={item.product.id} product={item.product} />
|
||||||
|
) : (
|
||||||
|
<PromoTile key={`promo-${index}`} category={item.category} className="min-h-[22rem]" />
|
||||||
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid min-h-80 place-items-center rounded-3xl border border-dashed border-slate-300 text-center">
|
<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 Link from "next/link";
|
||||||
import { ArrowUpRight } from "lucide-react";
|
import { ArrowUpRight } from "lucide-react";
|
||||||
import type { Category } from "@/lib/headless/types";
|
import type { Category } from "@/lib/headless/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type TileContent = { subtitle: string; image?: string; color: string };
|
type TileContent = { subtitle: string; image?: string; color: string };
|
||||||
|
|
||||||
@@ -20,6 +21,37 @@ const tileContent: Record<string, TileContent> = {
|
|||||||
|
|
||||||
const defaultTile: TileContent = { subtitle: "Explora esta categoría", color: "from-slate-400 to-slate-600" };
|
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 (
|
||||||
|
<Link
|
||||||
|
href={`/catalog?category=${encodeURIComponent(category.name)}`}
|
||||||
|
className={cn(`group relative min-h-64 overflow-hidden rounded-[2rem] bg-gradient-to-br ${content.color} shadow-xl`, className)}
|
||||||
|
>
|
||||||
|
{content.image && (
|
||||||
|
<>
|
||||||
|
<Image src={content.image} alt={category.name} fill sizes="(max-width: 640px) 100vw, (max-width: 1024px) 33vw, 16vw" className="object-cover transition duration-500 group-hover:scale-105" />
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-transparent to-white/5" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="absolute inset-x-0 bottom-0 flex items-end justify-between p-6 text-white">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-black">{category.name}</h3>
|
||||||
|
<p className="mt-1 text-sm font-semibold text-white/80">{content.subtitle}</p>
|
||||||
|
</div>
|
||||||
|
<span className="grid size-11 shrink-0 place-items-center rounded-full bg-white text-slate-950 transition group-hover:rotate-45">
|
||||||
|
<ArrowUpRight />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function CategoryTiles({ categories }: { categories: Category[] }) {
|
export function CategoryTiles({ categories }: { categories: Category[] }) {
|
||||||
const roots = categories.filter((category) => !category.parentId);
|
const roots = categories.filter((category) => !category.parentId);
|
||||||
|
|
||||||
@@ -35,33 +67,9 @@ export function CategoryTiles({ categories }: { categories: Category[] }) {
|
|||||||
<Link href="/catalog" className="hidden font-black text-cyan-700 sm:block">Ver todo →</Link>
|
<Link href="/catalog" className="hidden font-black text-cyan-700 sm:block">Ver todo →</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||||
{roots.map((category) => {
|
{roots.map((category) => (
|
||||||
const content = tileContent[category.handle] ?? defaultTile;
|
<PromoTile key={category.id} category={category} />
|
||||||
|
))}
|
||||||
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`}
|
|
||||||
>
|
|
||||||
{content.image && (
|
|
||||||
<>
|
|
||||||
<Image src={content.image} alt={category.name} fill sizes="(max-width: 640px) 100vw, (max-width: 1024px) 33vw, 16vw" className="object-cover transition duration-500 group-hover:scale-105" />
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-transparent to-white/5" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="absolute inset-x-0 bottom-0 flex items-end justify-between p-6 text-white">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-black">{category.name}</h3>
|
|
||||||
<p className="mt-1 text-sm font-semibold text-white/80">{content.subtitle}</p>
|
|
||||||
</div>
|
|
||||||
<span className="grid size-11 shrink-0 place-items-center rounded-full bg-white text-slate-950 transition group-hover:rotate-45">
|
|
||||||
<ArrowUpRight />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user