From 2135545c324c675ef5156d00f480a15f7e20b18c Mon Sep 17 00:00:00 2001 From: Vula Builder Date: Fri, 31 Jul 2026 14:12:35 +0000 Subject: [PATCH] Deploy --- src/app/products/page.tsx | 214 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/app/products/page.tsx diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx new file mode 100644 index 0000000..551d611 --- /dev/null +++ b/src/app/products/page.tsx @@ -0,0 +1,214 @@ +'use client' +import { useState, useMemo } from 'react' +import { useProducts } from '@/hooks/useProducts' +import { useVulaCart } from '@/hooks/useVulaCart' +import ProductFilters, { FilterState } from '@/components/shop/ProductFilters' +import ProductSort, { SortOption } from '@/components/shop/ProductSort' +import Image from 'next/image' +import Link from 'next/link' +import { Search, ShoppingBag, ShoppingCart, SlidersHorizontal, X } from 'lucide-react' + +const CURRENCY_SYMBOLS: Record = { zar: 'R', usd: '$', eur: '€', gbp: '£', kes: 'KSh', ngn: '₦', ghs: '₵' } +const formatPrice = (amount: number, code?: string) => { + const sym = CURRENCY_SYMBOLS[(code ?? '').toLowerCase()] ?? (code?.toUpperCase() ?? '') + return `${sym}${(amount / 100).toFixed(2)}` +} + +export default function ProductsPage() { + const { products, loading, error } = useProducts(100) + const { addToCart } = useVulaCart() + const [sort, setSort] = useState('featured') + const [filters, setFilters] = useState({ category: '', maxPrice: 0, inStockOnly: false }) + const [search, setSearch] = useState('') + const [filtersOpen, setFiltersOpen] = useState(false) + + const categories = useMemo(() => { + const seen = new Map() + products.forEach(p => { + if ((p as any).categories?.length) { + (p as any).categories.forEach((cat: { id: string; name: string }) => seen.set(cat.id, cat.name)) + } else if ((p as any).collection?.id) { + seen.set((p as any).collection.id, (p as any).collection.title ?? (p as any).collection.id) + } + }) + return Array.from(seen.entries()).map(([id, title]) => ({ id, title })) + }, [products]) + + const displayed = useMemo(() => { + let list = [...products] + if (search.trim()) { + const q = search.toLowerCase() + list = list.filter(p => p.title?.toLowerCase().includes(q) || p.description?.toLowerCase().includes(q)) + } + if (filters.category) list = list.filter(p => + (p as any).categories?.some((c: { id: string }) => c.id === filters.category) || (p as any).collection?.id === filters.category + ) + if (filters.maxPrice > 0) list = list.filter(p => { + const price = p.variants?.[0]?.prices?.[0]?.amount ?? 0 + return price > 0 && price / 100 <= filters.maxPrice + }) + if (filters.inStockOnly) list = list.filter(p => (p.variants?.length ?? 0) > 0) + switch (sort) { + case 'price-asc': list.sort((a, b) => (a.variants?.[0]?.prices?.[0]?.amount ?? 0) - (b.variants?.[0]?.prices?.[0]?.amount ?? 0)); break + case 'price-desc': list.sort((a, b) => (b.variants?.[0]?.prices?.[0]?.amount ?? 0) - (a.variants?.[0]?.prices?.[0]?.amount ?? 0)); break + case 'newest': list.sort((a, b) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime()); break + case 'name-asc': list.sort((a, b) => (a.title ?? '').localeCompare(b.title ?? '')); break + } + return list + }, [products, filters, sort, search]) + + const resetFilters = () => setFilters({ category: '', maxPrice: 0, inStockOnly: false }) + + if (loading) return ( +
+
+
+

Loading products...

+
+
+ ) + + if (error) return ( +
+

Failed to load products. Please try again.

+
+ ) + + return ( +
+
+ {/* Header */} +
+

Products

+ +
+ + {/* Search */} +
+ + setSearch(e.target.value)} + className="w-full pl-10 pr-4 py-2.5 bg-background border border-border rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary/50" + /> + {search && ( + + )} +
+ +
+ {/* Sidebar filters — desktop */} + {categories.length > 0 && ( + + )} + + {/* Mobile filter toggle */} +
+ {categories.length > 0 && ( + + )} + + {/* Sort bar */} +
+ +
+ + {/* Grid */} + {displayed.length === 0 ? ( +
+ +

No products found.

+ +
+ ) : ( +
+ {displayed.map((product) => { + const variant = product.variants?.[0] + const price = variant?.prices?.[0] + const imageUrl = product.thumbnail || product.images?.[0]?.url + return ( +
+ + {imageUrl ? ( +
+ {product.title +
+ ) : ( +
+ +
+ )} + +
+ +

{product.title}

+ + {product.collection?.title && ( + {product.collection.title} + )} + {product.description && ( +

{product.description}

+ )} + {price && ( +

+ {formatPrice(price.amount, price.currency_code)} +

+ )} + +
+
+ ) + })} +
+ )} +
+
+
+ + {/* Mobile filter drawer */} + {filtersOpen && ( +
+
setFiltersOpen(false)} /> +
+ setFiltersOpen(false)} + /> +
+
+ )} +
+ ) +}