import React, { useState, useMemo } from 'react'; import { DndContext, DragOverlay, closestCorners, KeyboardSensor, PointerSensor, useSensor, useSensors, DragStartEvent, DragOverEvent, DragEndEvent, } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, horizontalListSortingStrategy, } from '@dnd-kit/sortable'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; // Types interface Task { id: string; title: string; description: string; priority: 'low' | 'medium' | 'high' | 'urgent'; assignee?: string; tags: string[]; columnId: string; } interface Column { id: string; title: string; wipLimit?: number; color: string; } // Draggable Task Card function TaskCard({ task, isDragging }: { task: Task; isDragging?: boolean }) { const { attributes, listeners, setNodeRef, transform, transition, } = useSortable({ id: task.id, data: { type: 'task', task, }, }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; return (

{task.title}

{task.description}

{task.assignee && (
{task.assignee}
)}
{task.tags.map((tag) => ( {tag} ))}
); } // Droppable Column function BoardColumn({ column, tasks, isOver, }: { column: Column; tasks: Task[]; isOver?: boolean; }) { const { setNodeRef, attributes, listeners, transform, transition, } = useSortable({ id: column.id, data: { type: 'column', column, }, }); const style = { transform: CSS.Transform.toString(transform), transition, }; const isOverLimit = column.wipLimit && tasks.length >= column.wipLimit; return (

{column.title}

{tasks.length} {column.wipLimit && ` / ${column.wipLimit}`}
t.id)} strategy={verticalListSortingStrategy} >
{tasks.map((task) => ( ))} {tasks.length === 0 && (
Drop tasks here
)}
); } // Main Kanban Board export function KanbanBoard() { const [columns] = useState([ { id: 'backlog', title: 'Backlog', color: '#94a3b8' }, { id: 'todo', title: 'To Do', color: '#60a5fa', wipLimit: 5 }, { id: 'inprogress', title: 'In Progress', color: '#fbbf24', wipLimit: 3 }, { id: 'review', title: 'Review', color: '#a78bfa', wipLimit: 2 }, { id: 'done', title: 'Done', color: '#34d399' }, ]); const [tasks, setTasks] = useState([ { id: 'task-1', title: 'Setup project repository', description: 'Initialize git repo and add initial files', priority: 'high', assignee: 'John Doe', tags: ['setup', 'git'], columnId: 'done', }, { id: 'task-2', title: 'Design database schema', description: 'Create ERD and define relationships', priority: 'urgent', assignee: 'Jane Smith', tags: ['backend', 'database'], columnId: 'inprogress', }, { id: 'task-3', title: 'Implement authentication', description: 'Add JWT-based auth system', priority: 'high', assignee: 'John Doe', tags: ['backend', 'security'], columnId: 'todo', }, { id: 'task-4', title: 'Create landing page', description: 'Design and implement homepage', priority: 'medium', tags: ['frontend', 'design'], columnId: 'todo', }, { id: 'task-5', title: 'Write API documentation', description: 'Document all API endpoints', priority: 'low', tags: ['documentation'], columnId: 'backlog', }, ]); const [activeId, setActiveId] = useState(null); // Group tasks by column const tasksByColumn = useMemo(() => { const map: Record = {}; columns.forEach((col) => { map[col.id] = tasks.filter((task) => task.columnId === col.id); }); return map; }, [tasks, columns]); // Sensors for drag detection const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 10, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); // Find container for task function findContainer(id: string) { if (columns.find((col) => col.id === id)) { return id; } const task = tasks.find((t) => t.id === id); return task?.columnId; } // Drag handlers function handleDragStart(event: DragStartEvent) { const { active } = event; setActiveId(active.id as string); } function handleDragOver(event: DragOverEvent) { const { active, over } = event; if (!over) return; const activeContainer = findContainer(active.id as string); const overContainer = findContainer(over.id as string); if (!activeContainer || !overContainer || activeContainer === overContainer) { return; } setTasks((prev) => { const activeTask = prev.find((t) => t.id === active.id); if (!activeTask) return prev; // Check WIP limit const overColumn = columns.find((c) => c.id === overContainer); if (overColumn?.wipLimit) { const tasksInColumn = prev.filter((t) => t.columnId === overContainer); if (tasksInColumn.length >= overColumn.wipLimit) { // Show warning console.warn(`Column ${overColumn.title} has reached its WIP limit`); return prev; } } // Move task to new column return prev.map((task) => task.id === activeTask.id ? { ...task, columnId: overContainer } : task ); }); } function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (!over) { setActiveId(null); return; } const activeContainer = findContainer(active.id as string); const overContainer = findContainer(over.id as string); if (!activeContainer || !overContainer) { setActiveId(null); return; } if (activeContainer === overContainer) { // Reorder within same column const containerTasks = tasksByColumn[overContainer]; const oldIndex = containerTasks.findIndex((t) => t.id === active.id); const newIndex = containerTasks.findIndex((t) => t.id === over.id); if (oldIndex !== newIndex) { const newTasks = arrayMove(containerTasks, oldIndex, newIndex); setTasks((prev) => [ ...prev.filter((t) => t.columnId !== overContainer), ...newTasks, ]); } } setActiveId(null); } const activeTask = activeId ? tasks.find((t) => t.id === activeId) : null; return (

