import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { Facets, fetchFacets, fetchProducts, formatInt, ProductsResponse, } from '../api' import EmptyState from '../components/EmptyState' import Filters, { countActiveFilters, FilterValues, } from '../components/Filters' import FilterSheet from '../components/FilterSheet' import { IconFilter, IconSearch } from '../components/Icons' import Pagination from '../components/Pagination' import ProductGrid from '../components/ProductGrid' import { SkeletonGrid } from '../components/Skeleton' const PER_PAGE = 24 export default function Catalog() { const [searchParams, setSearchParams] = useSearchParams() const [facets, setFacets] = useState(null) const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [sheetOpen, setSheetOpen] = useState(false) const [shrunk, setShrunk] = useState(false) const values: FilterValues = useMemo( () => ({ q: searchParams.get('q') ?? '', category: searchParams.get('category') ?? '', region: searchParams.get('region') ?? '', origins: (searchParams.get('origin') ?? '') .split(',') .filter((o) => o.length > 0), priceMin: searchParams.get('price_min') ?? '', priceMax: searchParams.get('price_max') ?? '', sort: searchParams.get('sort') ?? 'recent', }), [searchParams] ) const page = Math.max(1, parseInt(searchParams.get('page') ?? '1', 10) || 1) const activeCount = countActiveFilters(values) // Mobile sticky search — local state, debounced to the URL. const [search, setSearch] = useState(values.q) const debounceRef = useRef(undefined) useEffect(() => { setSearch(values.q) }, [values.q]) // Shrink the sticky search bar once the page is scrolled. useEffect(() => { const onScroll = () => setShrunk(window.scrollY > 24) onScroll() window.addEventListener('scroll', onScroll, { passive: true }) return () => window.removeEventListener('scroll', onScroll) }, []) useEffect(() => { const controller = new AbortController() fetchFacets(controller.signal) .then(setFacets) .catch(() => {}) return () => controller.abort() }, []) useEffect(() => { const controller = new AbortController() setLoading(true) setError(null) fetchProducts( { q: values.q || undefined, category: values.category || undefined, region: values.region || undefined, origin: values.origins.length > 0 ? values.origins.join(',') : undefined, price_min: values.priceMin || undefined, price_max: values.priceMax || undefined, sort: values.sort || 'recent', page, per_page: PER_PAGE, }, controller.signal ) .then((res) => { setData(res) setLoading(false) }) .catch((err: unknown) => { if (err instanceof DOMException && err.name === 'AbortError') return setError('Impossible de charger les produits. Réessayez plus tard.') setLoading(false) }) return () => controller.abort() }, [values, page]) const applyPatch = useCallback( (patch: Partial, resetPage = true) => { setSearchParams( (prev) => { const next = new URLSearchParams(prev) const merged = { ...values, ...patch } const setOrDelete = (key: string, v: string) => { if (v) next.set(key, v) else next.delete(key) } setOrDelete('q', merged.q) setOrDelete('category', merged.category) setOrDelete('region', merged.region) setOrDelete('origin', merged.origins.join(',')) setOrDelete('price_min', merged.priceMin) setOrDelete('price_max', merged.priceMax) setOrDelete('sort', merged.sort !== 'recent' ? merged.sort : '') if (resetPage) next.delete('page') return next }, { replace: true } ) }, [setSearchParams, values] ) const resetFilters = useCallback(() => { setSearchParams(new URLSearchParams(), { replace: true }) }, [setSearchParams]) const goToPage = useCallback( (p: number) => { setSearchParams((prev) => { const next = new URLSearchParams(prev) if (p > 1) next.set('page', String(p)) else next.delete('page') return next }) window.scrollTo({ top: 0, behavior: 'smooth' }) }, [setSearchParams] ) function onMobileSearch(value: string) { setSearch(value) window.clearTimeout(debounceRef.current) debounceRef.current = window.setTimeout(() => { applyPatch({ q: value }) }, 300) } return (
{/* Mobile sticky search (shrinks on scroll) */}
onMobileSearch(e.target.value)} placeholder="Rechercher un produit…" aria-label="Rechercher un produit" enterKeyHint="search" />

Produits

{data && !loading && (

{formatInt(data.total)} produit{data.total === 1 ? '' : 's'}

)}
{loading ? ( ) : error ? ( ) : ( <> {data && ( )} )}
{/* Mobile filter pill (above the tab bar) */} setSheetOpen(false)} />
) }