From 68183b770e3010664ec8e3971d480cdd8ae49efe Mon Sep 17 00:00:00 2001 From: Chneemann Date: Wed, 26 Aug 2026 09:28:58 +0200 Subject: [PATCH] refactor(dashboard): split task modal into modular TaskModalContent component --- app/(app)/dashboard/modal/TaskModal.tsx | 211 ++---------------- .../dashboard/modal/TaskModalContent.tsx | 159 +++++++++++++ 2 files changed, 178 insertions(+), 192 deletions(-) create mode 100644 app/(app)/dashboard/modal/TaskModalContent.tsx diff --git a/app/(app)/dashboard/modal/TaskModal.tsx b/app/(app)/dashboard/modal/TaskModal.tsx index 89413ac..1cfd701 100644 --- a/app/(app)/dashboard/modal/TaskModal.tsx +++ b/app/(app)/dashboard/modal/TaskModal.tsx @@ -1,24 +1,15 @@ /** * @file app/(app)/dashboard/modal/TaskModal.tsx - * @description Client component rendering a detailed modal overlay for viewing task metadata, status, assignees, and quick actions with ESC key support. + * @description Client component orchestrating the task modal overlay, history sync, and action buttons. */ "use client"; import { useEffect } from "react"; -import { Task, PRIORITY_CONFIG } from "@/lib/types/task"; -import { getFullName, getStatusColor } from "@/lib/utils/user"; -import { - X, - CalendarDays, - AlertCircle, - User, - Users, - FileText, - Pencil, - Trash2, -} from "lucide-react"; +import { Task } from "@/lib/types/task"; import { useRouter } from "next/navigation"; +import { Pencil, Trash2 } from "lucide-react"; +import TaskModalContent from "./TaskModalContent"; /** * Properties for the TaskModal component. @@ -35,31 +26,26 @@ interface TaskModalProps { } /** - * Renders a full task detail modal with status indicators, priority details, description, assignees, and creator-only action buttons. - * Supports ESC key navigation and updates URL query parameters dynamically. + * Renders the task detail modal container with URL state synchronization, keyboard event handling, and action triggers. * * @param {TaskModalProps} props - The component props. - * @returns {JSX.Element | null} The rendered modal component or null when no task is selected. + * @returns {JSX.Element | null} The rendered modal overlay or null when no task is selected. */ export default function TaskModal({ task, onClose, onDelete }: TaskModalProps) { const router = useRouter(); useEffect(() => { /** - * Attaches a global keydown event listener to close the modal when the Escape key is pressed. + * Handles keyboard events to close the modal when the Escape key is pressed. * - * @param {KeyboardEvent} e - The keyboard event object. + * @param {KeyboardEvent} e - The keyboard event instance. */ const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - handleClose(); - } + if (e.key === "Escape") handleClose(); }; window.addEventListener("keydown", handleKeyDown); - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [onClose]); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); useEffect(() => { /** @@ -80,32 +66,22 @@ export default function TaskModal({ task, onClose, onDelete }: TaskModalProps) { }, [task]); /** - * Removes modal query parameters from the browser location history without causing a Next.js soft navigation, then triggers onClose. + * Removes modal query parameters from browser history without triggering Next.js routing, then executes the onClose callback. */ const handleClose = () => { const params = new URLSearchParams(window.location.search); params.delete("modal"); - const newQuery = params.toString(); - + const query = params.toString(); window.history.replaceState( {}, "", - newQuery - ? `${window.location.pathname}?${newQuery}` - : window.location.pathname, + query ? `${window.location.pathname}?${query}` : window.location.pathname, ); - onClose(); }; if (!task) return null; - const priorityConfig = task.priority && PRIORITY_CONFIG[task.priority]; - const isOverdue = - task.dueDate && - new Date(task.dueDate) < new Date() && - task.status !== "done"; - return (
e.stopPropagation()} > - {/* Dynamic Status Indicator Strip */} + {/* Status Indikator */}
- {/* --- SCROLLABLE CONTENT AREA --- */} -
- {/* Header / Status, Priority & Title */} -
-
-
- - {task.status} - - - {priorityConfig && ( - - {priorityConfig.label} - - )} -
- -

- {task.title} -

-
- - -
- - {/* Description Section */} -
-
- - - Description - -
-
- {task.description || "No description provided for this task."} -
-
- - {/* Meta Grid (Creator & Due Date) */} -
- {/* Creator */} -
-
- -
-
-

- Creator -

-

- {getFullName(task.creator.firstName, task.creator.lastName)} -

-
-
- - {/* Due Date */} -
-
- {isOverdue ? ( - - ) : ( - - )} -
-
-

- {isOverdue ? "Overdue Due Date" : "Due Date"} -

-

- {task.dueDate - ? `${new Date(task.dueDate).toLocaleDateString("de-DE", { - day: "2-digit", - month: "short", - year: "numeric", - })} (${new Date(task.dueDate).toLocaleTimeString( - "de-DE", - { - hour: "2-digit", - minute: "2-digit", - }, - )})` - : "No due date"} -

-
-
-
- - {/* Assignees Section */} -
-
- - - Assignees ({task.assignees.length}) - -
-
- {task.assignees.length > 0 ? ( - task.assignees.map((assignee) => ( -
- - - {getFullName(assignee.firstName, assignee.lastName)} - -
- )) - ) : ( -

- No assignees assigned to this task. -

- )} -
-
-
+ {/* Content */} + {/* Footer Actions */} {task.isCreator && ( @@ -282,11 +112,8 @@ export default function TaskModal({ task, onClose, onDelete }: TaskModalProps) { Delete - +
+ + {/* Description */} +
+
+ + + Description + +
+
+ {task.description || "No description provided for this task."} +
+
+ + {/* Meta Grid (Creator & Due Date) */} +
+
+
+ +
+
+

Creator

+

+ {getFullName(task.creator.firstName, task.creator.lastName)} +

+
+
+ +
+
+ {isOverdue ? ( + + ) : ( + + )} +
+
+

+ {isOverdue ? "Overdue Due Date" : "Due Date"} +

+

+ {task.dueDate + ? `${new Date(task.dueDate).toLocaleDateString("de-DE", { day: "2-digit", month: "short", year: "numeric" })} (${new Date(task.dueDate).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })})` + : "No due date"} +

+
+
+
+ + {/* Assignees */} +
+
+ + + Assignees ({task.assignees.length}) + +
+
+ {task.assignees.length > 0 ? ( + task.assignees.map((a) => ( +
+ + {getFullName(a.firstName, a.lastName)} +
+ )) + ) : ( +

+ No assignees assigned to this task. +

+ )} +
+
+
+ ); +}