refactor(dashboard): modularize header components and organize board structure
This commit is contained in:
parent
48d37c7907
commit
674adacc74
17 changed files with 387 additions and 282 deletions
|
|
@ -1,26 +1,26 @@
|
||||||
/**
|
/**
|
||||||
* @file dashboard/KanbanBoard.tsx
|
* @file dashboard/Board.tsx
|
||||||
* @description Client component wrapping the kanban columns grid, tracking individual task update/deletion states, and handling asynchronous mutations via API.
|
* @description Client component wrapping the columns grid, tracking individual task update/deletion states, and handling asynchronous mutations via API.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import KanbanColumn from "./KanbanColumn";
|
import Column from "./column/Column";
|
||||||
import { KANBAN_COLUMNS, Task, TaskStatus } from "@/types/tasks";
|
import { COLUMNS, Task, TaskStatus } from "@/types/tasks";
|
||||||
import KanbanBoardHeader from "./components/KanbanBoardHeader";
|
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.
|
* for active task updates/deletions, trash counts, and triggering mutation API requests.
|
||||||
*
|
*
|
||||||
* @param {Object} props - The component props.
|
* @param {Object} props - The component props.
|
||||||
* @param {Task[]} props.tasks - The array of task items displayed across the board.
|
* @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.
|
* @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,
|
tasks,
|
||||||
trashCount,
|
trashCount,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -100,14 +100,14 @@ export default function KanbanBoard({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Workspace Header */}
|
{/* Workspace Header */}
|
||||||
<KanbanBoardHeader onTaskDelete={deleteTask} trashCount={trashCount} />
|
<Header onTaskDelete={deleteTask} trashCount={trashCount} />
|
||||||
|
|
||||||
{/* Kanban Columns Grid */}
|
{/* Columns Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 items-start">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 items-start">
|
||||||
{KANBAN_COLUMNS.map((col) => {
|
{COLUMNS.map((col) => {
|
||||||
const columnTasks = tasks.filter((t) => t.status === col.id);
|
const columnTasks = tasks.filter((t) => t.status === col.id);
|
||||||
return (
|
return (
|
||||||
<KanbanColumn
|
<Column
|
||||||
key={col.id}
|
key={col.id}
|
||||||
id={col.id}
|
id={col.id}
|
||||||
title={col.title}
|
title={col.title}
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
/**
|
|
||||||
* @file dashboard/KanbanColumn.tsx
|
|
||||||
* @description Client component rendering a single kanban column container supporting drag-and-drop drop targets, dynamic status updates, and task lists.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Plus } from "lucide-react";
|
|
||||||
import KanbanCard from "./KanbanCard";
|
|
||||||
import { KanbanColumnProps, TaskStatus } from "@/types/tasks";
|
|
||||||
/**
|
|
||||||
* Renders an individual kanban column with an indicator, title, item counter,
|
|
||||||
* add-task button, and drag-and-drop target zone containing its associated task cards.
|
|
||||||
*
|
|
||||||
* @param {KanbanColumnProps} props - The component props containing column configurations, tasks, and event callbacks.
|
|
||||||
* @returns {JSX.Element} The rendered kanban column component.
|
|
||||||
*/
|
|
||||||
export default function KanbanColumn(props: KanbanColumnProps) {
|
|
||||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
|
||||||
const indicatorColor = props.color ?? "bg-primary";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the drag-over event to allow items to be dropped into the column.
|
|
||||||
*
|
|
||||||
* @param {React.DragEvent<HTMLDivElement>} e - The drag event object.
|
|
||||||
*/
|
|
||||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
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<HTMLDivElement>} e - The drop event object.
|
|
||||||
*/
|
|
||||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
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 (
|
|
||||||
<div
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
className={`w-full flex flex-col rounded-2xl p-4 transition-all duration-300 bg-background-muted/40 border ${
|
|
||||||
isDraggingOver
|
|
||||||
? "border-primary/80 bg-primary/5 shadow-lg ring-4 ring-primary/10"
|
|
||||||
: "border-border/60 shadow-sm"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{/* Column Header */}
|
|
||||||
<div className="flex items-center justify-between mb-4 pb-2 border-b border-border/40">
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
<span
|
|
||||||
className={`w-2.5 h-2.5 rounded-full shadow-sm ${indicatorColor}`}
|
|
||||||
/>
|
|
||||||
<h2 className="font-bold text-xs uppercase tracking-wider">
|
|
||||||
{props.title}
|
|
||||||
</h2>
|
|
||||||
<span className="text-xs bg-card text-foreground-muted px-2 py-0.5 rounded-full font-semibold border border-border/60">
|
|
||||||
{props.count}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button className="text-background hover:text-foreground p-1.5 rounded-lg bg-primary hover:bg-primary-hover transition-all duration-200 cursor-pointer shadow-sm active:scale-95">
|
|
||||||
<Plus size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card List */}
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{props.tasks.length === 0 ? (
|
|
||||||
<div className="h-28 rounded-xl border border-dashed border-border/80 flex flex-col items-center justify-center text-xs text-foreground-muted/60 bg-card/20 gap-1">
|
|
||||||
<span>No tasks</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
props.tasks.map((task) => (
|
|
||||||
<KanbanCard
|
|
||||||
key={task.id}
|
|
||||||
task={task}
|
|
||||||
isUpdating={props.updatingTaskIds?.has(task.id)}
|
|
||||||
onStatusChange={props.onTaskMove}
|
|
||||||
onDelete={props.onTaskDelete}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +1,30 @@
|
||||||
/**
|
/**
|
||||||
* @file dashboard/KanbanCard.tsx
|
* @file dashboard/card/Card.tsx
|
||||||
* @description Client component rendering a single kanban card container with individual loading states and modular sub-components.
|
* @description Client component rendering a single card container with individual loading states and modular sub-components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { KanbanCardProps, TaskStatus } from "@/types/tasks";
|
import { CardProps, TaskStatus } from "@/types/tasks";
|
||||||
import KanbanCardActions from "./components/KanbanCardActions";
|
import CardActions from "./CardActions";
|
||||||
import KanbanCardAvatars from "./components/KanbanCardAvatars";
|
import CardAvatars from "./CardAvatars";
|
||||||
import KanbanCardDueDate from "./components/KanbanCardDueDate";
|
import CardDueDate from "./CardDueDate";
|
||||||
import KanbanCardPriority from "./components/KanbanCardPriority";
|
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.
|
* 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.
|
* @param {CardProps} props - The component props containing the task object, updating status flag, and status change handler.
|
||||||
* @returns {JSX.Element} The rendered kanban card component.
|
* @returns {JSX.Element} The rendered card component.
|
||||||
*/
|
*/
|
||||||
export default function KanbanCard({
|
export default function Card({
|
||||||
task,
|
task,
|
||||||
isUpdating = false,
|
isUpdating = false,
|
||||||
onStatusChange,
|
onStatusChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: KanbanCardProps) {
|
}: CardProps) {
|
||||||
/**
|
/**
|
||||||
* Initiates the drag action on a task card if not currently updating, storing its ID and status payload.
|
* 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({
|
||||||
<h3 className="font-semibold leading-snug group-hover/card:text-primary transition-colors line-clamp-2">
|
<h3 className="font-semibold leading-snug group-hover/card:text-primary transition-colors line-clamp-2">
|
||||||
{task.title}
|
{task.title}
|
||||||
</h3>
|
</h3>
|
||||||
<KanbanCardPriority priority={task.priority} />
|
<CardPriority priority={task.priority} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card Description */}
|
{/* Card Description */}
|
||||||
|
|
@ -82,14 +82,11 @@ export default function KanbanCard({
|
||||||
|
|
||||||
{/* --- Card Footer --- */}
|
{/* --- Card Footer --- */}
|
||||||
<div className="flex items-center justify-between pt-3 mt-auto text-xs border-t border-border">
|
<div className="flex items-center justify-between pt-3 mt-auto text-xs border-t border-border">
|
||||||
<KanbanCardDueDate task={task} />
|
<CardDueDate task={task} />
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<KanbanCardAvatars
|
<CardAvatars creator={task.creator} assignees={task.assignees} />
|
||||||
creator={task.creator}
|
<CardActions
|
||||||
assignees={task.assignees}
|
|
||||||
/>
|
|
||||||
<KanbanCardActions
|
|
||||||
currentStatus={task.status}
|
currentStatus={task.status}
|
||||||
onMove={handleMove}
|
onMove={handleMove}
|
||||||
onDelete={() => onDelete?.(task.id)}
|
onDelete={() => onDelete?.(task.id)}
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
/**
|
/**
|
||||||
* @file dashboard/components/KanbanCardActions.tsx
|
* @file dashboard/card/CardActions.tsx
|
||||||
* @description Client component rendering the mobile status transition and deletion dropdown for a kanban card.
|
* @description Client component rendering the mobile status transition and deletion dropdown for a card.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { MoreHorizontal, CornerDownRight, Trash2 } from "lucide-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
|
* 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 {Object} props - The component props.
|
||||||
* @param {TaskStatus} props.currentStatus - The current status category of the task.
|
* @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.
|
* @param {() => void} props.onDelete - Callback function triggered when the delete action is selected.
|
||||||
* @returns {JSX.Element} The rendered mobile card actions component.
|
* @returns {JSX.Element} The rendered mobile card actions component.
|
||||||
*/
|
*/
|
||||||
export default function KanbanCardActions({
|
export default function CardActions({
|
||||||
currentStatus,
|
currentStatus,
|
||||||
onMove,
|
onMove,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
|
@ -94,7 +94,7 @@ export default function KanbanCardActions({
|
||||||
: "opacity-0 scale-10 pointer-events-none"
|
: "opacity-0 scale-10 pointer-events-none"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{KANBAN_COLUMNS.map((col) => {
|
{COLUMNS.map((col) => {
|
||||||
if (col.id === currentStatus) return null;
|
if (col.id === currentStatus) return null;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* @file dashboard/components/KanbanCardAvatars.tsx
|
* @file dashboard/card/CardAvatars.tsx
|
||||||
* @description Client component rendering team avatars and creator badge for a kanban card.
|
* @description Client component rendering team avatars and creator badge for a card.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
@ -17,7 +17,7 @@ import { Task } from "@/types/tasks";
|
||||||
* @param {Task["assignees"]} [props.assignees=[]] - An array of emails or identifiers for users assigned to the task.
|
* @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.
|
* @returns {JSX.Element | null} The rendered avatars container, or null if neither creator nor assignees exist.
|
||||||
*/
|
*/
|
||||||
export default function KanbanCardAvatars({
|
export default function CardAvatars({
|
||||||
creator,
|
creator,
|
||||||
assignees = [],
|
assignees = [],
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* @file dashboard/components/KanbanCardDueDate.tsx
|
* @file dashboard/card/CardDueDate.tsx
|
||||||
* @description Client component rendering the due date badge with overdue status indicators and hover time display.
|
* @description Client component rendering the due date badge with overdue status indicators and hover time display.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -7,14 +7,14 @@ import { AlertCircle, CalendarDays } from "lucide-react";
|
||||||
import { Task } from "@/types/tasks";
|
import { Task } from "@/types/tasks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a due date badge for a kanban card, showing an overdue alert animation
|
* Renders a due date badge for a card, showing an overdue alert animation
|
||||||
* if the deadline has passed and the task is not completed, alongside a hoverable time display.
|
* if the deadline has passed and the task is not completed, alongside a hoverable time display.
|
||||||
*
|
*
|
||||||
* @param {Object} props - The component props.
|
* @param {Object} props - The component props.
|
||||||
* @param {Task} props.task - The task object containing the due date and status information.
|
* @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.
|
* @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 }) {
|
export default function CardDueDate({ task }: { task: Task }) {
|
||||||
if (!task.dueDate) return null;
|
if (!task.dueDate) return null;
|
||||||
|
|
||||||
const isOverdue =
|
const isOverdue =
|
||||||
|
|
@ -1,22 +1,18 @@
|
||||||
/**
|
/**
|
||||||
* @file dashboard/components/KanbanCardPriority.tsx
|
* @file dashboard/card/CardPriority.tsx
|
||||||
* @description Client component rendering the dynamic priority badge for a kanban card based on configuration.
|
* @description Client component rendering the dynamic priority badge for a card based on configuration.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TaskPriority, PRIORITY_CONFIG } from "@/types/tasks";
|
import { TaskPriority, PRIORITY_CONFIG } from "@/types/tasks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a styled priority badge for a kanban card.
|
* Renders a styled priority badge for a card.
|
||||||
*
|
*
|
||||||
* @param {Object} props - The component props.
|
* @param {Object} props - The component props.
|
||||||
* @param {TaskPriority} props.priority - The priority level of the task.
|
* @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.
|
* @returns {JSX.Element | null} The rendered priority badge component, or null if priority is invalid.
|
||||||
*/
|
*/
|
||||||
export default function KanbanCardPriority({
|
export default function CardPriority({ priority }: { priority: TaskPriority }) {
|
||||||
priority,
|
|
||||||
}: {
|
|
||||||
priority: TaskPriority;
|
|
||||||
}) {
|
|
||||||
if (!priority || !PRIORITY_CONFIG[priority]) return null;
|
if (!priority || !PRIORITY_CONFIG[priority]) return null;
|
||||||
|
|
||||||
const { label, className } = PRIORITY_CONFIG[priority];
|
const { label, className } = PRIORITY_CONFIG[priority];
|
||||||
93
app/(app)/dashboard/column/Column.tsx
Normal file
93
app/(app)/dashboard/column/Column.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
/**
|
||||||
|
* @file dashboard/column/Column.tsx
|
||||||
|
* @description Client component rendering a single column container supporting drag-and-drop drop targets, task lists, and dynamic updating/deleting states.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Card from "../card/Card";
|
||||||
|
import ColumnHeader from "./ColumnHeader";
|
||||||
|
import ColumnEmptyState from "./ColumnEmptyState";
|
||||||
|
import { ColumnProps, TaskStatus } from "@/types/tasks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an interactive board column supporting drag-over drop target indicators,
|
||||||
|
* header info, and a mapped list of task cards with status change and deletion callbacks.
|
||||||
|
*
|
||||||
|
* @param {ColumnProps} props - The component props containing column metadata, task arrays, and handlers.
|
||||||
|
* @returns {JSX.Element} The rendered column component.
|
||||||
|
*/
|
||||||
|
export default function Column(props: ColumnProps) {
|
||||||
|
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the drag-over event to allow dropping tasks into the column.
|
||||||
|
*
|
||||||
|
* @param {React.DragEvent<HTMLDivElement>} e - The drag event object.
|
||||||
|
*/
|
||||||
|
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = "move";
|
||||||
|
setIsDraggingOver(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the drag-over highlight state when a dragged element leaves the column area.
|
||||||
|
*/
|
||||||
|
const handleDragLeave = () => {
|
||||||
|
setIsDraggingOver(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles dropping a task card onto the column, extracting task data and triggering the move handler.
|
||||||
|
*
|
||||||
|
* @param {React.DragEvent<HTMLDivElement>} e - The drop event object.
|
||||||
|
*/
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
className={`w-full flex flex-col rounded-2xl p-4 transition-all duration-300 bg-background-muted/40 border ${
|
||||||
|
isDraggingOver
|
||||||
|
? "border-primary/80 bg-primary/5 shadow-lg ring-4 ring-primary/10"
|
||||||
|
: "border-border/60 shadow-sm"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ColumnHeader
|
||||||
|
title={props.title}
|
||||||
|
color={props.color}
|
||||||
|
count={props.count}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{props.tasks.length === 0 ? (
|
||||||
|
<ColumnEmptyState />
|
||||||
|
) : (
|
||||||
|
props.tasks.map((task) => (
|
||||||
|
<Card
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
isUpdating={props.updatingTaskIds?.has(task.id)}
|
||||||
|
onStatusChange={props.onTaskMove}
|
||||||
|
onDelete={props.onTaskDelete}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
app/(app)/dashboard/column/ColumnEmptyState.tsx
Normal file
19
app/(app)/dashboard/column/ColumnEmptyState.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* @file dashboard/column/ColumnEmptyState.tsx
|
||||||
|
* @description Client component rendering a placeholder card when a specific column contains no tasks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a dashed empty state container with placeholder text for empty columns.
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} The rendered column empty state component.
|
||||||
|
*/
|
||||||
|
export default function ColumnEmptyState() {
|
||||||
|
return (
|
||||||
|
<div className="h-28 rounded-xl border border-dashed border-border/80 flex flex-col items-center justify-center text-xs text-foreground-muted/60 bg-card/20 gap-1">
|
||||||
|
<span>No tasks</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
app/(app)/dashboard/column/ColumnHeader.tsx
Normal file
43
app/(app)/dashboard/column/ColumnHeader.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* @file dashboard/column/ColumnHeader.tsx
|
||||||
|
* @description Component rendering the column header with title, color indicator, item count, and add button.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the header section of a column, displaying a color-coded status indicator,
|
||||||
|
* the column name, the total task count badge, and a button to add new tasks.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The component props.
|
||||||
|
* @param {string} props.title - The title of the column.
|
||||||
|
* @param {string} [props.color] - Tailwind CSS color class for the status indicator dot.
|
||||||
|
* @param {number} props.count - The number of tasks currently inside this column.
|
||||||
|
* @returns {JSX.Element} The rendered column header component.
|
||||||
|
*/
|
||||||
|
export default function ColumnHeader({
|
||||||
|
title,
|
||||||
|
color = "bg-primary",
|
||||||
|
count,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
color?: string;
|
||||||
|
count: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between mb-4 pb-2 border-b border-border/40">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<span className={`w-2.5 h-2.5 rounded-full shadow-sm ${color}`} />
|
||||||
|
<h2 className="font-bold text-xs uppercase tracking-wider">{title}</h2>
|
||||||
|
<span className="text-xs bg-card text-foreground-muted px-2 py-0.5 rounded-full font-semibold border border-border/60">
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button className="text-background hover:text-foreground p-1.5 rounded-lg bg-primary hover:bg-primary-hover transition-all duration-200 cursor-pointer shadow-sm active:scale-95">
|
||||||
|
<Plus size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 text-primary font-medium text-xs uppercase tracking-wider mb-1">
|
|
||||||
<Sparkles size={14} />
|
|
||||||
Workspace
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-extrabold tracking-tight">Dashboard</h1>
|
|
||||||
<p className="text-sm text-foreground-muted mt-1">
|
|
||||||
Manage your tasks and keep track of your progress.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{isDragging ? (
|
|
||||||
<div
|
|
||||||
onDragOver={(e) => {
|
|
||||||
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"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Trash2 size={16} className={isOver ? "animate-bounce" : ""} />
|
|
||||||
<span>Drop to Delete</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button className="inline-flex items-center gap-2 px-3.5 py-2.5 rounded-xl font-medium text-sm text-black hover:text-foreground bg-primary hover:bg-primary-hover active:scale-95 transition-colors duration-200 cursor-pointer">
|
|
||||||
<Plus size={16} />
|
|
||||||
New Task
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<Link
|
|
||||||
href="/trash"
|
|
||||||
className="relative group p-2 rounded-xl border border-border hover:border-primary transition-all duration-200"
|
|
||||||
title="View Trash"
|
|
||||||
>
|
|
||||||
{/* Icon */}
|
|
||||||
<Trash2
|
|
||||||
size={16}
|
|
||||||
className="text-foreground-muted group-hover:text-primary transition-colors"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Badge */}
|
|
||||||
{trashCount > 0 && (
|
|
||||||
<span className="absolute -top-1.5 -right-1.5 flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-destructive text-[9px] font-bold text-white shadow-sm ring-1 ring-background">
|
|
||||||
{trashCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
71
app/(app)/dashboard/header/DeleteDropZone.tsx
Normal file
71
app/(app)/dashboard/header/DeleteDropZone.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div
|
||||||
|
onDragOver={(e) => {
|
||||||
|
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"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Trash2 size={16} className={isOver ? "animate-bounce" : ""} />
|
||||||
|
<span>Drop to Delete</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
app/(app)/dashboard/header/Header.tsx
Normal file
49
app/(app)/dashboard/header/Header.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 text-primary font-medium text-xs uppercase tracking-wider mb-1">
|
||||||
|
<Sparkles size={14} />
|
||||||
|
Workspace
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-extrabold tracking-tight">Dashboard</h1>
|
||||||
|
<p className="text-sm text-foreground-muted mt-1">
|
||||||
|
Manage your tasks and keep track of your progress.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<DeleteDropZone onTaskDelete={onTaskDelete} />
|
||||||
|
<div className="flex items-center gap-3 peer-not-empty:hidden">
|
||||||
|
<NewTaskButton />
|
||||||
|
<TrashLink count={trashCount} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
app/(app)/dashboard/header/NewTaskButton.tsx
Normal file
22
app/(app)/dashboard/header/NewTaskButton.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<button className="inline-flex items-center gap-2 px-3.5 py-2.5 rounded-xl font-medium text-sm text-black hover:text-foreground bg-primary hover:bg-primary-hover active:scale-95 transition-colors duration-200 cursor-pointer">
|
||||||
|
<Plus size={16} />
|
||||||
|
New Task
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
app/(app)/dashboard/header/TrashLink.tsx
Normal file
36
app/(app)/dashboard/header/TrashLink.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<Link
|
||||||
|
href="/trash"
|
||||||
|
className="relative group p-2 rounded-xl border border-border hover:border-primary transition-all duration-200"
|
||||||
|
title="View Trash"
|
||||||
|
>
|
||||||
|
<Trash2
|
||||||
|
size={16}
|
||||||
|
className="text-foreground-muted group-hover:text-primary transition-colors"
|
||||||
|
/>
|
||||||
|
{count > 0 && (
|
||||||
|
<span className="absolute -top-1.5 -right-1.5 flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-destructive text-[9px] font-bold text-white shadow-sm ring-1 ring-background">
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* @file dashboard/page.tsx
|
* @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";
|
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 { and, count, eq, inArray, isNotNull, isNull, or } from "drizzle-orm";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import KanbanBoard from "./KanbanBoard";
|
import Board from "./Board";
|
||||||
import { Task } from "@/types/tasks";
|
import { Task } from "@/types/tasks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the dashboard page component with user session validation,
|
* Renders the dashboard page component with user session validation,
|
||||||
* database queries for active tasks, team assignees, and soft-deleted trash counts,
|
* 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
|
* @async
|
||||||
* @returns {Promise<JSX.Element>} The rendered dashboard page component.
|
* @returns {Promise<JSX.Element>} The rendered dashboard page component.
|
||||||
|
|
@ -95,7 +95,7 @@ export default async function Dashboard() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 max-w-7xl mx-auto pb-12">
|
<div className="space-y-8 max-w-7xl mx-auto pb-12">
|
||||||
<KanbanBoard tasks={tasks} trashCount={trashCount} />
|
<Board tasks={tasks} trashCount={trashCount} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* @file types/tasks.ts
|
* @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 {
|
import {
|
||||||
|
|
@ -35,13 +35,13 @@ export interface TaskPriorityConfig {
|
||||||
className: string;
|
className: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KanbanColumnConfig {
|
export interface ColumnConfig {
|
||||||
id: TaskStatus;
|
id: TaskStatus;
|
||||||
title: string;
|
title: string;
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KanbanColumnProps extends KanbanColumnConfig {
|
export interface ColumnProps extends ColumnConfig {
|
||||||
count: number;
|
count: number;
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
updatingTaskIds?: Set<string>;
|
updatingTaskIds?: Set<string>;
|
||||||
|
|
@ -49,7 +49,7 @@ export interface KanbanColumnProps extends KanbanColumnConfig {
|
||||||
onTaskDelete?: (taskId: string) => void;
|
onTaskDelete?: (taskId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KanbanCardProps {
|
export interface CardProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
isUpdating?: boolean;
|
isUpdating?: boolean;
|
||||||
onStatusChange?: (taskId: string, newStatus: TaskStatus) => void;
|
onStatusChange?: (taskId: string, newStatus: TaskStatus) => void;
|
||||||
|
|
@ -75,9 +75,9 @@ export const PRIORITY_CONFIG = {
|
||||||
},
|
},
|
||||||
} as const satisfies Record<TaskPriority, TaskPriorityConfig>;
|
} as const satisfies Record<TaskPriority, TaskPriorityConfig>;
|
||||||
|
|
||||||
export const KANBAN_COLUMNS = [
|
export const COLUMNS = [
|
||||||
{ id: "todo", title: "To-do", color: "bg-zinc-400" },
|
{ id: "todo", title: "To-do", color: "bg-zinc-400" },
|
||||||
{ id: "in_progress", title: "In Progress", color: "bg-indigo-500" },
|
{ id: "in_progress", title: "In Progress", color: "bg-indigo-500" },
|
||||||
{ id: "await_feedback", title: "Await Feedback", color: "bg-amber-500" },
|
{ id: "await_feedback", title: "Await Feedback", color: "bg-amber-500" },
|
||||||
{ id: "done", title: "Done", color: "bg-emerald-500" },
|
{ id: "done", title: "Done", color: "bg-emerald-500" },
|
||||||
] as const satisfies readonly KanbanColumnConfig[];
|
] as const satisfies readonly ColumnConfig[];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue