'use client'; import { useEffect, useId, useMemo, useRef, useState } from 'react'; export type PlayerResult = { id: string; full_name: string; team?: string; position?: string; headshot_url?: string; }; export type Sport = 'NBA' | 'MLB' | 'WNBA'; type Props = { sport: Sport; gameId?: string; placeholder?: string; initialValue?: string; onSelect: (player: PlayerResult) => void; autoFocus?: boolean; }; const SPORT_TINT: Record = { NBA: 'var(--nba)', MLB: 'var(--mlb)', WNBA: 'var(--wnba)', }; export default function PlayerSearch({ sport, gameId, placeholder = 'Search players…', initialValue = '', onSelect, autoFocus = false, }: Props) { const [query, setQuery] = useState(initialValue); const [results, setResults] = useState(null); const [loading, setLoading] = useState(false); const [highlight, setHighlight] = useState(0); const [open, setOpen] = useState(false); const inputId = useId(); const listboxId = `${inputId}-listbox`; const containerRef = useRef(null); const abortRef = useRef(null); // Debounced fetch useEffect(() => { const q = query.trim(); if (q.length < 2) { setResults(null); setLoading(false); return; } setLoading(true); abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; const t = setTimeout(async () => { try { const params = new URLSearchParams({ sport, q }); if (gameId) params.set('game_id', gameId); const res = await fetch(`/api/players/search?${params.toString()}`, { signal: ctrl.signal }); const data = await res.json().catch(() => ({})); if (!ctrl.signal.aborted) { setResults(Array.isArray(data?.players) ? data.players.slice(0, 5) : []); setHighlight(0); } } catch { if (!ctrl.signal.aborted) setResults([]); } finally { if (!ctrl.signal.aborted) setLoading(false); } }, 220); return () => { clearTimeout(t); ctrl.abort(); }; }, [query, sport, gameId]); // Close on outside click useEffect(() => { const onClick = (e: MouseEvent) => { if (!containerRef.current?.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', onClick); return () => document.removeEventListener('mousedown', onClick); }, []); const noResults = useMemo( () => !loading && results !== null && results.length === 0 && query.trim().length >= 2, [loading, results, query], ); const choose = (p: PlayerResult) => { onSelect(p); setQuery(p.full_name); setOpen(false); }; const onKeyDown = (e: React.KeyboardEvent) => { if (!results || results.length === 0) return; if (e.key === 'ArrowDown') { e.preventDefault(); setOpen(true); setHighlight((h) => Math.min(results.length - 1, h + 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlight((h) => Math.max(0, h - 1)); } else if (e.key === 'Enter') { e.preventDefault(); const sel = results[highlight]; if (sel) choose(sel); } else if (e.key === 'Escape') { setOpen(false); } }; return (
setOpen(true)} onChange={(e) => { setQuery(e.target.value); setOpen(true); }} onKeyDown={onKeyDown} style={{ width: '100%' }} /> {open && (loading || results !== null) && (
    {loading && (
  • SEARCHING…
  • )} {!loading && noResults && (
  • No players found for "{query}".

    Check spelling.

  • )} {!loading && results && results.map((p, i) => { const active = i === highlight; return (
  • { e.preventDefault(); choose(p); }} onMouseEnter={() => setHighlight(i)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 12px', borderRadius: 8, background: active ? 'var(--bg-2)' : 'transparent', cursor: 'pointer', }} > {p.full_name.split(' ').map((n) => n[0]).slice(0, 2).join('')} {p.full_name} {p.team ? ( {p.team} ) : null} {sport}
  • ); })}
)}
); }