From de697d436b5e6bf3b1aeef3c76e07b8507e8fa98 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Tue, 4 Nov 2025 11:50:01 +0100 Subject: [PATCH] feat(frontend): Implement Issue management and Kanban board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive Issue management functionality with drag-and-drop Kanban board. Changes: - Created Issue API client (issues.ts) with CRUD operations - Implemented React Query hooks for Issue data management - Added IssueCard component with drag-and-drop support using @dnd-kit - Created KanbanColumn component with droppable zones - Built CreateIssueDialog with form validation using zod - Implemented Kanban page at /projects/[id]/kanban with DnD status changes - Added missing UI components (textarea, select, skeleton) - Enhanced API client with helper methods (get, post, put, patch, delete) - Installed dependencies: @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities, @radix-ui/react-select, sonner - Fixed SignalR ConnectionManager TypeScript error - Preserved legacy KanbanBoard component for backward compatibility Features: - Drag and drop issues between Backlog, Todo, InProgress, and Done columns - Real-time status updates via API - Issue creation with type (Story, Task, Bug, Epic) and priority - Visual feedback with priority colors and type icons - Toast notifications for user actions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/(dashboard)/dashboard/page.tsx | 200 +++++++++++++----- app/(dashboard)/projects/[id]/kanban/page.tsx | 114 ++++++++++ .../features/issues/CreateIssueDialog.tsx | 184 ++++++++++++++++ components/features/kanban/IssueCard.tsx | 58 +++++ components/features/kanban/KanbanBoard.tsx | 26 ++- components/features/kanban/KanbanColumn.tsx | 56 ++--- components/ui/select.tsx | 160 ++++++++++++++ components/ui/skeleton.tsx | 15 ++ components/ui/textarea.tsx | 24 +++ lib/api/client.ts | 28 +++ lib/api/issues.ts | 84 ++++++++ lib/hooks/use-issues.ts | 102 +++++++++ lib/signalr/ConnectionManager.ts | 4 +- package-lock.json | 155 ++++++++++++++ package.json | 5 + 15 files changed, 1134 insertions(+), 81 deletions(-) create mode 100644 app/(dashboard)/projects/[id]/kanban/page.tsx create mode 100644 components/features/issues/CreateIssueDialog.tsx create mode 100644 components/features/kanban/IssueCard.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 lib/api/issues.ts create mode 100644 lib/hooks/use-issues.ts diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx index c523578..f53b7ed 100644 --- a/app/(dashboard)/dashboard/page.tsx +++ b/app/(dashboard)/dashboard/page.tsx @@ -1,108 +1,204 @@ 'use client'; import Link from 'next/link'; -import { FolderKanban, Plus } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import { useState } from 'react'; +import { Plus, FolderKanban, Archive, TrendingUp, ArrowRight } from 'lucide-react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Skeleton } from '@/components/ui/skeleton'; import { useProjects } from '@/lib/hooks/use-projects'; +import { CreateProjectDialog } from '@/components/features/projects/CreateProjectDialog'; export default function DashboardPage() { - const { data: projects } = useProjects(); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const { data: projects, isLoading } = useProjects(); + + // Calculate statistics + const stats = { + totalProjects: projects?.length || 0, + activeProjects: projects?.filter(p => p.status === 'Active').length || 0, + archivedProjects: projects?.filter(p => p.status === 'Archived').length || 0, + }; + + // Get recent projects (sort by creation time, take first 5) + const recentProjects = projects + ?.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .slice(0, 5) || []; return ( -
-
-

Dashboard

-

- Welcome to ColaFlow - Your AI-powered project management system -

+
+ {/* Header */} +
+
+

Dashboard

+

+ Welcome back! Here's an overview of your projects. +

+
+
-
+ {/* Statistics Cards */} +
Total Projects -
{projects?.length || 0}
-

- Active projects in your workspace -

+ {isLoading ? ( + + ) : ( + <> +
{stats.totalProjects}
+

+ Projects in your workspace +

+ + )}
Active Projects - + -
- {projects?.filter((p) => p.status === 'Active').length || 0} -
-

- Currently in progress -

