mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-19 05:54:07 +01:00
Merge pull request #34 from TurTaskProject/feature/kanban-board
Add Kanban Board without API
This commit is contained in:
commit
cceca8f482
@ -10,6 +10,10 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@asseinfo/react-kanban": "^2.2.0",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fullcalendar/core": "^6.1.9",
|
||||
@ -21,6 +25,8 @@
|
||||
"@mui/material": "^5.14.15",
|
||||
"@mui/system": "^5.14.15",
|
||||
"@react-oauth/google": "^0.11.1",
|
||||
"@syncfusion/ej2-base": "^23.1.41",
|
||||
"@syncfusion/ej2-kanban": "^23.1.36",
|
||||
"axios": "^1.5.1",
|
||||
"bootstrap": "^5.3.2",
|
||||
"dotenv": "^16.3.1",
|
||||
@ -28,6 +34,7 @@
|
||||
"gapi-script": "^1.2.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-bootstrap": "^2.9.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.11.0",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
import './App.css';
|
||||
import { BrowserRouter, Route, Routes, Link } from 'react-router-dom';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import TestAuth from './components/testAuth';
|
||||
import LoginPage from './components/authentication/LoginPage';
|
||||
@ -8,14 +8,20 @@ import NavBar from './components/nav/Navbar';
|
||||
import Home from './components/Home';
|
||||
import ProfileUpdate from './components/ProfileUpdatePage';
|
||||
import Calendar from './components/calendar/calendar';
|
||||
import KanbanBoard from './components/kanbanBoard/kanbanBoard';
|
||||
import IconSideNav from './components/IconSideNav'; // Import IconSideNav
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div className="App">
|
||||
<div className='display: flex'>
|
||||
<IconSideNav />
|
||||
<div className='flex-1'>
|
||||
<NavBar />
|
||||
<div className='flex items-center justify-center'>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/tasks" element={<KanbanBoard />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/signup" element={<SignUpPage />} />
|
||||
<Route path="/testAuth" element={<TestAuth />} />
|
||||
@ -23,6 +29,8 @@ const App = () => {
|
||||
<Route path="/calendar" element={<Calendar />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { SiFramer, SiTailwindcss, SiReact, SiJavascript, SiCss3 } from "react-icons/si";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import homeLogo from "../assets/home.png";
|
||||
import calendarLogo from "../assets/calendar.png";
|
||||
import planLogo from "../assets/planning.png";
|
||||
@ -8,18 +8,17 @@ import pieLogo from "../assets/pie-chart.png";
|
||||
import plusLogo from "../assets/plus.png";
|
||||
|
||||
const menuItems = [
|
||||
{ id: 0, icon: <homeLogo />, logo: homeLogo },
|
||||
{ id: 1, icon: <calendarLogo />, logo: calendarLogo },
|
||||
{ id: 2, icon: <planLogo />, logo: planLogo },
|
||||
{ id: 3, icon: <pieLogo />, logo: pieLogo },
|
||||
{ id: 4, icon: <plusLogo />, logo: plusLogo },
|
||||
{ id: 0, path: "/", icon: <homeLogo />, logo: homeLogo },
|
||||
{ id: 1, path: "/tasks", icon: <planLogo />, logo: planLogo },
|
||||
{ id: 2, path: "/calendar", icon: <calendarLogo />, logo: calendarLogo },
|
||||
{ id: 3, path: "/pie", icon: <pieLogo />, logo: pieLogo },
|
||||
{ id: 4, path: "/plus", icon: <plusLogo />, logo: plusLogo },
|
||||
];
|
||||
|
||||
const IconSideNav = () => {
|
||||
return (
|
||||
<div className="bg-slate-900 text-slate-100 flex">
|
||||
<SideNav />
|
||||
<div className="w-full"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -28,7 +27,7 @@ const SideNav = () => {
|
||||
const [selected, setSelected] = useState(0);
|
||||
|
||||
return (
|
||||
<nav className="h-[500px] w-fit bg-slate-950 p-4 flex flex-col items-center gap-2">
|
||||
<nav className="bg-slate-950 p-4 flex flex-col items-center gap-2 h-screen">
|
||||
{menuItems.map((item) => (
|
||||
<NavItem
|
||||
key={item.id}
|
||||
@ -37,17 +36,23 @@ const SideNav = () => {
|
||||
id={item.id}
|
||||
setSelected={setSelected}
|
||||
logo={item.logo}
|
||||
path={item.path}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
const NavItem = ({ icon, selected, id, setSelected, logo }) => {
|
||||
const NavItem = ({ icon, selected, id, setSelected, logo, path }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
className="p-3 text-xl bg-slate-800 hover-bg-slate-700 rounded-md transition-colors relative"
|
||||
onClick={() => setSelected(id)}
|
||||
onClick={() => {
|
||||
setSelected(id);
|
||||
navigate(path);
|
||||
}}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
|
||||
22
frontend/src/components/icons/plusIcon.jsx
Normal file
22
frontend/src/components/icons/plusIcon.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
function PlusIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlusIcon;
|
||||
23
frontend/src/components/icons/trashIcon.jsx
Normal file
23
frontend/src/components/icons/trashIcon.jsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
function TrashIcon() {
|
||||
return (
|
||||
React.createElement(
|
||||
"svg",
|
||||
{
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
fill: "none",
|
||||
viewBox: "0 0 24 24",
|
||||
strokeWidth: 1.5,
|
||||
className: "w-6 h-6"
|
||||
},
|
||||
React.createElement("path", {
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
d: "M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default TrashIcon;
|
||||
178
frontend/src/components/kanbanBoard/columnContainer.jsx
Normal file
178
frontend/src/components/kanbanBoard/columnContainer.jsx
Normal file
@ -0,0 +1,178 @@
|
||||
import { SortableContext, useSortable } from "@dnd-kit/sortable";
|
||||
import TrashIcon from "../icons/trashIcon";
|
||||
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;
|
||||
336
frontend/src/components/kanbanBoard/kanbanBoard.jsx
Normal file
336
frontend/src/components/kanbanBoard/kanbanBoard.jsx
Normal file
@ -0,0 +1,336 @@
|
||||
import PlusIcon from "../icons/plusIcon"
|
||||
import { useMemo, useState } from "react";
|
||||
import ColumnContainer from "./columnContainer";
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
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
|
||||
w-full
|
||||
items-center
|
||||
overflow-x-auto
|
||||
overflow-y-hidden
|
||||
"
|
||||
>
|
||||
<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