mirror of
https://github.com/SkalaraAI/skalara-core.git
synced 2025-04-09 23:20:15 -04:00
348 lines
8.1 KiB
TypeScript
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;
|