TrackSearch.tsx (2276B)
1 import { useCallback, useEffect, useState } from "react"; 2 3 import type { TrackInfo } from "../hooks/useMyMusicsPlayback"; 4 5 type Result = Pick<TrackInfo, "id" | "title" | "artist">; 6 7 type Props = { 8 onSelect: (id: string) => void; 9 disabled?: boolean; 10 }; 11 12 export function TrackSearch({ onSelect, disabled }: Props) { 13 const [q, setQ] = useState(""); 14 const [results, setResults] = useState<Result[]>([]); 15 const [busy, setBusy] = useState(false); 16 17 useEffect(() => { 18 const trimmed = q.trim(); 19 if (trimmed.length < 2) { 20 setResults([]); 21 return; 22 } 23 const t = window.setTimeout(() => { 24 void (async () => { 25 setBusy(true); 26 try { 27 const res = await fetch( 28 `/api/track/search?q=${encodeURIComponent(trimmed)}&limit=15`, 29 ); 30 const body = (await res.json()) as { tracks?: Result[] }; 31 setResults(body.tracks ?? []); 32 } catch { 33 setResults([]); 34 } finally { 35 setBusy(false); 36 } 37 })(); 38 }, 300); 39 return () => window.clearTimeout(t); 40 }, [q]); 41 42 const pick = useCallback( 43 (id: string) => { 44 onSelect(id); 45 setQ(""); 46 setResults([]); 47 }, 48 [onSelect], 49 ); 50 51 return ( 52 <section className="track-search card card--search" aria-label="Search tracks"> 53 <h2>Search</h2> 54 <input 55 type="search" 56 className="track-search-input" 57 placeholder="Artist or title…" 58 value={q} 59 onChange={(e) => setQ(e.target.value)} 60 disabled={disabled} 61 autoComplete="off" 62 /> 63 {busy ? <p className="muted track-search-hint">Searching…</p> : null} 64 {results.length > 0 ? ( 65 <ul className="track-search-results"> 66 {results.map((r) => ( 67 <li key={r.id}> 68 <button type="button" className="track-search-hit" onClick={() => pick(r.id)}> 69 <span className="h-artist">{r.artist}</span> 70 <span className="sep">—</span> 71 <span className="h-title">{r.title}</span> 72 </button> 73 </li> 74 ))} 75 </ul> 76 ) : q.trim().length >= 2 && !busy ? ( 77 <p className="muted track-search-hint">No matches.</p> 78 ) : null} 79 </section> 80 ); 81 }