mymusics

retro MySpace-style music player
Log | Files | Refs | README

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 }