skalara-core/components/kanban/board.tsx
Christopher Arraya 788b952127 initial commit
2023-11-18 21:09:24 -05:00

348 lines
8.1 KiB
TypeScript

"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;