/** * ============================================================================= * QWHPI — Quebec Weekly Housing Price Index * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : web/components/CommandPalette.tsx * Purpose : ⌘K command palette — fuzzy jump to any series or page, * full keyboard navigation, zero dependencies. * ============================================================================= */ "use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { fetchGeographies, type GeographyNode } from "../lib/api"; interface Item { label: string; hint: string; href: string; } const PAGES: Item[] = [ { label: "Overview", hint: "page", href: "/" }, { label: "Explore", hint: "page", href: "/explore" }, { label: "Compare", hint: "page", href: "/compare" }, { label: "Map", hint: "page", href: "/map" }, { label: "Methodology", hint: "page", href: "/methodology" }, { label: "API docs", hint: "page", href: "/api-docs" }, ]; const TYPES = ["all", "unifamilial", "condo", "plex"]; function norm(s: string): string { return s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase(); } export default function CommandPalette() { const router = useRouter(); const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const [sel, setSel] = useState(0); const [geos, setGeos] = useState([]); const inputRef = useRef(null); const listRef = useRef(null); useEffect(() => { const onKey = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setOpen((v) => !v); } else if (e.key === "Escape") { setOpen(false); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); useEffect(() => { if (open) { setQ(""); setSel(0); setTimeout(() => inputRef.current?.focus(), 30); if (!geos.length) fetchGeographies().then((g) => setGeos(g.published_series)).catch(() => {}); } }, [open, geos.length]); const items = useMemo(() => { const series: Item[] = geos.flatMap((g) => g.property_types.filter((t) => TYPES.includes(t)).map((t) => ({ label: `${g.geography_name} · ${t}`, hint: g.geography_level, href: `/explore?geography=${g.geography_id}&type=${t}`, }))); const all = [...PAGES, ...series]; if (!q) return all.slice(0, 12); const tokens = norm(q).split(/\s+/).filter(Boolean); return all .filter((i) => { const hay = norm(i.label + " " + i.hint); return tokens.every((t) => hay.includes(t)); }) .slice(0, 12); }, [geos, q]); const go = useCallback((item: Item) => { setOpen(false); router.push(item.href); }, [router]); const onInputKey = (e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { e.preventDefault(); setSel((s) => Math.min(s + 1, items.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setSel((s) => Math.max(s - 1, 0)); } else if (e.key === "Enter" && items[sel]) { go(items[sel]); } }; useEffect(() => { listRef.current?.children[sel]?.scrollIntoView({ block: "nearest" }); }, [sel]); if (!open) return null; return (
setOpen(false)} role="dialog" aria-modal="true" aria-label="Command palette">
e.stopPropagation()}> { setQ(e.target.value); setSel(0); }} onKeyDown={onInputKey} placeholder="Jump to a series or page… (e.g. montreal condo)" aria-label="Search series and pages" className="palette-input" />
    {items.map((it, i) => (
  • ))} {!items.length && (
  • No match — try a region or city name
  • )}
↑↓ navigate↵ openesc close
); }