'use client'; import { ArrowRight, Building2, Clock, CornerDownLeft, Factory, Globe2, Search, Tag, X } from 'lucide-react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useCallback, useEffect, useRef, useState } from 'react'; import { clientApi } from '@/lib/client-api'; import { cn } from '@/lib/cn'; import { EXAMPLE_QUERIES, primaryNav, routes } from '@/lib/site'; import type { Suggestion } from '@/lib/types'; import { useSearch } from './search-context'; const RECENT_KEY = 'ca-recent'; const RECENT_MAX = 6; function readRecent(): Suggestion[] { try { const arr = JSON.parse(localStorage.getItem(RECENT_KEY) ?? '[]') as unknown; return Array.isArray(arr) ? (arr.filter((x) => x && typeof (x as Suggestion).href === 'string') as Suggestion[]).slice(0, RECENT_MAX) : []; } catch { return []; } } function pushRecent(s: Suggestion) { try { localStorage.setItem(RECENT_KEY, JSON.stringify([s, ...readRecent().filter((r) => r.href !== s.href)].slice(0, RECENT_MAX))); } catch { /* ignore */ } } const ICON = { company: Building2, industry: Factory, country: Globe2, event_type: Tag } as const; type Row = { key: string; href: string; node: React.ReactNode; section: string; run?: () => void }; /** * ⌘K search: `/search/suggest` (debounced 120 ms) grouped by kind, recent picks, example natural-language queries and a * "Search everything" row. Keyboard: ↑↓ move · ↵ open · esc close. Mobile: full-screen sheet. */ export function SearchDialog() { const { open, setOpen } = useSearch(); const router = useRouter(); const [q, setQ] = useState(''); const [items, setItems] = useState([]); const [active, setActive] = useState(0); const [loading, setLoading] = useState(false); const [failed, setFailed] = useState(false); const [recent, setRecent] = useState([]); const inputRef = useRef(null); const listRef = useRef(null); const term = q.trim(); useEffect(() => { listRef.current?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: 'nearest' }); }, [active]); useEffect(() => { if (open) { setTimeout(() => inputRef.current?.focus(), 20); document.body.style.overflow = 'hidden'; setRecent(readRecent()); setActive(0); } else { document.body.style.overflow = ''; setQ(''); setItems([]); setFailed(false); } return () => { document.body.style.overflow = ''; }; }, [open]); useEffect(() => { if (!open || term.length < 1) { setItems([]); return; } const ctrl = new AbortController(); const t = setTimeout(async () => { setLoading(true); try { const res = await clientApi.suggest(term, ctrl.signal); setItems(res.items ?? []); setFailed(false); setActive(0); } catch (e) { if ((e as Error).name !== 'AbortError') setFailed(true); } finally { setLoading(false); } }, 120); return () => { clearTimeout(t); ctrl.abort(); }; }, [term, open]); const close = useCallback(() => setOpen(false), [setOpen]); const go = useCallback( (href: string) => { router.push(href); close(); }, [router, close], ); if (!open) return null; const rows: Row[] = []; const node = (s: Suggestion, icon?: React.ReactNode) => { const I = ICON[s.kind] ?? Tag; return ( <> {icon ?? } {s.label} {s.sublabel && {s.sublabel}} {s.kind.replace('_', ' ')} ); }; const order: Suggestion['kind'][] = ['company', 'industry', 'country', 'event_type']; for (const k of order) for (const s of items.filter((i) => i.kind === k)) rows.push({ key: `s-${s.href}`, href: s.href, section: k === 'company' ? 'Companies' : k === 'industry' ? 'Industries' : k === 'country' ? 'Countries' : 'Event types', node: node(s), run: () => pushRecent(s) }); if (!term && recent.length) for (const r of recent) rows.push({ key: `r-${r.href}`, href: r.href, section: 'Recent', node: node(r, ) }); if (term) rows.push({ key: '__all', href: routes.search(term), section: 'Search', node: ( <> {/\s/.test(term) ? `Ask Company Atlas: “${term}”` : `Search everything for “${term}”`} ), }); const clamp = Math.min(active, Math.max(0, rows.length - 1)); const onKey = (e: React.KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault(); setActive((a) => Math.min(a + 1, rows.length - 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); } else if (e.key === 'Enter') { e.preventDefault(); const it = rows[clamp]; if (it) { it.run?.(); go(it.href); } else if (term) go(routes.search(term)); } else if (e.key === 'Escape') close(); }; let lastSection = ''; return (
e.stopPropagation()}>
{ setQ(e.target.value); setActive(0); }} onKeyDown={onKey} placeholder="Search companies, industries, countries… or ask a question" className="h-11 min-w-0 flex-1 bg-transparent text-[16px] text-ink placeholder:text-ink-3 focus:outline-none" autoComplete="off" spellCheck={false} aria-label="Search" role="combobox" aria-expanded={rows.length > 0} aria-controls="palette-list" aria-activedescendant={rows[clamp] ? `pal-${rows[clamp].key}` : undefined} data-palette-input />
    {failed &&
  • Suggestions unavailable — press Enter to search.
  • } {term && !loading && !failed && items.length === 0 &&
  • No direct match — search everything below.
  • } {rows.map((r, i) => { const header = r.section !== lastSection; lastSection = r.section; return (
  • {header &&

    {r.section}

    } { r.run?.(); close(); }} onMouseEnter={() => setActive(i)} className={cn('flex min-h-[44px] w-full items-center gap-3 px-4 py-2 text-left', i === clamp ? 'bg-surface-3' : 'hover:bg-surface-2')} > {r.node} {i === clamp && ↵}
  • ); })}
{!term && (

Try asking

    {EXAMPLE_QUERIES.map((ex) => (
  • ))}

Browse

    {primaryNav .filter((n) => n.href !== '/') .map((n) => (
  • {n.label}
  • ))}
)}
↵ open ↑↓ move Natural-language questions are routed to /ask and always link back to events and sources. esc close
); }