'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: any) => ['movie', 'tv'].includes(item?.type) && Number(item?.tmdbId) > 0) .slice(0, 7) .map((item: any): SearchResult => ({ title: String(item?.title || 'Untitled'), year: typeof item?.year === 'number' ? item.year : null, type: item.type, 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 &&