From b4029ad65aa0bad775d1f85116908371a8fa7667 Mon Sep 17 00:00:00 2001 From: Mpho10111 Date: Tue, 11 Aug 2026 20:56:13 +0200 Subject: [PATCH] feat: connect task board and status workflow --- app/dashboard/page.tsx | 54 +----- components/Board.tsx | 426 +++++++++++++++++++++++++++++++++++++++-- components/Column.tsx | 203 +++----------------- 3 files changed, 437 insertions(+), 246 deletions(-) diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index a3fead6..901e7dc 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,54 +1,10 @@ -"use client"; - -import { useState } from "react"; -import Column from "@/components/Column"; -import AddTaskForm from "@/components/AddTaskForm"; +import Board from "@/components/Board"; export default function Dashboard() { - const [tasks, setTasks] = useState({ - todo: [ - { - title: "Finish lab", - description: "Complete physics lab report", - dueDate: "2026-08-10", - topic: "Physics", - }, - { - title: "Study Next.js", - description: "Learn routing and layouts", - dueDate: "2026-08-08", - topic: "Coding", - }, - ], - inProgress: [], - done: [], - }); - - const [showForm, setShowForm] = useState(false); - - const addTask = (task: any) => { - setTasks((prev) => ({ - ...prev, - todo: [...prev.todo, task], - })); - }; - return (

Planner Danner

- -
- setShowForm(true)} /> - - -
- - {showForm && ( - setShowForm(false)} - /> - )} +
); } @@ -71,10 +27,4 @@ const styles = { `, marginBottom: "30px", }, - board: { - display: "flex", - gap: "20px", - justifyContent: "center", - }, }; - diff --git a/components/Board.tsx b/components/Board.tsx index 24fbb79..e7610bd 100644 --- a/components/Board.tsx +++ b/components/Board.tsx @@ -1,36 +1,430 @@ "use client"; -import { useState } from "react"; + +import { useCallback, useEffect, useState } from "react"; +import type { Task, TaskInput, TaskSort, TaskStatus, TaskUpdate } from "@/types/task"; +import AddTaskForm from "./AddTaskForm"; import Column from "./Column"; +import EditTaskForm from "./EditTaskForm"; +import TaskCard from "./TaskCard"; + +type ViewMode = "board" | "list"; + +type TasksPayload = { + tasks: Task[]; +}; + +type TaskPayload = { + task: Task; +}; + +async function readJson(response: Response): Promise { + const payload = (await response.json()) as T & { error?: string }; + + if (!response.ok) { + throw new Error(payload.error ?? "Request failed."); + } + + return payload; +} export default function Board() { - const [tasks, setTasks] = useState([ - { id: 1, title: "Task 1", description: "Do something", status: "Todo", dueDate: "2026-08-10", topic: "Lab" }, - { id: 2, title: "Task 2", description: "Another thing", status: "In-Progress", dueDate: "2026-08-12", topic: "Project" }, - ]); + const [tasks, setTasks] = useState([]); + const [sortMode, setSortMode] = useState("createdAt"); + const [viewMode, setViewMode] = useState("board"); + const [includeArchived, setIncludeArchived] = useState(false); + const [showForm, setShowForm] = useState(false); + const [editingTask, setEditingTask] = useState(null); + const [popupTask, setPopupTask] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); - const handleStatusChange = (taskId: number, newStatus: "Todo" | "In-Progress" | "Complete") => { - setTasks(prev => - prev.map(t => (t.id === taskId ? { ...t, status: newStatus } : t)) + const loadTasks = useCallback(async () => { + setIsLoading(true); + setError(null); + + try { + const searchParams = new URLSearchParams({ + sort: sortMode, + includeArchived: String(includeArchived), + }); + const payload = await readJson( + await fetch(`/api/tasks?${searchParams.toString()}`) + ); + + setTasks(payload.tasks); + } catch (loadError) { + setError( + loadError instanceof Error ? loadError.message : "Tasks could not be loaded." + ); + } finally { + setIsLoading(false); + } + }, [includeArchived, sortMode]); + + useEffect(() => { + // Tasks are external server state, so the initial sync starts here. + // eslint-disable-next-line react-hooks/set-state-in-effect + void loadTasks(); + }, [loadTasks]); + + const createNewTask = async (task: TaskInput) => { + await readJson( + await fetch("/api/tasks", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(task), + }) ); + await loadTasks(); }; - return ( -
+ const saveTaskChanges = async (id: number, updates: TaskUpdate) => { + await readJson( + await fetch("/api/tasks", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id, ...updates }), + }) + ); + await loadTasks(); + }; + + const handleStatusChange = async (taskId: number, newStatus: TaskStatus) => { + try { + await saveTaskChanges(taskId, { status: newStatus }); + } catch (statusError) { + setError( + statusError instanceof Error + ? statusError.message + : "Task status could not be changed." + ); + } + }; + + const handleArchive = async (task: Task) => { + try { + await readJson( + await fetch("/api/tasks", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: task.id, action: "archive" }), + }) + ); + await loadTasks(); + } catch (archiveError) { + setError( + archiveError instanceof Error + ? archiveError.message + : "Task could not be archived." + ); + } + }; + + const handleStatusClick = (task: Task) => { + if (task.status !== "Complete" && !task.archivedAt) { + setPopupTask(task); + } + }; + + const handleConfirmStatus = async (yes: boolean) => { + const task = popupTask; + setPopupTask(null); + + if (!task || !yes) { + return; + } + + if (task.status === "Todo") { + await handleStatusChange(task.id, "In-Progress"); + } else if (task.status === "In-Progress") { + await handleStatusChange(task.id, "Complete"); + } + }; + + const renderBoard = () => ( +
t.status === "Todo")} - onStatusChange={handleStatusChange} + tasks={tasks.filter((task) => task.status === "Todo")} + onAdd={() => setShowForm(true)} + onArchive={handleArchive} + onEdit={setEditingTask} + onStatusClick={handleStatusClick} /> t.status === "In-Progress")} - onStatusChange={handleStatusChange} + tasks={tasks.filter((task) => task.status === "In-Progress")} + onArchive={handleArchive} + onEdit={setEditingTask} + onStatusClick={handleStatusClick} /> t.status === "Complete")} - onStatusChange={handleStatusChange} + tasks={tasks.filter((task) => task.status === "Complete")} + onArchive={handleArchive} + onEdit={setEditingTask} + onStatusClick={handleStatusClick} />
); + + const renderList = () => ( +
+ {tasks.map((task) => ( + + ))} +
+ ); + + return ( + <> +
+
+ + +
+ + + + + + +
+ + {error &&
{error}
} + {isLoading &&
Loading tasks...
} + {!isLoading && tasks.length === 0 && ( +
No tasks yet.
+ )} + {!isLoading && ( + <>{viewMode === "board" ? renderBoard() : renderList()} + )} + + {showForm && ( + setShowForm(false)} /> + )} + + {editingTask && ( + setEditingTask(null)} + onSubmit={(updates) => saveTaskChanges(editingTask.id, updates)} + /> + )} + + {popupTask && ( +
+
+

+ {popupTask.status === "Todo" + ? "Are you ready to start the task?" + : "Have you finished this task?"} +

+
+ + +
+
+
+ )} + + ); } + +const styles = { + toolbar: { + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: "16px", + flexWrap: "wrap" as const, + marginBottom: "24px", + }, + segmented: { + display: "flex", + border: "1px solid #bae6fd", + borderRadius: "8px", + overflow: "hidden", + background: "#f8fafc", + }, + segment: { + background: "#f8fafc", + color: "#0f172a", + border: "none", + padding: "8px 14px", + cursor: "pointer", + fontWeight: "700", + }, + activeSegment: { + background: "#0f172a", + color: "white", + border: "none", + padding: "8px 14px", + cursor: "pointer", + fontWeight: "700", + }, + controlLabel: { + display: "flex", + alignItems: "center", + gap: "8px", + color: "white", + fontWeight: "700", + }, + checkboxLabel: { + display: "flex", + alignItems: "center", + gap: "8px", + color: "white", + fontWeight: "700", + }, + select: { + padding: "8px", + border: "1px solid #bae6fd", + borderRadius: "8px", + color: "#0f172a", + fontWeight: "700", + }, + addButton: { + background: "#0f172a", + color: "white", + border: "1px solid #0f172a", + borderRadius: "8px", + padding: "8px 14px", + cursor: "pointer", + fontWeight: "700", + }, + board: { + display: "flex", + gap: "20px", + justifyContent: "center", + alignItems: "stretch", + }, + list: { + maxWidth: "760px", + margin: "0 auto", + }, + error: { + maxWidth: "760px", + margin: "0 auto 16px", + background: "#fee2e2", + color: "#991b1b", + border: "1px solid #fecaca", + borderRadius: "8px", + padding: "10px 12px", + fontWeight: "700", + }, + emptyState: { + background: "#f8fafc", + color: "#0f172a", + borderRadius: "8px", + padding: "24px", + maxWidth: "420px", + margin: "0 auto", + textAlign: "center" as const, + fontWeight: "700", + }, + overlay: { + position: "fixed" as const, + top: 0, + left: 0, + width: "100%", + height: "100%", + background: "rgba(0,0,0,0.4)", + display: "flex", + justifyContent: "center", + alignItems: "center", + zIndex: 1000, + }, + modal: { + background: "#f8fafc", + padding: "25px", + borderRadius: "12px", + boxShadow: "0 10px 25px rgba(0,0,0,0.2)", + width: "300px", + textAlign: "center" as const, + }, + modalTitle: { + marginBottom: "15px", + fontSize: "18px", + fontWeight: "600", + color: "#0f172a", + }, + actions: { + display: "flex", + justifyContent: "space-between", + marginTop: "15px", + }, + saveButton: { + background: "#0ea5e9", + color: "white", + border: "none", + padding: "8px 16px", + borderRadius: "8px", + cursor: "pointer", + fontWeight: "600", + }, + cancelButton: { + background: "#e2e8f0", + color: "#0f172a", + border: "none", + padding: "8px 16px", + borderRadius: "8px", + cursor: "pointer", + fontWeight: "600", + }, +}; diff --git a/components/Column.tsx b/components/Column.tsx index 024ef3c..304a397 100644 --- a/components/Column.tsx +++ b/components/Column.tsx @@ -1,42 +1,30 @@ "use client"; import { useState } from "react"; +import type { Task } from "@/types/task"; +import TaskCard from "./TaskCard"; + +type SortMode = "none" | "topic" | "dueDate"; export default function Column({ title, tasks, onAdd, - onStatusChange, + onArchive, + onEdit, + onStatusClick, }: { title: string; - tasks: any[]; + tasks: Task[]; onAdd?: () => void; - onStatusChange?: (index: number, newStatus: "Todo" | "In-Progress" | "Complete") => void; + onArchive: (task: Task) => void; + onEdit: (task: Task) => void; + onStatusClick: (task: Task) => void; }) { - const [popupTaskIndex, setPopupTaskIndex] = useState(null); - const [sortMode, setSortMode] = useState<"none" | "topic" | "dueDate">("none"); + const [sortMode, setSortMode] = useState("none"); const [showMenu, setShowMenu] = useState(false); - const handleCircleClick = (index: number) => { - const task = tasks[index]; - if (task.status !== "Complete") { - setPopupTaskIndex(index); - } - }; - - const handleConfirm = (yes: boolean) => { - if (popupTaskIndex !== null && onStatusChange) { - const task = tasks[popupTaskIndex]; - if (task.status === "Todo" && yes) { - onStatusChange(popupTaskIndex, "In-Progress"); - } else if (task.status === "In-Progress" && yes) { - onStatusChange(popupTaskIndex, "Complete"); - } - } - setPopupTaskIndex(null); - }; - - const handleSort = (mode: "none" | "topic" | "dueDate") => { + const handleSort = (mode: SortMode) => { setSortMode(mode); setShowMenu(false); }; @@ -62,14 +50,13 @@ export default function Column({ )} - {/* Sort button */}
{showMenu && (
@@ -85,8 +72,6 @@ export default function Column({
)}
- -
@@ -96,57 +81,15 @@ export default function Column({ )} - {sortedTasks.map((task, index) => ( -
-
- {/* Hollow circle */} -
handleCircleClick(index)} - title="Change Status" - > - {task.status === "In-Progress" || task.status === "Complete" ? "✔" : ""} -
- -
{task.title}
- -
-
- {task.description.slice(0, 30)} - {task.description.length > 30 && "..."} -
-
📅 {task.dueDate}
- {task.topic && ( -
- {task.topic} -
- )} -
+ {sortedTasks.map((task) => ( + ))} - - {/* Popup */} - {popupTaskIndex !== null && ( -
-
-

- {tasks[popupTaskIndex].status === "Todo" - ? "Are you ready to start the task?" - : "Have you finished this task?"} -

-
- - -
-
-
- )} ); } @@ -178,8 +121,8 @@ const styles = { background: "transparent", border: "none", color: "#0f172a", - fontSize: "22px", - fontWeight: "600", + fontSize: "16px", + fontWeight: "700", cursor: "pointer", padding: "0", display: "flex", @@ -198,102 +141,6 @@ const styles = { fontStyle: "italic", color: "#0f172a", }, - card: { - background: "#dae4f0", - padding: "18px", - borderRadius: "12px", - marginBottom: "15px", - color: "#0f172a", - fontSize: "16px", - }, - cardHeader: { - display: "flex", - justifyContent: "space-between", - alignItems: "center", - marginBottom: "6px", - gap: "10px", - }, - cardTitle: { - fontWeight: "700", - flex: 1, - }, - cardDescription: { - fontSize: "13px", - opacity: 0.7, - marginBottom: "8px", - }, - cardDate: { - fontSize: "12px", - opacity: 0.6, - }, - cardTopic: { - fontSize: "12px", - opacity: 0.7, - textAlign: "right" as const, - marginTop: "6px", - fontStyle: "italic", - }, - circle: { - width: "20px", - height: "20px", - border: "2px solid black", - borderRadius: "50%", - display: "flex", - alignItems: "center", - justifyContent: "center", - fontSize: "14px", - fontWeight: "bold", - backgroundColor: "white", - }, - overlay: { - position: "fixed" as const, - top: 0, - left: 0, - width: "100%", - height: "100%", - background: "rgba(0,0,0,0.4)", - display: "flex", - justifyContent: "center", - alignItems: "center", - zIndex: 1000, - }, - modal: { - background: "#f8fafc", - padding: "25px", - borderRadius: "12px", - boxShadow: "0 10px 25px rgba(0,0,0,0.2)", - width: "300px", - textAlign: "center" as const, - }, - title: { - marginBottom: "15px", - fontSize: "18px", - fontWeight: "600", - color: "#0f172a", - }, - actions: { - display: "flex", - justifyContent: "space-between", - marginTop: "15px", - }, - saveButton: { - background: "#0ea5e9", - color: "white", - border: "none", - padding: "8px 16px", - borderRadius: "8px", - cursor: "pointer", - fontWeight: "600", - }, - cancelButton: { - background: "#e2e8f0", - color: "#0f172a", - border: "none", - padding: "8px 16px", - borderRadius: "8px", - cursor: "pointer", - fontWeight: "600", - }, dropdown: { position: "absolute" as const, top: "30px", @@ -315,4 +162,4 @@ const styles = { fontSize: "14px", color: "#0f172a", }, -}; \ No newline at end of file +};