"use client"; import { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@/components/ui/command"; import { Search, LayoutDashboard, Activity, Calendar, Kanban, FolderKanban, FileText, Wrench, Target, ExternalLink, CheckCircle2, Clock, AlertCircle, Timer, Circle, } from "lucide-react"; import { siteUrls } from "@/lib/config/sites"; // Search result type from API - matches SearchableResult interface type SearchResultType = "task" | "project" | "document" | "sprint"; interface SearchResult { id: string; type: SearchResultType; title: string; snippet?: string; // Brief preview text (replaces subtitle/description) url: string; // Deep link to full view icon: string; status?: string; // For visual badges color?: string; // For project/task colors } const navItems = [ { name: "Dashboard", href: "/", icon: LayoutDashboard, shortcut: "D" }, { name: "Activity", href: "/activity", icon: Activity, shortcut: "A" }, { name: "Calendar", href: "/calendar", icon: Calendar, shortcut: "C" }, { name: "Tasks", href: "/tasks", icon: Kanban, shortcut: "T" }, { name: "Projects", href: "/projects", icon: FolderKanban, shortcut: "P" }, { name: "Documents", href: "/documents", icon: FileText, shortcut: "D" }, { name: "Tools", href: "/tools", icon: Wrench, shortcut: "O" }, { name: "Mission", href: "/mission", icon: Target, shortcut: "M" }, ]; const quickLinks = [ { name: "Gantt Board", url: siteUrls.ganttBoard, icon: ExternalLink }, { name: "Blog Backup", url: siteUrls.blogBackup, icon: ExternalLink }, { name: "Gitea", url: siteUrls.gitea, icon: ExternalLink }, ]; // Icon mapping for search result types const typeIcons: Record = { task: Kanban, project: FolderKanban, document: FileText, sprint: Timer, }; // Type labels const typeLabels: Record = { task: "Task", project: "Project", document: "Document", sprint: "Sprint", }; function getStatusIcon(status?: string) { switch (status) { case "done": case "completed": return ; case "in-progress": return ; case "open": case "todo": return ; default: return ; } } export function QuickSearch() { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const router = useRouter(); // Debounced search useEffect(() => { if (!open || query.length < 2) { setResults([]); setLoading(false); return; } setLoading(true); setResults([]); let cancelled = false; const timer = setTimeout(async () => { try { const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`); if (!res.ok) { throw new Error(`Search request failed with status ${res.status}`); } const data = (await res.json()) as { results?: SearchResult[] }; if (!cancelled) { setResults(Array.isArray(data.results) ? data.results : []); } } catch (err) { console.error("Search error:", err); if (!cancelled) { setResults([]); } } finally { if (!cancelled) { setLoading(false); } } }, 150); // 150ms debounce return () => { cancelled = true; clearTimeout(timer); }; }, [query, open]); // Keyboard shortcut useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen((open) => !open); } }; document.addEventListener("keydown", down); return () => document.removeEventListener("keydown", down); }, []); const runCommand = useCallback((command: () => void) => { setOpen(false); setQuery(""); command(); }, []); const handleResultSelect = (result: SearchResult) => { runCommand(() => { if (result.url?.startsWith("http")) { window.open(result.url, "_blank"); } else if (result.url) { router.push(result.url); } }); }; // Group results by type const groupedResults = results.reduce((acc, result) => { if (!acc[result.type]) acc[result.type] = []; acc[result.type].push(result); return acc; }, {} as Record); // Get ordered list of types that have results const activeTypes = (Object.keys(groupedResults) as SearchResultType[]).filter( (type) => groupedResults[type]?.length > 0 ); return ( <> {/* Search Button Trigger */} {/* Command Dialog */} {loading ? "Searching..." : query.length < 2 ? "Type to search..." : "No results found."} {/* Show search results when query exists */} {query.length >= 2 && activeTypes.length > 0 && ( <> {activeTypes.map((type) => { const Icon = typeIcons[type]; const typeResults = groupedResults[type]; return ( {typeResults.slice(0, 5).map((result) => ( handleResultSelect(result)} > {result.type === "task" ? ( getStatusIcon(result.status) ) : result.type === "project" && result.color ? (
) : ( )}
{result.title} {result.snippet && ( {result.snippet} )}
{result.status && result.type !== "task" && ( {result.status} )} ))} ); })} )} {/* Navigation */} {navItems.map((item) => { const Icon = item.icon; return ( runCommand(() => router.push(item.href))} > {item.name} ); })} {/* Quick Links */} {quickLinks.map((link) => { const Icon = link.icon; return ( runCommand(() => window.open(link.url, "_blank"))} > {link.name} ); })} ); }