Skip to content

Commit

Permalink
refactor(kanban): it works
Browse files Browse the repository at this point in the history
  • Loading branch information
danloh committed Feb 27, 2024
1 parent fb4efc8 commit 7b8392c
Show file tree
Hide file tree
Showing 8 changed files with 551 additions and 1,404 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"@headlessui/react": "^1.7.18",
"@popperjs/core": "^2.11.8",
"@react-spring/web": "^9.7.3",
"react-trello": "2.2.11",
"@tabler/icons-react": "^2.42.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
Expand Down
270 changes: 270 additions & 0 deletions src/components/kanban/Board.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { useMemo, useState } from "react";
import {
DndContext,
DragEndEvent,
DragOverEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
PointerSensor,
TouchSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
import { createPortal } from "react-dom";
import { IconPlus } from "@tabler/icons-react";
import { genId } from "utils/helper";
import { Column, Id, Card, KanbanData } from "./types";
import ColumnContainer from "./Column";
import TaskCard from "./Card";

interface Props {
initData: KanbanData,
onKanbanChange: (columns: Column[], cards: Card[]) => void;
}

export default function KanbanBoard({initData, onKanbanChange}: Props) {
const [columns, setColumns] = useState<Column[]>(initData.columns);
const columnsId = useMemo(() => columns.map((col) => col.id), [columns]);

const [tasks, setTasks] = useState<Card[]>(initData.cards);

const [activeColumn, setActiveColumn] = useState<Column | null>(null);
const [activeTask, setActiveTask] = useState<Card | null>(null);

const sensors = useSensors(
useSensor(MouseSensor, {
activationConstraint: {
distance: 10,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 250,
tolerance: 5,
},
}),
useSensor(PointerSensor, {
activationConstraint: {
distance: 10,
},
})
);

function createTask(columnId: Id) {
const newTask: Card = {
id: genId(),
columnId,
content: `Task ${tasks.length + 1}`,
};
const newTasks = [...tasks, newTask];

setTasks(newTasks);
// save to file
onKanbanChange(columns, newTasks);
}

function deleteTask(id: Id) {
const newTasks = tasks.filter((task) => task.id !== id);
setTasks(newTasks);
// save to file
onKanbanChange(columns, newTasks);
}

function updateTask(id: Id, content: string) {
const newTasks = tasks.map((task) => {
if (task.id !== id) return task;
return { ...task, content };
});

setTasks(newTasks);
// save to file
onKanbanChange(columns, newTasks);
}

function createNewColumn() {
const columnToAdd: Column = {
id: genId(),
title: `Column ${columns.length + 1}`,
};
const newColumns = [...columns, columnToAdd];
setColumns(newColumns);
// save to file
onKanbanChange(newColumns, tasks);
}

function deleteColumn(id: Id) {
const filteredColumns = columns.filter((col) => col.id !== id);
setColumns(filteredColumns);

const newTasks = tasks.filter((t) => t.columnId !== id);
setTasks(newTasks);
// save to file
onKanbanChange(filteredColumns, newTasks);
}

function updateColumn(id: Id, title: string) {
const newColumns = columns.map((col) => {
if (col.id !== id) return col;
return { ...col, title };
});

setColumns(newColumns);
// save to file
onKanbanChange(newColumns, tasks);
}

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);
const newColumns = arrayMove(columns, activeColumnIndex, overColumnIndex);
// save to file
onKanbanChange(newColumns, tasks);

return newColumns;
});
}

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

let newTasks = [];
if (tasks[activeIndex].columnId != tasks[overIndex].columnId) {
// Fix introduced after video recording
tasks[activeIndex].columnId = tasks[overIndex].columnId;
newTasks = arrayMove(tasks, activeIndex, overIndex - 1);
} else {
newTasks = arrayMove(tasks, activeIndex, overIndex);
}

// save to file
onKanbanChange(columns, newTasks);

return newTasks;
});
}

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].columnId = overId;
console.log("DROPPING TASK OVER COLUMN", { activeIndex });
const newTasks = arrayMove(tasks, activeIndex, activeIndex);
// save to file
onKanbanChange(columns, newTasks);

return newTasks;
});
}
}

return (
<div className="flex h-full w-full items-center overflow-x-auto overflow-y-hidden px-4">
<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 className="btn btn-neutral mt-[3rem]" onClick={createNewColumn} >
<IconPlus />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>
);
}
104 changes: 104 additions & 0 deletions src/components/kanban/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useState } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { IconTrash } from "@tabler/icons-react";
import { Id, Card } from "./types";

interface Props {
task: Card;
deleteTask: (id: Id) => void;
updateTask: (id: Id, content: string) => void;
}

export default function TaskCard({ task, deleteTask, updateTask }: Props) {
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="p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl border-2 border-green-500 cursor-grab relative bg-slate-500"
/>
);
}

if (editMode) {
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl hover:ring-2 hover:ring-inset hover:ring-green-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="p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl hover:ring-2 hover:ring-inset hover:ring-green-500 cursor-grab relative bg-slate-500 rounded-xl"
onMouseEnter={() => {setMouseIsOver(true);}}
onMouseLeave={() => {setMouseIsOver(false);}}
>
<p className="my-auto h-[90%] w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap no-scollbar">
{task.content}
</p>

{mouseIsOver && (
<button
onClick={() => {deleteTask(task.id);}}
className="stroke-white absolute right-4 top-1/2 -translate-y-1/2 p-2 rounded opacity-60 hover:opacity-100 hover:bg-red-600"
>
<IconTrash />
</button>
)}
</div>
);
}
Loading

0 comments on commit 7b8392c

Please sign in to comment.