({ value, onChange, placeholder = "Search location…" }: LocationAutocompleteProps)
| 11 | mapToken?: string; |
| 12 | } |
| 13 | |
| 14 | const LocationAutocomplete = ({ value, onChange, placeholder = "Search location…" }: LocationAutocompleteProps) => { |
| 15 | const [query, setQuery] = useState(value); |
| 16 | const [results, setResults] = useState<any[]>([]); |
| 17 | const [showResults, setShowResults] = useState(false); |
| 18 | const debounceRef = useRef<ReturnType<typeof setTimeout>>(); |
| 19 | const containerRef = useRef<HTMLDivElement>(null); |
| 20 | |
| 21 | useEffect(() => { setQuery(value); }, [value]); |
| 22 | |
| 23 | useEffect(() => { |
| 24 | const handleClickOutside = (e: MouseEvent) => { |
| 25 | if (containerRef.current && !containerRef.current.contains(e.target as Node)) { |
| 26 | setShowResults(false); |
| 27 | } |
| 28 | }; |
| 29 | document.addEventListener("mousedown", handleClickOutside); |
| 30 | return () => document.removeEventListener("mousedown", handleClickOutside); |
| 31 | }, []); |
| 32 | |
| 33 | const geocode = async (text: string) => { |
| 34 | if (text.length < 2) { setResults([]); return; } |
| 35 | try { |
| 36 | const { data } = await callBackend("mapbox-geocode", { mode: "forward", query: text, limit: 4 }); |
| 37 | setResults(data?.features || []); |
| 38 | setShowResults(true); |
| 39 | } catch { |
| 40 | setResults([]); |
| 41 | } |
| 42 | }; |
| 43 | |
| 44 | const handleChange = (val: string) => { |
| 45 | setQuery(val); |
| 46 | onChange(val); |
| 47 | if (debounceRef.current) clearTimeout(debounceRef.current); |
| 48 | debounceRef.current = setTimeout(() => geocode(val), 300); |
| 49 | }; |
| 50 | |
| 51 | return ( |
| 52 | <div className="relative" ref={containerRef}> |
| 53 | <input |
| 54 | type="text" |
| 55 | value={query} |
| 56 | onChange={e => handleChange(e.target.value)} |
| 57 | onFocus={() => results.length > 0 && setShowResults(true)} |
| 58 | placeholder={placeholder} |
| 59 | className="w-full bg-secondary/50 border border-border rounded-lg px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" |
| 60 | /> |
| 61 | {showResults && results.length > 0 && ( |
| 62 | <div className="absolute top-full mt-1 left-0 right-0 rounded-lg border border-border bg-card shadow-xl z-30 overflow-hidden"> |
| 63 | {results.map((r: any) => ( |
| 64 | <button |
| 65 | key={r.id} |
| 66 | onClick={() => { |
| 67 | setQuery(r.place_name); |
| 68 | onChange(r.place_name); |
| 69 | setShowResults(false); |
| 70 | }} |
nothing calls this directly
no test coverage detected