Files
thrillwiki_django_no_react/frontend/src/components/parks/ParkSearch.tsx

105 lines
3.3 KiB
TypeScript

'use client';
import { useState, useCallback } from 'react';
import debounce from 'lodash/debounce';
interface ParkSearchProps {
onSearch: (query: string) => void;
}
export function ParkSearch({ onSearch }: ParkSearchProps) {
const [query, setQuery] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [suggestions, setSuggestions] = useState<Array<{ id: string; name: string; slug: string }>>([]);
const debouncedFetchSuggestions = useCallback(
debounce(async (searchQuery: string) => {
if (!searchQuery.trim()) {
setSuggestions([]);
return;
}
try {
setIsLoading(true);
const response = await fetch(`/api/parks/suggest?search=${encodeURIComponent(searchQuery)}`);
const data = await response.json();
if (data.success) {
setSuggestions(data.data || []);
}
} catch (error) {
console.error('Failed to fetch suggestions:', error);
setSuggestions([]);
} finally {
setIsLoading(false);
}
}, 300),
[]
);
const handleSearch = (searchQuery: string) => {
setQuery(searchQuery);
debouncedFetchSuggestions(searchQuery);
onSearch(searchQuery);
};
const handleSuggestionClick = (suggestion: { name: string; slug: string }) => {
setQuery(suggestion.name);
setSuggestions([]);
onSearch(suggestion.name);
};
return (
<div className="max-w-3xl mx-auto relative mb-8">
<div className="w-full relative">
<div className="relative">
<input
type="search"
value={query}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search parks..."
className="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
aria-label="Search parks"
aria-controls="search-results"
aria-expanded={suggestions.length > 0}
/>
{isLoading && (
<div
className="absolute right-3 top-1/2 -translate-y-1/2"
role="status"
aria-label="Loading search results"
>
<svg className="w-5 h-5 text-gray-400 animate-spin" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
<span className="sr-only">Searching...</span>
</div>
)}
</div>
{suggestions.length > 0 && (
<div
id="search-results"
className="absolute z-50 mt-1 w-full bg-white dark:bg-gray-800 rounded-md shadow-lg"
role="listbox"
>
<ul>
{suggestions.map((suggestion) => (
<li
key={suggestion.id}
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
role="option"
onClick={() => handleSuggestionClick(suggestion)}
>
{suggestion.name}
</li>
))}
</ul>
</div>
)}
</div>
</div>
);
}