diff --git a/app/(app)/dashboard/KanbanCard.tsx b/app/(app)/dashboard/KanbanCard.tsx index ff32225..08a0145 100644 --- a/app/(app)/dashboard/KanbanCard.tsx +++ b/app/(app)/dashboard/KanbanCard.tsx @@ -1,20 +1,50 @@ /** * @file dashboard/KanbanCard.tsx - * @description Client component rendering an individual task card within a kanban column, featuring priority configuration, due dates, overdue highlights, and user avatars. + * @description Client component rendering a single kanban card with support for priority indicators, due date alerts, team avatars, and a mobile status dropdown. */ "use client"; -import { AlertCircle, Crown, CalendarDays } from "lucide-react"; -import { KanbanCardProps } from "@/types/tasks"; + +import { + AlertCircle, + Crown, + MoreHorizontal, + CornerDownRight, + CalendarDays, +} from "lucide-react"; +import { KANBAN_COLUMNS, KanbanCardProps, TaskStatus } from "@/types/tasks"; +import { useState, useEffect, useRef } from "react"; /** - * Renders a task card component displaying its title, priority badge, description, - * deadline with hover time details, and creator/assignee avatars. + * Renders an interactive kanban card featuring title, description, priority levels, + * deadline alerts, team avatars, and a mobile-friendly status-shifting dropdown menu. * - * @param {KanbanCardProps} props - The component props containing the task object. + * @param {KanbanCardProps} props - The component props containing the task object and status change handler. * @returns {JSX.Element} The rendered kanban card component. */ -export default function KanbanCard({ task }: KanbanCardProps) { +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() && @@ -24,6 +54,20 @@ export default function KanbanCard({ task }: KanbanCardProps) { (a) => a !== task.creator, ); + /** + * Handles shifting the task to a new status category. + * + * @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(); + if (onStatusChange) { + onStatusChange(task.id, newStatus); + } + setShowMobileActions(false); + }; + return (
{/* --- Card Header (Title & Priority) --- */} @@ -112,6 +156,52 @@ export default function KanbanCard({ task }: KanbanCardProps) { )}
)} + + {/* Mobile Switcher */} +
+ + + {/* Dropdown Menu */} +
+ {KANBAN_COLUMNS.map((col) => { + if (col.id === task.status) return null; + return ( + + ); + })} +
+
diff --git a/app/(app)/dashboard/KanbanColumn.tsx b/app/(app)/dashboard/KanbanColumn.tsx index 7a6bbc1..3fe31e2 100644 --- a/app/(app)/dashboard/KanbanColumn.tsx +++ b/app/(app)/dashboard/KanbanColumn.tsx @@ -1,33 +1,115 @@ /** * @file dashboard/KanbanColumn.tsx - * @description Client component rendering an individual kanban column container with its header, task count, action button, and list of task cards. + * @description Client component rendering an single kanban column container supporting drag-and-drop task reordering, status updates, and interactive card layouts. */ "use client"; +import { useState, useTransition } from "react"; import { Plus } from "lucide-react"; +import { useRouter } from "next/navigation"; import KanbanCard from "./KanbanCard"; -import { KanbanColumnProps } from "@/types/tasks"; +import { KanbanColumnProps, TaskStatus } from "@/types/tasks"; /** - * Renders a kanban column containing a header with category indicator and counter, - * an add button, and a sorted list of associated task cards or an empty placeholder. + * Renders a kanban column with title indicators, task counters, drag-and-drop event handlers, + * and lists of nested KanbanCard items. * - * @param {KanbanColumnProps} props - The component props including column details, task list, and styling options. + * @param {KanbanColumnProps} props - The component props including column ID, title, count, tasks, and color configuration. * @returns {JSX.Element} The rendered kanban column component. */ export default function KanbanColumn(props: KanbanColumnProps) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + const [isDraggingOver, setIsDraggingOver] = useState(false); const indicatorColor = props.color ?? "bg-primary"; + /** + * Updates the status of a task via a state transition and refreshes the router data. + * + * @param {string} taskId - The unique identifier of the task being updated. + * @param {TaskStatus} targetStatus - The new target status category for the task. + */ + const updateTaskStatus = (taskId: string, targetStatus: TaskStatus) => { + startTransition(async () => { + try { + // TODO: Implement actual API endpoint for updating task status + console.log(`Successfully moved task ${taskId} to ${targetStatus}`); + router.refresh(); + } catch (error) { + console.error("Error during task status update:", error); + } + }); + }; + + /** + * Handles the drag-over event to allow dropping items onto the column. + * + * @param {React.DragEvent} e - The drag event object. + */ + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDraggingOver(true); + }; + + /** + * Handles the drag-leave event when a dragged item leaves the column bounds. + */ + const handleDragLeave = () => { + setIsDraggingOver(false); + }; + + /** + * Initiates the drag action on a task card, storing its ID and current status in the data transfer payload. + * + * @param {React.DragEvent} e - The drag event object. + * @param {string} taskId - The identifier of the task being dragged. + * @param {TaskStatus} currentStatus - The original status category of the task. + */ + const handleDragStart = ( + e: React.DragEvent, + taskId: string, + currentStatus: TaskStatus, + ) => { + e.dataTransfer.setData("text/plain", taskId); + e.dataTransfer.setData("sourceStatus", currentStatus); + }; + + /** + * Handles dropping a task card onto the column, triggering status updates if valid. + * + * @param {React.DragEvent} e - The drop event object. + */ + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDraggingOver(false); + + const taskId = e.dataTransfer.getData("text/plain"); + const sourceStatus = e.dataTransfer.getData("sourceStatus") as TaskStatus; + + if (!taskId || sourceStatus === props.id) return; + + updateTaskStatus(taskId, props.id); + }; + return ( -
+
{/* Column Header */}
-

+

{props.title}

@@ -40,7 +122,6 @@ export default function KanbanColumn(props: KanbanColumnProps) {
{/* Card List */} -
{props.tasks.length === 0 ? (
@@ -50,9 +131,15 @@ export default function KanbanColumn(props: KanbanColumnProps) { props.tasks.map((task) => (
handleDragStart(e, task.id, task.status)} + 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" > - +
)) )}