228 lines
7.8 KiB
TypeScript
228 lines
7.8 KiB
TypeScript
"use client";
|
|
|
|
import { SlidersHorizontal, X } from "lucide-react";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { ProductCard } from "@/components/product/product-card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import type { Product } from "@/lib/headless/types";
|
|
|
|
type CatalogClientProps = {
|
|
products: Product[];
|
|
initialCategory?: string;
|
|
initialCollection?: string;
|
|
initialQuery?: string;
|
|
};
|
|
|
|
export function CatalogClient({
|
|
products,
|
|
initialCategory = "Todos",
|
|
initialCollection = "",
|
|
initialQuery = "",
|
|
}: CatalogClientProps) {
|
|
const [query, setQuery] = useState(initialQuery);
|
|
const [category, setCategory] = useState(initialCategory);
|
|
const [collection, setCollection] = useState(initialCollection);
|
|
const [brand, setBrand] = useState("Todas");
|
|
const [age, setAge] = useState("Todas");
|
|
const [sort, setSort] = useState("featured");
|
|
|
|
const categories = ["Todos", ...new Set(products.map((product) => product.category))];
|
|
const brands = ["Todas", ...new Set(products.map((product) => product.brand))];
|
|
const ages = ["Todas", ...new Set(products.map((product) => product.ageRange))];
|
|
const pricedProducts = products.filter((product) => product.price !== null);
|
|
const maxCatalogPrice = pricedProducts.length
|
|
? Math.max(...pricedProducts.map((product) => product.price ?? 0))
|
|
: 0;
|
|
const [maxPrice, setMaxPrice] = useState(maxCatalogPrice);
|
|
|
|
// Next.js conserva la instancia del componente cuando solo cambia el query string.
|
|
// Sin esta sincronización, el catálogo anterior permanece visible hasta presionar F5.
|
|
useEffect(() => {
|
|
setQuery(initialQuery);
|
|
setCategory(initialCategory || "Todos");
|
|
setCollection(initialCollection || "");
|
|
setBrand("Todas");
|
|
setAge("Todas");
|
|
setSort("featured");
|
|
setMaxPrice(maxCatalogPrice);
|
|
}, [initialCategory, initialCollection, initialQuery, maxCatalogPrice]);
|
|
|
|
const filtered = useMemo(
|
|
() =>
|
|
products
|
|
.filter((product) => {
|
|
const search = [
|
|
product.name,
|
|
product.category,
|
|
product.collection,
|
|
product.brand,
|
|
]
|
|
.join(" ")
|
|
.toLowerCase();
|
|
|
|
return (
|
|
(!query || search.includes(query.toLowerCase())) &&
|
|
(category === "Todos" || product.category === category) &&
|
|
(!collection || product.collection === collection) &&
|
|
(brand === "Todas" || product.brand === brand) &&
|
|
(age === "Todas" || product.ageRange === age) &&
|
|
(product.price === null || maxPrice === 0 || product.price <= maxPrice)
|
|
);
|
|
})
|
|
.sort((a, b) =>
|
|
sort === "name"
|
|
? a.name.localeCompare(b.name, "es")
|
|
: sort === "new"
|
|
? Number(b.isNew) - Number(a.isNew)
|
|
: Number(b.featured) - Number(a.featured),
|
|
),
|
|
[age, brand, category, collection, maxPrice, products, query, sort],
|
|
);
|
|
|
|
const reset = () => {
|
|
setQuery("");
|
|
setCategory("Todos");
|
|
setCollection("");
|
|
setBrand("Todas");
|
|
setAge("Todas");
|
|
setMaxPrice(maxCatalogPrice);
|
|
setSort("featured");
|
|
};
|
|
|
|
return (
|
|
<div className="mx-auto max-w-7xl px-4 py-10">
|
|
<div className="mb-8">
|
|
<span className="text-xs font-black uppercase tracking-[.18em] text-fuchsia-600">
|
|
Catálogo completo
|
|
</span>
|
|
<h1 className="mt-2 text-5xl font-black tracking-tight">Todos los tesoros</h1>
|
|
<p className="mt-3 text-slate-500">
|
|
Filtra por categoría, marca o edad sin recargar la página.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid gap-8 lg:grid-cols-[260px_1fr]">
|
|
<aside className="h-fit space-y-6 rounded-3xl border border-slate-100 bg-white p-6 shadow-lg shadow-slate-900/5 lg:sticky lg:top-44">
|
|
<div className="flex items-center justify-between">
|
|
<strong className="flex items-center gap-2 text-lg font-black">
|
|
<SlidersHorizontal className="size-5 text-cyan-600" />
|
|
Filtros
|
|
</strong>
|
|
<button onClick={reset} className="text-xs font-black text-fuchsia-600">
|
|
<X className="mr-1 inline size-3" />
|
|
Limpiar
|
|
</button>
|
|
</div>
|
|
|
|
<Input
|
|
value={query}
|
|
onChange={(event) => setQuery(event.target.value)}
|
|
placeholder="Buscar…"
|
|
/>
|
|
|
|
<Filter
|
|
title="Categoría"
|
|
values={categories}
|
|
selected={category}
|
|
onChange={setCategory}
|
|
/>
|
|
<Filter title="Marca" values={brands} selected={brand} onChange={setBrand} />
|
|
<Filter title="Edad" values={ages} selected={age} onChange={setAge} />
|
|
|
|
<fieldset>
|
|
<legend className="mb-3 text-sm font-black">Precio máximo</legend>
|
|
{maxCatalogPrice > 0 ? (
|
|
<>
|
|
<input
|
|
aria-label="Precio máximo"
|
|
type="range"
|
|
min={0}
|
|
max={maxCatalogPrice}
|
|
step={5000}
|
|
value={maxPrice}
|
|
onChange={(event) => setMaxPrice(Number(event.target.value))}
|
|
className="w-full accent-fuchsia-500"
|
|
/>
|
|
<p className="mt-2 text-xs font-bold text-slate-500">
|
|
Hasta ${maxPrice.toLocaleString("es-CO")}
|
|
</p>
|
|
</>
|
|
) : (
|
|
<p className="rounded-2xl bg-yellow-50 p-3 text-xs font-bold leading-5 text-yellow-800">
|
|
El filtro se activa automáticamente cuando el backend entregue precios.
|
|
</p>
|
|
)}
|
|
</fieldset>
|
|
</aside>
|
|
|
|
<section>
|
|
<div className="mb-5 flex items-center justify-between gap-4">
|
|
<p className="text-sm font-bold text-slate-500">{filtered.length} productos</p>
|
|
<select
|
|
value={sort}
|
|
onChange={(event) => setSort(event.target.value)}
|
|
className="h-11 rounded-full border border-slate-200 bg-white px-4 text-sm font-bold"
|
|
>
|
|
<option value="featured">Destacados</option>
|
|
<option value="new">Novedades</option>
|
|
<option value="name">Nombre</option>
|
|
</select>
|
|
</div>
|
|
|
|
{filtered.length ? (
|
|
<div className="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
|
{filtered.map((product) => (
|
|
<ProductCard key={product.id} product={product} />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="grid min-h-80 place-items-center rounded-3xl border border-dashed border-slate-300 text-center">
|
|
<div>
|
|
<p className="text-xl font-black">No encontramos productos</p>
|
|
<Button variant="magenta" className="mt-4" onClick={reset}>
|
|
Restablecer filtros
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Filter({
|
|
title,
|
|
values,
|
|
selected,
|
|
onChange,
|
|
}: {
|
|
title: string;
|
|
values: string[];
|
|
selected: string;
|
|
onChange: (value: string) => void;
|
|
}) {
|
|
return (
|
|
<fieldset>
|
|
<legend className="mb-3 text-sm font-black">{title}</legend>
|
|
<div className="flex flex-wrap gap-2">
|
|
{values.map((value) => (
|
|
<button
|
|
type="button"
|
|
key={value}
|
|
onClick={() => onChange(value)}
|
|
className={`rounded-full px-3 py-2 text-xs font-bold transition ${
|
|
selected === value
|
|
? "bg-slate-950 text-white"
|
|
: "bg-slate-100 text-slate-600 hover:bg-cyan-100"
|
|
}`}
|
|
>
|
|
{value}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</fieldset>
|
|
);
|
|
}
|