mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-19 05:54:07 +01:00
commit
f46ce0a4c3
@ -8,8 +8,10 @@ from users.models import CustomUser
|
|||||||
def create_default_board(sender, instance, created, **kwargs):
|
def create_default_board(sender, instance, created, **kwargs):
|
||||||
"""Signal handler to automatically create a default Board for a user upon creation."""
|
"""Signal handler to automatically create a default Board for a user upon creation."""
|
||||||
if created:
|
if created:
|
||||||
board = Board.objects.create(user=instance, name="My Default Board")
|
# Create unique board by user id
|
||||||
|
user_id = instance.id
|
||||||
ListBoard.objects.create(board=board, name="Todo", position=1)
|
board = Board.objects.create(user=instance, name=f"Board of #{user_id}")
|
||||||
ListBoard.objects.create(board=board, name="In Progress", position=2)
|
ListBoard.objects.create(board=board, name="Backlog", position=1)
|
||||||
ListBoard.objects.create(board=board, name="Done", position=3)
|
ListBoard.objects.create(board=board, name="Doing", position=2)
|
||||||
|
ListBoard.objects.create(board=board, name="Review", position=3)
|
||||||
|
ListBoard.objects.create(board=board, name="Done", position=4)
|
||||||
@ -27,15 +27,63 @@ def update_priority(sender, instance, **kwargs):
|
|||||||
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT
|
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Todo)
|
# @receiver(post_save, sender=Todo)
|
||||||
def assign_todo_to_listboard(sender, instance, created, **kwargs):
|
# def assign_todo_to_listboard(sender, instance, created, **kwargs):
|
||||||
"""Signal handler to automatically assign a Todo to the first ListBoard in the user's Board upon creation."""
|
# """Signal handler to automatically assign a Todo to the first ListBoard in the user's Board upon creation."""
|
||||||
|
# if created:
|
||||||
|
# user_board = instance.user.board_set.first()
|
||||||
|
|
||||||
|
# if user_board:
|
||||||
|
# first_list_board = user_board.listboard_set.order_by('position').first()
|
||||||
|
|
||||||
|
# if first_list_board:
|
||||||
|
# instance.list_board = first_list_board
|
||||||
|
# instance.save()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=ListBoard)
|
||||||
|
def create_placeholder_tasks(sender, instance, created, **kwargs):
|
||||||
|
"""
|
||||||
|
Signal handler to create placeholder tasks for each ListBoard.
|
||||||
|
"""
|
||||||
if created:
|
if created:
|
||||||
user_board = instance.user.board_set.first()
|
list_board_position = instance.position
|
||||||
|
|
||||||
if user_board:
|
if list_board_position == 1:
|
||||||
first_list_board = user_board.listboard_set.order_by('position').first()
|
placeholder_tasks = [
|
||||||
|
{"title": "Normal Task Example"},
|
||||||
|
{"title": "Task with Extra Information Example", "description": "Description for Task 2"},
|
||||||
|
]
|
||||||
|
elif list_board_position == 2:
|
||||||
|
placeholder_tasks = [
|
||||||
|
{"title": "Time Task Example #1", "description": "Description for Task 2",
|
||||||
|
"start_event": timezone.now(), "end_event": timezone.now() + timezone.timedelta(days=5)},
|
||||||
|
]
|
||||||
|
elif list_board_position == 3:
|
||||||
|
placeholder_tasks = [
|
||||||
|
{"title": "Time Task Example #2", "description": "Description for Task 2",
|
||||||
|
"start_event": timezone.now(), "end_event": timezone.now() + timezone.timedelta(days=30)},
|
||||||
|
]
|
||||||
|
elif list_board_position == 4:
|
||||||
|
placeholder_tasks = [
|
||||||
|
{"title": "Completed Task Example", "description": "Description for Task 2",
|
||||||
|
"start_event": timezone.now(), "completed": True},
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
placeholder_tasks = [
|
||||||
|
{"title": "Default Task Example"},
|
||||||
|
]
|
||||||
|
|
||||||
if first_list_board:
|
for task_data in placeholder_tasks:
|
||||||
instance.list_board = first_list_board
|
Todo.objects.create(
|
||||||
instance.save()
|
list_board=instance,
|
||||||
|
user=instance.board.user,
|
||||||
|
title=task_data["title"],
|
||||||
|
notes=task_data.get("description", ""),
|
||||||
|
is_active=True,
|
||||||
|
start_event=task_data.get("start_event"),
|
||||||
|
end_event=task_data.get("end_event"),
|
||||||
|
completed=task_data.get("completed", False),
|
||||||
|
creation_date=timezone.now(),
|
||||||
|
last_update=timezone.now(),
|
||||||
|
)
|
||||||
@ -8,14 +8,7 @@ export function FloatingParticles() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{ width: "0%", height: "100vh" }}>
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
width: "100%",
|
|
||||||
height: "100vh",
|
|
||||||
zIndex: 0,
|
|
||||||
backgroundColor: "#EBF2FA",
|
|
||||||
}}>
|
|
||||||
<Particles
|
<Particles
|
||||||
id="particles"
|
id="particles"
|
||||||
init={particlesInit}
|
init={particlesInit}
|
||||||
|
|||||||
@ -65,8 +65,15 @@ export function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<NavPreLogin text="Don't have account?" btn_text="Sign Up" link="/signup" />
|
<NavPreLogin
|
||||||
<div className="flex flex-row bg-neutral-400">
|
text="Don't have account?"
|
||||||
|
btn_text="Sign Up"
|
||||||
|
link="/signup"
|
||||||
|
/>
|
||||||
|
<div className="h-screen flex items-center justify-center bg-gradient-to-r from-zinc-100 via-gray-200 to-zinc-100">
|
||||||
|
{/* Particles Container */}
|
||||||
|
|
||||||
|
<FloatingParticles />
|
||||||
{/* Login Box */}
|
{/* Login Box */}
|
||||||
<div className="flex items-center justify-center flex-1 z-50">
|
<div className="flex items-center justify-center flex-1 z-50">
|
||||||
<div className="w-100 bg-white border-solid rounded-lg p-8 shadow space-y-4">
|
<div className="w-100 bg-white border-solid rounded-lg p-8 shadow space-y-4">
|
||||||
@ -78,7 +85,8 @@ export function LoginPage() {
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className="stroke-current shrink-0 h-6 w-6"
|
className="stroke-current shrink-0 h-6 w-6"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24">
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@ -122,20 +130,23 @@ export function LoginPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Login Button */}
|
{/* Login Button */}
|
||||||
<button className="btn bg-blue-700 hover:bg-blue-900 w-full text-white font-bold" onClick={handleSubmit}>
|
<button
|
||||||
|
className="btn bg-blue-700 hover:bg-blue-900 w-full text-white font-bold"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
<div className="divider">OR</div>
|
<div className="divider">OR</div>
|
||||||
{/* Login with Google Button */}
|
{/* Login with Google Button */}
|
||||||
<button className="btn bg-gray-200 btn-outline w-full " onClick={() => googleLoginImplicit()}>
|
<button
|
||||||
|
className="btn bg-gray-200 btn-outline w-full "
|
||||||
|
onClick={() => googleLoginImplicit()}
|
||||||
|
>
|
||||||
<FcGoogle className="rounded-full bg-white" />
|
<FcGoogle className="rounded-full bg-white" />
|
||||||
Login with Google
|
Login with Google
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="basis-1/2 bg-#ebf2fa h-screen z-0">
|
|
||||||
<FloatingParticles />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useGoogleLogin } from "@react-oauth/google";
|
|||||||
import { NavPreLogin } from "../navigations/NavPreLogin";
|
import { NavPreLogin } from "../navigations/NavPreLogin";
|
||||||
import { useAuth } from "src/hooks/AuthHooks";
|
import { useAuth } from "src/hooks/AuthHooks";
|
||||||
import { createUser, googleLogin } from "src/api/AuthenticationApi";
|
import { createUser, googleLogin } from "src/api/AuthenticationApi";
|
||||||
|
import { FloatingParticles } from "../FlaotingParticles";
|
||||||
|
|
||||||
export function SignUp() {
|
export function SignUp() {
|
||||||
const Navigate = useNavigate();
|
const Navigate = useNavigate();
|
||||||
@ -55,6 +56,8 @@ export function SignUp() {
|
|||||||
const googleLoginImplicit = useGoogleLogin({
|
const googleLoginImplicit = useGoogleLogin({
|
||||||
flow: "auth-code",
|
flow: "auth-code",
|
||||||
redirect_uri: "postmessage",
|
redirect_uri: "postmessage",
|
||||||
|
scope:
|
||||||
|
"https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/calendar.acls.readonly https://www.googleapis.com/auth/calendar.events.readonly",
|
||||||
onSuccess: async (response) => {
|
onSuccess: async (response) => {
|
||||||
try {
|
try {
|
||||||
const loginResponse = await googleLogin(response.code);
|
const loginResponse = await googleLogin(response.code);
|
||||||
@ -76,9 +79,13 @@ export function SignUp() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<NavPreLogin text="Already have an account?" btn_text="Log In" link="/login" />
|
<NavPreLogin
|
||||||
|
text="Already have an account?"
|
||||||
|
btn_text="Log In"
|
||||||
|
link="/login"
|
||||||
|
/>
|
||||||
<div className="h-screen flex items-center justify-center bg-gradient-to-r from-zinc-100 via-gray-200 to-zinc-100">
|
<div className="h-screen flex items-center justify-center bg-gradient-to-r from-zinc-100 via-gray-200 to-zinc-100">
|
||||||
{/* ... (other code) */}
|
<FloatingParticles />
|
||||||
<div className="w-1/4 h-1 flex items-center justify-center z-10">
|
<div className="w-1/4 h-1 flex items-center justify-center z-10">
|
||||||
<div className="w-96 bg-white rounded-lg p-8 space-y-4 z-10">
|
<div className="w-96 bg-white rounded-lg p-8 space-y-4 z-10">
|
||||||
{/* Register Form */}
|
{/* Register Form */}
|
||||||
@ -136,7 +143,10 @@ export function SignUp() {
|
|||||||
</button>
|
</button>
|
||||||
<div className="divider">OR</div>
|
<div className="divider">OR</div>
|
||||||
{/* Login with Google Button */}
|
{/* Login with Google Button */}
|
||||||
<button className="btn btn-outline btn-secondary w-full " onClick={() => googleLoginImplicit()}>
|
<button
|
||||||
|
className="btn btn-outline btn-secondary w-full "
|
||||||
|
onClick={() => googleLoginImplicit()}
|
||||||
|
>
|
||||||
<FcGoogle className="rounded-full bg-white" />
|
<FcGoogle className="rounded-full bg-white" />
|
||||||
Login with Google
|
Login with Google
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,53 +1,16 @@
|
|||||||
import { SortableContext, useSortable } from "@dnd-kit/sortable";
|
import { SortableContext, useSortable } from "@dnd-kit/sortable";
|
||||||
import { BsFillTrashFill } from "react-icons/bs";
|
|
||||||
import { AiOutlinePlusCircle } from "react-icons/ai";
|
import { AiOutlinePlusCircle } from "react-icons/ai";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { useMemo } from "react";
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import { TaskCard } from "./taskCard";
|
import { TaskCard } from "./taskCard";
|
||||||
|
|
||||||
export function ColumnContainer({ column, deleteColumn, updateColumn, createTask, tasks, deleteTask, updateTask }) {
|
export function ColumnContainer({ column, createTask, tasks, deleteTask, updateTask }) {
|
||||||
const [editMode, setEditMode] = useState(false);
|
// Memoize task IDs to prevent unnecessary recalculations
|
||||||
|
|
||||||
const tasksIds = useMemo(() => {
|
const tasksIds = useMemo(() => {
|
||||||
return tasks.map((task) => task.id);
|
return tasks.map((task) => task.id);
|
||||||
}, [tasks]);
|
}, [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="
|
|
||||||
opacity-40
|
|
||||||
border-2
|
|
||||||
border-blue-500
|
|
||||||
w-[350px]
|
|
||||||
max-h-[400px]
|
|
||||||
rounded-md
|
|
||||||
flex
|
|
||||||
flex-col
|
|
||||||
"></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
className="
|
className="
|
||||||
bg-[#f1f2f4]
|
bg-[#f1f2f4]
|
||||||
w-[280px]
|
w-[280px]
|
||||||
@ -58,62 +21,35 @@ export function ColumnContainer({ column, deleteColumn, updateColumn, createTask
|
|||||||
">
|
">
|
||||||
{/* Column title */}
|
{/* Column title */}
|
||||||
<div
|
<div
|
||||||
{...attributes}
|
|
||||||
{...listeners}
|
|
||||||
onClick={() => {
|
|
||||||
setEditMode(true);
|
|
||||||
}}
|
|
||||||
className="
|
className="
|
||||||
ml-3
|
ml-3
|
||||||
text-md
|
text-md
|
||||||
cursor-grab
|
|
||||||
font-bold
|
font-bold
|
||||||
flex
|
flex
|
||||||
items-center
|
items-center
|
||||||
justify-between
|
justify-between
|
||||||
">
|
">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">{column.title}</div>
|
||||||
{!editMode && column.title}
|
|
||||||
{editMode && (
|
|
||||||
<input
|
|
||||||
className="bg-gray-200 focus:border-blue-500 border rounded-md 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
|
|
||||||
">
|
|
||||||
<BsFillTrashFill />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Column task container */}
|
{/* Column task container */}
|
||||||
<div className="flex flex-grow flex-col gap-2 p-1 overflow-x-hidden overflow-y-auto">
|
<div className="flex flex-grow flex-col gap-2 p-1 overflow-x-hidden overflow-y-auto">
|
||||||
|
{/* Provide a SortableContext for the tasks within the column */}
|
||||||
<SortableContext items={tasksIds}>
|
<SortableContext items={tasksIds}>
|
||||||
|
{/* Render TaskCard for each task in the column */}
|
||||||
{tasks.map((task) => (
|
{tasks.map((task) => (
|
||||||
<TaskCard key={task.id} task={task} deleteTask={deleteTask} updateTask={updateTask} />
|
<TaskCard
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
deleteTask={deleteTask}
|
||||||
|
updateTask={updateTask}
|
||||||
|
// Adjust the useSortable hook for tasks to enable dragging
|
||||||
|
useSortable={(props) => useSortable({ ...props, disabled: false })}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Column footer */}
|
{/* Column footer */}
|
||||||
<button
|
<button
|
||||||
className="flex gap-2 items-center rounded-md p-2 my-2 hover:bg-zinc-200 active:bg-zinc-400"
|
className="flex gap-2 items-center rounded-md p-2 my-2 hover:bg-zinc-200 active:bg-zinc-400"
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import { ColumnContainer } from "./columnContainer";
|
import { ColumnContainer } from "./columnContainer";
|
||||||
|
|
||||||
export function ColumnContainerCard({ column, deleteColumn, updateColumn, createTask, tasks, deleteTask, updateTask }) {
|
export function ColumnContainerCard({ column, createTask, tasks, deleteTask, updateTask }) {
|
||||||
return (
|
return (
|
||||||
<div className="card bg-[#f1f2f4] shadow p-1 my-2 border-2">
|
<div className="card bg-[#f1f2f4] shadow border p-1 my-2">
|
||||||
<ColumnContainer
|
<ColumnContainer
|
||||||
column={column}
|
column={column}
|
||||||
deleteColumn={deleteColumn}
|
|
||||||
updateColumn={updateColumn}
|
|
||||||
createTask={createTask}
|
createTask={createTask}
|
||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
deleteTask={deleteTask}
|
deleteTask={deleteTask}
|
||||||
|
|||||||
@ -8,14 +8,13 @@ import { axiosInstance } from "src/api/AxiosConfig";
|
|||||||
|
|
||||||
export function KanbanBoard() {
|
export function KanbanBoard() {
|
||||||
const [columns, setColumns] = useState([]);
|
const [columns, setColumns] = useState([]);
|
||||||
const columnsId = useMemo(() => columns.map((col) => col.id), [columns]);
|
|
||||||
const [boardId, setBoardData] = useState();
|
const [boardId, setBoardData] = useState();
|
||||||
|
const [isLoading, setLoading] = useState(false);
|
||||||
const [tasks, setTasks] = useState([]);
|
const [tasks, setTasks] = useState([]);
|
||||||
|
|
||||||
const [activeColumn, setActiveColumn] = useState(null);
|
|
||||||
|
|
||||||
const [activeTask, setActiveTask] = useState(null);
|
const [activeTask, setActiveTask] = useState(null);
|
||||||
|
const columnsId = useMemo(() => columns.map((col) => col.id), [columns]);
|
||||||
|
|
||||||
|
// ---------------- END STATE INITIATE ----------------
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@ -25,6 +24,75 @@ export function KanbanBoard() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---------------- Task Handlers ----------------
|
||||||
|
const handleTaskUpdate = (tasks, updatedTask) => {
|
||||||
|
const updatedTasks = tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task));
|
||||||
|
setTasks(updatedTasks);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApiError = (error, action) => {
|
||||||
|
console.error(`Error ${action}:`, error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTask = async (columnId) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.post("todo/", {
|
||||||
|
title: `New Task`,
|
||||||
|
importance: 1,
|
||||||
|
difficulty: 1,
|
||||||
|
challenge: false,
|
||||||
|
fromSystem: false,
|
||||||
|
is_active: false,
|
||||||
|
is_full_day_event: false,
|
||||||
|
completed: false,
|
||||||
|
priority: 1,
|
||||||
|
list_board: columnId,
|
||||||
|
});
|
||||||
|
const newTask = {
|
||||||
|
id: response.data.id,
|
||||||
|
columnId,
|
||||||
|
content: response.data.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
setTasks((prevTasks) => [...prevTasks, newTask]);
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, "creating task");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTask = async (id) => {
|
||||||
|
try {
|
||||||
|
await axiosInstance.delete(`todo/${id}/`);
|
||||||
|
const newTasks = tasks.filter((task) => task.id !== id);
|
||||||
|
setTasks(newTasks);
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, "deleting task");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTask = async (id, content, tasks) => {
|
||||||
|
try {
|
||||||
|
if (content === "") {
|
||||||
|
await deleteTask(id);
|
||||||
|
} else {
|
||||||
|
const response = await axiosInstance.put(`todo/${id}/`, { content });
|
||||||
|
|
||||||
|
const updatedTask = {
|
||||||
|
id,
|
||||||
|
columnId: response.data.list_board,
|
||||||
|
content: response.data.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTaskUpdate(tasks, updatedTask);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, "updating task");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------- END Task Handlers ----------------
|
||||||
|
|
||||||
|
// ---------------- Fetch Data ----------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
@ -74,17 +142,22 @@ export function KanbanBoard() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchBoardData = async () => {
|
const fetchBoardData = async () => {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true);
|
||||||
const response = await axiosInstance.get("boards/");
|
const response = await axiosInstance.get("boards/");
|
||||||
if (response.data && response.data.length > 0) {
|
if (response.data && response.data.length > 0) {
|
||||||
setBoardData(response.data[0]);
|
setBoardData(response.data[0]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching board data:", error);
|
console.error("Error fetching board data:", error);
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
setLoading(false);
|
||||||
};
|
};
|
||||||
fetchBoardData();
|
fetchBoardData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// ---------------- END Fetch Data ----------------
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
@ -92,43 +165,36 @@ export function KanbanBoard() {
|
|||||||
flex
|
flex
|
||||||
w-full
|
w-full
|
||||||
items-center
|
items-center
|
||||||
|
justify-center
|
||||||
overflow-x-auto
|
overflow-x-auto
|
||||||
overflow-y-hidden
|
overflow-y-hidden
|
||||||
">
|
">
|
||||||
<DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver}>
|
<DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver}>
|
||||||
<div className="ml-2 flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<SortableContext items={columnsId}>
|
{!isLoading ? (
|
||||||
{columns.map((col) => (
|
<SortableContext items={columnsId}>
|
||||||
<ColumnContainerCard
|
{columns.map((col) => (
|
||||||
key={col.id}
|
<ColumnContainerCard
|
||||||
column={col}
|
key={col.id}
|
||||||
deleteColumn={deleteColumn}
|
column={col}
|
||||||
updateColumn={updateColumn}
|
createTask={createTask}
|
||||||
createTask={createTask}
|
deleteTask={deleteTask}
|
||||||
deleteTask={deleteTask}
|
updateTask={updateTask}
|
||||||
updateTask={updateTask}
|
tasks={(tasks || []).filter((task) => task.columnId === col.id)}
|
||||||
tasks={tasks.filter((task) => task.columnId === col.id)}
|
/>
|
||||||
/>
|
))}{" "}
|
||||||
))}
|
</SortableContext>
|
||||||
</SortableContext>
|
) : (
|
||||||
|
<span className="loading loading-dots loading-lg"></span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{createPortal(
|
{createPortal(
|
||||||
<DragOverlay className="bg-white" dropAnimation={null} zIndex={20}>
|
<DragOverlay className="bg-white" dropAnimation={null} zIndex={20}>
|
||||||
{activeColumn && (
|
{/* Render the active task as a draggable overlay */}
|
||||||
<ColumnContainerCard
|
<TaskCard task={activeTask} deleteTask={deleteTask} updateTask={updateTask} />
|
||||||
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>,
|
</DragOverlay>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
@ -136,149 +202,31 @@ export function KanbanBoard() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
function createTask(columnId, setTasks) {
|
// Handle the start of a drag event
|
||||||
const newTaskData = {
|
|
||||||
title: `Task ${tasks.length + 1}`,
|
|
||||||
importance: 1,
|
|
||||||
difficulty: 1,
|
|
||||||
challenge: false,
|
|
||||||
fromSystem: false,
|
|
||||||
is_active: false,
|
|
||||||
is_full_day_event: false,
|
|
||||||
completed: false,
|
|
||||||
priority: 1,
|
|
||||||
list_board: columnId,
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.post("todo/", newTaskData)
|
|
||||||
.then((response) => {
|
|
||||||
const newTask = {
|
|
||||||
id: response.data.id,
|
|
||||||
columnId,
|
|
||||||
content: response.data.title,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error creating task:", error);
|
|
||||||
});
|
|
||||||
setTasks((tasks) => [...tasks, newTask]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteTask(id) {
|
|
||||||
const newTasks = tasks.filter((task) => task.id !== id);
|
|
||||||
axiosInstance
|
|
||||||
.delete(`todo/${id}/`)
|
|
||||||
.then((response) => {
|
|
||||||
setTasks(newTasks);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error deleting Task:", error);
|
|
||||||
});
|
|
||||||
setTasks(newTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTask(id, content) {
|
|
||||||
const newTasks = tasks.map((task) => {
|
|
||||||
if (task.id !== id) return task;
|
|
||||||
return { ...task, content };
|
|
||||||
});
|
|
||||||
if (content === "") return deleteTask(id);
|
|
||||||
setTasks(newTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNewColumn() {
|
|
||||||
axiosInstance
|
|
||||||
.post("lists/", { name: `Column ${columns.length + 1}`, position: 1, board: boardId.id })
|
|
||||||
.then((response) => {
|
|
||||||
const newColumn = {
|
|
||||||
id: response.data.id,
|
|
||||||
title: response.data.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
setColumns((prevColumns) => [...prevColumns, newColumn]);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error creating ListBoard:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteColumn(id) {
|
|
||||||
axiosInstance
|
|
||||||
.delete(`lists/${id}/`)
|
|
||||||
.then((response) => {
|
|
||||||
setColumns((prevColumns) => prevColumns.filter((col) => col.id !== id));
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error deleting ListBoard:", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
const tasksToDelete = tasks.filter((t) => t.columnId === id);
|
|
||||||
|
|
||||||
tasksToDelete.forEach((task) => {
|
|
||||||
axiosInstance
|
|
||||||
.delete(`todo/${task.id}/`)
|
|
||||||
.then((response) => {
|
|
||||||
setTasks((prevTasks) => prevTasks.filter((t) => t.id !== task.id));
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error deleting Task:", error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateColumn(id, title) {
|
|
||||||
// Update the column
|
|
||||||
axiosInstance
|
|
||||||
.patch(`lists/${id}/`, { name: title }) // Adjust the payload based on your API requirements
|
|
||||||
.then((response) => {
|
|
||||||
setColumns((prevColumns) => prevColumns.map((col) => (col.id === id ? { ...col, title } : col)));
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error updating ListBoard:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDragStart(event) {
|
function onDragStart(event) {
|
||||||
if (event.active.data.current?.type === "Column") {
|
// Check if the dragged item is a Task
|
||||||
setActiveColumn(event.active.data.current.column);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.active.data.current?.type === "Task") {
|
if (event.active.data.current?.type === "Task") {
|
||||||
setActiveTask(event.active.data.current.task);
|
setActiveTask(event.active.data.current.task);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle the end of a drag event
|
||||||
function onDragEnd(event) {
|
function onDragEnd(event) {
|
||||||
setActiveColumn(null);
|
// Reset active column and task after the drag ends
|
||||||
setActiveTask(null);
|
setActiveTask(null);
|
||||||
|
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
if (!over) return;
|
if (!over) return; // If not dropped over anything, exit
|
||||||
|
|
||||||
const activeId = active.id;
|
const activeId = active.id;
|
||||||
const overId = over.id;
|
const overId = over.id;
|
||||||
|
|
||||||
const isActiveAColumn = active.data.current?.type === "Column";
|
|
||||||
const isActiveATask = active.data.current?.type === "Task";
|
const isActiveATask = active.data.current?.type === "Task";
|
||||||
const isOverAColumn = over.data.current?.type === "Column";
|
const isOverAColumn = over.data.current?.type === "Column";
|
||||||
const isOverATask = over.data.current?.type === "Task";
|
const isOverATask = over.data.current?.type === "Task";
|
||||||
|
|
||||||
// Reorder columns if the dragged item is a column
|
// Reorder logic for Tasks within the same column
|
||||||
if (isActiveAColumn && isOverAColumn) {
|
|
||||||
setColumns((columns) => {
|
|
||||||
const activeColumnIndex = columns.findIndex((col) => col.id === activeId);
|
|
||||||
const overColumnIndex = columns.findIndex((col) => col.id === overId);
|
|
||||||
|
|
||||||
const reorderedColumns = arrayMove(columns, activeColumnIndex, overColumnIndex);
|
|
||||||
|
|
||||||
return reorderedColumns;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reorder tasks within the same column
|
|
||||||
if (isActiveATask && isOverATask) {
|
if (isActiveATask && isOverATask) {
|
||||||
setTasks((tasks) => {
|
setTasks((tasks) => {
|
||||||
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
||||||
@ -297,6 +245,7 @@ export function KanbanBoard() {
|
|||||||
|
|
||||||
tasks[activeIndex].columnId = overId;
|
tasks[activeIndex].columnId = overId;
|
||||||
|
|
||||||
|
// API call to update task's columnId
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.put(`todo/change_task_list_board/`, { todo_id: activeId, new_list_board_id: overId, new_index: 0 })
|
.put(`todo/change_task_list_board/`, { todo_id: activeId, new_list_board_id: overId, new_index: 0 })
|
||||||
.then((response) => {})
|
.then((response) => {})
|
||||||
@ -309,25 +258,28 @@ export function KanbanBoard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle the drag-over event
|
||||||
function onDragOver(event) {
|
function onDragOver(event) {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
if (!over) return;
|
if (!over) return; // If not over anything, exit
|
||||||
|
|
||||||
const activeId = active.id;
|
const activeId = active.id;
|
||||||
const overId = over.id;
|
const overId = over.id;
|
||||||
|
|
||||||
if (activeId === overId) return;
|
if (activeId === overId) return; // If over the same element, exit
|
||||||
|
|
||||||
const isActiveATask = active.data.current?.type === "Task";
|
const isActiveATask = active.data.current?.type === "Task";
|
||||||
const isOverATask = over.data.current?.type === "Task";
|
const isOverATask = over.data.current?.type === "Task";
|
||||||
|
|
||||||
if (!isActiveATask) return;
|
if (!isActiveATask) return; // If not dragging a Task, exit
|
||||||
|
|
||||||
|
// Reorder logic for Tasks within the same column
|
||||||
if (isActiveATask && isOverATask) {
|
if (isActiveATask && isOverATask) {
|
||||||
setTasks((tasks) => {
|
setTasks((tasks) => {
|
||||||
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
||||||
const overIndex = tasks.findIndex((t) => t.id === overId);
|
const overIndex = tasks.findIndex((t) => t.id === overId);
|
||||||
|
|
||||||
|
// If moving to a different column, update columnId
|
||||||
if (tasks[activeIndex].columnId !== tasks[overIndex].columnId) {
|
if (tasks[activeIndex].columnId !== tasks[overIndex].columnId) {
|
||||||
tasks[activeIndex].columnId = tasks[overIndex].columnId;
|
tasks[activeIndex].columnId = tasks[overIndex].columnId;
|
||||||
return arrayMove(tasks, activeIndex, overIndex - 1);
|
return arrayMove(tasks, activeIndex, overIndex - 1);
|
||||||
@ -338,8 +290,8 @@ export function KanbanBoard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isOverAColumn = over.data.current?.type === "Column";
|
const isOverAColumn = over.data.current?.type === "Column";
|
||||||
|
// Move the Task to a different column and update columnId
|
||||||
if (isActiveATask && isOverAColumn) {
|
if (isActiveATask && isOverAColumn && tasks.some((task) => task.columnId !== overId)) {
|
||||||
setTasks((tasks) => {
|
setTasks((tasks) => {
|
||||||
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,6 @@ export const KanbanPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<KanbanBoard />
|
<KanbanBoard />
|
||||||
<div className="flex justify-center border-2 "></div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,13 +2,10 @@ import { FloatingParticles } from "../FlaotingParticles";
|
|||||||
|
|
||||||
export function LandingPage() {
|
export function LandingPage() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="h-screen flex items-center justify-center bg-gradient-to-r from-zinc-100 via-gray-200 to-zinc-100">
|
||||||
{/* Particles Container */}
|
{/* Particles Container */}
|
||||||
<FloatingParticles />
|
<FloatingParticles />
|
||||||
{/* Navbar */}
|
{/* Navbar */}
|
||||||
<div className="navbar bg-white z-10">
|
|
||||||
<div className="navbar-end space-x-3 z-10"></div>
|
|
||||||
</div>
|
|
||||||
<div className="relative" id="home">
|
<div className="relative" id="home">
|
||||||
<div className="max-w-7xl mx-auto px-6 md:px-12 xl:px-6">
|
<div className="max-w-7xl mx-auto px-6 md:px-12 xl:px-6">
|
||||||
<div className="relative pt-36 ml-auto">
|
<div className="relative pt-36 ml-auto">
|
||||||
@ -24,12 +21,18 @@ export function LandingPage() {
|
|||||||
</label>
|
</label>
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-8 text-#143D6C">Unleash productivity with our personal task and project management.</p>
|
<p className="mt-8 text-#143D6C">
|
||||||
|
Unleash productivity with our personal task and project
|
||||||
|
management.
|
||||||
|
</p>
|
||||||
<div className="mt-8 flex flex-wrap justify-center gap-y-4 gap-x-6">
|
<div className="mt-8 flex flex-wrap justify-center gap-y-4 gap-x-6">
|
||||||
<a
|
<a
|
||||||
href="/login"
|
href="/login"
|
||||||
className="relative flex h-11 w-full items-center justify-center px-6 before:absolute before:inset-0 before:rounded-full before:bg-primary before:transition before:duration-300 hover:before:scale-105 active:duration-75 active:before:scale-95 sm:w-max">
|
className="relative flex h-11 w-full items-center justify-center px-6 before:absolute before:inset-0 before:rounded-full before:bg-primary before:transition before:duration-300 hover:before:scale-105 active:duration-75 active:before:scale-95 sm:w-max"
|
||||||
<span className="relative text-base font-semibold text-white">Get started</span>
|
>
|
||||||
|
<span className="relative text-base font-semibold text-white">
|
||||||
|
Get started
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,12 +32,12 @@ export const SideNav = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NavItem = ({ icon, selected, id, setSelected, logo, path }) => {
|
const NavItem = ({ icon, selected, id, setSelected, path }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.button
|
<motion.button
|
||||||
className="p-3 text-xl bg-slate-800 hover-bg-slate-700 rounded-md transition-colors relative"
|
className="p-3 text-xl text-white bg-slate-800 hover-bg-slate-700 rounded-md transition-colors relative"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelected(id);
|
setSelected(id);
|
||||||
navigate(path);
|
navigate(path);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user