'use client'; import { useRouter } from 'next/navigation'; import { Search } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; interface Suggestion { type: string; label: string; sublabel: string | null; href: string; } /** Header search with instant suggestions (/api/search/suggest); full results at /search. */ export function HeaderSearch() { const router = useRouter(); const [q, setQ] = useState(''); const [items, setItems] = useState([]); const [open, setOpen] = useState(false); const [active, setActive] = useState(-1); const box = useRef(null); useEffect(() => { if (q.trim().length < 2) return; const ctrl = new AbortController(); const t = setTimeout(() => { fetch(`/api/search/suggest?q=${encodeURIComponent(q)}`, { signal: ctrl.signal }) .then((r) => r.json()) .then((d: { items: Suggestion[] }) => { setItems(d.items ?? []); setOpen(true); setActive(-1); }) .catch(() => {}); }, 120); return () => { clearTimeout(t); ctrl.abort(); }; }, [q]); useEffect(() => { const onDoc = (e: MouseEvent) => { if (!box.current?.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, []); const go = (href: string) => { setOpen(false); router.push(href); }; return (
{ e.preventDefault(); if (active >= 0 && items[active]) return go(items[active]!.href); const query = q.trim(); if (query) go(`/search?q=${encodeURIComponent(query)}`); }} > { setQ(e.target.value); if (e.target.value.trim().length < 2) { setItems([]); setOpen(false); } }} role="combobox" aria-controls="header-search-listbox" onFocus={() => items.length && setOpen(true)} onKeyDown={(e) => { if (e.key === 'ArrowDown') { e.preventDefault(); setActive((a) => Math.min(items.length - 1, a + 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((a) => Math.max(-1, a - 1)); } else if (e.key === 'Escape') setOpen(false); }} placeholder="Search cards, watches, games, sneakers, LEGO…" className="h-8 w-64 rounded-md border border-border bg-sunken pl-8 pr-2 text-[13px] text-fg placeholder:text-subtle transition-[width] focus:w-80 focus:border-border-strong focus:outline-none xl:w-80 xl:focus:w-96" aria-label="Search collectibles" aria-autocomplete="list" aria-expanded={open} autoComplete="off" /> {open && items.length ? (
    {items.map((it, i) => (
  • ))}
) : null} ); }