Search
ComponentsReactTailwindDebounced search input (300ms) with filterable results, regex-based match highlighting, clear button, typing/empty/no-results states.
Debounced Search
Start typing to search 8 items
300ms debounce. Searches across title, category, and description. Match highlighting via regex split + <mark>.
1 "use client"; 2 3 import { useMemo, useState } from "react"; 4 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 5 import { faSpinner } from "@fortawesome/free-solid-svg-icons"; 6 import { SearchInput, highlightMatch } from "@/shared/ui/search-input"; 7 import { useDebounced } from "@/shared/hooks/use-debounced"; 8 9 type Result = { id: string; title: string; category: string; description: string }; 10 11 export function Search({ items }: { items: Result[] }) { 12 const [query, setQuery] = useState(""); 13 const debounced = useDebounced(query, 300); 14 const isTyping = query !== debounced; 15 16 const results = useMemo(() => { 17 if (!debounced.trim()) return []; 18 const q = debounced.toLowerCase(); 19 return items.filter( 20 (r) => 21 r.title.toLowerCase().includes(q) || 22 r.category.toLowerCase().includes(q) || 23 r.description.toLowerCase().includes(q), 24 ); 25 }, [debounced, items]); 26 27 return ( 28 <div className="bg-card rounded-lg border p-5"> 29 <SearchInput 30 value={query} 31 onChange={setQuery} 32 placeholder="Search…" 33 /> 34 35 <div className="mt-4"> 36 {isTyping ? ( 37 <div className="text-muted-foreground flex items-center gap-2 py-8 text-sm"> 38 <FontAwesomeIcon icon={faSpinner} className="h-3.5 w-3.5 animate-spin" /> 39 Searching… 40 </div> 41 ) : !debounced.trim() ? ( 42 <div className="text-muted-foreground py-8 text-center text-sm"> 43 Start typing to search {items.length} items 44 </div> 45 ) : results.length === 0 ? ( 46 <div className="py-8 text-center"> 47 <p className="text-foreground text-sm font-medium">No results for "{debounced}"</p> 48 <p className="text-muted-foreground mt-1 text-xs"> 49 Try a different keyword or check spelling. 50 </p> 51 </div> 52 ) : ( 53 <div className="space-y-2"> 54 <p className="text-muted-foreground text-xs"> 55 {results.length} result{results.length === 1 ? "" : "s"} 56 </p> 57 {results.map((r) => ( 58 <button key={r.id} className="block w-full rounded-md border p-3 text-left hover:bg-muted"> 59 <div className="flex items-baseline justify-between gap-3"> 60 <div className="font-medium">{highlightMatch(r.title, debounced)}</div> 61 <span className="text-muted-foreground bg-muted/50 shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wider uppercase"> 62 {r.category} 63 </span> 64 </div> 65 <p className="text-muted-foreground text-xs">{highlightMatch(r.description, debounced)}</p> 66 </button> 67 ))} 68 </div> 69 )} 70 </div> 71 </div> 72 ); 73 }