232 lines
6.9 KiB
TypeScript
232 lines
6.9 KiB
TypeScript
"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,
|
|
} from "lucide-react";
|
|
import { supabaseClient } from "@/lib/supabase/client";
|
|
|
|
interface Task {
|
|
id: string;
|
|
title: string;
|
|
status: string;
|
|
priority: string;
|
|
project_id: string;
|
|
}
|
|
|
|
interface Project {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
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: "https://gantt-board.vercel.app", icon: ExternalLink },
|
|
{ name: "Blog Backup", url: "https://blog-backup-two.vercel.app", icon: ExternalLink },
|
|
{ name: "Gitea", url: "http://192.168.1.128:3000", icon: ExternalLink },
|
|
];
|
|
|
|
function getStatusIcon(status: string) {
|
|
switch (status) {
|
|
case "done":
|
|
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
|
|
case "in-progress":
|
|
return <Clock className="w-4 h-4 text-blue-500" />;
|
|
default:
|
|
return <AlertCircle className="w-4 h-4 text-muted-foreground" />;
|
|
}
|
|
}
|
|
|
|
function getPriorityColor(priority: string): string {
|
|
switch (priority) {
|
|
case "urgent":
|
|
return "text-red-500";
|
|
case "high":
|
|
return "text-orange-500";
|
|
case "medium":
|
|
return "text-yellow-500";
|
|
default:
|
|
return "text-blue-500";
|
|
}
|
|
}
|
|
|
|
export function QuickSearch() {
|
|
const [open, setOpen] = useState(false);
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const router = useRouter();
|
|
|
|
// Fetch tasks when search opens
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
setLoading(true);
|
|
const fetchData = async () => {
|
|
try {
|
|
// Fetch tasks
|
|
const { data: tasksData } = await supabaseClient
|
|
.from('tasks')
|
|
.select('id, title, status, priority, project_id')
|
|
.order('updated_at', { ascending: false })
|
|
.limit(50);
|
|
|
|
// Fetch projects for names
|
|
const { data: projectsData } = await supabaseClient
|
|
.from('projects')
|
|
.select('id, name');
|
|
|
|
setTasks(tasksData || []);
|
|
setProjects(projectsData || []);
|
|
} catch (err) {
|
|
console.error('Error fetching search data:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, [open]);
|
|
|
|
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);
|
|
command();
|
|
}, []);
|
|
|
|
const getProjectName = (projectId: string) => {
|
|
return projects.find(p => p.id === projectId)?.name || 'Unknown';
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Search Button Trigger */}
|
|
<button
|
|
onClick={() => setOpen(true)}
|
|
className="flex items-center gap-2 px-3 py-1.5 text-sm text-muted-foreground bg-muted hover:bg-muted/80 rounded-md border border-border/50 transition-colors"
|
|
aria-label="Open search (⌘K)"
|
|
>
|
|
<Search className="w-4 h-4" aria-hidden="true" />
|
|
<span className="hidden sm:inline">Search...</span>
|
|
<kbd className="hidden sm:inline-flex h-5 select-none items-center gap-1 rounded border bg-background px-1.5 font-mono text-[10px] font-medium text-muted-foreground" aria-hidden="true">
|
|
<span className="text-xs">⌘</span>K
|
|
</kbd>
|
|
</button>
|
|
|
|
{/* Command Dialog */}
|
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
<CommandInput placeholder="Type a command or search..." />
|
|
<CommandList>
|
|
<CommandEmpty>No results found.</CommandEmpty>
|
|
|
|
{/* Navigation */}
|
|
<CommandGroup heading="Navigation">
|
|
{navItems.map((item) => {
|
|
const Icon = item.icon;
|
|
return (
|
|
<CommandItem
|
|
key={item.href}
|
|
onSelect={() => runCommand(() => router.push(item.href))}
|
|
>
|
|
<Icon className="w-4 h-4 mr-2" />
|
|
<span>{item.name}</span>
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</CommandGroup>
|
|
|
|
<CommandSeparator />
|
|
|
|
{/* Quick Links */}
|
|
<CommandGroup heading="Quick Links">
|
|
{quickLinks.map((link) => {
|
|
const Icon = link.icon;
|
|
return (
|
|
<CommandItem
|
|
key={link.url}
|
|
onSelect={() => runCommand(() => window.open(link.url, "_blank"))}
|
|
>
|
|
<Icon className="w-4 h-4 mr-2" />
|
|
<span>{link.name}</span>
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</CommandGroup>
|
|
|
|
{/* Tasks */}
|
|
{tasks.length > 0 && (
|
|
<>
|
|
<CommandSeparator />
|
|
<CommandGroup heading={`Tasks (${tasks.length})`}>
|
|
{tasks.slice(0, 10).map((task) => (
|
|
<CommandItem
|
|
key={task.id}
|
|
onSelect={() =>
|
|
runCommand(() =>
|
|
window.open(
|
|
`https://gantt-board.vercel.app/tasks/${task.id}`,
|
|
"_blank"
|
|
)
|
|
)
|
|
}
|
|
>
|
|
{getStatusIcon(task.status)}
|
|
<span className="mr-2 truncate">{task.title}</span>
|
|
<span className={`text-xs ${getPriorityColor(task.priority)}`}>
|
|
{task.priority}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground ml-auto">
|
|
{getProjectName(task.project_id)}
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</>
|
|
)}
|
|
</CommandList>
|
|
</CommandDialog>
|
|
</>
|
|
);
|
|
} |