'use client'; import { useRouter } from 'next/navigation'; import { useCallback, useEffect, useRef, useState } from 'react'; import { fmt } from '@/lib/format'; import { pressureColor } from '@/lib/pressure'; import type { SearchResult } from '@/lib/types'; const TYPE_LABEL: Record = { country: 'Country', region: 'Region', asn: 'ASN', service: 'Service', target: 'Target', incident: 'Incident' }; /** ⌘K search over /api/v1/search — countries, regions, ASNs, services, targets, incidents. */ export function Search() { const [open, setOpenState] = useState(false); const [q, setQ] = useState(''); const [found, setFound] = useState<{ q: string; results: SearchResult[] }>({ q: '', results: [] }); const [active, setActive] = useState(0); const router = useRouter(); const inputRef = useRef(null); const results = q.trim() && found.q === q.trim() ? found.results : []; const loading = Boolean(q.trim()) && found.q !== q.trim(); // open/close is an event: reset query + results here, not in an effect const setOpen = useCallback((next: boolean | ((o: boolean) => boolean)) => { setOpenState((o) => { const v = typeof next === 'function' ? next(o) : next; if (!v) { setQ(''); setFound({ q: '', results: [] }); } else setTimeout(() => inputRef.current?.focus(), 10); return v; }); }, []); useEffect(() => { const onKey = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setOpen((o) => !o); } else if (e.key === 'Escape') setOpen(false); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [setOpen]); useEffect(() => { const s = q.trim(); if (!open || !s) return; const ctrl = new AbortController(); const t = setTimeout(() => { fetch(`/api/v1/search?q=${encodeURIComponent(s)}`, { signal: ctrl.signal }) .then((r) => (r.ok ? r.json() : { results: [] })) .then((d: { results?: SearchResult[] }) => { setFound({ q: s, results: d.results ?? [] }); setActive(0); }) .catch(() => {}); }, 120); return () => { clearTimeout(t); ctrl.abort(); }; }, [q, open]); const go = useCallback( (r: SearchResult | undefined) => { if (!r) return; setOpen(false); router.push(r.href); }, [router, setOpen], ); return ( <> {open && (
setOpen(false)} role="presentation">
e.stopPropagation()}>
setQ(e.target.value)} onKeyDown={(e) => { if (e.key === 'ArrowDown') { e.preventDefault(); setActive((a) => Math.min(results.length - 1, a + 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((a) => Math.max(0, a - 1)); } else if (e.key === 'Enter') go(results[active]); }} placeholder="Country, region, AS number, service, hostname, incident…" className="h-11 w-full bg-transparent text-[14px] text-ink placeholder:text-ink-3 focus:outline-none" aria-activedescendant={results[active] ? `sr-${active}` : undefined} /> {loading && …}
    {results.map((r, i) => (
  • setActive(i)} onClick={() => go(r)} className={`flex cursor-pointer items-center gap-3 px-3 py-2 text-[13px] ${i === active ? 'bg-panel-2' : ''}`} > {TYPE_LABEL[r.type]} {r.label} {r.pressure != null && ( {fmt(r.pressure)} )}
  • ))} {q.trim() && !loading && results.length === 0 &&
  • No match in the observatory.
  • } {!q.trim() &&
  • Type to search. ↑↓ to move, ⏎ to open, esc to close.
  • }
)} ); }