"use client"; import { PlusIcon } from "@radix-ui/react-icons"; import { useMemo, useState } from "react"; import { Column, TaskCard as Task } from "@/types"; import ColumnContainer from "@/components/kanban/column"; import { DndContext, DragEndEvent, DragOverEvent, DragOverlay, DragStartEvent, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { SortableContext, arrayMove } from "@dnd-kit/sortable"; import { createPortal } from "react-dom"; import TaskCard from "@/components/kanban/task"; const defaultCols: Column[] = [ { id: "todo", title: "Todo", }, { id: "doing", title: "Work in progress", }, { id: "done", title: "Done", }, ]; const defaultTasks: Task[] = [ { id: "1", status: "todo", name: "List admin APIs for dashboard", }, { id: "2", status: "todo", name: "Develop user registration functionality with OTP delivered on SMS after email confirmation and phone number confirmation", }, { id: "3", status: "doing", name: "Conduct security testing", }, { id: "4", status: "doing", name: "Analyze competitors", }, { id: "5", status: "done", name: "Create UI kit documentation", }, { id: "6", status: "done", name: "Dev meeting", }, { id: "7", status: "done", name: "Deliver dashboard prototype", }, { id: "8", status: "todo", name: "Optimize application performance", }, { id: "9", status: "todo", name: "Implement data validation", }, { id: "10", status: "todo", name: "Design database schema", }, { id: "11", status: "todo", name: "Integrate SSL web certificates into workflow", }, { id: "12", status: "doing", name: "Implement error logging and monitoring", }, { id: "13", status: "doing", name: "Design and implement responsive UI", }, ]; function KanbanBoard() { const [columns, setColumns] = useState<Column[]>(defaultCols); const columnsId = useMemo(() => columns.map((col) => col.id), [columns]); const [tasks, setTasks] = useState<Task[]>(defaultTasks); const [activeColumn, setActiveColumn] = useState<Column | null>(null); const [activeTask, setActiveTask] = useState<Task | null>(null); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 10, }, }) ); return ( <div className=" m-auto flex w-full items-center overflow-x-auto overflow-y-hidden px-[40px] " > <DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver} > <div className="m-auto flex gap-4"> <div className="flex gap-4"> <SortableContext items={columnsId}> {columns.map((col) => ( <ColumnContainer key={col.id} column={col} deleteColumn={deleteColumn} updateColumn={updateColumn} createTask={createTask} deleteTask={deleteTask} updateTask={updateTask} tasks={tasks.filter((task) => task.status === col.id)} /> ))} </SortableContext> </div> <button onClick={() => { createNewColumn(); }} className=" h-[60px] w-[350px] min-w-[350px] cursor-pointer rounded-lg bg-mainBackgroundColor border-2 border-columnBackgroundColor p-4 ring-rose-500 hover:ring-2 flex gap-2 " > <PlusIcon /> Add Column </button> </div> {createPortal( <DragOverlay> {activeColumn && ( <ColumnContainer column={activeColumn} deleteColumn={deleteColumn} updateColumn={updateColumn} createTask={createTask} deleteTask={deleteTask} updateTask={updateTask} tasks={tasks.filter((task) => task.status === activeColumn.id)} /> )} {activeTask && ( <TaskCard task={activeTask} deleteTask={deleteTask} updateTask={updateTask} /> )} </DragOverlay>, document.body )} </DndContext> </div> ); function createTask(status: string) { const newTask: Task = { id: String(generateId()), status, name: `Task ${tasks.length + 1}`, }; setTasks([...tasks, newTask]); } function deleteTask(id: string) { const newTasks = tasks.filter((task) => task.id !== id); setTasks(newTasks); } function updateTask(id: string, name: string) { const newTasks = tasks.map((task) => { if (task.id !== id) return task; return { ...task, name }; }); setTasks(newTasks); } function createNewColumn() { const columnToAdd: Column = { id: String(generateId()), title: `Column ${columns.length + 1}`, }; setColumns([...columns, columnToAdd]); } function deleteColumn(id: string) { const filteredColumns = columns.filter((col) => col.id !== id); setColumns(filteredColumns); const newTasks = tasks.filter((t) => t.status !== id); setTasks(newTasks); } function updateColumn(id: string, title: string) { const newColumns = columns.map((col) => { if (col.id !== id) return col; return { ...col, title }; }); setColumns(newColumns); } function onDragStart(event: DragStartEvent) { if (event.active.data.current?.type === "Column") { setActiveColumn(event.active.data.current.column); return; } if (event.active.data.current?.type === "Task") { setActiveTask(event.active.data.current.task); return; } } function onDragEnd(event: DragEndEvent) { setActiveColumn(null); setActiveTask(null); const { active, over } = event; if (!over) return; const activeId = active.id; const overId = over.id; if (activeId === overId) return; const isActiveAColumn = active.data.current?.type === "Column"; if (!isActiveAColumn) return; console.log("DRAG END"); setColumns((columns) => { const activeColumnIndex = columns.findIndex((col) => col.id === activeId); const overColumnIndex = columns.findIndex((col) => col.id === overId); return arrayMove(columns, activeColumnIndex, overColumnIndex); }); } function onDragOver(event: DragOverEvent) { const { active, over } = event; if (!over) return; const activeId = active.id; const overId = over.id; if (activeId === overId) return; const isActiveATask = active.data.current?.type === "Task"; const isOverATask = over.data.current?.type === "Task"; if (!isActiveATask) return; // Im dropping a Task over another Task if (isActiveATask && isOverATask) { setTasks((tasks) => { const activeIndex = tasks.findIndex((t) => t.id === activeId); const overIndex = tasks.findIndex((t) => t.id === overId); if (tasks[activeIndex].status != tasks[overIndex].status) { // Fix introduced after video recording tasks[activeIndex].status = tasks[overIndex].status; return arrayMove(tasks, activeIndex, overIndex - 1); } return arrayMove(tasks, activeIndex, overIndex); }); } const isOverAColumn = over.data.current?.type === "Column"; // Im dropping a Task over a column if (isActiveATask && isOverAColumn) { setTasks((tasks) => { const activeIndex = tasks.findIndex((t) => t.id === activeId); tasks[activeIndex].status = String(overId); console.log("DROPPING TASK OVER COLUMN", { activeIndex }); return arrayMove(tasks, activeIndex, activeIndex); }); } } } function generateId() { /* Generate a random number between 0 and 10000 */ return Math.floor(Math.random() * 10001); } export default KanbanBoard;