Project Board

c.id)} strategy={horizontalListSortingStrategy} >
{columns.map((column) => ( ))}
{activeTask && }
); } // Styles const styles = ` .kanban-board { padding: 2rem; background: var(--color-gray-50); min-height: 100vh; } .columns-container { display: flex; gap: 1.5rem; overflow-x: auto; padding-bottom: 2rem; } .board-column { flex: 0 0 320px; background: var(--color-white); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); display: flex; flex-direction: column; max-height: calc(100vh - 120px); } .board-column.column-over { background: var(--color-primary-50); box-shadow: var(--shadow-md); } .board-column.over-limit { background: var(--color-red-50); } .column-header { padding: 1rem; border-top: 3px solid; border-radius: var(--radius-lg) var(--radius-lg) 0 0; display: flex; align-items: center; gap: 0.5rem; } .column-drag-handle { cursor: grab; padding: 0.25rem; background: transparent; border: none; color: var(--color-text-tertiary); } .column-title { flex: 1; font-size: 1rem; font-weight: 600; margin: 0; } .task-count { background: var(--color-gray-100); padding: 0.25rem 0.5rem; border-radius: var(--radius-full); font-size: 0.875rem; font-weight: 500; } .tasks-container { flex: 1; overflow-y: auto; padding: 0.5rem; display: flex; flex-direction: column; gap: 0.75rem; } .task-card { background: var(--color-white); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 1rem; cursor: grab; transition: all 0.2s; } .task-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); } .task-card.priority-urgent { border-left: 3px solid var(--color-red-500); } .task-card.priority-high { border-left: 3px solid var(--color-orange-500); } .task-card.priority-medium { border-left: 3px solid var(--color-yellow-500); } .task-card.priority-low { border-left: 3px solid var(--color-blue-500); } .task-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.5rem; } .task-title { font-size: 0.875rem; font-weight: 600; margin: 0; flex: 1; } .priority-indicator { width: 8px; height: 8px; border-radius: 50%; } .priority-indicator.priority-urgent { background: var(--color-red-500); } .priority-indicator.priority-high { background: var(--color-orange-500); } .priority-indicator.priority-medium { background: var(--color-yellow-500); } .priority-indicator.priority-low { background: var(--color-blue-500); } .task-description { font-size: 0.813rem; color: var(--color-text-secondary); margin: 0.5rem 0; line-height: 1.4; } .task-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 0.75rem; } .assignee img { width: 24px; height: 24px; border-radius: 50%; border: 2px solid var(--color-white); } .tags { display: flex; gap: 0.25rem; flex-wrap: wrap; } .tag { background: var(--color-gray-100); color: var(--color-text-secondary); padding: 0.125rem 0.375rem; border-radius: var(--radius-sm); font-size: 0.75rem; } .empty-column { padding: 2rem; text-align: center; color: var(--color-text-tertiary); border: 2px dashed var(--color-border); border-radius: var(--radius-md); margin: 0.5rem; } .add-task-btn { margin: 0.5rem; padding: 0.75rem; background: transparent; border: 1px dashed var(--color-border); border-radius: var(--radius-md); color: var(--color-text-secondary); cursor: pointer; transition: all 0.2s; } .add-task-btn:hover { background: var(--color-gray-50); border-color: var(--color-primary); color: var(--color-primary); } `;