turash/bugulma/frontend/components/map/SearchSuggestions.tsx
2025-12-15 10:06:41 +01:00

168 lines
5.5 KiB
TypeScript

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Card } from '@/components/ui/Card';
import Spinner from '@/components/ui/Spinner';
import { useTranslation } from '@/hooks/useI18n';
import { useKeyboard } from '@/hooks/useKeyboard';
import { Search } from 'lucide-react';
interface SearchSuggestionsProps {
suggestions: string[];
isLoading: boolean;
error?: string | null;
onSelect: (suggestion: string) => void;
searchTerm: string;
}
const SearchSuggestions = ({
suggestions,
isLoading,
error,
onSelect,
searchTerm,
}: SearchSuggestionsProps) => {
const { t } = useTranslation();
const [selectedIndex, setSelectedIndex] = useState(-1);
const suggestionRefs = useRef<(HTMLButtonElement | null)[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const showSuggestions = isLoading || error || (suggestions && suggestions.length > 0);
const hasResults = suggestions && suggestions.length > 0;
// Reset selected index when suggestions change
useEffect(() => {
setSelectedIndex(-1);
}, [suggestions]);
// Handle keyboard navigation
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (!showSuggestions) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex((prev) => (prev < suggestions.length - 1 ? prev + 1 : prev));
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex((prev) => (prev > -1 ? prev - 1 : -1));
break;
case 'Enter':
if (selectedIndex >= 0 && suggestions[selectedIndex]) {
e.preventDefault();
onSelect(suggestions[selectedIndex]);
}
break;
case 'Escape':
// Let parent handle escape to close suggestions
break;
}
},
[showSuggestions, suggestions, selectedIndex, onSelect]
);
useKeyboard(handleKeyDown);
// Scroll selected item into view
useEffect(() => {
if (selectedIndex >= 0 && suggestionRefs.current[selectedIndex]) {
suggestionRefs.current[selectedIndex]?.scrollIntoView({
block: 'nearest',
behavior: 'smooth',
});
}
}, [selectedIndex]);
if (!showSuggestions) {
return null;
}
return (
<Card
ref={containerRef}
className="absolute top-full mt-1 w-full z-50 shadow-xl border border-border/50 bg-background/95 backdrop-blur-sm max-h-80 overflow-hidden"
>
{/* Loading State */}
{isLoading && (
<div className="p-4 flex items-center justify-center text-muted-foreground">
<Spinner className="h-4 w-4 mr-3" />
<span className="text-sm">{t('searchSuggestions.searching', 'Searching...')}</span>
</div>
)}
{/* Error State */}
{error && !isLoading && (
<div className="p-4 flex items-center justify-center text-destructive">
<span className="text-sm">{error}</span>
</div>
)}
{/* No Results State */}
{!isLoading && !error && !hasResults && searchTerm.length >= 2 && (
<div className="p-4 text-center text-muted-foreground">
<Search className="h-4 h-8 mb-2 mx-auto opacity-50 text-current w-4 w-8" />
<p className="text-sm font-medium">
{t('searchSuggestions.noResults', 'No results found')}
</p>
<p className="text-xs mt-1">
{t('searchSuggestions.tryDifferent', 'Try a different search term')}
</p>
</div>
)}
{/* Suggestions List */}
{!isLoading && !error && hasResults && (
<div className="max-h-64 overflow-y-auto">
<div className="px-2 py-1 border-b border-border/50">
<p className="text-xs text-muted-foreground font-medium px-2">
{t('searchSuggestions.suggestions', 'Suggestions')}
</p>
</div>
<ul className="py-1">
{suggestions.map((suggestion, index) => {
const isSelected = index === selectedIndex;
return (
<li key={suggestion}>
<button
ref={(el) => (suggestionRefs.current[index] = el)}
onClick={() => onSelect(suggestion)}
onMouseEnter={() => setSelectedIndex(index)}
className={`
w-full text-left px-4 py-3 text-sm transition-all duration-150
hover:bg-accent/80 focus:bg-accent focus:outline-none
${
isSelected
? 'bg-accent text-accent-foreground shadow-sm'
: 'text-foreground hover:text-accent-foreground'
}
`}
>
<div className="flex items-center gap-3">
<Search className="flex-shrink-0 h-4 text-current text-muted-foreground w-4" />
<span className="truncate">{suggestion}</span>
</div>
</button>
</li>
);
})}
</ul>
</div>
)}
{/* Footer with keyboard hints */}
{!isLoading && hasResults && (
<div className="px-4 py-2 border-t border-border/50 bg-muted/30">
<p className="text-xs text-muted-foreground text-center">
{t(
'searchSuggestions.keyboardHint',
'Use ↑↓ to navigate, Enter to select, Esc to close'
)}
</p>
</div>
)}
</Card>
);
};
export default React.memo(SearchSuggestions);