import { useState, useCallback } from 'react'; import { useParams } from 'react-router'; import { ChevronDown, SlidersHorizontal, X } 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 { 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] }); } const hasActiveFilters = active.availability || active.productTypes.length > 0 || active.vendors.length > 0 || active.tags.length > 0 || active.colors.length > 0 || active.styles.length > 0 || active.sizes.length > 0 || active.materials.length > 0 || active.minPrice !== '' || active.maxPrice !== '' || Object.values(active.metafieldValues).some((arr) => arr.length > 0); return ( ); } // ─── 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 { handle: paramHandle } = useParams<{ handle?: string }>(); const handle = selected?.handle ?? paramHandle ?? ''; const [sort, setSort] = useState(defaultSort); const [reverse, setReverse] = useState(false); const [mobileFiltersOpen, setMobileFiltersOpen] = 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 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}
{/* Mobile filter toggle */}
{/* Mobile filter panel */} {mobileFiltersOpen && (
)}
{/* Desktop sidebar */}
{/* Product area */}
{/* Sort + count bar */}

{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 && (
)}
); }