"use client"; import { useState, useMemo, useEffect } from "react"; import Image from "next/image"; import { DEFAULT_CURRENCY, symbolFor, type CurrencyCode } from "@/lib/currency"; interface Product { id: string; name: string; slug: string; description: string; thumbnail_url: string | null; price: { amount: string; currency: string; display_amount: string }; } function extractBrand(description: string, name: string): string { if (description && description.includes(" - ")) { return description.split(" - ")[0].trim(); } const known = [ "G-Shock", "H&M", "Lacoste", "Tommy Hilfiger", "Cotton On", "GAP", "Giordano", "Superdry", "Under Armour", "Calvin Klein", "Camel Active", "Electro Denim Lab", "Trendyol", "Swiss Polo", "Adidas", "Nike", "New Balance", "Levi's", "Mango Man", "Urban Revivo", "Puma", "Jack & Jones", "Harley-Davidson", "Clarks", "Ben Sherman", "Connor", "Jordan", "Desigual", "Dr. Cardin", "Veja", "Hunter Boots", ]; for (const b of known) { if (name.toLowerCase().includes(b.toLowerCase())) return b; } return "Other"; } type SortOption = "default" | "price-asc" | "price-desc" | "name"; // 24, not 25: the grid is 2 columns on mobile and 3 on desktop, so 24 is the // largest round page size that fills every row in both layouts. const PER_PAGE = 24; export default function ProductGrid({ products, currency = DEFAULT_CURRENCY, }: { products: Product[]; currency?: CurrencyCode; }) { const [selectedBrands, setSelectedBrands] = useState>(new Set()); const [sort, setSort] = useState("default"); const [showFilters, setShowFilters] = useState(false); const [page, setPage] = useState(1); const symbol = symbolFor(currency); const productsWithBrand = useMemo( () => products.map((p) => ({ ...p, brand: extractBrand(p.description, p.name) })), [products] ); const brands = useMemo(() => { const counts = new Map(); for (const p of productsWithBrand) { counts.set(p.brand, (counts.get(p.brand) || 0) + 1); } return Array.from(counts.entries()) .sort((a, b) => b[1] - a[1]) .map(([name, count]) => ({ name, count })); }, [productsWithBrand]); const maxPrice = useMemo(() => { const amounts = productsWithBrand.map((p) => parseFloat(p.price.amount)); if (!amounts.length) return 0; return Math.ceil(Math.max(...amounts) / 100) * 100; }, [productsWithBrand]); // Roughly 100 notches across whatever range this currency spans. A fixed // step of 50 was unusable in MYR (max ~800) and too coarse in ZAR. const step = useMemo(() => Math.max(1, Math.round(maxPrice / 100)), [maxPrice]); const [priceRange, setPriceRange] = useState<[number, number]>([0, maxPrice]); // Prices change scale entirely when the currency changes (R3,185 vs RM799 vs // $196), so a range carried over from the previous currency would silently // hide most of the catalogue. Reset it whenever the ceiling moves. useEffect(() => { setPriceRange([0, maxPrice]); }, [maxPrice, currency]); const filtered = useMemo(() => { let result = productsWithBrand; if (selectedBrands.size > 0) { result = result.filter((p) => selectedBrands.has(p.brand)); } const price = parseFloat; result = result.filter( (p) => price(p.price.amount) >= priceRange[0] && price(p.price.amount) <= priceRange[1] ); switch (sort) { case "price-asc": result = [...result].sort((a, b) => price(a.price.amount) - price(b.price.amount)); break; case "price-desc": result = [...result].sort((a, b) => price(b.price.amount) - price(a.price.amount)); break; case "name": result = [...result].sort((a, b) => a.name.localeCompare(b.name)); break; } return result; }, [productsWithBrand, selectedBrands, sort, priceRange]); const totalPages = Math.max(1, Math.ceil(filtered.length / PER_PAGE)); // A filter that shrinks the result set can strand the viewer on a page that no // longer exists (page 6 of 2), which renders an empty grid with no explanation. // Snap back to the first page whenever the selection changes. useEffect(() => { setPage(1); }, [selectedBrands, sort, priceRange, currency]); const paged = useMemo( () => filtered.slice((page - 1) * PER_PAGE, page * PER_PAGE), [filtered, page] ); const firstShown = filtered.length === 0 ? 0 : (page - 1) * PER_PAGE + 1; const lastShown = Math.min(page * PER_PAGE, filtered.length); function goToPage(next: number) { setPage(Math.min(Math.max(1, next), totalPages)); if (typeof window !== "undefined") { window.scrollTo({ top: 0, behavior: "smooth" }); } } // Up to 7 page buttons; longer runs collapse the middle to an ellipsis so the // row never wraps on a phone. const pageNumbers = useMemo<(number | "gap")[]>(() => { if (totalPages <= 7) { return Array.from({ length: totalPages }, (_, i) => i + 1); } const out: (number | "gap")[] = [1]; const from = Math.max(2, page - 1); const to = Math.min(totalPages - 1, page + 1); if (from > 2) out.push("gap"); for (let i = from; i <= to; i++) out.push(i); if (to < totalPages - 1) out.push("gap"); out.push(totalPages); return out; }, [page, totalPages]); function toggleBrand(brand: string) { setSelectedBrands((prev) => { const next = new Set(prev); if (next.has(brand)) next.delete(brand); else next.add(brand); return next; }); } function clearFilters() { setSelectedBrands(new Set()); setPriceRange([0, maxPrice]); setSort("default"); } // Keep the chosen currency when following a product link. function productHref(slug: string) { return currency === DEFAULT_CURRENCY ? `/product/${slug}` : `/product/${slug}?currency=${currency}`; } const activeFilterCount = selectedBrands.size + (priceRange[0] > 0 || priceRange[1] < maxPrice ? 1 : 0); return (
{/* Header */}

All Products

{filtered.length === 0 ? "0 items" : `${firstShown}–${lastShown} of ${filtered.length} items`}
{/* Sidebar Filters */} {/* Product Grid */}
{filtered.length === 0 ? (

No products match your filters

) : ( )} {totalPages > 1 && ( )}
); }