+ {isLoading ? ( + + ) : ( + <> +
{stats.activeProjects}
+

+ Currently in progress +

+ + )}
- Quick Actions - + Archived Projects + - - - + {isLoading ? ( + + ) : ( + <> +
{stats.archivedProjects}
+

+ Completed or on hold +

+ + )}
+ {/* Recent Projects */} - Recent Projects - - Your most recently updated projects - +
+
+ Recent Projects + Your recently created projects +
+ +
- {projects && projects.length > 0 ? ( -
- {projects.slice(0, 5).map((project) => ( + {isLoading ? ( +
+ {[1, 2, 3].map((i) => ( +
+ +
+ + +
+
+ ))} +
+ ) : recentProjects.length > 0 ? ( +
+ {recentProjects.map((project) => ( -
-
-

{project.name}

-

{project.key}

+
+
+

{project.name}

+ + {project.status} +
- - {project.status} - +

+ {project.key} • {project.description || 'No description'} +

+
+
+ {new Date(project.createdAt).toLocaleDateString()}
))}
) : ( -

- No projects yet. Create your first project to get started. -

+
+ +

+ No projects yet. Create your first project to get started. +

+ +
)} + + {/* Quick Actions Card */} + + + Quick Actions + Common tasks to get you started + + + + + + + +
); } diff --git a/app/(dashboard)/projects/[id]/kanban/page.tsx b/app/(dashboard)/projects/[id]/kanban/page.tsx new file mode 100644 index 0000000..22ae338 --- /dev/null +++ b/app/(dashboard)/projects/[id]/kanban/page.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { + DndContext, + DragEndEvent, + DragOverlay, + DragStartEvent, + closestCorners, +} from '@dnd-kit/core'; +import { useState } from 'react'; +import { useIssues, useChangeIssueStatus } from '@/lib/hooks/use-issues'; +import { Button } from '@/components/ui/button'; +import { Plus, Loader2 } from 'lucide-react'; +import { Issue } from '@/lib/api/issues'; +import { KanbanColumn } from '@/components/features/kanban/KanbanColumn'; +import { IssueCard } from '@/components/features/kanban/IssueCard'; +import { CreateIssueDialog } from '@/components/features/issues/CreateIssueDialog'; + +const COLUMNS = [ + { id: 'Backlog', title: 'Backlog', color: 'bg-gray-100' }, + { id: 'Todo', title: 'To Do', color: 'bg-blue-100' }, + { id: 'InProgress', title: 'In Progress', color: 'bg-yellow-100' }, + { id: 'Done', title: 'Done', color: 'bg-green-100' }, +]; + +export default function KanbanPage() { + const params = useParams(); + const projectId = params.id as string; + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [activeIssue, setActiveIssue] = useState(null); + + const { data: issues, isLoading } = useIssues(projectId); + const changeStatusMutation = useChangeIssueStatus(projectId); + + // Group issues by status + const issuesByStatus = { + Backlog: issues?.filter((i) => i.status === 'Backlog') || [], + Todo: issues?.filter((i) => i.status === 'Todo') || [], + InProgress: issues?.filter((i) => i.status === 'InProgress') || [], + Done: issues?.filter((i) => i.status === 'Done') || [], + }; + + const handleDragStart = (event: DragStartEvent) => { + const issue = issues?.find((i) => i.id === event.active.id); + setActiveIssue(issue || null); + }; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + setActiveIssue(null); + + if (!over || active.id === over.id) return; + + const newStatus = over.id as string; + const issue = issues?.find((i) => i.id === active.id); + + if (issue && issue.status !== newStatus) { + changeStatusMutation.mutate({ issueId: issue.id, status: newStatus }); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+

Kanban Board

+

+ Drag and drop to update issue status +

+
+ +
+ + +
+ {COLUMNS.map((column) => ( + + ))} +
+ + + {activeIssue && } + +
+ + +
+ ); +} diff --git a/components/features/issues/CreateIssueDialog.tsx b/components/features/issues/CreateIssueDialog.tsx new file mode 100644 index 0000000..e1db188 --- /dev/null +++ b/components/features/issues/CreateIssueDialog.tsx @@ -0,0 +1,184 @@ +'use client'; + +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { useCreateIssue } from '@/lib/hooks/use-issues'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; + +const createIssueSchema = z.object({ + title: z.string().min(1, 'Title is required'), + description: z.string().min(1, 'Description is required'), + type: z.enum(['Story', 'Task', 'Bug', 'Epic']), + priority: z.enum(['Low', 'Medium', 'High', 'Critical']), +}); + +interface CreateIssueDialogProps { + projectId: string; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function CreateIssueDialog({ + projectId, + open, + onOpenChange, +}: CreateIssueDialogProps) { + const form = useForm({ + resolver: zodResolver(createIssueSchema), + defaultValues: { + title: '', + description: '', + type: 'Task' as const, + priority: 'Medium' as const, + }, + }); + + const createMutation = useCreateIssue(projectId); + + const onSubmit = (data: z.infer) => { + createMutation.mutate(data, { + onSuccess: () => { + form.reset(); + onOpenChange(false); + }, + }); + }; + + return ( + + + + Create New Issue + +
+ + ( + + Title + + + + + + )} + /> + + ( + + Description + +