"use client"; import type { SearchHit, SearchResponse } from "@dci/core"; import { ArrowRight, Clock, CornerDownLeft, Map as MapIcon, Search, X } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { cn } from "@/components/ui/cn"; import { Kbd } from "@/components/ui/kbd"; import { apiGet, clientApiBase } from "@/lib/api"; import { fmtMw } from "@/lib/format"; import { useDebounced, useLocalStorage } from "@/lib/hooks"; import { SEARCH_GROUP_LABEL } from "@/lib/labels"; import { routes } from "@/lib/routes"; const OPEN_EVENT = "dci:palette:open"; export function openPalette(query?: string) { window.dispatchEvent(new CustomEvent(OPEN_EVENT, { detail: { query } })); } const GROUP_ORDER: SearchHit["type"][] = ["facility", "operator", "country", "metro", "city", "cloud_region", "project", "ixp"]; interface Row { kind: "hit" | "action" | "recent"; id: string; title: string; subtitle?: string | null; href: string; group: string; meta?: Record; } export function CommandPalette() { const router = useRouter(); const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const dq = useDebounced(q.trim(), 160); const [result, setResult] = useState<{ q: string; data: SearchResponse | null; error: string | null } | null>(null); const res = result && result.q === dq ? result.data : null; const error = result && result.q === dq ? result.error : null; const loading = open && dq.length >= 2 && result?.q !== dq; const [idx, setIdx] = useState(0); const [recent, setRecent] = useLocalStorage("dci:recent-searches", []); const inputRef = useRef(null); const listRef = useRef(null); const abortRef = useRef(null); useEffect(() => { const onOpen = (e: Event) => { const d = (e as CustomEvent<{ query?: string }>).detail; if (d?.query) setQ(d.query); setOpen(true); }; const onKey = (e: KeyboardEvent) => { const target = e.target as HTMLElement | null; const typing = target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable); if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setOpen((o) => !o); } else if (e.key === "/" && !typing && !e.metaKey && !e.ctrlKey) { e.preventDefault(); setOpen(true); } }; window.addEventListener(OPEN_EVENT, onOpen); document.addEventListener("keydown", onKey); return () => { window.removeEventListener(OPEN_EVENT, onOpen); document.removeEventListener("keydown", onKey); }; }, []); useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; setTimeout(() => inputRef.current?.focus(), 10); return () => { document.body.style.overflow = prev; }; }, [open]); useEffect(() => { if (!open || dq.length < 2) return; abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; apiGet("/search", { q: dq }, { base: clientApiBase(), signal: ctrl.signal, timeoutMs: 6000 }).then((r) => { if (ctrl.signal.aborted) return; setResult(r.ok ? { q: dq, data: r.data, error: null } : { q: dq, data: null, error: r.error }); }); return () => ctrl.abort(); }, [dq, open]); const rows = useMemo(() => { const out: Row[] = []; const term = q.trim(); if (term.length >= 1) { out.push({ kind: "action", id: "a-search", title: `Search “${term}”`, subtitle: "All results", href: routes.search(term), group: "Actions" }); out.push({ kind: "action", id: "a-map", title: `Show “${term}” on the map`, subtitle: "Filtered map", href: routes.map(`q=${encodeURIComponent(term)}`), group: "Actions" }); } if (dq.length >= 2 && res?.hits?.length) { const sorted = [...res.hits].sort((a, b) => GROUP_ORDER.indexOf(a.type) - GROUP_ORDER.indexOf(b.type) || b.score - a.score); for (const h of sorted) out.push({ kind: "hit", id: `${h.type}:${h.id}`, title: h.title, subtitle: h.subtitle, href: h.href || routes.search(h.title), group: SEARCH_GROUP_LABEL[h.type] ?? h.type, meta: h.meta }); } if (!term && recent.length) for (const r of recent.slice(0, 6)) out.push({ kind: "recent", id: `r-${r}`, title: r, href: routes.search(r), group: "Recent searches" }); if (!term) { out.push({ kind: "action", id: "a-live", title: "Live changes", subtitle: "Latest detected updates", href: routes.live(), group: "Go to" }); out.push({ kind: "action", id: "a-map0", title: "Global map", subtitle: "All indexed facilities", href: routes.map(), group: "Go to" }); out.push({ kind: "action", id: "a-dc", title: "Data centers", subtitle: "Filterable index", href: routes.datacenters(), group: "Go to" }); out.push({ kind: "action", id: "a-rank", title: "Rankings", subtitle: "Countries, markets, operators", href: routes.rankings(), group: "Go to" }); } return out; }, [q, dq, res, recent]); const rowsKey = `${rows.length}:${dq}`; const [prevRowsKey, setPrevRowsKey] = useState(rowsKey); if (prevRowsKey !== rowsKey) { setPrevRowsKey(rowsKey); setIdx(0); } const go = useCallback( (row: Row) => { const term = q.trim(); if (term) setRecent((p) => [term, ...p.filter((x) => x !== term)].slice(0, 8)); setOpen(false); setQ(""); router.push(row.href); }, [q, router, setRecent], ); const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { e.preventDefault(); setIdx((i) => Math.min(rows.length - 1, i + 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setIdx((i) => Math.max(0, i - 1)); } else if (e.key === "Enter") { e.preventDefault(); const r = rows[idx]; if (r) go(r); } else if (e.key === "Escape") { e.preventDefault(); setOpen(false); } }; useEffect(() => { const el = listRef.current?.querySelector(`[data-idx="${idx}"]`); el?.scrollIntoView({ block: "nearest" }); }, [idx]); if (!open || typeof document === "undefined") return null; let lastGroup = ""; return createPortal(
e.target === e.currentTarget && setOpen(false)}>
setQ(e.target.value)} onKeyDown={onKeyDown} role="combobox" aria-expanded aria-controls="palette-list" aria-activedescendant={rows[idx] ? `palette-${rows[idx].id}` : undefined} aria-autocomplete="list" placeholder="Search data centers, operators, markets, countries…" className="h-full min-w-0 flex-1 bg-transparent text-[15px] outline-none placeholder:text-ink-4" autoComplete="off" spellCheck={false} /> esc
    {rows.length === 0 && (
  • {error ? "Search is unavailable right now." : loading ? "Searching…" : dq.length >= 2 ? "No matches." : "Type to search."}
  • )} {rows.map((r, i) => { const header = r.group !== lastGroup; lastGroup = r.group; const active = i === idx; const mw = r.meta && typeof r.meta.mw === "number" ? fmtMw(r.meta.mw) : null; return (
  • {header &&
    {r.group}
    }
  • ); })}
↑ ↓ navigate ↵ open Tip: try “hyperscale in Ireland” or “> 100 MW under construction”
, document.body, ); }