"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { useRouter } from "next/navigation"; interface SearchResult { type: "page" | "container" | "node" | "dns" | "action"; title: string; description?: string; href?: string; action?: () => void; } const PAGES: SearchResult[] = [ { type: "page", title: "Dashboard", href: "/" }, { type: "page", title: "Infrastructure", href: "/infrastructure" }, { type: "page", title: "Media", href: "/media" }, { type: "page", title: "Automations", href: "/automations" }, { type: "page", title: "Expenses", href: "/expenses" }, { type: "page", title: "Network", href: "/network" }, { type: "page", title: "Logs", href: "/logs" }, ]; export function CommandSearch() { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [selected, setSelected] = useState(0); const inputRef = useRef(null); const router = useRouter(); // Open on Cmd+K / Ctrl+K useEffect(() => { function handler(e: KeyboardEvent) { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); setOpen(prev => !prev); } if (e.key === "Escape") setOpen(false); } window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, []); // Focus input when opened useEffect(() => { if (open) { setQuery(""); setSelected(0); setTimeout(() => inputRef.current?.focus(), 50); } }, [open]); // Search useEffect(() => { if (!query.trim()) { setResults(PAGES); return; } const q = query.toLowerCase(); const filtered = PAGES.filter(p => p.title.toLowerCase().includes(q)); // Also search for dynamic items (async fetch could go here in future) // For now just do page search + some action shortcuts const actions: SearchResult[] = []; if ("restart".includes(q) || "jellyfin".includes(q)) { actions.push({ type: "action", title: "Restart Jellyfin", description: "Restart Jellyfin on Olares" }); } if ("restart".includes(q) || "ollama".includes(q)) { actions.push({ type: "action", title: "Restart Ollama", description: "Restart Ollama on Olares" }); } if ("backup".includes(q)) { actions.push({ type: "action", title: "Run Backup", description: "Run email backup now" }); } setResults([...filtered, ...actions]); setSelected(0); }, [query]); const execute = useCallback((result: SearchResult) => { setOpen(false); if (result.href) router.push(result.href); if (result.action) result.action(); }, [router]); // Keyboard nav const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { e.preventDefault(); setSelected(s => Math.min(s + 1, results.length - 1)); } if (e.key === "ArrowUp") { e.preventDefault(); setSelected(s => Math.max(s - 1, 0)); } if (e.key === "Enter" && results[selected]) { execute(results[selected]); } }, [results, selected, execute]); if (!open) return null; const typeIcons: Record = { page: "->", container: "[]", node: "(o)", dns: "<>", action: "!", }; const typeColors: Record = { page: "text-blue-400", container: "text-cyan-400", node: "text-green-400", dns: "text-amber-400", action: "text-violet-400", }; return ( <>
setOpen(false)} />
Ctrl+K setQuery(e.target.value)} onKeyDown={handleKeyDown} placeholder="Search pages, containers, actions..." className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground/50 outline-none" style={{ color: "#f1f5f9" }} />
{results.length === 0 && (

No results

)} {results.map((r, i) => ( ))}
Up/Down navigate Enter select esc close
); }