'use client'; import React, { useState, useEffect, useRef } from 'react'; import { API_PATHS } from '../types/api'; interface SearchSuggestion { id: string; text: string; type: 'song' | 'album' | 'artist'; entity?: { id: string; title: string; artist?: string; album?: string; }; } interface SearchBarProps { onSearch?: (query: string) => void; onSuggestionSelect?: (suggestion: SearchSuggestion) => void; placeholder?: string; autoFocus?: boolean; showSuggestions?: boolean; debounceMs?: number; className?: string; } export default function SearchBar({ onSearch, onSuggestionSelect, placeholder = 'Search songs, artists, albums...', autoFocus = false, showSuggestions = true, debounceMs = 300, className = '', }: SearchBarProps) { const [query, setQuery] = useState(''); const [suggestions, setSuggestions] = useState([]); const [isLoading, setIsLoading] = useState(false); const [showSuggestionsList, setShowSuggestionsList] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const inputRef = useRef(null); const debounceTimeoutRef = useRef(null); const abortControllerRef = useRef(null); // Fetch search suggestions const fetchSuggestions = async (searchQuery: string) => { if (!searchQuery.trim()) { setSuggestions([]); return; } // Cancel previous request if (abortControllerRef.current) { abortControllerRef.current.abort(); } abortControllerRef.current = new AbortController(); setIsLoading(true); try { const response = await fetch( `${API_PATHS.SEARCH_SUGGESTIONS}?q=${encodeURIComponent(searchQuery)}&limit=8`, { signal: abortControllerRef.current.signal, } ); if (!response.ok) { throw new Error('Failed to fetch suggestions'); } // Assuming the API returns an array of suggestions // The actual structure would depend on the backend implementation const data = await response.json(); const suggestionsData: SearchSuggestion[] = data.map((item: any) => ({ id: item.id || Math.random().toString(36), text: item.title || item.name || item.text, type: item.type || 'song', entity: item, })); setSuggestions(suggestionsData); } catch (error) { if (error instanceof Error && error.name !== 'AbortError') { console.error('Failed to fetch suggestions:', error); } } finally { setIsLoading(false); } }; // Handle input change with debouncing const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; setQuery(value); setActiveIndex(-1); // Clear existing timeout if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } // Debounce the search debounceTimeoutRef.current = setTimeout(() => { if (showSuggestions) { fetchSuggestions(value); } onSearch?.(value); }, debounceMs); }; // Handle form submission const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setShowSuggestionsList(false); onSearch?.(query); // Trigger immediate search if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } onSearch?.(query); }; // Handle suggestion click const handleSuggestionClick = (suggestion: SearchSuggestion) => { setQuery(suggestion.text); setShowSuggestionsList(false); onSuggestionSelect?.(suggestion); onSearch?.(suggestion.text); }; // Handle keyboard navigation const handleKeyDown = (e: React.KeyboardEvent) => { if (!showSuggestionsList || suggestions.length === 0) return; switch (e.key) { case 'ArrowDown': e.preventDefault(); setActiveIndex(prev => (prev + 1) % suggestions.length); break; case 'ArrowUp': e.preventDefault(); setActiveIndex(prev => (prev - 1 + suggestions.length) % suggestions.length); break; case 'Enter': e.preventDefault(); if (activeIndex >= 0) { handleSuggestionClick(suggestions[activeIndex]); } else { handleSubmit(e); } break; case 'Escape': setShowSuggestionsList(false); setActiveIndex(-1); inputRef.current?.blur(); break; } }; // Handle input focus const handleFocus = () => { if (showSuggestions && query.trim()) { setShowSuggestionsList(true); } }; // Handle input blur const handleBlur = () => { // Delay hiding suggestions to allow click events to fire setTimeout(() => { setShowSuggestionsList(false); }, 150); }; // Clear search const handleClear = () => { setQuery(''); setSuggestions([]); setActiveIndex(-1); setShowSuggestionsList(false); onSearch?.(''); inputRef.current?.focus(); }; // Clean up on unmount useEffect(() => { return () => { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); // Get suggestion icon const getSuggestionIcon = (type: string) => { switch (type) { case 'song': return ( ); case 'album': return ( ); case 'artist': return ( ); default: return ( ); } }; return (
{/* Search icon */}
{isLoading ? ( ) : ( )}
{/* Input field */} {/* Clear button */} {query && ( )}
{/* Suggestions dropdown */} {showSuggestionsList && suggestions.length > 0 && (
{suggestions.map((suggestion, index) => ( ))} {/* Search for full query option */} {query.trim() && ( )}
)}
); }