diff --git a/apps/web/components/kanban-board.tsx b/apps/web/components/kanban-board.tsx index 714a9e9adf..ae7f4af3b1 100644 --- a/apps/web/components/kanban-board.tsx +++ b/apps/web/components/kanban-board.tsx @@ -16,15 +16,52 @@ import { ThemeToggle } from './theme-toggle'; import { Button } from '@/components/ui/button'; import { IconPlus } from '@tabler/icons-react'; import { TaskCreateDialog } from './task-create-dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; -type ColumnId = 'todo' | 'in-progress' | 'in-review' | 'done'; - -const COLUMNS: Column[] = [ - { id: 'todo', title: 'To Do', color: 'bg-neutral-400' }, - { id: 'in-progress', title: 'In Progress', color: 'bg-blue-500' }, - { id: 'in-review', title: 'In Review', color: 'bg-yellow-500' }, - { id: 'done', title: 'Done', color: 'bg-green-500' }, -]; +const VIEWS: Record< + string, + { + label: string; + columns: Column[]; + } +> = { + team: { + label: 'Team view', + columns: [ + { id: 'backlog', title: 'Backlog', color: 'bg-neutral-400' }, + { id: 'solution-design', title: 'Solution Design', color: 'bg-sky-500' }, + { id: 'ready-for-dev', title: 'Ready for Dev', color: 'bg-indigo-500' }, + { id: 'in-progress', title: 'In Progress', color: 'bg-blue-500' }, + { id: 'review', title: 'Review', color: 'bg-yellow-500' }, + { id: 'done', title: 'Done', color: 'bg-green-500' }, + ], + }, + user: { + label: 'User view', + columns: [ + { id: 'todo', title: 'To Do', color: 'bg-neutral-400' }, + { id: 'in-progress', title: 'In Progress', color: 'bg-blue-500' }, + { id: 'review', title: 'Review', color: 'bg-yellow-500' }, + { id: 'done', title: 'Done', color: 'bg-green-500' }, + ], + }, + architect: { + label: 'Architect view', + columns: [ + { id: 'backlog', title: 'Backlog', color: 'bg-neutral-400' }, + { id: 'high-level-design', title: 'High Level Design', color: 'bg-cyan-500' }, + { id: 'low-level-design', title: 'Low Level Design', color: 'bg-violet-500' }, + { id: 'review', title: 'Review', color: 'bg-yellow-500' }, + { id: 'done', title: 'Done', color: 'bg-green-500' }, + ], + }, +}; const initialTasks: Task[] = [ { id: '1', title: 'Design database schema', status: 'todo' }, @@ -37,6 +74,8 @@ export function KanbanBoard() { const [isDialogOpen, setIsDialogOpen] = useState(false); const [activeTaskId, setActiveTaskId] = useState(null); const [isMounted, setIsMounted] = useState(false); + const [activeViewId, setActiveViewId] = useState('team'); + const [editingTaskId, setEditingTaskId] = useState(null); useEffect(() => { setIsMounted(true); @@ -54,6 +93,12 @@ export function KanbanBoard() { () => tasks.find((task) => task.id === activeTaskId) ?? null, [tasks, activeTaskId] ); + const editingTask = useMemo( + () => tasks.find((task) => task.id === editingTaskId) ?? null, + [tasks, editingTaskId] + ); + const activeView = VIEWS[activeViewId] ?? VIEWS.team; + const activeColumns = activeView.columns; const handleDragStart = (event: DragStartEvent) => { setActiveTaskId(event.active.id as string); @@ -79,7 +124,18 @@ export function KanbanBoard() { setActiveTaskId(null); }; - const handleAddTask = (columnId: string, title: string, description?: string) => { + const handleDialogSubmit = (title: string, description?: string) => { + if (editingTaskId) { + setTasks((tasks) => + tasks.map((task) => + task.id === editingTaskId ? { ...task, title, description } : task + ) + ); + setEditingTaskId(null); + return; + } + + const columnId = activeColumns[0]?.id ?? 'todo'; const newTask: Task = { id: crypto.randomUUID(), title, @@ -89,6 +145,11 @@ export function KanbanBoard() { setTasks((tasks) => [...tasks, newTask]); }; + const handleEditTask = (task: Task) => { + setEditingTaskId(task.id); + setIsDialogOpen(true); + }; + const getTasksForColumn = (columnId: string) => { return tasks.filter((task) => task.status === columnId); }; @@ -100,9 +161,26 @@ export function KanbanBoard() { return (
-

Kanban Board

