mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-19 05:54:07 +01:00
Constructing kanban system.
This commit is contained in:
parent
2898668579
commit
c8a6d66ea1
179
frontend/src/components/kanbanBoard/columnContainer.jsx
Normal file
179
frontend/src/components/kanbanBoard/columnContainer.jsx
Normal 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;
|
||||||
342
frontend/src/components/kanbanBoard/kanbanBoard.jsx
Normal file
342
frontend/src/components/kanbanBoard/kanbanBoard.jsx
Normal 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;
|
||||||
111
frontend/src/components/kanbanBoard/taskCard.jsx
Normal file
111
frontend/src/components/kanbanBoard/taskCard.jsx
Normal 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;
|
||||||
Loading…
Reference in New Issue
Block a user