'use client'; import React, { useState, useEffect, useRef, useCallback } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { useRouter } from 'next/navigation'; import { Search, X, Music, Disc, User, Clock, Heart, Play, Plus, ExternalLink, Info, Star, TrendingUp, Users, Calendar, Globe } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import { useNavidrome } from '@/app/components/NavidromeContext'; import { useAudioPlayer } from '@/app/components/AudioPlayerContext'; import { lastFmAPI } from '@/lib/lastfm-api'; import { Song, Album, Artist, getNavidromeAPI } from '@/lib/navidrome'; import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'; import Image from 'next/image'; interface SpotlightSearchProps { isOpen: boolean; onClose: () => void; } interface LastFmTrackInfo { name: string; artist: { name: string; url: string; }; album?: { title: string; image?: string; }; wiki?: { summary: string; content: string; }; duration?: string; playcount?: string; listeners?: string; tags?: Array<{ name: string; url: string; }>; } interface LastFmTag { name: string; url: string; } interface LastFmArtist { name: string; url: string; image?: Array<{ '#text': string; size: string }>; } interface LastFmBio { summary: string; content: string; } interface LastFmStats { listeners: string; playcount: string; } interface LastFmArtistInfo { name: string; bio?: LastFmBio; stats?: LastFmStats; tags?: { tag: LastFmTag[]; }; similar?: { artist: LastFmArtist[]; }; image?: Array<{ '#text': string; size: string }>; } interface SearchResult { type: 'track' | 'album' | 'artist'; id: string; title: string; subtitle: string; image?: string; data: Song | Album | Artist; lastFmData?: LastFmArtistInfo; } export function SpotlightSearch({ isOpen, onClose }: SpotlightSearchProps) { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [selectedIndex, setSelectedIndex] = useState(0); const [isLoading, setIsLoading] = useState(false); const [showDetails, setShowDetails] = useState(false); const [selectedResult, setSelectedResult] = useState(null); const [lastFmDetails, setLastFmDetails] = useState(null); const inputRef = useRef(null); const resultsRef = useRef(null); const router = useRouter(); const api = getNavidromeAPI(); const { search2 } = useNavidrome(); const { playTrack, addToQueue, insertAtBeginningOfQueue } = useAudioPlayer(); // Convert Song to Track with proper URL generation const songToTrack = useCallback((song: Song) => { if (!api) { throw new Error('Navidrome API not configured'); } return { id: song.id, name: song.title, url: api.getStreamUrl(song.id), artist: song.artist, album: song.album, duration: song.duration, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 512) : undefined, albumId: song.albumId, artistId: song.artistId, starred: !!song.starred, replayGain: song.replayGain || 0 }; }, [api]); // Focus input when opened useEffect(() => { if (isOpen && inputRef.current) { inputRef.current.focus(); } }, [isOpen]); // Close on escape useKeyboardShortcuts({ disabled: !isOpen }); // Handle keyboard navigation useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault(); setSelectedIndex(prev => Math.min(prev + 1, results.length - 1)); break; case 'ArrowUp': e.preventDefault(); setSelectedIndex(prev => Math.max(prev - 1, 0)); break; case 'Enter': e.preventDefault(); if (results[selectedIndex]) { handleResultSelect(results[selectedIndex]); } break; case 'Escape': e.preventDefault(); if (showDetails) { setShowDetails(false); setSelectedResult(null); } else { onClose(); } break; case 'Tab': e.preventDefault(); if (results[selectedIndex]) { handleShowDetails(results[selectedIndex]); } break; } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, results, selectedIndex, showDetails, onClose]); // Search function with debouncing const performSearch = useCallback(async (searchQuery: string) => { if (!searchQuery.trim()) { setResults([]); return; } setIsLoading(true); try { const searchResults = await search2(searchQuery); const formattedResults: SearchResult[] = []; // Add tracks searchResults.songs?.forEach(song => { formattedResults.push({ type: 'track', id: song.id, title: song.title, subtitle: `${song.artist} • ${song.album}`, image: song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 256) : undefined, data: song }); }); // Add albums searchResults.albums?.forEach(album => { formattedResults.push({ type: 'album', id: album.id, title: album.name, subtitle: `${album.artist} • ${album.songCount} tracks`, image: album.coverArt && api ? api.getCoverArtUrl(album.coverArt, 256) : undefined, data: album }); }); // Add artists searchResults.artists?.forEach(artist => { formattedResults.push({ type: 'artist', id: artist.id, title: artist.name, subtitle: `${artist.albumCount} albums`, image: artist.coverArt && api ? api.getCoverArtUrl(artist.coverArt, 256) : undefined, data: artist }); }); setResults(formattedResults); setSelectedIndex(0); } catch (error) { console.error('Search failed:', error); setResults([]); } finally { setIsLoading(false); } }, [search2]); // Debounced search useEffect(() => { const timeoutId = setTimeout(() => { performSearch(query); }, 300); return () => clearTimeout(timeoutId); }, [query, performSearch]); const handleResultSelect = (result: SearchResult) => { switch (result.type) { case 'track': const songData = result.data as Song; const track = songToTrack(songData); playTrack(track, true); onClose(); break; case 'album': router.push(`/album/${result.id}`); onClose(); break; case 'artist': router.push(`/artist/${result.id}`); onClose(); break; } }; const handleShowDetails = async (result: SearchResult) => { setSelectedResult(result); setShowDetails(true); setLastFmDetails(null); // Fetch Last.fm data try { let lastFmData = null; if (result.type === 'artist') { const artistData = result.data as Artist; lastFmData = await lastFmAPI.getArtistInfo(artistData.name); } else if (result.type === 'album') { // For albums, get artist info as Last.fm album info is limited const albumData = result.data as Album; lastFmData = await lastFmAPI.getArtistInfo(albumData.artist); } else if (result.type === 'track') { // For tracks, get artist info const songData = result.data as Song; lastFmData = await lastFmAPI.getArtistInfo(songData.artist); } setLastFmDetails(lastFmData); } catch (error) { console.error('Failed to fetch Last.fm data:', error); } }; const handlePlayNext = (result: SearchResult) => { if (result.type === 'track') { const songData = result.data as Song; const track = songToTrack(songData); insertAtBeginningOfQueue(track); } }; const handleAddToQueue = (result: SearchResult) => { if (result.type === 'track') { const songData = result.data as Song; const track = songToTrack(songData); addToQueue(track); } }; const getResultIcon = (type: string) => { switch (type) { case 'track': return ; case 'album': return ; case 'artist': return ; default: return ; } }; if (!isOpen) return null; return (
e.stopPropagation()} > {/* Search Input */}
setQuery(e.target.value)} placeholder="Search tracks, albums, artists..." className="border-0 focus-visible:ring-0 text-lg bg-transparent" /> {query && ( )}
{/* Results */}
{isLoading ? (
Searching...
) : results.length > 0 ? (
{results.map((result, index) => (
handleResultSelect(result)} onMouseEnter={() => setSelectedIndex(index)} >
{result.image ? ( {result.title} ) : (
{getResultIcon(result.type)}
)}
{result.title}
{result.subtitle}
{result.type}
{/* Quick Actions */}
{result.type === 'track' && ( <> )}
))}
) : query.trim() ? (

No results found for “{query}”

) : (

Start typing to search your music library

• Use ↑↓ to navigate • Enter to select

• Tab for details • Esc to close

)}
{/* Details Panel */} {showDetails && selectedResult && (

Details

{/* Basic Info */}
{selectedResult.image && ( {selectedResult.title} )}

{selectedResult.title}

{selectedResult.subtitle}

{selectedResult.type}
{/* Last.fm Data */} {lastFmDetails && ( <>
Last.fm Info
{/* Stats */} {lastFmDetails.stats && (
Listeners
{parseInt(lastFmDetails.stats.listeners).toLocaleString()}
Plays
{parseInt(lastFmDetails.stats.playcount).toLocaleString()}
)} {/* Bio */} {lastFmDetails.bio?.summary && (
Biography

{lastFmDetails.bio.summary.replace(/<[^>]*>/g, '').split('\n')[0]}

)} {/* Tags */} {lastFmDetails.tags?.tag && (
Tags
{lastFmDetails.tags.tag.slice(0, 6).map((tag: LastFmTag, index: number) => ( {tag.name} ))}
)} {/* Similar Artists */} {lastFmDetails.similar?.artist && (
Similar Artists
{lastFmDetails.similar.artist.slice(0, 4).map((artist: LastFmArtist, index: number) => (
{artist.name}
))}
)}
)} {/* Actions */}
{selectedResult.type === 'track' && ( <> )}
)}
); }