import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { useDebounce } from '@/hooks/useDebounce'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { cn } from '@/lib/utils'; import { Search as SearchIcon, X, Clock, Music, User, List, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { logger } from '@/utils/logger'; export interface SearchResult { id: string; type: 'track' | 'user' | 'playlist'; title: string; subtitle?: string; image?: string; } export interface SearchProps { onSearch: (query: string) => void; onResultSelect?: (result: SearchResult) => void; placeholder?: string; showSuggestions?: boolean; showHistory?: boolean; maxHistoryItems?: number; fetchSuggestions?: ( query: string, ) => Promise | SearchResult[]; className?: string; debounceDelay?: number; } const HISTORY_KEY = 'veza_search_history'; const MAX_HISTORY_ITEMS = 10; /** * Composant Search avec autocomplete, suggestions, et historique. */ export function Search({ onSearch, onResultSelect, placeholder = 'Rechercher...', showSuggestions = true, showHistory = true, maxHistoryItems = MAX_HISTORY_ITEMS, fetchSuggestions, className, debounceDelay = 300, }: SearchProps) { const [query, setQuery] = useState(''); const [suggestions, setSuggestions] = useState([]); const [isLoading, setIsLoading] = useState(false); const [showDropdown, setShowDropdown] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const [history, setHistory, removeHistory] = useLocalStorage( HISTORY_KEY, [], ); const searchRef = useRef(null); const inputRef = useRef(null); // Debounce de la query pour la recherche const debouncedQuery = useDebounce(query, debounceDelay); // Fetch suggestions lorsque la query change useEffect(() => { if (!debouncedQuery.trim() || !fetchSuggestions || !showSuggestions) { setSuggestions([]); setIsLoading(false); return; } setIsLoading(true); const loadSuggestions = async () => { try { const results = await Promise.resolve(fetchSuggestions(debouncedQuery)); setSuggestions(results); } catch (error) { logger.error('Error fetching suggestions', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, query: debouncedQuery, }); setSuggestions([]); } finally { setIsLoading(false); } }; loadSuggestions(); }, [debouncedQuery, fetchSuggestions, showSuggestions]); // FIX: Ne pas appeler onSearch automatiquement à chaque changement de query // onSearch sera appelé seulement quand l'utilisateur soumet la recherche (Enter ou clic) // Cela évite de naviguer vers /search à chaque frappe // L'effet précédent qui appelait onSearch à chaque debouncedQuery a été supprimé // Gérer le clic en dehors pour fermer le dropdown useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( searchRef.current && !searchRef.current.contains(event.target as Node) ) { setShowDropdown(false); setActiveIndex(-1); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, []); const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; setQuery(value); setShowDropdown(true); setActiveIndex(-1); }; const handleInputFocus = () => { if (query.trim() || (showHistory && history.length > 0)) { setShowDropdown(true); } }; const handleInputKeyDown = (e: React.KeyboardEvent) => { const items = [ ...(showHistory && query.trim() === '' ? history.slice(0, 5) : []), ...suggestions, ]; switch (e.key) { case 'ArrowDown': e.preventDefault(); setActiveIndex((prev) => (prev < items.length - 1 ? prev + 1 : prev)); break; case 'ArrowUp': e.preventDefault(); setActiveIndex((prev) => (prev > 0 ? prev - 1 : -1)); break; case 'Enter': e.preventDefault(); if (activeIndex >= 0 && activeIndex < items.length) { if ( activeIndex < (showHistory && query.trim() === '' ? history.length : 0) ) { // C'est un item de l'historique const historyItem = history[activeIndex]; setQuery(historyItem); handleSearch(historyItem); } else { // C'est une suggestion const suggestionIndex = activeIndex - (showHistory && query.trim() === '' ? Math.min(history.length, 5) : 0); const result = suggestions[suggestionIndex]; handleResultSelect(result); } } else if (query.trim()) { handleSearch(query); } break; case 'Escape': setShowDropdown(false); setActiveIndex(-1); inputRef.current?.blur(); break; } }; const handleSearch = useCallback( (searchQuery: string) => { if (searchQuery.trim()) { // Ajouter à l'historique if (showHistory) { setHistory((prev) => { const filtered = prev.filter((item) => item !== searchQuery); const updated = [searchQuery, ...filtered].slice( 0, maxHistoryItems, ); return updated; }); } setShowDropdown(false); onSearch(searchQuery); } }, [onSearch, showHistory, setHistory, maxHistoryItems], ); const handleResultSelect = useCallback( (result: SearchResult) => { setQuery(result.title); setShowDropdown(false); setActiveIndex(-1); onResultSelect?.(result); }, [onResultSelect], ); const handleHistoryItemClick = (item: string) => { setQuery(item); handleSearch(item); }; const handleClearHistory = () => { removeHistory(); }; const handleClear = () => { setQuery(''); setSuggestions([]); setShowDropdown(false); inputRef.current?.focus(); }; const getResultIcon = (type: SearchResult['type']) => { switch (type) { case 'track': return ; case 'user': return ; case 'playlist': return ; } }; const displayItems = useMemo(() => { const items: Array<{ type: 'history' | 'suggestion'; data: string | SearchResult; }> = []; if (showHistory && query.trim() === '' && history.length > 0) { items.push( ...history .slice(0, 5) .map((item) => ({ type: 'history' as const, data: item })), ); } if (showSuggestions && suggestions.length > 0) { items.push( ...suggestions.map((result) => ({ type: 'suggestion' as const, data: result, })), ); } return items; }, [showHistory, showSuggestions, query, history, suggestions]); const shouldShowDropdown = showDropdown && (displayItems.length > 0 || isLoading) && (showSuggestions || (showHistory && query.trim() === '')); return (
{query && ( )}
{shouldShowDropdown && (
{isLoading && (
Recherche en cours...
)} {!isLoading && displayItems.length === 0 && query.trim() && (
Aucun résultat trouvé
)} {!isLoading && displayItems.length > 0 && (
{showHistory && query.trim() === '' && history.length > 0 && (
RÉCENTS
{history.slice(0, 5).map((item, index) => ( ))}
)} {showSuggestions && suggestions.length > 0 && (
{query.trim() && (
SUGGESTIONS
)} {suggestions.map((result, index) => { const itemIndex = (showHistory && query.trim() === '' ? Math.min(history.length, 5) : 0) + index; return ( ); })}
)}
)}
)}
); }