import { useRef, useEffect, useCallback, useState } from 'react' import { useStore } from '../store' import { searchGeocode } from '../api' export default function SearchBar() { const inputRef = useRef(null) const [activeIndex, setActiveIndex] = useState(-1) const debounceRef = useRef(null) const query = useStore((s) => s.query) const results = useStore((s) => s.results) const searchLoading = useStore((s) => s.searchLoading) const autocompleteOpen = useStore((s) => s.autocompleteOpen) const stops = useStore((s) => s.stops) const setQuery = useStore((s) => s.setQuery) const setResults = useStore((s) => s.setResults) const setSearchLoading = useStore((s) => s.setSearchLoading) const setAbortController = useStore((s) => s.setAbortController) const setAutocompleteOpen = useStore((s) => s.setAutocompleteOpen) const addStop = useStore((s) => s.addStop) // Focus on mount useEffect(() => { inputRef.current?.focus() }, []) const doSearch = useCallback( async (q) => { // Abort previous const prev = useStore.getState().abortController if (prev) prev.abort() if (!q.trim()) { setResults([]) setAutocompleteOpen(false) setSearchLoading(false) return } const ctrl = new AbortController() setAbortController(ctrl) setSearchLoading(true) try { const data = await searchGeocode(q.trim(), 6, ctrl.signal) setResults(data.results || []) setAutocompleteOpen(data.results?.length > 0) setActiveIndex(-1) } catch (e) { if (e.name !== 'AbortError') { setResults([]) setAutocompleteOpen(false) } } finally { setSearchLoading(false) } }, [setResults, setAutocompleteOpen, setSearchLoading, setAbortController] ) const handleChange = (e) => { const val = e.target.value setQuery(val) if (debounceRef.current) clearTimeout(debounceRef.current) debounceRef.current = setTimeout(() => doSearch(val), 150) } const selectResult = (result) => { addStop({ lat: result.lat, lon: result.lon, name: result.name, source: result.source, matchCode: result.match_code, }) setQuery('') setResults([]) setAutocompleteOpen(false) setActiveIndex(-1) inputRef.current?.focus() } const handleKeyDown = (e) => { if (!autocompleteOpen || results.length === 0) { if (e.key === 'Escape') { setAutocompleteOpen(false) } return } switch (e.key) { case 'ArrowDown': e.preventDefault() setActiveIndex((prev) => Math.min(prev + 1, results.length - 1)) break case 'ArrowUp': e.preventDefault() setActiveIndex((prev) => Math.max(prev - 1, -1)) break case 'Enter': e.preventDefault() if (activeIndex >= 0 && activeIndex < results.length) { selectResult(results[activeIndex]) } break case 'Escape': e.preventDefault() setAutocompleteOpen(false) setActiveIndex(-1) break } } const atCap = stops.length >= 10 return (
results.length > 0 && setAutocompleteOpen(true)} placeholder={atCap ? 'Max 10 stops reached' : 'Search for a place...'} disabled={atCap} className="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-cyan-400 focus:ring-1 focus:ring-cyan-400 disabled:opacity-50 disabled:cursor-not-allowed text-sm" aria-label="Search places" aria-expanded={autocompleteOpen} aria-autocomplete="list" role="combobox" /> {searchLoading && (
)}
{/* Autocomplete dropdown */} {autocompleteOpen && results.length > 0 && ( )}
) }