SearchBar.tsx (2094B)
1 import { useState, useCallback, useRef, useEffect } from "react"; 2 3 interface SearchBarProps { 4 onSearch: (query: string) => void; 5 isSearching: boolean; 6 } 7 8 export function SearchBar({ onSearch, isSearching }: SearchBarProps) { 9 const [query, setQuery] = useState(""); 10 const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined); 11 const inputRef = useRef<HTMLInputElement>(null); 12 13 const debouncedSearch = useCallback( 14 (value: string) => { 15 if (debounceRef.current) clearTimeout(debounceRef.current); 16 debounceRef.current = setTimeout(() => { 17 onSearch(value.trim()); 18 }, 300); 19 }, 20 [onSearch], 21 ); 22 23 const handleChange = useCallback( 24 (e: React.ChangeEvent<HTMLInputElement>) => { 25 const value = e.target.value; 26 setQuery(value); 27 debouncedSearch(value); 28 }, 29 [debouncedSearch], 30 ); 31 32 const handleClear = useCallback(() => { 33 setQuery(""); 34 onSearch(""); 35 inputRef.current?.focus(); 36 }, [onSearch]); 37 38 useEffect(() => { 39 return () => { 40 if (debounceRef.current) clearTimeout(debounceRef.current); 41 }; 42 }, []); 43 44 return ( 45 <div className="relative"> 46 <input 47 ref={inputRef} 48 type="text" 49 value={query} 50 onChange={handleChange} 51 placeholder="Search messages... (Ctrl+K)" 52 aria-label="Search messages" 53 className="w-full rounded-lg border border-gray-700 bg-gray-900 px-4 py-2 pl-10 text-sm text-gray-100 placeholder-gray-500 focus:border-gray-500 focus:outline-none" 54 /> 55 <span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"> 56 🔍 57 </span> 58 {query && ( 59 <button 60 type="button" 61 onClick={handleClear} 62 className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300" 63 aria-label="Clear search" 64 > 65 ✕ 66 </button> 67 )} 68 {isSearching && ( 69 <span className="absolute right-8 top-1/2 -translate-y-1/2 text-xs text-gray-500"> 70 Searching... 71 </span> 72 )} 73 </div> 74 ); 75 }