Constructing kanban system.

This commit is contained in:
Pattadon 2023-11-10 12:57:28 +07:00
parent 2898668579
commit c8a6d66ea1
3 changed files with 632 additions and 0 deletions

View File

@ -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 (
<div
ref={setNodeRef}
style={style}
className="
bg-columnBackgroundColor
opacity-40
border-2
border-pink-500
w-[350px]
h-[500px]
max-h-[500px]
rounded-md
flex
flex-col
"
></div>
);
}
return (
<div
ref={setNodeRef}
style={style}
className="
bg-columnBackgroundColor
w-[350px]
h-[500px]
max-h-[500px]
rounded-md
flex
flex-col
"
>
{/* Column title */}
<div
{...attributes}
{...listeners}
onClick={() => {
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
"
>
<div className="flex gap-2">
<div
className="
flex
justify-center
items-center
bg-columnBackgroundColor
px-2
py-1
text-sm
rounded-full
"
>
0
</div>
{!editMode && column.title}
{editMode && (
<input
className="bg-black focus:border-rose-500 border rounded outline-none px-2"
value={column.title}
onChange={(e) => updateColumn(column.id, e.target.value)}
autoFocus
onBlur={() => {
setEditMode(false);
}}
onKeyDown={(e) => {
if (e.key !== "Enter") return;
setEditMode(false);
}}
/>
)}
</div>
<button
onClick={() => {
deleteColumn(column.id);
}}
className="
stroke-gray-500
hover:stroke-white
hover:bg-columnBackgroundColor
rounded
px-1
py-2
"
>
<TrashIcon />
</button>
</div>
{/* Column task container */}
<div className="flex flex-grow flex-col gap-4 p-2 overflow-x-hidden overflow-y-auto">
<SortableContext items={tasksIds}>
{tasks.map((task) => (
<TaskCard
key={task.id}
task={task}
deleteTask={deleteTask}
updateTask={updateTask}
/>
))}
</SortableContext>
</div>
{/* Column footer */}
<button
className="flex gap-2 items-center border-columnBackgroundColor border-2 rounded-md p-4 border-x-columnBackgroundColor hover:bg-mainBackgroundColor hover:text-rose-500 active:bg-black"
onClick={() => {
createTask(column.id);
}}
>
<PlusIcon />
Add task
</button>
</div>
);
}
export default ColumnContainer;

View File

@ -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 (
<div
className="
m-auto
flex
min-h-screen
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.columnId === 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.columnId === activeColumn.id
)}
/>
)}
{activeTask && (
<TaskCard
task={activeTask}
deleteTask={deleteTask}
updateTask={updateTask}
/>
)}
</DragOverlay>,
document.body
)}
</DndContext>
</div>
);
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;

View File

@ -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 (
<div
ref={setNodeRef}
style={style}
className="
opacity-30
bg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl border-2 border-rose-500 cursor-grab relative
"
/>
);
}
if (editMode) {
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="bg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl hover:ring-2 hover:ring-inset hover:ring-rose-500 cursor-grab relative"
>
<textarea
className="
h-[90%]
w-full resize-none border-none rounded bg-transparent text-white focus:outline-none
"
value={task.content}
autoFocus
placeholder="Task content here"
onBlur={toggleEditMode}
onKeyDown={(e) => {
if (e.key === "Enter" && e.shiftKey) {
toggleEditMode();
}
}}
onChange={(e) => updateTask(task.id, e.target.value)}
/>
</div>
);
}
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
onClick={toggleEditMode}
className="bg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl hover:ring-2 hover:ring-inset hover:ring-rose-500 cursor-grab relative task"
onMouseEnter={() => {
setMouseIsOver(true);
}}
onMouseLeave={() => {
setMouseIsOver(false);
}}
>
<p className="my-auto h-[90%] w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap">
{task.content}
</p>
{mouseIsOver && (
<button
onClick={() => {
deleteTask(task.id);
}}
className="stroke-white absolute right-4 top-1/2 -translate-y-1/2 bg-columnBackgroundColor p-2 rounded opacity-60 hover:opacity-100"
>
<TrashIcon />
</button>
)}
</div>
);
}
export default TaskCard;