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 + +