({ onSearch, onLocationSelect }: SearchBarProps)
| 16 | center: [number, number]; |
| 17 | } |
| 18 | |
| 19 | const SearchBar = ({ onSearch, onLocationSelect }: SearchBarProps) => { |
| 20 | const [query, setQuery] = useState(""); |
| 21 | const [results, setResults] = useState<GeocodingResult[]>([]); |
| 22 | const [showResults, setShowResults] = useState(false); |
| 23 | const debounceRef = useRef<ReturnType<typeof setTimeout>>(); |
| 24 | const containerRef = useRef<HTMLDivElement>(null); |
| 25 | |
| 26 | useEffect(() => { |
| 27 | const handleClickOutside = (e: MouseEvent) => { |
| 28 | if (containerRef.current && !containerRef.current.contains(e.target as Node)) { |
| 29 | setShowResults(false); |
| 30 | } |
| 31 | }; |
| 32 | document.addEventListener("mousedown", handleClickOutside); |
| 33 | return () => document.removeEventListener("mousedown", handleClickOutside); |
| 34 | }, []); |
| 35 | |
| 36 | const geocode = async (text: string) => { |
| 37 | if (text.length < 2) { |
| 38 | setResults([]); |
| 39 | return; |
| 40 | } |
| 41 | try { |
| 42 | const { data } = await callBackend("mapbox-geocode", { mode: "forward", query: text, limit: 4 }); |
| 43 | setResults(((data?.features as GeocodingResult[]) || []).slice(0, 4)); |
| 44 | setShowResults(true); |
| 45 | } catch { |
| 46 | setResults([]); |
| 47 | } |
| 48 | }; |
| 49 | |
| 50 | const handleChange = (value: string) => { |
| 51 | setQuery(value); |
| 52 | onSearch(value); |
| 53 | if (debounceRef.current) clearTimeout(debounceRef.current); |
| 54 | debounceRef.current = setTimeout(() => geocode(value), 300); |
| 55 | }; |
| 56 | |
| 57 | const handleSelect = (result: GeocodingResult) => { |
| 58 | setQuery(result.place_name); |
| 59 | setShowResults(false); |
| 60 | onLocationSelect?.(result.center[0], result.center[1], result.place_name); |
| 61 | }; |
| 62 | |
| 63 | return ( |
| 64 | <div className="absolute top-4 left-4 z-10" ref={containerRef}> |
| 65 | <div className="relative opacity-85"> |
| 66 | <input |
| 67 | type="text" |
| 68 | placeholder="Location…" |
| 69 | value={query} |
| 70 | onChange={(e) => handleChange(e.target.value)} |
| 71 | onFocus={() => results.length > 0 && setShowResults(true)} |
| 72 | className="w-72 backdrop-blur-sm border border-border rounded-lg px-4 py-2.5 pr-10 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" |
| 73 | style={{ backgroundColor: "#041009" }} /> |
| 74 | |
| 75 | <Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" /> |
nothing calls this directly
no test coverage detected