import { ref, watch } from 'vue'; import { addressService, TownSuggestion, StreetSuggestion } from '../services/addressService'; /** * Debounce utility function */ function debounce void>(fn: T, delay: number): T { let timeout: ReturnType; return ((...args: any[]) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); }) as T; } /** * Composable for town autocomplete functionality */ export function useTownAutocomplete() { const townSuggestions = ref([]); const isLoadingTowns = ref(false); const showTownDropdown = ref(false); const highlightedTownIndex = ref(-1); const townError = ref(''); const searchTowns = debounce(async (query: string) => { if (!query || query.length < 2) { townSuggestions.value = []; showTownDropdown.value = false; return; } isLoadingTowns.value = true; townError.value = ''; try { const response = await addressService.searchTowns(query); if (response.data.ok) { townSuggestions.value = response.data.suggestions; showTownDropdown.value = townSuggestions.value.length > 0; highlightedTownIndex.value = -1; } } catch (error) { console.error('Town search error:', error); townError.value = 'Unable to search towns'; townSuggestions.value = []; } finally { isLoadingTowns.value = false; } }, 300); const closeTownDropdown = () => { // Delay to allow click events to fire setTimeout(() => { showTownDropdown.value = false; highlightedTownIndex.value = -1; }, 150); }; const handleTownKeydown = ( event: KeyboardEvent, onSelect: (suggestion: TownSuggestion) => void ) => { if (!showTownDropdown.value || townSuggestions.value.length === 0) return; switch (event.key) { case 'ArrowDown': event.preventDefault(); highlightedTownIndex.value = Math.min( highlightedTownIndex.value + 1, townSuggestions.value.length - 1 ); break; case 'ArrowUp': event.preventDefault(); highlightedTownIndex.value = Math.max(highlightedTownIndex.value - 1, 0); break; case 'Enter': event.preventDefault(); if (highlightedTownIndex.value >= 0) { onSelect(townSuggestions.value[highlightedTownIndex.value]); } else if (townSuggestions.value.length === 1) { onSelect(townSuggestions.value[0]); } break; case 'Escape': showTownDropdown.value = false; highlightedTownIndex.value = -1; break; } }; return { townSuggestions, isLoadingTowns, showTownDropdown, highlightedTownIndex, townError, searchTowns, closeTownDropdown, handleTownKeydown, }; } /** * Composable for street/address autocomplete functionality */ export function useStreetAutocomplete() { const streetSuggestions = ref([]); const isLoadingStreets = ref(false); const showStreetDropdown = ref(false); const highlightedStreetIndex = ref(-1); const streetError = ref(''); const searchStreets = debounce(async (town: string, state: string, query: string) => { if (!town || !state || !query || query.length < 1) { streetSuggestions.value = []; showStreetDropdown.value = false; return; } isLoadingStreets.value = true; streetError.value = ''; try { const response = await addressService.searchStreets(town, state, query); if (response.data.ok) { streetSuggestions.value = response.data.suggestions; showStreetDropdown.value = streetSuggestions.value.length > 0; highlightedStreetIndex.value = -1; } } catch (error) { console.error('Street search error:', error); streetError.value = 'Unable to search addresses'; streetSuggestions.value = []; } finally { isLoadingStreets.value = false; } }, 300); const closeStreetDropdown = () => { setTimeout(() => { showStreetDropdown.value = false; highlightedStreetIndex.value = -1; }, 150); }; const handleStreetKeydown = ( event: KeyboardEvent, onSelect: (suggestion: StreetSuggestion) => void ) => { if (!showStreetDropdown.value || streetSuggestions.value.length === 0) return; switch (event.key) { case 'ArrowDown': event.preventDefault(); highlightedStreetIndex.value = Math.min( highlightedStreetIndex.value + 1, streetSuggestions.value.length - 1 ); break; case 'ArrowUp': event.preventDefault(); highlightedStreetIndex.value = Math.max(highlightedStreetIndex.value - 1, 0); break; case 'Enter': event.preventDefault(); if (highlightedStreetIndex.value >= 0) { onSelect(streetSuggestions.value[highlightedStreetIndex.value]); } else if (streetSuggestions.value.length === 1) { onSelect(streetSuggestions.value[0]); } break; case 'Escape': showStreetDropdown.value = false; highlightedStreetIndex.value = -1; break; } }; return { streetSuggestions, isLoadingStreets, showStreetDropdown, highlightedStreetIndex, streetError, searchStreets, closeStreetDropdown, handleStreetKeydown, }; }