SPB Git

spb/rareindex

Public
4.1 KB · 117 lines tsx
1'use client';23import { useRouter } from 'next/navigation';4import { Search } from 'lucide-react';5import { useEffect, useRef, useState } from 'react';67interface Suggestion {8  type: string;9  label: string;10  sublabel: string | null;11  href: string;12}1314/** Header search with instant suggestions (/api/search/suggest); full results at /search. */15export function HeaderSearch() {16  const router = useRouter();17  const [q, setQ] = useState('');18  const [items, setItems] = useState<Suggestion[]>([]);19  const [open, setOpen] = useState(false);20  const [active, setActive] = useState(-1);21  const box = useRef<HTMLFormElement>(null);2223  useEffect(() => {24    if (q.trim().length < 2) return;25    const ctrl = new AbortController();26    const t = setTimeout(() => {27      fetch(`/api/search/suggest?q=${encodeURIComponent(q)}`, { signal: ctrl.signal })28        .then((r) => r.json())29        .then((d: { items: Suggestion[] }) => {30          setItems(d.items ?? []);31          setOpen(true);32          setActive(-1);33        })34        .catch(() => {});35    }, 120);36    return () => {37      clearTimeout(t);38      ctrl.abort();39    };40  }, [q]);4142  useEffect(() => {43    const onDoc = (e: MouseEvent) => {44      if (!box.current?.contains(e.target as Node)) setOpen(false);45    };46    document.addEventListener('mousedown', onDoc);47    return () => document.removeEventListener('mousedown', onDoc);48  }, []);4950  const go = (href: string) => {51    setOpen(false);52    router.push(href);53  };5455  return (56    <form57      ref={box}58      role="search"59      className="relative hidden items-center md:flex"60      onSubmit={(e) => {61        e.preventDefault();62        if (active >= 0 && items[active]) return go(items[active]!.href);63        const query = q.trim();64        if (query) go(`/search?q=${encodeURIComponent(query)}`);65      }}66    >67      <Search className="pointer-events-none absolute left-2.5 h-3.5 w-3.5 text-subtle" />68      <input69        type="search"70        value={q}71        onChange={(e) => {72          setQ(e.target.value);73          if (e.target.value.trim().length < 2) {74            setItems([]);75            setOpen(false);76          }77        }}78        role="combobox"79        aria-controls="header-search-listbox"80        onFocus={() => items.length && setOpen(true)}81        onKeyDown={(e) => {82          if (e.key === 'ArrowDown') {83            e.preventDefault();84            setActive((a) => Math.min(items.length - 1, a + 1));85          } else if (e.key === 'ArrowUp') {86            e.preventDefault();87            setActive((a) => Math.max(-1, a - 1));88          } else if (e.key === 'Escape') setOpen(false);89        }}90        placeholder="Search cards, watches, games, sneakers, LEGO…"91        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"92        aria-label="Search collectibles"93        aria-autocomplete="list"94        aria-expanded={open}95        autoComplete="off"96      />97      {open && items.length ? (98        <ul id="header-search-listbox" className="absolute left-0 top-full z-50 mt-1 w-[26rem] overflow-hidden rounded-md border border-border bg-elevated py-1 text-[13px] shadow-pop" role="listbox">99          {items.map((it, i) => (100            <li key={`${it.type}-${it.href}`} role="option" aria-selected={i === active}>101              <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => go(it.href)} className={`flex w-full items-center justify-between gap-3 px-3 py-1.5 text-left ${i === active ? 'bg-inset' : 'hover:bg-sunken'}`}>102                <span className="truncate text-fg">{it.label}</span>103                <span className="shrink-0 text-[10px] uppercase tracking-wider text-subtle">{it.sublabel ?? it.type}</span>104              </button>105            </li>106          ))}107          <li>108            <button type="submit" className="w-full px-3 py-1.5 text-left text-[12px] text-muted hover:bg-sunken">109              Search all results for “{q}” →110            </button>111          </li>112        </ul>113      ) : null}114    </form>115  );116}117
Files · f9a634ec80792524177fc3e5fba5c7953d672423
Menu

Theme