"use client"; import { IconSearch } from "@tabler/icons-react"; import { Command as CommandPrimitive } from "cmdk"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { useDebounce } from "@/hooks/use-debounce"; interface SearchResult { tmdbId: number; type: "movie" | "tv"; title: string; overview: string; releaseDate: string | null; posterPath: string | null; popularity: number; voteAverage: number; } interface SearchAutocompleteProps { onResults?: (results: SearchResult[]) => void; onLoading?: (loading: boolean) => void; } export function SearchAutocomplete({ onResults, onLoading, }: SearchAutocompleteProps) { const router = useRouter(); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const [filter, setFilter] = useState<"all" | "movie" | "tv">("all"); const debouncedQuery = useDebounce(query, 300); const inputRef = useRef(null); useEffect(() => { if (!debouncedQuery.trim()) { setResults([]); onResults?.([]); return; } let cancelled = false; setLoading(true); onLoading?.(true); const typeParam = filter !== "all" ? `&type=${filter}` : ""; fetch(`/api/search?query=${encodeURIComponent(debouncedQuery)}${typeParam}`) .then((r) => r.json()) .then((data) => { if (!cancelled) { if (data.code === "TMDB_NOT_CONFIGURED") { toast.error("TMDB API key not configured", { description: "Visit /setup for instructions.", action: { label: "Setup", onClick: () => (window.location.href = "/setup"), }, }); return; } const res = data.results ?? []; setResults(res); onResults?.(res); setOpen(res.length > 0); } }) .finally(() => { if (!cancelled) { setLoading(false); onLoading?.(false); } }); return () => { cancelled = true; }; }, [debouncedQuery, filter, onResults, onLoading]); const handleSelect = useCallback( (result: SearchResult) => { router.push(`/titles/tmdb-${result.tmdbId}-${result.type}`); }, [router], ); return (
{/* Filter tabs */}
{(["all", "movie", "tv"] as const).map((t) => ( ))}
{ setQuery(val); if (val.trim()) setOpen(true); }} onFocus={() => { if (results.length > 0) setOpen(true); }} onBlur={() => { // Delay to allow click on result setTimeout(() => setOpen(false), 200); }} className="flex h-13 w-full rounded-xl border border-border/50 bg-card/50 pl-11 pr-4 text-base backdrop-blur-sm transition-all placeholder:text-muted-foreground/50 focus:border-primary/40 focus:outline-none focus:ring-1 focus:ring-primary/20" /> {loading && (
)}
{open && results.length > 0 && ( {results.map((r) => ( handleSelect(r)} className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-sm data-[selected=true]:bg-accent" >
{r.posterPath ? ( {r.title} ) : (
?
)}

{r.title}

{r.type}
{r.releaseDate && ( {r.releaseDate.slice(0, 4)} )} {r.voteAverage > 0 && ( ★ {r.voteAverage.toFixed(1)} )}
{r.overview && (

{r.overview}

)}
))}
)}
); }