import { useState, useCallback } from 'react'; import { useParams } from 'next/navigation'; import { ChevronDown, SlidersHorizontal } from 'lucide-react'; import type { ShopifyCollection } from '@reacteditor/field-shopify'; import { useCollectionProducts, type CollectionSortKey, type ProductFilter, } from '@/hooks/use-shopify-collections'; import { ProductCard } from './product-card'; import { Typography } from '@/components/Typography'; import { Skeleton } from '@/components/ui/skeleton'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetFooter } from '@/components/ui/sheet'; import { Button } from '@/components/ui/button'; import { Container } from '@/components/layout/Container'; import { cn } from '@/lib/utils'; type FilterOption = { label: string }; type ColorOption = { label: string; color: string }; export type CollectionProps = { collection: ShopifyCollection | null; showDescription: 'yes' | 'no'; showCoverImage: 'yes' | 'no'; customCoverImage: string; columns: '2' | '3' | '4'; limit: number; defaultSort: CollectionSortKey; showAvailability: 'yes' | 'no'; showPriceRange: 'yes' | 'no'; showProductType: 'yes' | 'no'; productTypeOptions: FilterOption[]; showVendor: 'yes' | 'no'; vendorOptions: FilterOption[]; showTags: 'yes' | 'no'; tagOptions: FilterOption[]; showColor: 'yes' | 'no'; colorOptions: ColorOption[]; showStyle: 'yes' | 'no'; styleOptions: FilterOption[]; showSize: 'yes' | 'no'; sizeOptions: FilterOption[]; showMaterial: 'yes' | 'no'; materialOptions: FilterOption[]; metafieldFilters: { namespace: string; key: string; label: string; values: { label: string }[] }[]; }; const SORT_OPTIONS: { label: string; value: CollectionSortKey }[] = [ { label: 'Best Selling', value: 'BEST_SELLING' }, { label: 'Newest', value: 'CREATED' }, { label: 'Price: Low to High', value: 'PRICE' }, { label: 'Alphabetical', value: 'TITLE' }, ]; const colClass: Record = { '2': 'grid-cols-2', '3': 'grid-cols-2 md:grid-cols-3', '4': 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4', }; // ─── Filter group (collapsible) ──────────────────────────────────────────────── function FilterGroup({ label, children, defaultOpen = true }: { label: string; children: React.ReactNode; defaultOpen?: boolean; }) { const [open, setOpen] = useState(defaultOpen); return (
{open &&
{children}
}
); } function Checkbox({ checked, onChange, label }: { checked: boolean; onChange: (v: boolean) => void; label: string; }) { return ( ); } // ─── Sidebar ──────────────────────────────────────────────────────────────────── type ActiveFilters = { availability: boolean; productTypes: string[]; vendors: string[]; tags: string[]; colors: string[]; styles: string[]; sizes: string[]; materials: string[]; minPrice: string; maxPrice: string; metafieldValues: Record; }; function Sidebar({ props, active, onChange, }: { props: CollectionProps; active: ActiveFilters; onChange: (patch: Partial) => void; }) { const productTypes = (props.productTypeOptions ?? []).map((o) => o.label).filter(Boolean); const vendors = (props.vendorOptions ?? []).map((o) => o.label).filter(Boolean); const tags = (props.tagOptions ?? []).map((o) => o.label).filter(Boolean); const colors = (props.colorOptions ?? []) as ColorOption[]; const styles = (props.styleOptions ?? []).map((o) => o.label).filter(Boolean); const sizes = (props.sizeOptions ?? []).map((o) => o.label).filter(Boolean); const materials = (props.materialOptions ?? []).map((o) => o.label).filter(Boolean); const metafieldFilters = (props.metafieldFilters ?? []).filter((mf) => mf.namespace && mf.key); function toggle(key: 'productTypes' | 'vendors' | 'tags' | 'colors' | 'styles' | 'sizes' | 'materials', value: string) { const arr = active[key]; onChange({ [key]: arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value] }); } return (
{props.showAvailability === 'yes' && ( onChange({ availability: v })} label="In stock" /> )} {props.showPriceRange === 'yes' && (
onChange({ minPrice: e.target.value })} className="w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm" /> onChange({ maxPrice: e.target.value })} className="w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm" />
)} {props.showVendor === 'yes' && vendors.length > 0 && ( {vendors.map((v) => ( toggle('vendors', v)} label={v} /> ))} )} {props.showColor === 'yes' && colors.length > 0 && ( {colors.filter((c) => c.label).map((c) => ( ))} )} {props.showStyle === 'yes' && styles.length > 0 && ( {styles.map((s) => ( toggle('styles', s)} label={s} /> ))} )} {props.showSize === 'yes' && sizes.length > 0 && ( {sizes.map((s) => ( toggle('sizes', s)} label={s} /> ))} )} {props.showMaterial === 'yes' && materials.length > 0 && ( {materials.map((m) => ( toggle('materials', m)} label={m} /> ))} )} {props.showProductType === 'yes' && productTypes.length > 0 && ( {productTypes.map((pt) => ( toggle('productTypes', pt)} label={pt} /> ))} )} {props.showTags === 'yes' && tags.length > 0 && ( {tags.map((t) => ( toggle('tags', t)} label={t} /> ))} )} {metafieldFilters.map((mf, i) => { const mfKey = `${mf.namespace}.${mf.key}`; const selected = active.metafieldValues[mfKey] ?? []; return ( {mf.values.map((v) => v.label).filter(Boolean).map((val) => ( { const next = checked ? [...selected, val] : selected.filter((v) => v !== val); onChange({ metafieldValues: { ...active.metafieldValues, [mfKey]: next } }); }} label={val} /> ))} ); })}
); } // ─── Build Shopify ProductFilter array ────────────────────────────────────────── function buildProductFilters(active: ActiveFilters): ProductFilter[] { const filters: ProductFilter[] = []; if (active.availability) filters.push({ available: true }); if (active.minPrice !== '' || active.maxPrice !== '') { filters.push({ price: { min: active.minPrice !== '' ? parseFloat(active.minPrice) : undefined, max: active.maxPrice !== '' ? parseFloat(active.maxPrice) : undefined, }, }); } for (const pt of active.productTypes) filters.push({ productType: pt }); for (const v of active.vendors) filters.push({ productVendor: v }); for (const t of active.tags) filters.push({ tag: t }); for (const c of active.colors) filters.push({ variantOption: { name: 'Color', value: c } }); for (const s of active.styles) filters.push({ variantOption: { name: 'Style', value: s } }); for (const s of active.sizes) filters.push({ variantOption: { name: 'Size', value: s } }); for (const m of active.materials) filters.push({ variantOption: { name: 'Material', value: m } }); for (const [mfKey, vals] of Object.entries(active.metafieldValues)) { const [namespace, key] = mfKey.split('.'); for (const value of vals) { filters.push({ productMetafield: { namespace, key, value } }); } } return filters; } // ─── Main component ────────────────────────────────────────────────────────── export function CollectionView(props: CollectionProps) { const { collection: selected, showDescription, showCoverImage, customCoverImage, columns, limit, defaultSort } = props; const params = useParams(); const paramHandle = typeof params?.handle === 'string' ? params.handle : undefined; const handle = selected?.handle ?? paramHandle ?? ''; const [sort, setSort] = useState(defaultSort); const [reverse, setReverse] = useState(false); const [filtersOpen, setFiltersOpen] = useState(false); const [active, setActive] = useState({ availability: false, productTypes: [], vendors: [], tags: [], colors: [], styles: [], sizes: [], materials: [], minPrice: '', maxPrice: '', metafieldValues: {}, }); const patchActive = useCallback((patch: Partial) => { setActive((prev) => ({ ...prev, ...patch })); }, []); const clearAll = useCallback(() => { setActive({ availability: false, productTypes: [], vendors: [], tags: [], colors: [], styles: [], sizes: [], materials: [], minPrice: '', maxPrice: '', metafieldValues: {}, }); }, []); const productFilters = buildProductFilters(active); const handleSortChange = (value: string) => { if (value === 'PRICE_DESC') { setSort('PRICE'); setReverse(true); } else { setSort(value as CollectionSortKey); setReverse(false); } }; const sortValue = sort === 'PRICE' && reverse ? 'PRICE_DESC' : sort; const { collection, loading, hasNextPage, fetchMore } = useCollectionProducts(handle, { first: limit, sortKey: sort, reverse, filters: productFilters.length ? productFilters : undefined, }); const products = collection?.products ?? []; const description = collection?.description ?? (selected as any)?.description; const collectionImage = customCoverImage || collection?.image?.url; if (!selected && !paramHandle) { return (
{Array.from({ length: 8 }).map((_, i) => ( ))}
); } return (
{/* Cover image */} {showCoverImage === 'yes' && collectionImage && (
{collection?.title
)} {/* Header */}

Collection

{collection?.title ?? (selected as any)?.title ?? paramHandle} {showDescription === 'yes' && description ? ( {description} ) : null}
{/* Filter + sort bar */}
Filters

{loading ? 'Loading…' : `${products.length} product${products.length === 1 ? '' : 's'}`}

{/* Grid */}
{loading ? Array.from({ length: limit }).map((_, i) => ( )) : products.map((p: any) => )}
{!loading && products.length === 0 && (
No products found in this collection.
)} {hasNextPage && !loading && (
)}
); }