"use client"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { authFetch, getApiBase } from "../lib/auth"; import { canAccess } from "../lib/features"; import { useFeatureUser } from "./FeatureGate"; type SearchResult = { title: string; year?: number | null; type: "movie" | "tv"; tmdbId: number; requestId?: number | null; statusLabel?: string | null; }; export default function GlobalSearch() { const router = useRouter(); const { user, ready } = useFeatureUser(); const root = useRef(null); const requestVersion = useRef(0); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [open, setOpen] = useState(false); const [searching, setSearching] = useState(false); const [message, setMessage] = useState(null); const canSearch = canAccess(user, "new_requests") || canAccess(user, "issues"); const canOpenRequests = canAccess(user, "requests"); const canCreateRequests = canAccess(user, "new_requests"); useEffect(() => { const close = (event: PointerEvent) => { if (!root.current?.contains(event.target as Node)) setOpen(false); }; document.addEventListener("pointerdown", close); return () => document.removeEventListener("pointerdown", close); }, []); useEffect(() => { const term = query.trim(); requestVersion.current += 1; const version = requestVersion.current; if (term.length < 2 || !canSearch) { setResults([]); setSearching(false); setMessage(null); return; } setSearching(true); setMessage(null); const controller = new AbortController(); const timer = window.setTimeout(async () => { try { const params = new URLSearchParams({ query: term }); const response = await authFetch(`${getApiBase()}/requests/search?${params.toString()}`, { signal: controller.signal, cache: "no-store", }); if (!response.ok) throw new Error("Search is unavailable right now."); const payload = await response.json(); if (version !== requestVersion.current) return; const mapped = (Array.isArray(payload?.results) ? payload.results : []) .filter( (item: Record) => typeof item.type === "string" && ["movie", "tv"].includes(item.type) && Number(item.tmdbId) > 0, ) .slice(0, 7) .map( (item: Record): SearchResult => ({ title: String(item?.title || "Untitled"), year: typeof item?.year === "number" ? item.year : null, type: item.type === "tv" ? "tv" : "movie", tmdbId: Number(item.tmdbId), requestId: typeof item?.requestId === "number" ? item.requestId : null, statusLabel: typeof item?.statusLabel === "string" ? item.statusLabel : null, }), ); setResults(mapped); setMessage(mapped.length ? null : "No matching titles found."); } catch (error) { if (controller.signal.aborted || version !== requestVersion.current) return; console.error(error); setResults([]); setMessage("Search is unavailable right now."); } finally { if (version === requestVersion.current) setSearching(false); } }, 280); return () => { window.clearTimeout(timer); controller.abort(); }; }, [query, canSearch]); if (!ready || !user || !canSearch) return null; const openResult = (result: SearchResult) => { setOpen(false); setQuery(""); if (result.requestId && canOpenRequests) { router.push(`/requests/${result.requestId}`); return; } if (canCreateRequests) { const params = new URLSearchParams({ type: result.type, query: result.title }); router.push(`/new-requests?${params.toString()}`); return; } router.push("/portal/issues"); }; const showResults = open && query.trim().length >= 2; return (
{ event.preventDefault(); if (results[0]) openResult(results[0]); }} > setOpen(true)} onKeyDown={(event) => { if (event.key === "Escape") setOpen(false); }} onChange={(event) => { setQuery(event.target.value); setOpen(true); }} /> {searching &&