"use client"; import { useState, useEffect, useCallback } from "react"; import type { ReactElement } from "react"; import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; import type { DropResult, DroppableProvided, DraggableProvided, DraggableStateSnapshot, } from "@hello-pangea/dnd"; import { MosaicSpinner } from "@/components/ui/MosaicSpinner"; import { fetchTasks, updateTask } from "@/lib/api/tasks"; import { useWorkspaceId } from "@/lib/hooks"; import type { Task } from "@mosaic/shared"; import { TaskStatus, TaskPriority } from "@mosaic/shared"; /* --------------------------------------------------------------------------- Column configuration --------------------------------------------------------------------------- */ interface ColumnConfig { status: TaskStatus; label: string; accent: string; } const COLUMNS: ColumnConfig[] = [ { status: TaskStatus.NOT_STARTED, label: "To Do", accent: "var(--ms-blue-400)" }, { status: TaskStatus.IN_PROGRESS, label: "In Progress", accent: "var(--ms-amber-400)" }, { status: TaskStatus.PAUSED, label: "Paused", accent: "var(--ms-purple-400)" }, { status: TaskStatus.COMPLETED, label: "Done", accent: "var(--ms-teal-400)" }, { status: TaskStatus.ARCHIVED, label: "Archived", accent: "var(--muted)" }, ]; /* --------------------------------------------------------------------------- Priority badge helper --------------------------------------------------------------------------- */ interface PriorityStyle { label: string; bg: string; color: string; } function getPriorityStyle(priority: TaskPriority): PriorityStyle { switch (priority) { case TaskPriority.HIGH: return { label: "High", bg: "rgba(229,72,77,0.15)", color: "var(--danger)" }; case TaskPriority.MEDIUM: return { label: "Medium", bg: "rgba(245,158,11,0.15)", color: "var(--warn)" }; case TaskPriority.LOW: return { label: "Low", bg: "rgba(143,157,183,0.15)", color: "var(--muted)" }; default: return { label: String(priority), bg: "rgba(143,157,183,0.15)", color: "var(--muted)" }; } } /* --------------------------------------------------------------------------- Task Card --------------------------------------------------------------------------- */ interface TaskCardProps { task: Task; provided: DraggableProvided; snapshot: DraggableStateSnapshot; columnAccent: string; } function TaskCard({ task, provided, snapshot, columnAccent }: TaskCardProps): ReactElement { const [hovered, setHovered] = useState(false); const priorityStyle = getPriorityStyle(task.priority); return (
{ setHovered(true); }} onMouseLeave={() => { setHovered(false); }} style={{ background: "var(--surface)", border: `1px solid ${hovered || snapshot.isDragging ? columnAccent : "var(--border)"}`, borderRadius: "var(--r)", padding: 12, marginBottom: 8, cursor: "grab", transition: "border-color 0.15s, box-shadow 0.15s", boxShadow: snapshot.isDragging ? "var(--shadow-lg)" : "none", ...provided.draggableProps.style, }} > {/* Title */}
{task.title}
{/* Priority badge */} {priorityStyle.label} {/* Description */} {task.description && (

{task.description}

)}
); } /* --------------------------------------------------------------------------- Kanban Column --------------------------------------------------------------------------- */ interface KanbanColumnProps { config: ColumnConfig; tasks: Task[]; } function KanbanColumn({ config, tasks }: KanbanColumnProps): ReactElement { return (
{/* Column header */}
{config.label} {tasks.length}
{/* Droppable area */} {(provided: DroppableProvided) => (
{tasks.map((task, index) => ( {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( )} ))} {provided.placeholder}
)}
); } /* --------------------------------------------------------------------------- Kanban Board Page --------------------------------------------------------------------------- */ export default function KanbanPage(): ReactElement { const workspaceId = useWorkspaceId(); const [tasks, setTasks] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); /* --- data fetching --- */ const loadTasks = useCallback(async (wsId: string | null): Promise => { try { setIsLoading(true); setError(null); const filters = wsId !== null ? { workspaceId: wsId } : {}; const data = await fetchTasks(filters); setTasks(data); } catch (err: unknown) { console.error("[Kanban] Failed to fetch tasks:", err); setError( err instanceof Error ? err.message : "Something went wrong loading tasks. You could try again when ready." ); } finally { setIsLoading(false); } }, []); useEffect(() => { if (!workspaceId) { setIsLoading(false); return; } let cancelled = false; async function load(): Promise { try { setIsLoading(true); setError(null); const filters = workspaceId !== null ? { workspaceId } : {}; const data = await fetchTasks(filters); if (!cancelled) { setTasks(data); } } catch (err: unknown) { console.error("[Kanban] Failed to fetch tasks:", err); if (!cancelled) { setError( err instanceof Error ? err.message : "Something went wrong loading tasks. You could try again when ready." ); } } finally { if (!cancelled) { setIsLoading(false); } } } void load(); return (): void => { cancelled = true; }; }, [workspaceId]); /* --- group tasks by status --- */ function groupByStatus(allTasks: Task[]): Record { const grouped: Record = { [TaskStatus.NOT_STARTED]: [], [TaskStatus.IN_PROGRESS]: [], [TaskStatus.PAUSED]: [], [TaskStatus.COMPLETED]: [], [TaskStatus.ARCHIVED]: [], }; for (const task of allTasks) { grouped[task.status].push(task); } return grouped; } const grouped = groupByStatus(tasks); /* --- drag-and-drop handler --- */ const handleDragEnd = useCallback( (result: DropResult) => { const { source, destination, draggableId } = result; // Dropped outside a droppable area if (!destination) return; // Dropped in same position if (source.droppableId === destination.droppableId && source.index === destination.index) { return; } const newStatus = destination.droppableId as TaskStatus; const taskId = draggableId; // Optimistic update: move card in local state setTasks((prev) => prev.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t))); // Persist to API const wsId = workspaceId ?? undefined; updateTask(taskId, { status: newStatus }, wsId).catch((err: unknown) => { console.error("[Kanban] Failed to update task status:", err); // Revert on failure by re-fetching void loadTasks(workspaceId); }); }, [workspaceId, loadTasks] ); /* --- retry handler --- */ function handleRetry(): void { void loadTasks(workspaceId); } /* --- render --- */ return (
{/* Page header */}

Kanban Board

Visualize and manage task progress across stages

{/* Loading state */} {isLoading ? (
) : error !== null ? ( /* Error state */

{error}

) : tasks.length === 0 ? ( /* Empty state */

No tasks yet. Create some tasks to see them here.

) : ( /* Board */
{COLUMNS.map((col) => ( ))}
)}
); }