"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; import { IoCloseOutline } from "react-icons/io5"; import { useDebounced } from "@/hooks/useDebounced"; import { SortDropdown, type SortOption } from "@/components/SortDropdown"; import { FilterDropdown, type FilterGroup } from "@/components/FilterDropdown"; import { IconButton } from "@/components/IconButton"; const EMPTY_FILTERS: Record = {}; type Props = { initialQuery: string; initialSort?: T; initialPage?: number; defaultSort: T; placeholder: string; sortOptions: SortOption[]; sortAriaLabel: string; sortMenuClassName?: string; filterGroups?: FilterGroup[]; initialFilters?: Record; filterMenuClassName?: string; }; export function SearchBar({ initialQuery, initialSort, initialPage = 1, defaultSort, placeholder, sortOptions, sortAriaLabel, sortMenuClassName, filterGroups, initialFilters, filterMenuClassName, }: Props) { const sort0 = initialSort ?? defaultSort; const filters0 = initialFilters ?? EMPTY_FILTERS; const router = useRouter(); const pathname = usePathname(); const [q, setQ] = useState(initialQuery); const debouncedQ = useDebounced(q, 250); const [sort, setSort] = useState(sort0); const [filters, setFilters] = useState>(filters0); const prevPropsRef = useRef({ initialQuery, initialSort: sort0, initialFilters: filters0 }); const lastPushedQRef = useRef(initialQuery); useEffect(() => { if (initialQuery !== lastPushedQRef.current) { setQ(initialQuery); } lastPushedQRef.current = initialQuery; }, [initialQuery]); useEffect(() => { setSort(sort0); }, [sort0]); useEffect(() => { setFilters(filters0); }, [filters0]); const handleFilterChange = useCallback((param: string, selected: string[]) => { setFilters((prev) => ({ ...prev, [param]: selected })); }, []); const filtersKey = useMemo( () => JSON.stringify(filters, Object.keys(filters).sort()), [filters], ); const filters0Key = useMemo( () => JSON.stringify(filters0, Object.keys(filters0).sort()), [filters0], ); const nextUrl = useMemo(() => { const params = new URLSearchParams(); const normalizedQ = debouncedQ.trim(); const q0 = (initialQuery ?? "").trim(); const qChanged = normalizedQ !== q0; const sortChanged = sort !== sort0; const filtersChanged = filtersKey !== filters0Key; if (normalizedQ) params.set("q", normalizedQ); if (sort !== defaultSort) params.set("sort", sort); for (const [param, selected] of Object.entries(filters)) { if (selected.length > 0) params.set(param, selected.join(",")); } if (!qChanged && !sortChanged && !filtersChanged && initialPage > 1) { params.set("page", String(initialPage)); } const qs = params.toString(); return qs ? `${pathname}?${qs}` : pathname; }, [debouncedQ, initialQuery, initialPage, sort0, defaultSort, pathname, sort, filtersKey, filters0Key, filters]); useEffect(() => { const propsChanged = prevPropsRef.current.initialQuery !== initialQuery || prevPropsRef.current.initialSort !== sort0 || JSON.stringify(prevPropsRef.current.initialFilters) !== JSON.stringify(filters0); prevPropsRef.current = { initialQuery, initialSort: sort0, initialFilters: filters0 }; if (propsChanged) { return; } const params = new URLSearchParams(); const q0 = (initialQuery ?? "").trim(); if (q0) params.set("q", q0); if (sort0 && sort0 !== defaultSort) params.set("sort", sort0); for (const [param, selected] of Object.entries(filters0)) { if (selected.length > 0) params.set(param, selected.join(",")); } if (initialPage > 1) params.set("page", String(initialPage)); const qs = params.toString(); const currentUrlFromProps = qs ? `${pathname}?${qs}` : pathname; if (nextUrl !== currentUrlFromProps) { lastPushedQRef.current = debouncedQ.trim(); router.replace(nextUrl, { scroll: false }); } }, [nextUrl, debouncedQ, initialQuery, sort0, defaultSort, initialPage, pathname, router, filters0]); return (
setQ(e.target.value)} placeholder={placeholder} className="no-ring relative z-10 w-full rounded-xl border border-lightline bg-lightbg px-6 py-3 text-base text-lighttext shadow-lg transition-all duration-200 ease-in-out hover:border-lightline-hover hover:text-trptkblue focus:border-lightline-focus dark:border-darkline dark:bg-darkbg dark:text-darktext dark:hover:border-darkline-hover dark:hover:text-white dark:focus:border-darkline-focus" aria-label={placeholder} /> {q || Object.values(filters).some((arr) => arr.length > 0) ? ( { setQ(""); setFilters(Object.fromEntries(Object.keys(filters).map((k) => [k, []]))); }} className="text-lg" aria-label="Clear search and filters" > ) : null} {filterGroups && filterGroups.length > 0 ? ( ) : null}
); }