import React, { useState, useEffect, useRef, useCallback } from 'react'; import TextField from '../TextField/TextField'; import { BaseTextFieldProps } from '../TextField/TextFieldTypes'; import { autocompleteAddress, AddressSuggestion } from '../../../api/realEstateApi'; import styles from './AddressAutocomplete.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; interface AddressAutocompleteProps extends BaseTextFieldProps { onSelect?: (suggestion: AddressSuggestion) => void; onKeyDown?: (e: React.KeyboardEvent) => void; debounceMs?: number; minQueryLength?: number; maxSuggestions?: number; } const AddressAutocomplete: React.FC = ({ value = '', onChange, onSelect, placeholder, disabled = false, required = false, readonly = false, size = 'md', error, helperText, label, className = '', type = 'text', name, id, onKeyDown, debounceMs = 300, minQueryLength = 2, maxSuggestions = 10, ...props }) => { const { t } = useLanguage(); const [suggestions, setSuggestions] = useState([]); const [isLoading, setIsLoading] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1); const [showSuggestions, setShowSuggestions] = useState(false); const [query, setQuery] = useState(value); const [autocompleteError, setAutocompleteError] = useState(null); const containerRef = useRef(null); const suggestionsRef = useRef(null); const debounceTimerRef = useRef(null); // Sync query with value prop useEffect(() => { setQuery(value); }, [value]); // Debounced search function const performSearch = useCallback(async (searchQuery: string) => { if (searchQuery.length < minQueryLength) { if (import.meta.env.DEV) { console.log('🔍 [AddressAutocomplete] Query too short:', { query: searchQuery, length: searchQuery.length, minLength: minQueryLength }); } setSuggestions([]); setShowSuggestions(false); setIsLoading(false); return; } if (import.meta.env.DEV) { console.log('🔍 [AddressAutocomplete] Starting search:', { query: searchQuery, length: searchQuery.length, maxSuggestions: maxSuggestions }); } setIsLoading(true); setAutocompleteError(null); // Clear previous errors try { const results = await autocompleteAddress(searchQuery, maxSuggestions); if (import.meta.env.DEV) { console.log('✅ [AddressAutocomplete] Search completed:', { query: searchQuery, resultCount: results.length, results: results.slice(0, 3) // Log first 3 }); } setSuggestions(results); setShowSuggestions(results.length > 0 || true); // Show dropdown even if empty to show "no results" or error setSelectedIndex(-1); setAutocompleteError(null); // Clear any previous errors on success } catch (err: any) { console.error('❌ [AddressAutocomplete] Error in performSearch:', err); const errorMessage = err?.response?.data?.detail || err?.message || t('Fehler beim Laden der Adressvorschläge'); setAutocompleteError(errorMessage); setSuggestions([]); setShowSuggestions(true); // Show dropdown to display error setSelectedIndex(-1); } finally { setIsLoading(false); } }, [minQueryLength, maxSuggestions, t]); // Handle input change with debouncing const handleInputChange = useCallback((newValue: string) => { if (import.meta.env.DEV) { console.log('⌨️ [AddressAutocomplete] Input changed:', { newValue: newValue, length: newValue.length, willSearch: newValue.length >= minQueryLength }); } setQuery(newValue); setAutocompleteError(null); // Clear error on new input // Update parent component immediately if (onChange) { onChange(newValue); } // Clear existing timer if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } // Set new timer for debounced search debounceTimerRef.current = setTimeout(() => { if (import.meta.env.DEV) { console.log('⏱️ [AddressAutocomplete] Debounce timer fired, calling performSearch'); } performSearch(newValue); }, debounceMs); }, [onChange, debounceMs, performSearch, minQueryLength]); // Handle suggestion selection const handleSelectSuggestion = useCallback((suggestion: AddressSuggestion) => { setQuery(suggestion.value); setShowSuggestions(false); setSelectedIndex(-1); if (onChange) { onChange(suggestion.value); } if (onSelect) { onSelect(suggestion); } }, [onChange, onSelect]); // Handle keyboard navigation const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (!showSuggestions || suggestions.length === 0) { if (onKeyDown) { onKeyDown(e); } 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 && selectedIndex < suggestions.length) { e.preventDefault(); handleSelectSuggestion(suggestions[selectedIndex]); } else if (onKeyDown) { onKeyDown(e); } break; case 'Escape': e.preventDefault(); setShowSuggestions(false); setSelectedIndex(-1); break; default: if (onKeyDown) { onKeyDown(e); } } }, [showSuggestions, suggestions, selectedIndex, handleSelectSuggestion, onKeyDown]); // Click outside handler useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setShowSuggestions(false); setSelectedIndex(-1); } }; if (showSuggestions) { document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; } }, [showSuggestions]); // Scroll selected item into view useEffect(() => { if (selectedIndex >= 0 && suggestionsRef.current) { const selectedElement = suggestionsRef.current.children[selectedIndex] as HTMLElement; if (selectedElement) { selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } } }, [selectedIndex]); // Highlight matching text in suggestion const highlightText = (text: string, query: string): React.ReactNode => { if (!query || query.length < minQueryLength) { return text; } const parts = text.split(new RegExp(`(${query})`, 'gi')); return ( <> {parts.map((part, index) => part.toLowerCase() === query.toLowerCase() ? ( {part} ) : ( {part} ) )} ); }; // Cleanup on unmount useEffect(() => { return () => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } }; }, []); return (
{showSuggestions && (
    {isLoading && (
  • {t('search addresses')}
  • )} {!isLoading && autocompleteError && (
  • {autocompleteError}
  • )} {!isLoading && !autocompleteError && suggestions.length === 0 && query.length >= minQueryLength && (
  • {t('Keine Adressen gefunden')}
  • )} {!isLoading && suggestions.map((suggestion, index) => (
  • handleSelectSuggestion(suggestion)} onMouseEnter={() => setSelectedIndex(index)} > {highlightText(suggestion.label, query)}
  • ))}
)}
); }; export default AddressAutocomplete;