/** * @file app/components/layout/SearchBar.tsx * @description Client component managing URL search parameters with debounced input and smooth transition state. */ "use client"; import { useState, useEffect, useTransition } from "react"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { Search, X } from "lucide-react"; const SEARCHABLE_ROUTES = ["/dashboard", "/trash"]; /** * Properties for the SearchBar component. * * @interface SearchBarProps * @property {string} [placeholder] - Placeholder text displayed inside the search input. * @property {number} [debounceMs] - Delay in milliseconds before updating the URL search parameter. * @property {boolean} [autoFocus] - Flag indicating whether the input should automatically gain focus on mount. */ interface SearchBarProps { placeholder?: string; debounceMs?: number; autoFocus?: boolean; } /** * Renders a debounced search bar input that syncs its local state with the URL's "search" query parameter. * * @param {SearchBarProps} props - The component props. * @returns {JSX.Element | null} The rendered search bar component or null if the current route is not searchable. */ export default function SearchBar({ placeholder = "Search...", debounceMs = 400, autoFocus = false, }: SearchBarProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); // Check whether the current route supports the search const isSearchableRoute = SEARCHABLE_ROUTES.some( (route) => pathname === route || pathname.startsWith(`${route}/`), ); const searchQuery = searchParams.get("search") || ""; const [localValue, setLocalValue] = useState(searchQuery); const [isPending, startTransition] = useTransition(); // Synchronize state when the path or URL changes useEffect(() => { setLocalValue(searchQuery); }, [searchQuery, pathname]); // Debounced URL Update useEffect(() => { if (!isSearchableRoute) return; const timer = setTimeout(() => { if (localValue !== searchQuery) { const params = new URLSearchParams(searchParams.toString()); if (localValue.trim()) { params.set("search", localValue.trim()); } else { params.delete("search"); } startTransition(() => { router.push(`?${params.toString()}`, { scroll: false }); }); } }, debounceMs); return () => clearTimeout(timer); }, [ localValue, searchQuery, searchParams, debounceMs, router, isSearchableRoute, ]); if (!isSearchableRoute) { return null; } /** * Resets the local search input value to an empty string. */ const handleClear = () => { setLocalValue(""); }; return (
setLocalValue(e.target.value)} autoFocus={autoFocus} className="bg-transparent focus:outline-none w-full placeholder:text-foreground-muted/60" /> {localValue && ( )}
); }