import { useRef, useEffect, useCallback, useState, forwardRef, useImperativeHandle } from 'react'
import { MapPin, Building2, Star, Crosshair, Coffee, Fuel, ShoppingBag, Hotel, X } from 'lucide-react'
import toast from 'react-hot-toast'
import { useStore } from '../store'
import { searchGeocode } from '../api'
/** Get category icon based on result type/source */
function CategoryIcon({ result }) {
const type = result.type || ''
const source = result.source || ''
const size = 14
if (source === 'nickname') return
if (type === 'coordinates') return
if (type === 'locality' || type === 'city') return
// POI subcategories from osm_value if available
const osmVal = result.raw?.osm_value || ''
if (osmVal.includes('cafe') || osmVal.includes('coffee')) return
if (osmVal.includes('fuel') || osmVal.includes('gas')) return
if (osmVal.includes('shop') || osmVal.includes('supermarket')) return
if (osmVal.includes('hotel') || osmVal.includes('motel')) return
return
}
const SearchBar = forwardRef(function SearchBar(_, ref) {
const inputRef = useRef(null)
const [activeIndex, setActiveIndex] = useState(-1)
const debounceRef = useRef(null)
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
}))
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 pendingDestination = useStore((s) => s.pendingDestination)
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)
const setSelectedPlace = useStore((s) => s.setSelectedPlace)
const clearPendingDestination = useStore((s) => s.clearPendingDestination)
useEffect(() => {
inputRef.current?.focus()
}, [])
const doSearch = useCallback(
async (q) => {
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 handleClear = () => {
setQuery('')
setResults([])
setAutocompleteOpen(false)
inputRef.current?.focus()
}
const selectResult = (result) => {
const { pendingDestination: pending } = useStore.getState()
if (pending) {
// GPS-denied Directions flow: this result becomes the starting point
addStop({ lat: result.lat, lon: result.lon, name: result.name, source: result.source, matchCode: result.match_code })
addStop({ lat: pending.lat, lon: pending.lon, name: pending.name, source: pending.source, matchCode: pending.matchCode })
clearPendingDestination()
toast(`Routing from ${result.name} to ${pending.name}`, { icon: '\u{1F9ED}' })
} else {
// Normal flow: open PlaceDetail
setSelectedPlace({
lat: result.lat,
lon: result.lon,
name: result.name,
address: result.address || null,
type: result.type,
source: result.source,
matchCode: result.match_code,
raw: result.raw || {},
})
}
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' : pendingDestination ? 'Starting point...' : 'Search for a place...'}
disabled={atCap}
className="navi-input w-full pr-8"
aria-label="Search places"
aria-expanded={autocompleteOpen}
aria-autocomplete="list"
role="combobox"
/>
{/* Clear / Loading indicator */}
{searchLoading ? (
) : query ? (
) : null}
{/* Autocomplete dropdown */}
{autocompleteOpen && results.length > 0 && (
)}
)
})
export default SearchBar