mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 15:11:09 -05:00
105 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
} |