From cf72de43d942cf392ca8712cd21c95ead82fa20d Mon Sep 17 00:00:00 2001 From: Chneemann Date: Thu, 13 Aug 2026 10:34:33 +0200 Subject: [PATCH] feat(tasks): implement unified task creation and editing mode with query parameters and service layer integration --- app/(app)/tasks/TaskForm.tsx | 105 ++++++++++++++++ app/(app)/tasks/page.tsx | 88 +++++++++++++ app/(app)/tasks/task/TaskAssignees.tsx | 72 +++++++++++ app/(app)/tasks/task/TaskBasicInfo.tsx | 67 ++++++++++ app/(app)/tasks/task/TaskDateTime.tsx | 82 ++++++++++++ app/(app)/tasks/task/TaskHeader.tsx | 33 +++++ app/(app)/tasks/task/TaskOptions.tsx | 73 +++++++++++ app/(app)/tasks/useTaskForm.ts | 121 ++++++++++++++++++ app/api/tasks/route.ts | 97 +++++++++++++++ app/components/layout/Navbar.tsx | 3 +- next.config.ts | 2 +- services/task.service.ts | 165 +++++++++++++++++-------- types/task.ts | 7 ++ 13 files changed, 863 insertions(+), 52 deletions(-) create mode 100644 app/(app)/tasks/TaskForm.tsx create mode 100644 app/(app)/tasks/page.tsx create mode 100644 app/(app)/tasks/task/TaskAssignees.tsx create mode 100644 app/(app)/tasks/task/TaskBasicInfo.tsx create mode 100644 app/(app)/tasks/task/TaskDateTime.tsx create mode 100644 app/(app)/tasks/task/TaskHeader.tsx create mode 100644 app/(app)/tasks/task/TaskOptions.tsx create mode 100644 app/(app)/tasks/useTaskForm.ts create mode 100644 app/api/tasks/route.ts diff --git a/app/(app)/tasks/TaskForm.tsx b/app/(app)/tasks/TaskForm.tsx new file mode 100644 index 0000000..57fef31 --- /dev/null +++ b/app/(app)/tasks/TaskForm.tsx @@ -0,0 +1,105 @@ +/** + * @file tasks/TaskForm.tsx + * @description Client component orchestrating modular sub-components for task creation and editing. + */ + +"use client"; + +import { AlertCircle } from "lucide-react"; +import Link from "next/link"; +import { DbTask } from "@/types/task"; +import { useTaskForm } from "./useTaskForm"; +import TaskHeader from "./task/TaskHeader"; +import TaskBasicInfo from "./task/TaskBasicInfo"; +import TaskOptions from "./task/TaskOptions"; +import TaskAssignees from "./task/TaskAssignees"; +import TaskDateTime from "./task/TaskDateTime"; + +/** + * Properties for the TaskForm component. + * + * @interface TaskFormProps + * @property {{ id: string; email: string }[]} users - The list of available users who can be assigned to the task. + * @property {DbTask & { assignees?: { id: string }[] }} [initialData] - Optional initial task data for editing an existing task. + * @property {string} [mode] - Optional mode indicator (e.g., create or edit). + */ +interface TaskFormProps { + users: { id: string; email: string }[]; + initialData?: DbTask & { assignees?: { id: string }[] }; + mode?: string; + serverError?: string | null; +} + +/** + * Renders a complete task form layout combining various sub-components for basic info, + * options, assignees, and date/time selections, alongside error alerts and form submission controls. + * + * @param {TaskFormProps} props - The component props. + * @returns {JSX.Element} The rendered task form component. + */ +export default function TaskForm({ users, initialData, mode }: TaskFormProps) { + const { + form, + error, + isEditMode, + todayString, + updateField, + handleAssigneeToggle, + handleSubmit, + } = useTaskForm(initialData, mode); + + return ( +
+ + + {error && ( +
+ + {error} +
+ )} + +
+ updateField("title", v)} + description={form.description} + setDescription={(v) => updateField("description", v)} + /> + updateField("priority", v)} + status={form.status} + setStatus={(v) => updateField("status", v)} + /> + + updateField("dueDate", v)} + dueTime={form.dueTime} + setDueTime={(v) => updateField("dueTime", v)} + todayString={todayString} + /> +
+ +
+ + Cancel + + +
+ + ); +} diff --git a/app/(app)/tasks/page.tsx b/app/(app)/tasks/page.tsx new file mode 100644 index 0000000..ce3916a --- /dev/null +++ b/app/(app)/tasks/page.tsx @@ -0,0 +1,88 @@ +/** + * @file tasks/page.tsx + * @description Server component rendering the task creation or edit page with relations. + */ + +import { auth } from "@/auth"; +import { db } from "@/db"; +import { usersTable, tasksTable, taskAssigneesTable } from "@/db/schema"; +import { redirect } from "next/navigation"; +import TaskForm from "./TaskForm"; +import { not, eq, isNull, and } from "drizzle-orm"; + +/** + * Properties for the TaskPage component. + * + * @interface TaskPageProps + * @property {Promise<{ task?: string; id?: string; }>} searchParams - A promise resolving to the search parameters containing mode and task ID. + */ +interface TaskPageProps { + searchParams: Promise<{ + task?: string; + id?: string; + }>; +} + +/** + * Renders the task creation or edit page, verifying user authentication, + * fetching existing task data and assignees if in edit mode, loading available users, + * and passing the context down to the task form component. + * + * @async + * @param {TaskPageProps} props - The component props. + * @returns {Promise} The rendered task page component. + */ +export default async function TaskPage({ searchParams }: TaskPageProps) { + const session = await auth(); + if (!session?.user?.id) { + redirect("/login"); + } + + const params = await searchParams; + const mode = params.task; + const taskId = params.id; + + let initialData = undefined; + + if (mode === "edit" && taskId) { + const [task] = await db + .select() + .from(tasksTable) + .where( + and( + eq(tasksTable.id, taskId), + eq(tasksTable.userId, session.user.id), + isNull(tasksTable.deletedAt), + ), + ); + + const assignedRows = await db + .select({ id: taskAssigneesTable.userId }) + .from(taskAssigneesTable) + .where(eq(taskAssigneesTable.taskId, taskId)); + + initialData = { + ...task, + assignees: assignedRows, + }; + } + + const users = await db + .select({ + id: usersTable.id, + email: usersTable.email, + }) + .from(usersTable) + .where(not(eq(usersTable.id, session.user.id))); + + return ( +
+ +
+ ); +} diff --git a/app/(app)/tasks/task/TaskAssignees.tsx b/app/(app)/tasks/task/TaskAssignees.tsx new file mode 100644 index 0000000..1bfa72e --- /dev/null +++ b/app/(app)/tasks/task/TaskAssignees.tsx @@ -0,0 +1,72 @@ +/** + * @file tasks/task/TaskAssignees.tsx + * @description Component for selecting task assignees. + */ + +/** + * Properties for the TaskAssignees component. + * + * @interface TaskAssigneesProps + * @property {Array<{ id: string; email: string }>} users - The list of available users to assign. + * @property {string[]} selectedAssignees - An array containing the IDs of currently selected assignees. + * @property {(userId: string) => void} onToggle - Callback function triggered when a user selection is toggled. + */ +interface TaskAssigneesProps { + users: { id: string; email: string }[]; + selectedAssignees: string[]; + onToggle: (userId: string) => void; +} + +/** + * Renders an interactive list of users allowing selection or deselection of assignees for a task. + * + * @param {TaskAssigneesProps} props - The component props. + * @returns {JSX.Element} The rendered task assignees selector component. + */ +export default function TaskAssignees({ + users, + selectedAssignees, + onToggle, +}: TaskAssigneesProps) { + return ( +
+ +
+ {users.length === 0 ? ( + + No other users found + + ) : ( + users.map((user) => { + const isSelected = selectedAssignees.includes(user.id); + return ( + + ); + }) + )} +
+
+ ); +} diff --git a/app/(app)/tasks/task/TaskBasicInfo.tsx b/app/(app)/tasks/task/TaskBasicInfo.tsx new file mode 100644 index 0000000..5d1a17d --- /dev/null +++ b/app/(app)/tasks/task/TaskBasicInfo.tsx @@ -0,0 +1,67 @@ +/** + * @file tasks/task/TaskBasicInfo.tsx + * @description Component for title and description input fields. + */ + +/** + * Properties for the TaskBasicInfo component. + * + * @interface TaskBasicInfoProps + * @property {string} title - The current title value of the task. + * @property {(val: string) => void} setTitle - Callback function to update the task title state. + * @property {string} description - The current description value of the task. + * @property {(val: string) => void} setDescription - Callback function to update the task description state. + */ +interface TaskBasicInfoProps { + title: string; + setTitle: (val: string) => void; + description: string; + setDescription: (val: string) => void; +} + +/** + * Renders form fields for entering and updating a task's basic information (title and description). + * + * @param {TaskBasicInfoProps} props - The component props. + * @returns {JSX.Element} The rendered task basic information form inputs. + */ +export default function TaskBasicInfo({ + title, + setTitle, + description, + setDescription, +}: TaskBasicInfoProps) { + return ( +
+ {/* Title */} +
+ + setTitle(e.target.value)} + placeholder="e.g. Redesign Landing Page" + className="w-full bg-background border border-border rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/40" + /> +
+ + {/* Description */} +
+ +