'use client'; /** Search-to-focus: type a name / NORAD / COSPAR, pick a satellite, the globe flies to its live position. */ import { Loader2, Search, X } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { clientApi } from '@/lib/client-api'; import { cn } from '@/lib/cn'; import type { SearchResult } from '@/lib/types'; export interface FocusTarget { norad: number; name: string; slug: string; } function noradFrom(r: SearchResult): number | null { const m = /NORAD\s+(\d+)/i.exec(r.subtitle ?? ''); if (m) return Number(m[1]); const tail = /-(\d+)$/.exec(r.slug); return tail ? Number(tail[1]) : null; } export function FocusSearch({ onPick, current, onClear, className }: { onPick: (t: FocusTarget) => void; current: FocusTarget | null; onClear: () => void; className?: string }) { const [q, setQ] = useState(''); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [items, setItems] = useState<{ r: SearchResult; norad: number }[]>([]); const [active, setActive] = useState(0); const boxRef = useRef(null); useEffect(() => { const term = q.trim(); if (term.length < 1) { setItems([]); return; } const ctrl = new AbortController(); const t = setTimeout(async () => { setLoading(true); try { const res = await clientApi.search(term, 12, ctrl.signal); const sats = res.data.results .filter((r) => r.entity_type === 'satellite') .map((r) => ({ r, norad: noradFrom(r) })) .filter((x): x is { r: SearchResult; norad: number } => x.norad !== null); setItems(sats); setActive(0); setOpen(true); } catch { /* aborted */ } finally { setLoading(false); } }, 160); return () => { clearTimeout(t); ctrl.abort(); }; }, [q]); useEffect(() => { const onDoc = (e: PointerEvent) => { if (!boxRef.current?.contains(e.target as Node)) setOpen(false); }; document.addEventListener('pointerdown', onDoc); return () => document.removeEventListener('pointerdown', onDoc); }, []); const pick = (it: { r: SearchResult; norad: number }) => { onPick({ norad: it.norad, name: it.r.title, slug: it.r.slug }); setQ(''); setItems([]); setOpen(false); }; return (
{loading ? : } {current && !q ? (
{current.name} {current.norad}
) : ( setQ(e.target.value)} onFocus={() => items.length && setOpen(true)} onKeyDown={(e) => { if (e.key === 'ArrowDown') { e.preventDefault(); setActive((a) => Math.min(a + 1, items.length - 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); } else if (e.key === 'Enter') { const it = items[active]; if (it) pick(it); } else if (e.key === 'Escape') setOpen(false); }} placeholder="Focus a satellite: name, NORAD, COSPAR…" className="min-w-0 flex-1 bg-transparent text-[16px] text-ink placeholder:text-ink-3 focus:outline-none md:text-sm" aria-label="Search a satellite to focus" autoComplete="off" spellCheck={false} /> )}
{open && q.trim() && ( )}
); }