fix(frontend): synchronize catalog navigation with URL
Build and Push Frontend / Construir y Subir Imagen (push) Failing after 29s
Build and Push Frontend / Construir y Subir Imagen (push) Failing after 29s
This commit is contained in:
@@ -2,7 +2,10 @@ import type { Metadata } from "next";
|
|||||||
import { CatalogClient } from "@/components/catalog/catalog-client";
|
import { CatalogClient } from "@/components/catalog/catalog-client";
|
||||||
import { getProducts } from "@/lib/headless/client";
|
import { getProducts } from "@/lib/headless/client";
|
||||||
|
|
||||||
export const metadata: Metadata = { title: "Catálogo", description: "Explora todos los productos de Ari Shopping." };
|
export const metadata: Metadata = {
|
||||||
|
title: "Catálogo",
|
||||||
|
description: "Explora todos los productos de Ari Shopping.",
|
||||||
|
};
|
||||||
|
|
||||||
type CatalogPageProps = {
|
type CatalogPageProps = {
|
||||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||||
@@ -15,12 +18,19 @@ function single(value: string | string[] | undefined): string {
|
|||||||
export default async function CatalogPage({ searchParams }: CatalogPageProps) {
|
export default async function CatalogPage({ searchParams }: CatalogPageProps) {
|
||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
const products = await getProducts();
|
const products = await getProducts();
|
||||||
|
|
||||||
|
const category = single(params.category) || "Todos";
|
||||||
|
const collection = single(params.collection);
|
||||||
|
const query = single(params.q);
|
||||||
|
const section = single(params.section);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CatalogClient
|
<CatalogClient
|
||||||
|
key={`${category}:${collection}:${query}:${section}`}
|
||||||
products={products}
|
products={products}
|
||||||
initialCategory={single(params.category) || "Todos"}
|
initialCategory={category}
|
||||||
initialCollection={single(params.collection)}
|
initialCollection={collection}
|
||||||
initialQuery={single(params.q)}
|
initialQuery={query}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,227 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { SlidersHorizontal, X } from "lucide-react";
|
import { SlidersHorizontal, X } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
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 { Product } from "@/lib/headless/types";
|
import type { Product } from "@/lib/headless/types";
|
||||||
|
|
||||||
export function CatalogClient({ products, initialCategory = "Todos", initialCollection = "", initialQuery = "" }: { products: Product[]; initialCategory?: string; initialCollection?: string; initialQuery?: string }) {
|
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 [query, setQuery] = useState(initialQuery);
|
||||||
const [category, setCategory] = useState(initialCategory);
|
const [category, setCategory] = useState(initialCategory);
|
||||||
const [collection, setCollection] = useState(initialCollection);
|
const [collection, setCollection] = useState(initialCollection);
|
||||||
const [brand, setBrand] = useState("Todas");
|
const [brand, setBrand] = useState("Todas");
|
||||||
const [age, setAge] = useState("Todas");
|
const [age, setAge] = useState("Todas");
|
||||||
const [sort, setSort] = useState("featured");
|
const [sort, setSort] = useState("featured");
|
||||||
|
|
||||||
const categories = ["Todos", ...new Set(products.map((product) => product.category))];
|
const categories = ["Todos", ...new Set(products.map((product) => product.category))];
|
||||||
const brands = ["Todas", ...new Set(products.map((product) => product.brand))];
|
const brands = ["Todas", ...new Set(products.map((product) => product.brand))];
|
||||||
const ages = ["Todas", ...new Set(products.map((product) => product.ageRange))];
|
const ages = ["Todas", ...new Set(products.map((product) => product.ageRange))];
|
||||||
const pricedProducts = products.filter((product) => product.price !== null);
|
const pricedProducts = products.filter((product) => product.price !== null);
|
||||||
const maxCatalogPrice = pricedProducts.length ? Math.max(...pricedProducts.map((product) => product.price ?? 0)) : 0;
|
const maxCatalogPrice = pricedProducts.length
|
||||||
|
? Math.max(...pricedProducts.map((product) => product.price ?? 0))
|
||||||
|
: 0;
|
||||||
const [maxPrice, setMaxPrice] = useState(maxCatalogPrice);
|
const [maxPrice, setMaxPrice] = useState(maxCatalogPrice);
|
||||||
|
|
||||||
const filtered = useMemo(() => products.filter((product) => {
|
// Next.js conserva la instancia del componente cuando solo cambia el query string.
|
||||||
const search = [product.name, product.category, product.collection, product.brand].join(" ").toLowerCase();
|
// Sin esta sincronización, el catálogo anterior permanece visible hasta presionar F5.
|
||||||
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);
|
useEffect(() => {
|
||||||
}).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]);
|
setQuery(initialQuery);
|
||||||
|
setCategory(initialCategory || "Todos");
|
||||||
|
setCollection(initialCollection || "");
|
||||||
|
setBrand("Todas");
|
||||||
|
setAge("Todas");
|
||||||
|
setSort("featured");
|
||||||
|
setMaxPrice(maxCatalogPrice);
|
||||||
|
}, [initialCategory, initialCollection, initialQuery, maxCatalogPrice]);
|
||||||
|
|
||||||
const reset = () => { setQuery(""); setCategory("Todos"); setCollection(""); setBrand("Todas"); setAge("Todas"); setMaxPrice(maxCatalogPrice); setSort("featured"); };
|
const filtered = useMemo(
|
||||||
|
() =>
|
||||||
|
products
|
||||||
|
.filter((product) => {
|
||||||
|
const search = [
|
||||||
|
product.name,
|
||||||
|
product.category,
|
||||||
|
product.collection,
|
||||||
|
product.brand,
|
||||||
|
]
|
||||||
|
.join(" ")
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
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} />
|
return (
|
||||||
<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>
|
(!query || search.includes(query.toLowerCase())) &&
|
||||||
</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>;
|
(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 }) {
|
function Filter({
|
||||||
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>;
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { usePathname, useSearchParams } from "next/navigation";
|
||||||
import { Heart, Menu, MessageCircle, PackageCheck, ShoppingCart, UserRound } from "lucide-react";
|
import { Heart, Menu, MessageCircle, PackageCheck, ShoppingCart, UserRound } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { Suspense, useState } from "react";
|
||||||
import { PredictiveSearch } from "@/components/search/predictive-search";
|
import { PredictiveSearch } from "@/components/search/predictive-search";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
@@ -12,13 +13,27 @@ import type { Product } from "@/lib/headless/types";
|
|||||||
import { useCartStore } from "@/store/cart-store";
|
import { useCartStore } from "@/store/cart-store";
|
||||||
import { useWishlistStore } from "@/store/wishlist-store";
|
import { useWishlistStore } from "@/store/wishlist-store";
|
||||||
|
|
||||||
const categories = [
|
type CategoryLink = {
|
||||||
{ label: "Juguetes para bebés", href: "/catalog?category=Figuras" },
|
label: string;
|
||||||
{ label: "Educativos", href: "/catalog?category=Figuras" },
|
href: string;
|
||||||
{ label: "Muñecas y peluches", href: "/catalog?category=Peluches" },
|
category?: string;
|
||||||
{ label: "Acción y aventura", href: "/catalog?collection=Marvel" },
|
collection?: string;
|
||||||
{ label: "Mochilas", href: "/catalog?category=Mochilas" },
|
section?: string;
|
||||||
{ label: "Variedades", href: "/catalog" },
|
root?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const categories: CategoryLink[] = [
|
||||||
|
{ label: "Juguetes para bebés", href: "/catalog?category=Figuras", category: "Figuras" },
|
||||||
|
{
|
||||||
|
label: "Educativos",
|
||||||
|
href: "/catalog?category=Figuras§ion=educativos",
|
||||||
|
category: "Figuras",
|
||||||
|
section: "educativos",
|
||||||
|
},
|
||||||
|
{ label: "Muñecas y peluches", href: "/catalog?category=Peluches", category: "Peluches" },
|
||||||
|
{ label: "Acción y aventura", href: "/catalog?collection=Marvel", collection: "Marvel" },
|
||||||
|
{ label: "Mochilas", href: "/catalog?category=Mochilas", category: "Mochilas" },
|
||||||
|
{ label: "Variedades", href: "/catalog", root: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const collections = ["Toy Story", "Marvel", "Intensamente", "Lilo & Stitch", "Frozen"];
|
const collections = ["Toy Story", "Marvel", "Intensamente", "Lilo & Stitch", "Frozen"];
|
||||||
@@ -62,16 +77,9 @@ export function Navbar({ products }: { products: Product[] }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="border-b-4 border-fuchsia-500 bg-slate-950 text-white">
|
<Suspense fallback={<DesktopCategoryNavigationFallback onOpenMenu={() => setMenuOpen(true)} />}>
|
||||||
<div className="mx-auto flex max-w-7xl items-center gap-1 overflow-x-auto px-4 py-2 [scrollbar-width:none]">
|
<DesktopCategoryNavigation onOpenMenu={() => setMenuOpen(true)} />
|
||||||
<Button variant="ghost" size="icon" onClick={() => setMenuOpen(true)} aria-label="Abrir menú principal" className="mr-3 shrink-0 text-white hover:bg-white/10"><Menu /></Button>
|
</Suspense>
|
||||||
{categories.map((category) => (
|
|
||||||
<Link key={category.label} href={category.href} className="shrink-0 rounded-full px-4 py-2 text-sm font-extrabold text-white/90 transition hover:bg-gradient-to-r hover:from-cyan-500 hover:to-violet-500 hover:text-white">
|
|
||||||
{category.label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
|
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
|
||||||
<SheetContent side="left" className="max-w-sm">
|
<SheetContent side="left" className="max-w-sm">
|
||||||
@@ -82,7 +90,9 @@ export function Navbar({ products }: { products: Product[] }) {
|
|||||||
<div className="flex-1 overflow-y-auto p-6">
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
<p className="mb-3 text-xs font-black uppercase tracking-[.16em] text-fuchsia-600">Categorías</p>
|
<p className="mb-3 text-xs font-black uppercase tracking-[.16em] text-fuchsia-600">Categorías</p>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
{categories.map((category) => <Link key={category.label} href={category.href} onClick={() => setMenuOpen(false)} className="rounded-2xl border border-slate-200 px-4 py-3 font-extrabold hover:border-cyan-400 hover:bg-cyan-50">{category.label}</Link>)}
|
<Suspense fallback={<MobileCategoryLinksFallback onNavigate={() => setMenuOpen(false)} />}>
|
||||||
|
<MobileCategoryLinks onNavigate={() => setMenuOpen(false)} />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-3 mt-8 text-xs font-black uppercase tracking-[.16em] text-violet-600">Colecciones</p>
|
<p className="mb-3 mt-8 text-xs font-black uppercase tracking-[.16em] text-violet-600">Colecciones</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -112,3 +122,145 @@ export function Navbar({ products }: { products: Product[] }) {
|
|||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useCategoryNavigationState() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const currentCategory = searchParams.get("category") ?? "";
|
||||||
|
const currentCollection = searchParams.get("collection") ?? "";
|
||||||
|
const currentSection = searchParams.get("section") ?? "";
|
||||||
|
const isCatalog = pathname === "/catalog";
|
||||||
|
|
||||||
|
const isCategoryActive = (item: CategoryLink) => {
|
||||||
|
if (!isCatalog) return false;
|
||||||
|
|
||||||
|
if (item.root) {
|
||||||
|
return !currentCategory && !currentCollection && !currentSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
(item.category ?? "") === currentCategory &&
|
||||||
|
(item.collection ?? "") === currentCollection &&
|
||||||
|
(item.section ?? "") === currentSection
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { isCategoryActive };
|
||||||
|
}
|
||||||
|
|
||||||
|
function DesktopCategoryNavigation({ onOpenMenu }: { onOpenMenu: () => void }) {
|
||||||
|
const { isCategoryActive } = useCategoryNavigationState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="border-b-4 border-fuchsia-500 bg-slate-950 text-white">
|
||||||
|
<div className="mx-auto flex max-w-7xl items-center gap-1 overflow-x-auto px-4 py-2 [scrollbar-width:none]">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onOpenMenu}
|
||||||
|
aria-label="Abrir menú principal"
|
||||||
|
className="mr-3 shrink-0 text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<Menu />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{categories.map((category) => {
|
||||||
|
const active = isCategoryActive(category);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={category.label}
|
||||||
|
href={category.href}
|
||||||
|
prefetch={false}
|
||||||
|
aria-current={active ? "page" : undefined}
|
||||||
|
className={`shrink-0 rounded-full px-4 py-2 text-sm font-extrabold transition ${
|
||||||
|
active
|
||||||
|
? "bg-gradient-to-r from-cyan-500 to-violet-500 text-white shadow-lg shadow-violet-950/30"
|
||||||
|
: "text-white/90 hover:bg-gradient-to-r hover:from-cyan-500 hover:to-violet-500 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category.label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DesktopCategoryNavigationFallback({ onOpenMenu }: { onOpenMenu: () => void }) {
|
||||||
|
return (
|
||||||
|
<nav className="border-b-4 border-fuchsia-500 bg-slate-950 text-white">
|
||||||
|
<div className="mx-auto flex max-w-7xl items-center gap-1 overflow-x-auto px-4 py-2 [scrollbar-width:none]">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onOpenMenu}
|
||||||
|
aria-label="Abrir menú principal"
|
||||||
|
className="mr-3 shrink-0 text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<Menu />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{categories.map((category) => (
|
||||||
|
<Link
|
||||||
|
key={category.label}
|
||||||
|
href={category.href}
|
||||||
|
prefetch={false}
|
||||||
|
className="shrink-0 rounded-full px-4 py-2 text-sm font-extrabold text-white/90 transition hover:bg-gradient-to-r hover:from-cyan-500 hover:to-violet-500 hover:text-white"
|
||||||
|
>
|
||||||
|
{category.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileCategoryLinks({ onNavigate }: { onNavigate: () => void }) {
|
||||||
|
const { isCategoryActive } = useCategoryNavigationState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{categories.map((category) => {
|
||||||
|
const active = isCategoryActive(category);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={category.label}
|
||||||
|
href={category.href}
|
||||||
|
prefetch={false}
|
||||||
|
aria-current={active ? "page" : undefined}
|
||||||
|
onClick={onNavigate}
|
||||||
|
className={`rounded-2xl border px-4 py-3 font-extrabold transition ${
|
||||||
|
active
|
||||||
|
? "border-violet-500 bg-violet-50 text-violet-700"
|
||||||
|
: "border-slate-200 hover:border-cyan-400 hover:bg-cyan-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category.label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileCategoryLinksFallback({ onNavigate }: { onNavigate: () => void }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{categories.map((category) => (
|
||||||
|
<Link
|
||||||
|
key={category.label}
|
||||||
|
href={category.href}
|
||||||
|
prefetch={false}
|
||||||
|
onClick={onNavigate}
|
||||||
|
className="rounded-2xl border border-slate-200 px-4 py-3 font-extrabold transition hover:border-cyan-400 hover:bg-cyan-50"
|
||||||
|
>
|
||||||
|
{category.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user