diff --git a/app/(app)/dashboard/KanbanCard.tsx b/app/(app)/dashboard/KanbanCard.tsx index 1283297..591e26a 100644 --- a/app/(app)/dashboard/KanbanCard.tsx +++ b/app/(app)/dashboard/KanbanCard.tsx @@ -1,59 +1,24 @@ /** * @file dashboard/KanbanCard.tsx - * @description Client component rendering a single kanban card with native drag-and-drop source handlers, dynamic priority styling, and mobile status dropdown. + * @description Client component rendering a single kanban card container with native drag-and-drop source handlers and modular sub-components. */ "use client"; -import { - AlertCircle, - Crown, - MoreHorizontal, - CornerDownRight, - CalendarDays, -} from "lucide-react"; -import { KANBAN_COLUMNS, KanbanCardProps, TaskStatus } from "@/types/tasks"; -import { useState, useEffect, useRef } from "react"; +import { KanbanCardProps, TaskStatus } from "@/types/tasks"; +import KanbanCardActions from "./components/KanbanCardActions"; +import KanbanCardAvatars from "./components/KanbanCardAvatars"; +import KanbanCardDueDate from "./components/KanbanCardDueDate"; +import KanbanCardPriority from "./components/KanbanCardPriority"; /** - * Renders an interactive kanban card featuring title, description, priority levels, - * deadline alerts, team avatars, and a mobile-friendly status-shifting dropdown menu. + * Renders an interactive kanban card container holding title, description, + * modular priority badges, deadline elements, assignees, and action triggers. * * @param {KanbanCardProps} props - The component props containing the task object and status change handler. - * @returns {JSX.Element} The rendered kanban card component. + * @returns {JSX.Element} The rendered kanban card container component. */ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) { - const [showMobileActions, setShowMobileActions] = useState(false); - const dropdownRef = useRef(null); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setShowMobileActions(false); - } - }; - - if (showMobileActions) { - document.addEventListener("mousedown", handleClickOutside); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [showMobileActions]); - - const isOverdue = - task.dueDate && - new Date(task.dueDate) < new Date() && - task.status !== "done"; - - const filteredAssignees = (task.assignees || []).filter( - (a) => a !== task.creator, - ); - /** * Initiates the drag action on a task card, storing its ID and current status in the data transfer payload. * @@ -66,17 +31,14 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) { }; /** - * Handles shifting the task to a new status category. + * Triggers the status change callback with the target task ID and new status. * - * @param {React.MouseEvent} e - The mouse event triggered by clicking a column destination. * @param {TaskStatus} newStatus - The target task status to transition to. */ - const handleMove = (e: React.MouseEvent, newStatus: TaskStatus) => { - e.stopPropagation(); + const handleMove = (newStatus: TaskStatus) => { if (onStatusChange) { onStatusChange(task.id, newStatus); } - setShowMobileActions(false); }; return ( @@ -85,25 +47,13 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) { onDragStart={handleDragStart} className="group relative bg-card/40 border border-border/80 hover:border-primary/60 p-4 rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 space-y-3 cursor-grab active:cursor-grabbing hover:-translate-y-0.5 flex flex-col h-full" > - {/* --- Card Header (Title & Priority) --- */} + {/* --- Card Header --- */}

{task.title}

- - {task.priority && ( - - {task.priority.charAt(0).toUpperCase() + task.priority.slice(1)} - - )} + {/* Card Priority */} +
{/* Card Description */} @@ -113,104 +63,16 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) { {/* --- Card Footer --- */}
- {/* Date Badge */} - {task.dueDate && ( -
- {isOverdue ? : } - - {new Date(task.dueDate).toLocaleDateString("de-DE", { - day: "2-digit", - month: "short", - })} - - - - {new Date(task.dueDate).toLocaleTimeString("de-DE", { - hour: "2-digit", - minute: "2-digit", - })} - - -
- )} + {/* Date Badge Component */} + {/* Avatars & Mobile Switcher */}
- {(task.creator || filteredAssignees.length > 0) && ( -
- {filteredAssignees.map((assignee, index) => ( -
- {assignee.substring(0, 2).toUpperCase()} -
- ))} - {task.creator && ( -
- {task.creator.substring(0, 2).toUpperCase()} - - - -
- )} -
- )} - - {/* Mobile Switcher */} -
- - -
- {KANBAN_COLUMNS.map((col) => { - if (col.id === task.status) return null; - return ( - - ); - })} -
-
+ +
diff --git a/app/(app)/dashboard/components/KanbanCardActions.tsx b/app/(app)/dashboard/components/KanbanCardActions.tsx new file mode 100644 index 0000000..667077d --- /dev/null +++ b/app/(app)/dashboard/components/KanbanCardActions.tsx @@ -0,0 +1,105 @@ +/** + * @file dashboard/components/KanbanCardActions.tsx + * @description Client component rendering the mobile status transition dropdown for a kanban card. + */ + +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { MoreHorizontal, CornerDownRight } from "lucide-react"; +import { KANBAN_COLUMNS, TaskStatus } from "@/types/tasks"; + +/** + * Renders a mobile-only action menu component allowing users to move a task + * between different kanban columns via a dropdown interface. + * + * @param {Object} props - The component props. + * @param {TaskStatus} props.currentStatus - The current status category of the task. + * @param {(newStatus: TaskStatus) => void} props.onMove - Callback function triggered when a new status column is selected. + * @returns {JSX.Element} The rendered mobile card actions component. + */ +export default function KanbanCardActions({ + currentStatus, + onMove, +}: { + currentStatus: TaskStatus; + onMove: (newStatus: TaskStatus) => void; +}) { + const [showMobileActions, setShowMobileActions] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setShowMobileActions(false); + } + }; + + if (showMobileActions) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [showMobileActions]); + + /** + * Handles clicking an action item to move the task to a target status category. + * + * @param {React.MouseEvent} e - The mouse event object. + * @param {TaskStatus} targetStatus - The target status category to move the task to. + */ + const handleActionClick = (e: React.MouseEvent, targetStatus: TaskStatus) => { + e.stopPropagation(); + onMove(targetStatus); + setShowMobileActions(false); + }; + + return ( +
+ + +
+ {KANBAN_COLUMNS.map((col) => { + if (col.id === currentStatus) return null; + return ( + + ); + })} +
+
+ ); +} diff --git a/app/(app)/dashboard/components/KanbanCardAvatars.tsx b/app/(app)/dashboard/components/KanbanCardAvatars.tsx new file mode 100644 index 0000000..08cc0d1 --- /dev/null +++ b/app/(app)/dashboard/components/KanbanCardAvatars.tsx @@ -0,0 +1,55 @@ +/** + * @file dashboard/components/KanbanCardAvatars.tsx + * @description Client component rendering team avatars and creator badge for a kanban card. + */ + +"use client"; + +import { Crown } from "lucide-react"; +import { Task } from "@/types/tasks"; + +/** + * Renders overlapping avatar indicators for task assignees and a dedicated, crowned badge for the task creator. + * Filters out duplicate entries where the assignee matches the creator. + * + * @param {Object} props - The component props. + * @param {Task["creator"]} props.creator - The email or identifier of the task creator. + * @param {Task["assignees"]} [props.assignees=[]] - An array of emails or identifiers for users assigned to the task. + * @returns {JSX.Element | null} The rendered avatars container, or null if neither creator nor assignees exist. + */ +export default function KanbanCardAvatars({ + creator, + assignees = [], +}: { + creator: Task["creator"]; + assignees: Task["assignees"]; +}) { + const filteredAssignees = (assignees || []).filter((a) => a !== creator); + + if (!creator && filteredAssignees.length === 0) return null; + + return ( +
+ {filteredAssignees.map((assignee, index) => ( +
+ {assignee.substring(0, 2).toUpperCase()} +
+ ))} + {creator && ( +
+ {creator.substring(0, 2).toUpperCase()} + + + +
+ )} +
+ ); +} diff --git a/app/(app)/dashboard/components/KanbanCardDueDate.tsx b/app/(app)/dashboard/components/KanbanCardDueDate.tsx new file mode 100644 index 0000000..3b6f8ef --- /dev/null +++ b/app/(app)/dashboard/components/KanbanCardDueDate.tsx @@ -0,0 +1,48 @@ +/** + * @file dashboard/components/KanbanCardDueDate.tsx + * @description Client component rendering the due date badge with overdue status indicators and hover time display. + */ + +import { AlertCircle, CalendarDays } from "lucide-react"; +import { Task } from "@/types/tasks"; + +/** + * Renders a due date badge for a kanban card, showing an overdue alert animation + * if the deadline has passed and the task is not completed, alongside a hoverable time display. + * + * @param {Object} props - The component props. + * @param {Task} props.task - The task object containing the due date and status information. + * @returns {JSX.Element | null} The rendered due date badge component or null if no due date is set. + */ +export default function KanbanCardDueDate({ task }: { task: Task }) { + if (!task.dueDate) return null; + + const isOverdue = + new Date(task.dueDate) < new Date() && task.status !== "done"; + + return ( +
+ {isOverdue ? : } + + {new Date(task.dueDate).toLocaleDateString("de-DE", { + day: "2-digit", + month: "short", + })} + + + + {new Date(task.dueDate).toLocaleTimeString("de-DE", { + hour: "2-digit", + minute: "2-digit", + })} + + +
+ ); +} diff --git a/app/(app)/dashboard/components/KanbanCardPriority.tsx b/app/(app)/dashboard/components/KanbanCardPriority.tsx new file mode 100644 index 0000000..72aec0c --- /dev/null +++ b/app/(app)/dashboard/components/KanbanCardPriority.tsx @@ -0,0 +1,31 @@ +/** + * @file dashboard/components/KanbanCardPriority.tsx + * @description Client component rendering the dynamic priority badge for a kanban card based on configuration. + */ + +import { TaskPriority, PRIORITY_CONFIG } from "@/types/tasks"; + +/** + * Renders a styled priority badge for a kanban card. + * + * @param {Object} props - The component props. + * @param {TaskPriority} props.priority - The priority level of the task. + * @returns {JSX.Element | null} The rendered priority badge component, or null if priority is invalid. + */ +export default function KanbanCardPriority({ + priority, +}: { + priority: TaskPriority; +}) { + if (!priority || !PRIORITY_CONFIG[priority]) return null; + + const { label, className } = PRIORITY_CONFIG[priority]; + + return ( + + {label} + + ); +}