import PropTypes from 'prop-types'; import React, { useEffect, useRef, useState } from 'react'; import { Compass, Loader2, MapPin, Navigation, X } from 'lucide-react'; import { getAddressSuggestions, geocodeAddress as serviceGeocodeAddress, standardizePlace } from '@/lib/geocodingService'; export const toPlace = (result) => standardizePlace(result); export const geocodeAddress = async (address, options = {}) => { return await serviceGeocodeAddress(address, options); }; const useDebouncedCallback = (callback, delay) => { const timeoutRef = useRef(null); return React.useCallback( (...args) => { if (timeoutRef.current) clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => { callback(...args); }, delay); }, [callback, delay] ); }; const AddressAutocomplete = ({ id, label, placeholder = 'Search address or locality…', value, onChange, onPlaceSelected, onOpenMapPin, bias, fullWidth = true, disabled = false, className = '', inputClassName = '', required = false }) => { const [inputValue, setInputValue] = useState(value || ''); const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); const [isOpen, setIsOpen] = useState(false); const [activeOption, setActiveOption] = useState(-1); const wrapRef = useRef(null); const activeRef = useRef(0); const skipNextSearchRef = useRef(false); const listId = `${id || 'address'}-suggestions`; const optionId = (index) => `${listId}-option-${index}`; useEffect(() => { if (value !== undefined && value !== inputValue) { setInputValue(value || ''); } }, [value]); const biasKey = `${bias?.lat ?? ''},${bias?.lng ?? ''}`; const fetchPredictions = useDebouncedCallback((query) => { const seq = ++activeRef.current; getAddressSuggestions(query, { bias, limit: 6 }) .then((results) => { if (seq !== activeRef.current) return; setLoading(false); setOptions(results || []); setIsOpen((results || []).length > 0); }) .catch(() => { if (seq !== activeRef.current) return; setLoading(false); setOptions([]); }); }, 220); useEffect(() => { if (skipNextSearchRef.current) { skipNextSearchRef.current = false; return; } if (!inputValue || inputValue.length < 2) { activeRef.current++; setOptions([]); setLoading(false); setIsOpen(false); return; } setLoading(true); fetchPredictions(inputValue); }, [inputValue, biasKey]); useEffect(() => { setActiveOption(-1); }, [options]); useEffect(() => { const onDocDown = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) { setIsOpen(false); } }; document.addEventListener('mousedown', onDocDown); return () => document.removeEventListener('mousedown', onDocDown); }, []); const pick = (option) => { const formatted = option.formatted_address || option.name || ''; skipNextSearchRef.current = true; activeRef.current++; setInputValue(formatted); onChange?.(formatted); onPlaceSelected?.(option); setIsOpen(false); setActiveOption(-1); }; const onKeyDown = (e) => { const listOpen = isOpen && options.length > 0; if (e.key === 'Escape') { setIsOpen(false); setActiveOption(-1); return; } if (!listOpen) return; if (e.key === 'ArrowDown') { e.preventDefault(); setActiveOption((current) => (current + 1) % options.length); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveOption((current) => (current <= 0 ? options.length - 1 : current - 1)); } else if (e.key === 'Enter' && activeOption >= 0) { e.preventDefault(); pick(options[activeOption]); } }; return (