import { useRef, useEffect, useState } from 'react'; import { Search, X, Clock, Zap, Castle, FerrisWheel, Factory } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { useSearch, SearchResult } from '@/hooks/useSearch'; import { useNavigate } from 'react-router-dom'; import { useToast } from '@/components/ui/use-toast'; interface AutocompleteSearchProps { onResultSelect?: (result: SearchResult) => void; onSearch?: (query: string) => void; placeholder?: string; className?: string; types?: ('park' | 'ride' | 'company')[]; limit?: number; showRecentSearches?: boolean; variant?: 'default' | 'hero'; } const DEFAULT_SEARCH_TYPES: ('park' | 'ride' | 'company')[] = ['park', 'ride', 'company']; export function AutocompleteSearch({ onResultSelect, onSearch, placeholder = "Search parks, rides, or companies...", className = "", types = DEFAULT_SEARCH_TYPES, limit = 8, showRecentSearches = true, variant = 'default' }: AutocompleteSearchProps) { const navigate = useNavigate(); const { toast } = useToast(); const searchRef = useRef(null); const inputRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1); const { query, setQuery, results, suggestions, loading, saveSearch, clearRecentSearches } = useSearch({ types, limit }); const displayItems = query.length > 0 ? results : (showRecentSearches ? suggestions : []); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (searchRef.current && !searchRef.current.contains(event.target as Node)) { setIsOpen(false); setSelectedIndex(-1); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // Handle keyboard navigation useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (!isOpen) return; switch (event.key) { case 'ArrowDown': event.preventDefault(); setSelectedIndex(prev => Math.min(prev + 1, displayItems.length - 1)); break; case 'ArrowUp': event.preventDefault(); setSelectedIndex(prev => Math.max(prev - 1, -1)); break; case 'Enter': event.preventDefault(); if (selectedIndex >= 0 && displayItems[selectedIndex]) { handleResultClick(displayItems[selectedIndex]); } else if (query) { handleSearch(); } break; case 'Escape': setIsOpen(false); setSelectedIndex(-1); inputRef.current?.blur(); break; } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, selectedIndex, displayItems, query]); const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; setQuery(value); setIsOpen(true); setSelectedIndex(-1); }; const handleInputFocus = () => { setIsOpen(true); }; type SearchResultOrSuggestion = SearchResult | { id: string; type: 'suggestion'; title: string; subtitle: string; data: Record | null; }; const handleResultClick = (result: SearchResultOrSuggestion) => { if (result.type === 'suggestion') { setQuery(result.title); setIsOpen(false); return; } const searchResult = result as SearchResult; saveSearch(searchResult.title); if (onResultSelect) { onResultSelect(searchResult); } else { // Default navigation with null/undefined safety checks switch (searchResult.type) { case 'park': const parkIdentifier = searchResult.slug || searchResult.id; if (parkIdentifier) { navigate(`/parks/${parkIdentifier}`); } else { toast({ title: "Navigation Error", description: "Unable to navigate to this park. Missing park identifier.", variant: "destructive", }); navigate(`/search?q=${encodeURIComponent(searchResult.title)}`); } break; case 'ride': const parkSlug = (searchResult.data as { park?: { slug?: string } })?.park?.slug; const rideSlug = searchResult.slug; const rideId = searchResult.id; if (parkSlug && rideSlug) { navigate(`/parks/${parkSlug}/rides/${rideSlug}`); } else if (rideId) { navigate(`/rides/${rideId}`); } else { toast({ title: "Navigation Error", description: "Unable to navigate to this ride. Missing ride identifier.", variant: "destructive", }); navigate(`/search?q=${encodeURIComponent(searchResult.title)}`); } break; case 'company': const companyType = (searchResult.data as { company_type?: string })?.company_type; const companySlug = searchResult.slug; if (companyType && companySlug) { switch (companyType) { case 'operator': navigate(`/operators/${companySlug}`); break; case 'property_owner': navigate(`/owners/${companySlug}`); break; case 'manufacturer': navigate(`/manufacturers/${companySlug}`); break; case 'designer': navigate(`/designers/${companySlug}`); break; default: toast({ title: "Unknown Company Type", description: `Unable to navigate to this company type. Showing search results instead.`, variant: "default", }); navigate(`/search?q=${encodeURIComponent(searchResult.title)}`); } } else { toast({ title: "Navigation Error", description: "Unable to navigate to this company. Missing required information.", variant: "destructive", }); navigate(`/search?q=${encodeURIComponent(searchResult.title)}`); } break; } } setIsOpen(false); setSelectedIndex(-1); }; const handleSearch = () => { if (!query.trim()) return; saveSearch(query); if (onSearch) { onSearch(query); } else { // Default search behavior - navigate to search results navigate(`/search?q=${encodeURIComponent(query)}`); } setIsOpen(false); setSelectedIndex(-1); }; const handleClear = () => { setQuery(''); setIsOpen(false); setSelectedIndex(-1); inputRef.current?.focus(); }; const getResultIcon = (result: SearchResult) => { switch (result.type) { case 'park': return ; case 'ride': return ; case 'company': return ; default: return ; } }; const getTypeColor = (type: string) => { switch (type) { case 'park': return 'bg-primary/10 text-primary border-primary/20'; case 'ride': return 'bg-secondary/10 text-secondary border-secondary/20'; case 'company': return 'bg-accent/10 text-accent border-accent/20'; default: return 'bg-muted text-muted-foreground'; } }; const getTypeLabel = (type: string) => { switch (type) { case 'park': return 'Park'; case 'ride': return 'Ride'; case 'company': return 'Manufacturer'; default: return type.charAt(0).toUpperCase() + type.slice(1); } }; const isHero = variant === 'hero'; return (
e.key === 'Enter' && handleSearch()} className={`w-full ${isHero ? 'pl-12 pr-28 sm:pr-32 h-14 text-base sm:text-lg rounded-full' : 'pl-10 pr-10'} bg-background/95 backdrop-blur border-border/50 focus:border-primary/50 transition-all duration-300 ${ isOpen ? 'shadow-lg shadow-primary/10' : '' }`} /> {query && ( )} {isHero && ( )}
{isOpen && (displayItems.length > 0 || loading) && (
{query.length === 0 && showRecentSearches && suggestions.length > 0 && ( <>
Recent searches
)} {displayItems.length > 0 && displayItems.map((item, index) => (
handleResultClick(item)} className={`flex items-start gap-2 sm:gap-3 p-2 sm:p-3 rounded-lg cursor-pointer transition-colors ${ index === selectedIndex ? 'bg-accent/10 border border-accent/20' : 'hover:bg-muted/50' }`} > {item.type === 'suggestion' ? ( <>
{item.title}
) : ( <>
{item.title}
{getTypeLabel(item.type)} {item.subtitle && ( {item.subtitle} )}
{((item as SearchResult).rating ?? 0) > 0 && (
{(item as SearchResult).rating!.toFixed(1)}
)} )}
))} {loading && (
Searching...
)} {query.length > 0 && results.length > 0 && !loading && ( <> )}
)}
); }