'use client'; import React, { useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import ProductCard from './product-card'; import ProductFilters from './product-filters'; import ProductToolbar from './product-toolbar'; import { Button } from '@/components/ui/button'; import { Loader } from '@/components/ui/loader'; import { searchProducts, type SearchFilter, type SearchSortKey, } from '@/hooks/use-shopify-search'; import type { Product } from '@/hooks/use-shopify-products'; const PAGE_SIZE = 24; interface SortOption { label: string; sortKey: SearchSortKey; reverse: boolean; } const SORT_OPTIONS: SortOption[] = [ { label: 'Best Matches', sortKey: 'RELEVANCE', reverse: false }, { label: 'Price: Low to High', sortKey: 'PRICE', reverse: false }, { label: 'Price: High to Low', sortKey: 'PRICE', reverse: true }, ]; const SearchResults: React.FC = () => { const searchParams = useSearchParams(); const query = searchParams.get('q') ?? ''; const [products, setProducts] = useState([]); const [filters, setFilters] = useState([]); const [totalCount, setTotalCount] = useState(0); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); const [cursor, setCursor] = useState(null); const [hasNextPage, setHasNextPage] = useState(false); const [sortIndex, setSortIndex] = useState(0); const [filtersOpen, setFiltersOpen] = useState(false); const [activeFilters, setActiveFilters] = useState([]); const sort = SORT_OPTIONS[sortIndex]; // Serialised so the effect re-runs when the selection changes, not the array. const activeKey = useMemo(() => activeFilters.join('|'), [activeFilters]); useEffect(() => { let cancelled = false; const run = async () => { try { setLoading(true); setError(null); const result = await searchProducts({ // An empty term still returns the catalogue, which is what an // unqualified /search visit should show. query, first: PAGE_SIZE, sortKey: sort.sortKey, reverse: sort.reverse, filterInputs: activeFilters, }); if (cancelled) return; setProducts(result.products); setTotalCount(result.totalCount); setCursor(result.endCursor); setHasNextPage(result.hasNextPage); // Facet counts change with the result set, but keep the panel stable // while filters are applied so options don't vanish mid-selection. if (activeFilters.length === 0) setFilters(result.filters); } catch (err) { if (cancelled) return; console.error('Search failed:', err); setError(err instanceof Error ? err.message : 'Search failed'); } finally { if (!cancelled) setLoading(false); } }; run(); return () => { cancelled = true; }; }, [query, sort.sortKey, sort.reverse, activeKey]); const handleLoadMore = async () => { if (loadingMore || !hasNextPage) return; try { setLoadingMore(true); const result = await searchProducts({ query, first: PAGE_SIZE, after: cursor, sortKey: sort.sortKey, reverse: sort.reverse, filterInputs: activeFilters, }); setProducts((prev) => { const seen = new Set(prev.map((p) => p.id)); return [...prev, ...result.products.filter((p) => !seen.has(p.id))]; }); setCursor(result.endCursor); setHasNextPage(result.hasNextPage); } catch (err) { console.error('Failed to load more results:', err); } finally { setLoadingMore(false); } }; return (

Search

setFiltersOpen(true)} sortOptions={SORT_OPTIONS} sortIndex={sortIndex} onSortChange={setSortIndex} activeFilterCount={activeFilters.length} />
{/* Results */}
{loading ? (
{Array.from({ length: 10 }).map((_, index) => (
))}
) : error ? (

{error}

) : products.length === 0 ? (

No products matched{query ? ` “${query}”` : ' your filters'}.

) : ( <>
{products.map((product) => ( ))}
{hasNextPage && (
)} )}
); }; export default SearchResults;