"use client"; import { useState, useEffect, useCallback } from "react"; import { Plus, FolderKanban, ExternalLink, MoreHorizontal, Pencil, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Progress } from "@/components/ui/progress"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { ProjectCreateDialog } from "@/components/projects/project-create-dialog"; import Link from "next/link"; import { toast } from "sonner"; interface Project { id: string; name: string; description: string | null; status: "active" | "paused" | "completed" | "archived"; domainId: string; color: string | null; icon: string | null; targetDate: string | null; taskCount: number; completedCount: number; progress: number; tags: { id: string; name: string; color: string | null }[]; } const statusColors: Record = { active: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200", paused: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200", completed: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200", archived: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200", }; export default function ProjectsPage() { const [projects, setProjects] = useState([]); const [domainId, setDomainId] = useState(null); const [domains, setDomains] = useState<{ id: string; name: string }[]>([]); const [createOpen, setCreateOpen] = useState(false); const [loading, setLoading] = useState(true); const [editingProject, setEditingProject] = useState(null); const [editName, setEditName] = useState(""); const [editDescription, setEditDescription] = useState(""); const [editStatus, setEditStatus] = useState("active"); const [saveSaving, setSaveSaving] = useState(false); const [deleteProjectId, setDeleteProjectId] = useState(null); const [deleting, setDeleting] = useState(false); // Fetch domains useEffect(() => { fetch("/api/domains?sort=sort_order") .then((res) => res.json()) .then((data) => { const items = data.items || []; setDomains(items); if (items.length > 0 && !domainId) { setDomainId(items[0].id); } }) .catch(() => {}); }, []); // eslint-disable-line react-hooks/exhaustive-deps // Fetch projects const fetchProjects = useCallback(async () => { if (!domainId) return; setLoading(true); try { const res = await fetch(`/api/domains/${domainId}/projects`); const data = await res.json(); setProjects(data.items || []); } catch { toast.error("Failed to load projects"); } finally { setLoading(false); } }, [domainId]); useEffect(() => { fetchProjects(); }, [fetchProjects]); // Edit a project const handleEdit = async () => { if (!editingProject || !editName.trim()) return; setSaveSaving(true); try { const res = await fetch(`/api/domains/${domainId}/projects/${editingProject.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: editName.trim(), description: editDescription || null, status: editStatus, }), }); if (!res.ok) throw new Error("Failed to update"); toast.success("Project updated"); setEditingProject(null); fetchProjects(); } catch { toast.error("Failed to update project"); } finally { setSaveSaving(false); } }; // Delete a project const handleDelete = async () => { if (!deleteProjectId) return; setDeleting(true); try { const res = await fetch(`/api/domains/${domainId}/projects/${deleteProjectId}`, { method: "DELETE" }); if (!res.ok) throw new Error("Failed to delete"); toast.success("Project deleted"); setDeleteProjectId(null); fetchProjects(); } catch { toast.error("Failed to delete project"); } finally { setDeleting(false); } }; // Open edit dialog const openEdit = (project: Project, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setEditName(project.name); setEditDescription(project.description || ""); setEditStatus(project.status); setEditingProject(project); }; // Open delete confirmation const confirmDelete = (id: string, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setDeleteProjectId(id); }; // Listen for custom event to open create dialog useEffect(() => { const handler = () => setCreateOpen(true); document.addEventListener("open-create-project", handler); return () => document.removeEventListener("open-create-project", handler); }, []); return (

Projects

Organize work into milestones and track progress.

{domains.length > 1 && ( )}
{loading ? (
Loading projects...
) : projects.length === 0 ? (
) : (
{projects.map((project) => (
{project.color && (
)} {project.name}
{project.description && (

{project.description}

)}
{project.status} {project.targetDate && ( Due {new Date(project.targetDate).toLocaleDateString()} )}
{project.completedCount}/{project.taskCount} tasks {project.progress}%
{project.tags.length > 0 && (
{project.tags.map((tag) => ( {tag.name} ))}
)}
{/* Actions overlay */}
openEdit(project, e)}> Edit confirmDelete(project.id, e)}> Delete
))}
)} {/* Edit Project Dialog */} { if (!open) setEditingProject(null); }}> Edit project Update project details.
setEditName(e.target.value)} autoFocus required />