-
- @@ -111,8 +189,19 @@ export function KanbanBoard() {
handleAddTask('todo', title, description)} + onOpenChange={(open) => { + setIsDialogOpen(open); + if (!open) { + setEditingTaskId(null); + } + }} + onSubmit={(title, description) => handleDialogSubmit(title, description)} + initialValues={ + editingTask + ? { title: editingTask.title, description: editingTask.description } + : undefined + } + submitLabel={editingTask ? 'Save' : 'Create'} /> -
-
- {COLUMNS.map((column) => ( +
+
+ {activeColumns.map((column) => ( ))}
diff --git a/apps/web/components/kanban-card.tsx b/apps/web/components/kanban-card.tsx index a7de1deb6c..862a4ee513 100644 --- a/apps/web/components/kanban-card.tsx +++ b/apps/web/components/kanban-card.tsx @@ -14,22 +14,23 @@ export interface Task { interface KanbanCardProps { task: Task; + onClick?: (task: Task) => void; } function KanbanCardLayout({ task, className }: KanbanCardProps & { className?: string }) { return ( - -

{task.title}

+ +

{task.title}

{task.description && ( -

{task.description}

+

{task.description}

)}
); } -export function KanbanCard({ task }: KanbanCardProps) { +export function KanbanCard({ task, onClick }: KanbanCardProps) { const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: task.id, }); @@ -49,13 +50,14 @@ export function KanbanCard({ task }: KanbanCardProps) { 'cursor-grab active:cursor-grabbing mb-2 w-full py-0', isDragging && 'opacity-50 z-50' )} + onClick={() => onClick?.(task)} {...listeners} {...attributes} > - -

{task.title}

+ +

{task.title}

{task.description && ( -

{task.description}

+

{task.description}

)}
diff --git a/apps/web/components/kanban-column.tsx b/apps/web/components/kanban-column.tsx index 004bf832c0..f00daf75ca 100644 --- a/apps/web/components/kanban-column.tsx +++ b/apps/web/components/kanban-column.tsx @@ -14,9 +14,10 @@ export interface Column { interface KanbanColumnProps { column: Column; tasks: Task[]; + onEditTask: (task: Task) => void; } -export function KanbanColumn({ column, tasks }: KanbanColumnProps) { +export function KanbanColumn({ column, tasks, onEditTask }: KanbanColumnProps) { const { setNodeRef, isOver } = useDroppable({ id: column.id, }); @@ -25,12 +26,12 @@ export function KanbanColumn({ column, tasks }: KanbanColumnProps) {
{/* Column Header */} -
+

{column.title}

@@ -41,11 +42,13 @@ export function KanbanColumn({ column, tasks }: KanbanColumnProps) {
{/* Tasks */} -
+
{tasks.length === 0 ? (

No tasks yet

) : ( - tasks.map((task) => ) + tasks.map((task) => ( + + )) )}
diff --git a/apps/web/components/task-create-dialog.tsx b/apps/web/components/task-create-dialog.tsx index cdcead3033..c09e34741c 100644 --- a/apps/web/components/task-create-dialog.tsx +++ b/apps/web/components/task-create-dialog.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, FormEvent } from 'react'; +import { useEffect, useState, FormEvent } from 'react'; import { AlertDialog, AlertDialogContent, @@ -18,16 +18,29 @@ interface TaskCreateDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSubmit: (title: string, description: string) => void; + initialValues?: { + title: string; + description?: string; + }; + submitLabel?: string; } export function TaskCreateDialog({ open, onOpenChange, onSubmit, + initialValues, + submitLabel = 'Create', }: TaskCreateDialogProps) { const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); + useEffect(() => { + if (!open) return; + setTitle(initialValues?.title ?? ''); + setDescription(initialValues?.description ?? ''); + }, [open, initialValues]); + const handleSubmit = (e: FormEvent) => { e.preventDefault(); if (title.trim()) { @@ -46,9 +59,14 @@ export function TaskCreateDialog({ return ( - + onOpenChange(false), + }} + > - Create Task + {submitLabel === 'Create' ? 'Create Task' : 'Edit Task'}
@@ -74,7 +92,7 @@ export function TaskCreateDialog({ Cancel - +
diff --git a/apps/web/components/ui/alert-dialog.tsx b/apps/web/components/ui/alert-dialog.tsx index ebf9f00867..9e79fded23 100644 --- a/apps/web/components/ui/alert-dialog.tsx +++ b/apps/web/components/ui/alert-dialog.tsx @@ -35,7 +35,10 @@ function AlertDialogOverlay({ return ( ) @@ -44,18 +47,20 @@ function AlertDialogOverlay({ function AlertDialogContent({ className, size = "default", + overlayProps, ...props }: React.ComponentProps & { size?: "default" | "sm" + overlayProps?: React.ComponentProps }) { return ( - +