From c8a6d66ea11a08cd23144657b82c909f3e0b4297 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Fri, 10 Nov 2023 12:57:28 +0700 Subject: [PATCH] Constructing kanban system. --- .../kanbanBoard/columnContainer.jsx | 179 +++++++++ .../components/kanbanBoard/kanbanBoard.jsx | 342 ++++++++++++++++++ .../src/components/kanbanBoard/taskCard.jsx | 111 ++++++ 3 files changed, 632 insertions(+) create mode 100644 frontend/src/components/kanbanBoard/columnContainer.jsx create mode 100644 frontend/src/components/kanbanBoard/kanbanBoard.jsx create mode 100644 frontend/src/components/kanbanBoard/taskCard.jsx diff --git a/frontend/src/components/kanbanBoard/columnContainer.jsx b/frontend/src/components/kanbanBoard/columnContainer.jsx new file mode 100644 index 0000000..03bc011 --- /dev/null +++ b/frontend/src/components/kanbanBoard/columnContainer.jsx @@ -0,0 +1,179 @@ +import { SortableContext, useSortable } from "@dnd-kit/sortable"; +import TrashIcon from "../icons/TrashIcon"; +import { Column, Id, Task } from "../types"; +import { CSS } from "@dnd-kit/utilities"; +import { useMemo, useState } from "react"; +import PlusIcon from "../icons/PlusIcon"; +import TaskCard from "./taskCard"; + +function ColumnContainer({ + column, + deleteColumn, + updateColumn, + createTask, + tasks, + deleteTask, + updateTask, +}) { + const [editMode, setEditMode] = useState(false); + + const tasksIds = useMemo(() => { + return tasks.map((task) => task.id); + }, [tasks]); + + const { + setNodeRef, + attributes, + listeners, + transform, + transition, + isDragging, + } = useSortable({ + id: column.id, + data: { + type: "Column", + column, + }, + disabled: editMode, + }); + + const style = { + transition, + transform: CSS.Transform.toString(transform), + }; + + if (isDragging) { + return ( +
+ ); + } + + return ( +
+ {/* Column title */} +
{ + setEditMode(true); + }} + className=" + bg-mainBackgroundColor + text-md + h-[60px] + cursor-grab + rounded-md + rounded-b-none + p-3 + font-bold + border-columnBackgroundColor + border-4 + flex + items-center + justify-between + " + > +
+
+ 0 +
+ {!editMode && column.title} + {editMode && ( + updateColumn(column.id, e.target.value)} + autoFocus + onBlur={() => { + setEditMode(false); + }} + onKeyDown={(e) => { + if (e.key !== "Enter") return; + setEditMode(false); + }} + /> + )} +
+ +
+ + {/* Column task container */} +
+ + {tasks.map((task) => ( + + ))} + +
+ {/* Column footer */} + +
+ ); +} + +export default ColumnContainer; diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx new file mode 100644 index 0000000..a289652 --- /dev/null +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -0,0 +1,342 @@ +import PlusIcon from "../icons/PlusIcon"; +import { useMemo, useState } from "react"; +import { Column, Id, Task } from "../types"; +import ColumnContainer from "./columnContainer"; +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 "./taskCard"; + +const defaultCols = [ + { + id: "todo", + title: "Todo", + }, + { + id: "doing", + title: "Work in progress", + }, + { + id: "done", + title: "Done", + }, +]; + +const defaultTasks = [ + { + id: "1", + columnId: "todo", + content: "List admin APIs for dashboard", + }, + { + id: "2", + columnId: "todo", + content: + "Develop user registration functionality with OTP delivered on SMS after email confirmation and phone number confirmation", + }, + { + id: "3", + columnId: "doing", + content: "Conduct security testing", + }, + { + id: "4", + columnId: "doing", + content: "Analyze competitors", + }, + { + id: "5", + columnId: "done", + content: "Create UI kit documentation", + }, + { + id: "6", + columnId: "done", + content: "Dev meeting", + }, + { + id: "7", + columnId: "done", + content: "Deliver dashboard prototype", + }, + { + id: "8", + columnId: "todo", + content: "Optimize application performance", + }, + { + id: "9", + columnId: "todo", + content: "Implement data validation", + }, + { + id: "10", + columnId: "todo", + content: "Design database schema", + }, + { + id: "11", + columnId: "todo", + content: "Integrate SSL web certificates into workflow", + }, + { + id: "12", + columnId: "doing", + content: "Implement error logging and monitoring", + }, + { + id: "13", + columnId: "doing", + content: "Design and implement responsive UI", + }, +]; + +function KanbanBoard() { + const [columns, setColumns] = useState(defaultCols); + const columnsId = useMemo(() => columns.map((col) => col.id), [columns]); + + const [tasks, setTasks] = useState(defaultTasks); + + const [activeColumn, setActiveColumn] = useState(null); + + const [activeTask, setActiveTask] = useState(null); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 10, + }, + }) + ); + + return ( +
+ +
+
+ + {columns.map((col) => ( + task.columnId === col.id)} + /> + ))} + +
+ +
+ + {createPortal( + + {activeColumn && ( + task.columnId === activeColumn.id + )} + /> + )} + {activeTask && ( + + )} + , + document.body + )} +
+
+ ); + + function createTask(columnId) { + const newTask = { + id: generateId(), + columnId, + content: `Task ${tasks.length + 1}`, + }; + + setTasks([...tasks, newTask]); + } + + function deleteTask(id) { + const newTasks = tasks.filter((task) => task.id !== id); + setTasks(newTasks); + } + + function updateTask(id, content) { + const newTasks = tasks.map((task) => { + if (task.id !== id) return task; + return { ...task, content }; + }); + + setTasks(newTasks); + } + + function createNewColumn() { + const columnToAdd = { + id: generateId(), + title: `Column ${columns.length + 1}`, + }; + + setColumns([...columns, columnToAdd]); + } + + function deleteColumn(id) { + const filteredColumns = columns.filter((col) => col.id !== id); + setColumns(filteredColumns); + + const newTasks = tasks.filter((t) => t.columnId !== id); + setTasks(newTasks); + } + + function updateColumn(id, title) { + const newColumns = columns.map((col) => { + if (col.id !== id) return col; + return { ...col, title }; + }); + + setColumns(newColumns); + } + + function onDragStart(event) { + 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) { + 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; + + 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) { + 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; + + if (isActiveATask && isOverATask) { + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); + const overIndex = tasks.findIndex((t) => t.id === overId); + + if (tasks[activeIndex].columnId !== tasks[overIndex].columnId) { + tasks[activeIndex].columnId = tasks[overIndex].columnId; + return arrayMove(tasks, activeIndex, overIndex - 1); + } + + return arrayMove(tasks, activeIndex, overIndex); + }); + } + + const isOverAColumn = over.data.current?.type === "Column"; + + if (isActiveATask && isOverAColumn) { + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); + + tasks[activeIndex].columnId = overId; + return arrayMove(tasks, activeIndex, activeIndex); + }); + } + } + + function generateId() { + return Math.floor(Math.random() * 10001); + } +} + +export default KanbanBoard; diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx new file mode 100644 index 0000000..226b319 --- /dev/null +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -0,0 +1,111 @@ +import { useState } from "react"; +import TrashIcon from "../icons/TrashIcon"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +function TaskCard({ task, deleteTask, updateTask }) { + const [mouseIsOver, setMouseIsOver] = useState(false); + const [editMode, setEditMode] = useState(true); + + const { + setNodeRef, + attributes, + listeners, + transform, + transition, + isDragging, + } = useSortable({ + id: task.id, + data: { + type: "Task", + task, + }, + disabled: editMode, + }); + + const style = { + transition, + transform: CSS.Transform.toString(transform), + }; + + const toggleEditMode = () => { + setEditMode((prev) => !prev); + setMouseIsOver(false); + }; + + if (isDragging) { + return ( +
+ ); + } + + if (editMode) { + return ( +
+