diff --git a/app/(app)/dashboard/KanbanBoard.tsx b/app/(app)/dashboard/Board.tsx similarity index 81% rename from app/(app)/dashboard/KanbanBoard.tsx rename to app/(app)/dashboard/Board.tsx index b25c67c..362a9ba 100644 --- a/app/(app)/dashboard/KanbanBoard.tsx +++ b/app/(app)/dashboard/Board.tsx @@ -1,26 +1,26 @@ /** - * @file dashboard/KanbanBoard.tsx - * @description Client component wrapping the kanban columns grid, tracking individual task update/deletion states, and handling asynchronous mutations via API. + * @file dashboard/Board.tsx + * @description Client component wrapping the columns grid, tracking individual task update/deletion states, and handling asynchronous mutations via API. */ "use client"; import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; -import KanbanColumn from "./KanbanColumn"; -import { KANBAN_COLUMNS, Task, TaskStatus } from "@/types/tasks"; -import KanbanBoardHeader from "./components/KanbanBoardHeader"; +import Column from "./column/Column"; +import { COLUMNS, Task, TaskStatus } from "@/types/tasks"; +import Header from "./header/Header"; /** - * Renders the responsive grid container of kanban columns, coordinating state tracking + * Renders the responsive grid container of columns, coordinating state tracking * for active task updates/deletions, trash counts, and triggering mutation API requests. * * @param {Object} props - The component props. * @param {Task[]} props.tasks - The array of task items displayed across the board. * @param {number} props.trashCount - The count of items currently in the trash bin. - * @returns {JSX.Element} The rendered kanban board component. + * @returns {JSX.Element} The rendered board component. */ -export default function KanbanBoard({ +export default function Board({ tasks, trashCount, }: { @@ -100,14 +100,14 @@ export default function KanbanBoard({ return (
{/* Workspace Header */} - +
- {/* Kanban Columns Grid */} + {/* Columns Grid */}
- {KANBAN_COLUMNS.map((col) => { + {COLUMNS.map((col) => { const columnTasks = tasks.filter((t) => t.status === col.id); return ( - } e - The drag event object. - */ - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - setIsDraggingOver(true); - }; - - /** - * Resets the drag-over state when the dragged element leaves the column boundary. - */ - const handleDragLeave = () => { - setIsDraggingOver(false); - }; - - /** - * Handles dropping a task card onto the column, extracting task metadata and triggering the move action. - * - * @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; - - props.onTaskMove?.(taskId, props.id); - }; - - return ( -
- {/* Column Header */} -
-
- -

- {props.title} -

- - {props.count} - -
- -
- - {/* Card List */} -
- {props.tasks.length === 0 ? ( -
- No tasks -
- ) : ( - props.tasks.map((task) => ( - - )) - )} -
-
- ); -} diff --git a/app/(app)/dashboard/KanbanCard.tsx b/app/(app)/dashboard/card/Card.tsx similarity index 70% rename from app/(app)/dashboard/KanbanCard.tsx rename to app/(app)/dashboard/card/Card.tsx index 6c286db..db0503a 100644 --- a/app/(app)/dashboard/KanbanCard.tsx +++ b/app/(app)/dashboard/card/Card.tsx @@ -1,30 +1,30 @@ /** - * @file dashboard/KanbanCard.tsx - * @description Client component rendering a single kanban card container with individual loading states and modular sub-components. + * @file dashboard/card/Card.tsx + * @description Client component rendering a single card container with individual loading states and modular sub-components. */ "use client"; import { Loader2 } from "lucide-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"; +import { CardProps, TaskStatus } from "@/types/tasks"; +import CardActions from "./CardActions"; +import CardAvatars from "./CardAvatars"; +import CardDueDate from "./CardDueDate"; +import CardPriority from "./CardPriority"; /** - * Renders an interactive kanban card container handling drag-and-drop actions, loading states, + * Renders an interactive card container handling drag-and-drop actions, loading states, * and assembling modular sub-components for priorities, due dates, avatars, and actions. * - * @param {KanbanCardProps} props - The component props containing the task object, updating status flag, and status change handler. - * @returns {JSX.Element} The rendered kanban card component. + * @param {CardProps} props - The component props containing the task object, updating status flag, and status change handler. + * @returns {JSX.Element} The rendered card component. */ -export default function KanbanCard({ +export default function Card({ task, isUpdating = false, onStatusChange, onDelete, -}: KanbanCardProps) { +}: CardProps) { /** * Initiates the drag action on a task card if not currently updating, storing its ID and status payload. * @@ -72,7 +72,7 @@ export default function KanbanCard({

{task.title}

- +
{/* Card Description */} @@ -82,14 +82,11 @@ export default function KanbanCard({ {/* --- Card Footer --- */}
- +
- - + onDelete?.(task.id)} diff --git a/app/(app)/dashboard/components/KanbanCardActions.tsx b/app/(app)/dashboard/card/CardActions.tsx similarity index 93% rename from app/(app)/dashboard/components/KanbanCardActions.tsx rename to app/(app)/dashboard/card/CardActions.tsx index 43b26bf..05d23ac 100644 --- a/app/(app)/dashboard/components/KanbanCardActions.tsx +++ b/app/(app)/dashboard/card/CardActions.tsx @@ -1,17 +1,17 @@ /** - * @file dashboard/components/KanbanCardActions.tsx - * @description Client component rendering the mobile status transition and deletion dropdown for a kanban card. + * @file dashboard/card/CardActions.tsx + * @description Client component rendering the mobile status transition and deletion dropdown for a card. */ "use client"; import { useState, useEffect, useRef } from "react"; import { MoreHorizontal, CornerDownRight, Trash2 } from "lucide-react"; -import { KANBAN_COLUMNS, TaskStatus } from "@/types/tasks"; +import { COLUMNS, TaskStatus } from "@/types/tasks"; /** * Renders a mobile-only action menu component allowing users to move a task - * between different kanban columns or delete it entirely via a dropdown interface. + * between different columns or delete it entirely via a dropdown interface. * * @param {Object} props - The component props. * @param {TaskStatus} props.currentStatus - The current status category of the task. @@ -19,7 +19,7 @@ import { KANBAN_COLUMNS, TaskStatus } from "@/types/tasks"; * @param {() => void} props.onDelete - Callback function triggered when the delete action is selected. * @returns {JSX.Element} The rendered mobile card actions component. */ -export default function KanbanCardActions({ +export default function CardActions({ currentStatus, onMove, onDelete, @@ -94,7 +94,7 @@ export default function KanbanCardActions({ : "opacity-0 scale-10 pointer-events-none" }`} > - {KANBAN_COLUMNS.map((col) => { + {COLUMNS.map((col) => { if (col.id === currentStatus) return null; return ( +
+ ); +} diff --git a/app/(app)/dashboard/components/KanbanBoardHeader.tsx b/app/(app)/dashboard/components/KanbanBoardHeader.tsx deleted file mode 100644 index 193b6c9..0000000 --- a/app/(app)/dashboard/components/KanbanBoardHeader.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @file dashboard/components/KanbanBoardHeader.tsx - * @description Component rendering the top title header with an integrated delete drop zone that appears during drag-and-drop interactions, plus a link to the trash bin view. - */ - -"use client"; - -import { useState, useEffect } from "react"; -import { Plus, Sparkles, Trash2 } from "lucide-react"; -import Link from "next/link"; - -/** - * Renders the dashboard header featuring a workspace title, subtitle, new task action button, - * a dynamic delete drop zone during drag operations, and a trash navigation link with a counter badge. - * - * @param {Object} props - The component props. - * @param {(taskId: string) => void} props.onTaskDelete - Callback function invoked when a task is dropped into the delete zone. - * @param {number} props.trashCount - The current count of items in the trash bin. - * @returns {JSX.Element} The rendered kanban board header component. - */ -export default function KanbanBoardHeader({ - onTaskDelete, - trashCount, -}: { - onTaskDelete: (taskId: string) => void; - trashCount: number; -}) { - const [isDragging, setIsDragging] = useState(false); - const [isOver, setIsOver] = useState(false); - - useEffect(() => { - const handleDragStart = () => setIsDragging(true); - const handleDragEnd = () => { - setIsDragging(false); - setIsOver(false); - }; - - window.addEventListener("dragstart", handleDragStart); - window.addEventListener("dragend", handleDragEnd); - - return () => { - window.removeEventListener("dragstart", handleDragStart); - window.removeEventListener("dragend", handleDragEnd); - }; - }, []); - - return ( -
-
-
- - Workspace -
-

Dashboard

-

- Manage your tasks and keep track of your progress. -

-
- -
- {isDragging ? ( -
{ - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - setIsOver(true); - }} - onDragLeave={() => setIsOver(false)} - onDrop={(e) => { - e.preventDefault(); - setIsOver(false); - setIsDragging(false); - const taskId = e.dataTransfer.getData("text/plain"); - if (taskId) { - onTaskDelete(taskId); - } - }} - className={`flex items-center gap-2 px-4 py-2.5 rounded-xl font-medium text-sm border-2 border-dashed cursor-pointer select-none transition-all duration-300 ease-out transform ${ - isOver - ? "bg-destructive border-destructive scale-110 shadow-lg animate-pulse" - : "bg-destructive/10 border-destructive/40 text-destructive scale-100 opacity-90 hover:opacity-100 animate-in fade-in zoom-in-95 duration-200" - }`} - > - - Drop to Delete -
- ) : ( - - )} - - {/* Icon */} - - - {/* Badge */} - {trashCount > 0 && ( - - {trashCount} - - )} - -
-
- ); -} diff --git a/app/(app)/dashboard/header/DeleteDropZone.tsx b/app/(app)/dashboard/header/DeleteDropZone.tsx new file mode 100644 index 0000000..995fdd5 --- /dev/null +++ b/app/(app)/dashboard/header/DeleteDropZone.tsx @@ -0,0 +1,71 @@ +/** + * @file dashboard/header/DeleteDropZone.tsx + * @description Client component rendering an interactive drop zone for deleting tasks during drag-and-drop. + */ + +"use client"; + +import { useState, useEffect } from "react"; +import { Trash2 } from "lucide-react"; + +/** + * Renders a drop target zone that appears dynamically during drag-and-drop operations, + * allowing users to delete tasks by dragging them onto the designated area. + * + * @param {Object} props - The component props. + * @param {(taskId: string) => void} props.onTaskDelete - Callback triggered when a task is dropped into the delete zone. + * @returns {JSX.Element | null} The rendered delete drop zone component, or null if no drag operation is active. + */ +export default function DeleteDropZone({ + onTaskDelete, +}: { + onTaskDelete: (taskId: string) => void; +}) { + const [isDragging, setIsDragging] = useState(false); + const [isOver, setIsOver] = useState(false); + + useEffect(() => { + const handleDragStart = () => setIsDragging(true); + const handleDragEnd = () => { + setIsDragging(false); + setIsOver(false); + }; + + window.addEventListener("dragstart", handleDragStart); + window.addEventListener("dragend", handleDragEnd); + + return () => { + window.removeEventListener("dragstart", handleDragStart); + window.removeEventListener("dragend", handleDragEnd); + }; + }, []); + + if (!isDragging) return null; + + return ( +
{ + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + setIsOver(true); + }} + onDragLeave={() => setIsOver(false)} + onDrop={(e) => { + e.preventDefault(); + setIsOver(false); + const taskId = e.dataTransfer.getData("text/plain"); + if (taskId) { + onTaskDelete(taskId); + } + }} + className={`peer flex items-center gap-2 px-4 py-2.5 rounded-xl font-medium text-sm border-2 border-dashed cursor-pointer select-none transition-all duration-300 ease-out transform ${ + isOver + ? "bg-destructive border-destructive scale-110 shadow-lg animate-pulse" + : "bg-destructive/10 border-destructive/40 text-destructive scale-100 opacity-90 hover:opacity-100" + }`} + > + + Drop to Delete +
+ ); +} diff --git a/app/(app)/dashboard/header/Header.tsx b/app/(app)/dashboard/header/Header.tsx new file mode 100644 index 0000000..40d3c1a --- /dev/null +++ b/app/(app)/dashboard/header/Header.tsx @@ -0,0 +1,49 @@ +/** + * @file dashboard/header/Header.tsx + * @description Client component rendering the dashboard top header section, including workspace info, deletion drop zones, task creation buttons, and trash links. + */ + +"use client"; + +import { Sparkles } from "lucide-react"; +import DeleteDropZone from "./DeleteDropZone"; +import TrashLink from "./TrashLink"; +import NewTaskButton from "./NewTaskButton"; + +/** + * Renders the dashboard header section featuring title text, a task deletion drop zone, + * a new task action button, and a link to the trash bin. + * + * @param {HeaderProps} props - The component props. + * @returns {JSX.Element} The rendered dashboard header component. + */ +export default function Header({ + onTaskDelete, + trashCount, +}: { + onTaskDelete: (taskId: string) => void; + trashCount: number; +}) { + return ( +
+
+
+ + Workspace +
+

Dashboard

+

+ Manage your tasks and keep track of your progress. +

+
+ +
+ +
+ + +
+
+
+ ); +} diff --git a/app/(app)/dashboard/header/NewTaskButton.tsx b/app/(app)/dashboard/header/NewTaskButton.tsx new file mode 100644 index 0000000..895974b --- /dev/null +++ b/app/(app)/dashboard/header/NewTaskButton.tsx @@ -0,0 +1,22 @@ +/** + * @file dasboard/header/NewTaskButton.tsx + * @description Client component rendering an interactive trigger button for creating new tasks. + */ + +"use client"; + +import { Plus } from "lucide-react"; + +/** + * Renders a stylized button component with an icon and active state animations to initiate task creation. + * + * @returns {JSX.Element} The rendered new task button component. + */ +export default function NewTaskButton() { + return ( + + ); +} diff --git a/app/(app)/dashboard/header/TrashLink.tsx b/app/(app)/dashboard/header/TrashLink.tsx new file mode 100644 index 0000000..3d46907 --- /dev/null +++ b/app/(app)/dashboard/header/TrashLink.tsx @@ -0,0 +1,36 @@ +/** + * @file dasboard/header/TrashLink.tsx + * @description Client component rendering a navigation link button to the trash view with a dynamic item counter badge. + */ + +"use client"; + +import Link from "next/link"; +import { Trash2 } from "lucide-react"; + +/** + * Renders an interactive trash icon link featuring hover animations and an optional item count badge. + * + * @param {Object} props - The component props. + * @param {number} props.count - The number of items currently in the trash. + * @returns {JSX.Element} The rendered trash button component. + */ +export default function TrashLink({ count }: { count: number }) { + return ( + + + {count > 0 && ( + + {count} + + )} + + ); +} diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx index 5d4841c..bb55dfb 100644 --- a/app/(app)/dashboard/page.tsx +++ b/app/(app)/dashboard/page.tsx @@ -1,6 +1,6 @@ /** * @file dashboard/page.tsx - * @description Server component rendering the main dashboard page, handling authentication, fetching active user-related tasks and assignees, computing trash counts, and passing data to the kanban board container. + * @description Server component rendering the main dashboard page, handling authentication, fetching active user-related tasks and assignees, computing trash counts, and passing data to the board container. */ import { db } from "@/db"; @@ -8,13 +8,13 @@ import { tasksTable, taskAssigneesTable, usersTable } from "@/db/schema"; import { and, count, eq, inArray, isNotNull, isNull, or } from "drizzle-orm"; import { auth } from "@/auth"; import { redirect } from "next/navigation"; -import KanbanBoard from "./KanbanBoard"; +import Board from "./Board"; import { Task } from "@/types/tasks"; /** * Renders the dashboard page component with user session validation, * database queries for active tasks, team assignees, and soft-deleted trash counts, - * before passing the structured dataset to the Kanban board container. + * before passing the structured dataset to the board container. * * @async * @returns {Promise} The rendered dashboard page component. @@ -95,7 +95,7 @@ export default async function Dashboard() { return (
- +
); } diff --git a/types/tasks.ts b/types/tasks.ts index 5c396b4..fbeb282 100644 --- a/types/tasks.ts +++ b/types/tasks.ts @@ -1,6 +1,6 @@ /** * @file types/tasks.ts - * @description Type definitions, interfaces, and UI configuration mappings for task management and kanban views. + * @description Type definitions, interfaces, and UI configuration mappings for task management and views. */ import { @@ -35,13 +35,13 @@ export interface TaskPriorityConfig { className: string; } -export interface KanbanColumnConfig { +export interface ColumnConfig { id: TaskStatus; title: string; color: string; } -export interface KanbanColumnProps extends KanbanColumnConfig { +export interface ColumnProps extends ColumnConfig { count: number; tasks: Task[]; updatingTaskIds?: Set; @@ -49,7 +49,7 @@ export interface KanbanColumnProps extends KanbanColumnConfig { onTaskDelete?: (taskId: string) => void; } -export interface KanbanCardProps { +export interface CardProps { task: Task; isUpdating?: boolean; onStatusChange?: (taskId: string, newStatus: TaskStatus) => void; @@ -75,9 +75,9 @@ export const PRIORITY_CONFIG = { }, } as const satisfies Record; -export const KANBAN_COLUMNS = [ +export const COLUMNS = [ { id: "todo", title: "To-do", color: "bg-zinc-400" }, { id: "in_progress", title: "In Progress", color: "bg-indigo-500" }, { id: "await_feedback", title: "Await Feedback", color: "bg-amber-500" }, { id: "done", title: "Done", color: "bg-emerald-500" }, -] as const satisfies readonly KanbanColumnConfig[]; +] as const satisfies readonly ColumnConfig[];