"use client"; 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([]); 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 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(); }; 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 = () => (
task.status === "Todo")} onAdd={() => setShowForm(true)} onArchive={handleArchive} onEdit={setEditingTask} onStatusClick={handleStatusClick} /> task.status === "In-Progress")} onArchive={handleArchive} onEdit={setEditingTask} onStatusClick={handleStatusClick} /> 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", }, };