"use client"; import { useRef, useState } from "react"; import type { FavoriteGame } from "@/lib/integrations/types"; // Searchable favorite-games picker. Mirrors the console picker UX, but results // come from the server (Steam store search via /games/search) since the catalog // is far too large to bundle. Typing debounces a fetch; clicking a result adds // it as a chip with its cover art. Free-text entry is always available for // console/retro titles the search doesn't surface. type Result = { name: string; image?: string; url?: string }; function Cover({ image }: { image?: string }) { if (!image) return 🎲; // eslint-disable-next-line @next/next/no-img-element return ; } export default function GamePicker({ name, initial, }: { name: string; initial: FavoriteGame[]; }) { const [items, setItems] = useState(initial); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const abortRef = useRef(null); const timerRef = useRef | null>(null); // Search is event-driven (no effect): each keystroke reschedules a debounced // fetch against the admin-only Steam search route, cancelling any in-flight // request so only the latest query's results land. function onQueryChange(next: string) { setQuery(next); setOpen(true); if (timerRef.current) clearTimeout(timerRef.current); const q = next.trim(); if (q.length < 2) { abortRef.current?.abort(); setResults([]); setLoading(false); return; } setLoading(true); timerRef.current = setTimeout(async () => { abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; try { const res = await fetch( `/admin/integrations/games/search?q=${encodeURIComponent(q)}`, { signal: ctrl.signal } ); const json = (await res.json()) as { games?: Result[] }; setResults(json.games ?? []); } catch { // aborted or network error — leave prior results, drop the spinner } finally { setLoading(false); } }, 300); } function add(g: Result) { setItems((prev) => [...prev, { name: g.name, image: g.image, url: g.url }]); setQuery(""); setResults([]); setOpen(false); } function addFreeform() { const label = query.trim(); if (!label) return; setItems((prev) => [...prev, { name: label }]); setQuery(""); setResults([]); setOpen(false); } function remove(idx: number) { setItems((prev) => prev.filter((_, i) => i !== idx)); } function setNote(idx: number, note: string) { setItems((prev) => prev.map((it, i) => (i === idx ? { ...it, note } : it))); } return ( onQueryChange(e.target.value)} onFocus={() => setOpen(true)} onBlur={() => setTimeout(() => setOpen(false), 150)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); if (results[0]) add(results[0]); else addFreeform(); } }} /> {open && query.trim().length >= 2 && ( {loading && results.length === 0 && ( Searching… )} {results.map((g, i) => ( e.preventDefault()} onClick={() => add(g)}> {g.name} ))} e.preventDefault()} onClick={addFreeform} > + Add “{query.trim()}” as custom )} {items.length > 0 && ( {items.map((it, i) => ( {it.name} setNote(i, e.target.value)} /> remove(i)} > ✕ ))} )} ); }