import { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { createPortal } from 'react-dom'; import type { Destination } from '../../hooks/useTravelDestinations'; import { useDropdownPosition } from '../../hooks/useDropdownPosition'; import { MapPinIcon } from './icons/MapPinIcon'; import { ChevronIcon } from './icons/ChevronIcon'; import { CloseIcon } from './icons/CloseIcon'; interface DestinationDropdownProps { destinations: Destination[]; loading: boolean; onSelect: (slug: string, label: string, lat: number, lon: number) => void; onClear?: () => void; value?: string; placeholder?: string; } export function DestinationDropdown({ destinations, loading, onSelect, onClear, value, placeholder, }: DestinationDropdownProps) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [filter, setFilter] = useState(''); const [activeIndex, setActiveIndex] = useState(-1); const containerRef = useRef(null); const dropdownRef = useRef(null); const inputRef = useRef(null); const listRef = useRef(null); const pos = useDropdownPosition(containerRef, open); const filtered = useMemo(() => { if (!filter) return destinations; const lower = filter.toLowerCase(); return destinations.filter( (d) => d.name.toLowerCase().includes(lower) || d.city?.toLowerCase().includes(lower) ); }, [destinations, filter]); // Close on outside click useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(e.target as Node) && !dropdownRef.current?.contains(e.target as Node) ) { setOpen(false); setFilter(''); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); // Scroll active item into view useEffect(() => { if (activeIndex < 0 || !listRef.current) return; const item = listRef.current.children[activeIndex] as HTMLElement; item?.scrollIntoView({ block: 'nearest' }); }, [activeIndex]); const handleSelect = useCallback( (dest: Destination) => { onSelect(dest.slug, dest.name, dest.lat, dest.lon); setOpen(false); setFilter(''); setActiveIndex(-1); }, [onSelect] ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIndex((prev) => (prev < filtered.length - 1 ? prev + 1 : prev)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIndex((prev) => (prev > 0 ? prev - 1 : -1)); } else if (e.key === 'Enter') { e.preventDefault(); if (activeIndex >= 0 && activeIndex < filtered.length) { handleSelect(filtered[activeIndex]); } } else if (e.key === 'Escape') { setOpen(false); setFilter(''); } }, [filtered, activeIndex, handleSelect] ); const handleOpen = useCallback(() => { setOpen(true); setActiveIndex(-1); // Focus input after opening requestAnimationFrame(() => inputRef.current?.focus()); }, []); const dropdown = open && (
{/* Filter input */}
{ setFilter(e.target.value); setActiveIndex(-1); }} onKeyDown={handleKeyDown} placeholder={t('travel.typeToFilter')} className="w-full px-2 py-1 text-xs rounded border border-warm-200 dark:border-warm-600 bg-warm-50 dark:bg-warm-900 text-navy-950 dark:text-warm-200 placeholder-warm-400 dark:placeholder-warm-500 outline-none focus:ring-1 focus:ring-teal-400" />
{/* Results list */}
{filtered.length === 0 ? (
{loading ? t('common.loading') : t('travel.noDestinations')}
) : ( filtered.map((dest, idx) => ( )) )}
); return (
{value && onClear ? ( ) : ( )}
{open && createPortal(dropdown, document.body)}
); }