diff --git a/workloads/ecommerce/app/catalog/page.tsx b/workloads/ecommerce/app/catalog/page.tsx index 4b9c963..5d5c175 100644 --- a/workloads/ecommerce/app/catalog/page.tsx +++ b/workloads/ecommerce/app/catalog/page.tsx @@ -2,7 +2,10 @@ import type { Metadata } from "next"; import { CatalogClient } from "@/components/catalog/catalog-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 = { searchParams: Promise>; @@ -15,12 +18,19 @@ function single(value: string | string[] | undefined): string { export default async function CatalogPage({ searchParams }: CatalogPageProps) { const params = await searchParams; 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 ( ); } diff --git a/workloads/ecommerce/components/catalog/catalog-client.tsx b/workloads/ecommerce/components/catalog/catalog-client.tsx index ef44a8f..eb06762 100644 --- a/workloads/ecommerce/components/catalog/catalog-client.tsx +++ b/workloads/ecommerce/components/catalog/catalog-client.tsx @@ -1,38 +1,227 @@ "use client"; 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 { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; 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 [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 maxCatalogPrice = pricedProducts.length + ? Math.max(...pricedProducts.map((product) => product.price ?? 0)) + : 0; const [maxPrice, setMaxPrice] = useState(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]); + // 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 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
Catálogo completo

Todos los tesoros

Filtra por categoría, marca o edad sin recargar la página.

{filtered.length} productos

{filtered.length ?
{filtered.map((product) => )}
:

No encontramos productos

}
; + 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 ( +
+
+ + Catálogo completo + +

Todos los tesoros

+

+ Filtra por categoría, marca o edad sin recargar la página. +

+
+ +
+ + +
+
+

{filtered.length} productos

+ +
+ + {filtered.length ? ( +
+ {filtered.map((product) => ( + + ))} +
+ ) : ( +
+
+

No encontramos productos

+ +
+
+ )} +
+
+
+ ); } -function Filter({ title, values, selected, onChange }: { title: string; values: string[]; selected: string; onChange: (value: string) => void }) { - return
{title}
{values.map((value) => )}
; +function Filter({ + title, + values, + selected, + onChange, +}: { + title: string; + values: string[]; + selected: string; + onChange: (value: string) => void; +}) { + return ( +
+ {title} +
+ {values.map((value) => ( + + ))} +
+
+ ); } diff --git a/workloads/ecommerce/components/layout/navbar.tsx b/workloads/ecommerce/components/layout/navbar.tsx index a0df780..b5087bb 100644 --- a/workloads/ecommerce/components/layout/navbar.tsx +++ b/workloads/ecommerce/components/layout/navbar.tsx @@ -2,8 +2,9 @@ import Image from "next/image"; import Link from "next/link"; +import { usePathname, useSearchParams } from "next/navigation"; 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 { Button } from "@/components/ui/button"; 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 { useWishlistStore } from "@/store/wishlist-store"; -const categories = [ - { label: "Juguetes para bebés", href: "/catalog?category=Figuras" }, - { label: "Educativos", href: "/catalog?category=Figuras" }, - { label: "Muñecas y peluches", href: "/catalog?category=Peluches" }, - { label: "Acción y aventura", href: "/catalog?collection=Marvel" }, - { label: "Mochilas", href: "/catalog?category=Mochilas" }, - { label: "Variedades", href: "/catalog" }, +type CategoryLink = { + label: string; + href: string; + category?: string; + collection?: string; + section?: string; + 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"]; @@ -62,16 +77,9 @@ export function Navbar({ products }: { products: Product[] }) { - + setMenuOpen(true)} />}> + setMenuOpen(true)} /> + @@ -82,7 +90,9 @@ export function Navbar({ products }: { products: Product[] }) {

Categorías

- {categories.map((category) => 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})} + setMenuOpen(false)} />}> + setMenuOpen(false)} /> +

Colecciones

@@ -112,3 +122,145 @@ export function Navbar({ products }: { products: Product[] }) { ); } + +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 ( + + ); +} + +function DesktopCategoryNavigationFallback({ onOpenMenu }: { onOpenMenu: () => void }) { + return ( + + ); +} + +function MobileCategoryLinks({ onNavigate }: { onNavigate: () => void }) { + const { isCategoryActive } = useCategoryNavigationState(); + + return ( + <> + {categories.map((category) => { + const active = isCategoryActive(category); + + return ( + + {category.label} + + ); + })} + + ); +} + +function MobileCategoryLinksFallback({ onNavigate }: { onNavigate: () => void }) { + return ( + <> + {categories.map((category) => ( + + {category.label} + + ))} + + ); +} +