import { useState, useEffect, useRef } from 'react' import { MapPinIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline' import { stateNameToCode } from '../utils/stateMapping' import { useLocation as useLocationContext } from '../contexts/LocationContext' interface LocationData { address: string state: string county: string city: string latitude?: number longitude?: number } interface AddressLookupProps { onLocationFound: (location: LocationData) => void initialAddress?: string compact?: boolean } export default function AddressLookup({ onLocationFound, initialAddress = '', compact = false }: AddressLookupProps) { const { clearLocation } = useLocationContext() const [address, setAddress] = useState(initialAddress) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const [suggestions, setSuggestions] = useState([]) const [foundLocation, setFoundLocation] = useState(null) const [showSuggestions, setShowSuggestions] = useState(false) const [selectedIndex, setSelectedIndex] = useState(-1) const debounceTimer = useRef(null) const inputRef = useRef(null) // Fetch suggestions as user types const fetchSuggestions = async (query: string) => { if (query.trim().length < 3) { setSuggestions([]) setShowSuggestions(false) return } try { const response = await fetch( `https://nominatim.openstreetmap.org/search?` + `q=${encodeURIComponent(query)}&` + `format=json&` + `addressdetails=1&` + `countrycodes=us&` + `limit=5`, { headers: { 'User-Agent': 'CommunityOne-Navigator/1.0' } } ) if (!response.ok) { return } const data = await response.json() // Deduplicate results using OSM unique IDs const uniqueResults = data.reduce((acc: any[], current: any) => { const osmKey = `${current.osm_type}_${current.osm_id}` const exists = acc.some((item) => { const itemKey = `${item.osm_type}_${item.osm_id}` return itemKey === osmKey }) if (!exists) { acc.push(current) } return acc }, []) setSuggestions(uniqueResults) setShowSuggestions(uniqueResults.length > 0) setSelectedIndex(-1) } catch (err) { console.error('Autocomplete error:', err) } } // Handle address input change with debouncing const handleAddressChange = (value: string) => { setAddress(value) setError(null) // Clear previous timer if (debounceTimer.current) { clearTimeout(debounceTimer.current) } // Set new timer debounceTimer.current = setTimeout(() => { fetchSuggestions(value) }, 300) } // Cleanup timer on unmount useEffect(() => { return () => { if (debounceTimer.current) { clearTimeout(debounceTimer.current) } } }, []) const lookupAddress = async (addressToLookup: string) => { if (!addressToLookup.trim()) { setError('Please enter an address') return } setIsLoading(true) setError(null) setSuggestions([]) setShowSuggestions(false) try { // Use Nominatim (OpenStreetMap) geocoding service const response = await fetch( `https://nominatim.openstreetmap.org/search?` + `q=${encodeURIComponent(addressToLookup)}&` + `format=json&` + `addressdetails=1&` + `countrycodes=us&` + `limit=5`, { headers: { 'User-Agent': 'CommunityOne-Navigator/1.0' } } ) if (!response.ok) { throw new Error('Failed to lookup address') } const data = await response.json() if (data.length === 0) { setError('Address not found. Please try a different address or be more specific.') return } // Deduplicate results using OSM unique IDs const uniqueResults = data.reduce((acc: any[], current: any) => { // Use OSM type + ID as unique key (most reliable) const osmKey = `${current.osm_type}_${current.osm_id}` const exists = acc.some((item) => { const itemKey = `${item.osm_type}_${item.osm_id}` return itemKey === osmKey }) if (!exists) { acc.push(current) } return acc }, []) // If we have multiple unique results, show suggestions if (uniqueResults.length > 1) { setSuggestions(uniqueResults) setShowSuggestions(true) return } // Single result - process it processResult(uniqueResults[0]) } catch (err) { console.error('Address lookup error:', err) setError('Failed to lookup address. Please try again.') } finally { setIsLoading(false) } } const processResult = (result: any) => { const addr = result.address // Convert state name to 2-letter code const stateName = addr.state || '' const stateCode = stateNameToCode(stateName) console.log(`πŸ—ΊοΈ [AddressLookup] State conversion: "${stateName}" β†’ "${stateCode}"`) const locationData: LocationData = { address: result.display_name, state: stateCode, county: addr.county || '', city: addr.city || addr.town || addr.village || addr.municipality || '', latitude: parseFloat(result.lat), longitude: parseFloat(result.lon), } // Validate we got the essential data if (!locationData.state || !locationData.city) { setError('Could not determine city and state from this address. Please be more specific.') setSuggestions([]) setShowSuggestions(false) return } console.log('πŸ“ [AddressLookup] Location found:', locationData) setSuggestions([]) setShowSuggestions(false) setFoundLocation(locationData) onLocationFound(locationData) } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() // If a suggestion is selected, use that if (selectedIndex >= 0 && suggestions[selectedIndex]) { processResult(suggestions[selectedIndex]) } else { lookupAddress(address) } } const handleSuggestionClick = (suggestion: any) => { setAddress(suggestion.display_name) processResult(suggestion) } const handleKeyDown = (e: React.KeyboardEvent) => { if (!showSuggestions || suggestions.length === 0) return switch (e.key) { case 'ArrowDown': e.preventDefault() setSelectedIndex(prev => prev < suggestions.length - 1 ? prev + 1 : prev ) break case 'ArrowUp': e.preventDefault() setSelectedIndex(prev => prev > 0 ? prev - 1 : -1) break case 'Enter': if (selectedIndex >= 0) { e.preventDefault() processResult(suggestions[selectedIndex]) } break case 'Escape': setShowSuggestions(false) setSelectedIndex(-1) break } } const useMyLocation = () => { if (!navigator.geolocation) { setError('Geolocation is not supported by your browser') return } setIsLoading(true) setError(null) setSuggestions([]) navigator.geolocation.getCurrentPosition( async (position) => { const { latitude, longitude } = position.coords try { // Reverse geocode using Nominatim const response = await fetch( `https://nominatim.openstreetmap.org/reverse?` + `lat=${latitude}&` + `lon=${longitude}&` + `format=json&` + `addressdetails=1`, { headers: { 'User-Agent': 'CommunityOne-Navigator/1.0' } } ) if (!response.ok) { throw new Error('Failed to reverse geocode location') } const data = await response.json() // Update the address input field setAddress(data.display_name) // Process the result processResult(data) } catch (err) { console.error('Reverse geocoding error:', err) setError('Failed to determine your location. Please enter your address manually.') } finally { setIsLoading(false) } }, (error) => { console.error('Geolocation error:', error) setIsLoading(false) switch (error.code) { case error.PERMISSION_DENIED: setError('Location access denied. Please enter your address manually or enable location permissions.') break case error.POSITION_UNAVAILABLE: setError('Location information unavailable. Please enter your address manually.') break case error.TIMEOUT: setError('Location request timed out. Please try again or enter your address manually.') break default: setError('An error occurred while getting your location. Please enter your address manually.') } }, { enableHighAccuracy: false, // Use fast network-based location instead of GPS timeout: 5000, // Reduced timeout since network location is faster maximumAge: 30000 // Allow 30s cached location for faster response } ) } if (compact) { return (
handleAddressChange(e.target.value)} onKeyDown={handleKeyDown} placeholder="Enter your address..." className="w-full px-4 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-900" disabled={isLoading} autoComplete="off" /> {/* Autocomplete suggestions dropdown */} {showSuggestions && suggestions.length > 0 && (
{suggestions.map((suggestion, index) => { const addr = suggestion.address const locationName = addr.city || addr.town || addr.village || addr.county || 'Unknown' return ( ) })}
)}
{error && (

{error}

)}
) } return (
handleAddressChange(e.target.value)} onKeyDown={handleKeyDown} placeholder="123 Main St, Los Angeles, CA 90001" className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-base text-gray-900" disabled={isLoading} autoComplete="off" /> {/* Autocomplete suggestions dropdown */} {showSuggestions && suggestions.length > 0 && (
{suggestions.map((suggestion, index) => { const addr = suggestion.address const locationName = addr.city || addr.town || addr.village || addr.county || 'Unknown' return ( ) })}
)}

We'll find your local organizations based on your address

{/* Use My Location Button */}
{/* Divider */}
or enter manually
{/* Error Message */} {error && (

{error}

)} {/* Note: Suggestions now appear as autocomplete dropdown above */} {/* Location Results */} {foundLocation && !compact && (

Your Local Community

Select a jurisdiction level below to explore organizations, meeting minutes, and contacts:

{/* City */} {foundLocation.city && ( )} {/* County */} {foundLocation.county && ( )} {/* State */} {foundLocation.state && ( )} {/* School District */} {foundLocation.city && ( )}
{/* Action Buttons */}

Quick access to all local resources:

{/* Start Over */}
)}
) }