"use client"; import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import type { ReactElement } from "react"; import { useSearchParams, useRouter } from "next/navigation"; 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, createTask, type TaskFilters } from "@/lib/api/tasks"; import { fetchProjects, type Project } from "@/lib/api/projects"; 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)" }, ]; const PRIORITY_OPTIONS: { value: string; label: string }[] = [ { value: "", label: "All Priorities" }, { value: TaskPriority.HIGH, label: "High" }, { value: TaskPriority.MEDIUM, label: "Medium" }, { value: TaskPriority.LOW, label: "Low" }, ]; /* --------------------------------------------------------------------------- Filter select shared styles --------------------------------------------------------------------------- */ const selectStyle: React.CSSProperties = { padding: "6px 10px", borderRadius: "var(--r)", border: "1px solid var(--border)", background: "var(--surface)", color: "var(--text)", fontSize: "0.83rem", outline: "none", minWidth: 130, }; const inputStyle: React.CSSProperties = { ...selectStyle, minWidth: 180, }; /* --------------------------------------------------------------------------- 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[]; onAddTask: (status: TaskStatus, title: string) => Promise; } function KanbanColumn({ config, tasks, onAddTask }: KanbanColumnProps): ReactElement { const [showAddForm, setShowAddForm] = useState(false); const [inputValue, setInputValue] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const inputRef = useRef(null); // Focus input when form is shown useEffect(() => { if (showAddForm && inputRef.current) { inputRef.current.focus(); } }, [showAddForm]); const handleSubmit = async (e: React.SyntheticEvent): Promise => { e.preventDefault(); if (!inputValue.trim() || isSubmitting) { return; } setIsSubmitting(true); try { await onAddTask(config.status, inputValue.trim()); setInputValue(""); setShowAddForm(false); } catch (err) { console.error("[KanbanColumn] Failed to add task:", err); } finally { setIsSubmitting(false); } }; const handleKeyDown = (e: React.KeyboardEvent): void => { if (e.key === "Escape") { setShowAddForm(false); setInputValue(""); } }; return (
{/* Column header */}
{config.label} {tasks.length}
{/* Droppable area */} {(provided: DroppableProvided) => (
{tasks.map((task, index) => ( {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( )} ))} {provided.placeholder}
)}
{/* Add Task Form */} {!showAddForm ? ( ) : (
{ setInputValue(e.target.value); }} onKeyDown={handleKeyDown} placeholder="Task title..." disabled={isSubmitting} style={{ width: "100%", padding: "8px 10px", borderRadius: "var(--r)", border: `1px solid ${inputValue ? "var(--primary)" : "var(--border)"}`, background: "var(--surface)", color: "var(--text)", fontSize: "0.85rem", outline: "none", opacity: isSubmitting ? 0.6 : 1, }} autoFocus />
Press{" "} Enter {" "} to save,{" "} Escape {" "} to cancel
)}
); } /* --------------------------------------------------------------------------- Filter Bar --------------------------------------------------------------------------- */ interface FilterBarProps { projects: Project[]; projectId: string; priority: string; search: string; myTasks: boolean; onProjectChange: (value: string) => void; onPriorityChange: (value: string) => void; onSearchChange: (value: string) => void; onMyTasksToggle: () => void; onClear: () => void; hasActiveFilters: boolean; } function FilterBar({ projects, projectId, priority, search, myTasks, onProjectChange, onPriorityChange, onSearchChange, onMyTasksToggle, onClear, hasActiveFilters, }: FilterBarProps): ReactElement { return (
{/* Search */} { onSearchChange(e.target.value); }} style={inputStyle} /> {/* Project filter */} {/* Priority filter */} {/* My Tasks toggle */} {/* Clear filters */} {hasActiveFilters && ( )}
); } /* --------------------------------------------------------------------------- Kanban Board Page --------------------------------------------------------------------------- */ export default function KanbanPage(): ReactElement { const workspaceId = useWorkspaceId(); const router = useRouter(); const searchParams = useSearchParams(); const [tasks, setTasks] = useState([]); const [projects, setProjects] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // Read filters from URL params const filterProject = searchParams.get("project") ?? ""; const filterPriority = searchParams.get("priority") ?? ""; const filterSearch = searchParams.get("q") ?? ""; const filterMyTasks = searchParams.get("my") === "1"; const hasActiveFilters = filterProject !== "" || filterPriority !== "" || filterSearch !== "" || filterMyTasks; /** Update a single URL param (preserving others) */ const setParam = useCallback( (key: string, value: string) => { const params = new URLSearchParams(searchParams.toString()); if (value) { params.set(key, value); } else { params.delete(key); } router.replace(`/kanban?${params.toString()}`, { scroll: false }); }, [searchParams, router] ); const handleProjectChange = useCallback( (value: string) => { setParam("project", value); }, [setParam] ); const handlePriorityChange = useCallback( (value: string) => { setParam("priority", value); }, [setParam] ); const handleSearchChange = useCallback( (value: string) => { setParam("q", value); }, [setParam] ); const handleMyTasksToggle = useCallback(() => { setParam("my", filterMyTasks ? "" : "1"); }, [setParam, filterMyTasks]); const handleClearFilters = useCallback(() => { router.replace("/kanban", { scroll: false }); }, [router]); /* --- 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; } const ac = new AbortController(); async function load(): Promise { try { setIsLoading(true); setError(null); const filters: TaskFilters = {}; if (workspaceId) filters.workspaceId = workspaceId; const [taskData, projectData] = await Promise.all([ fetchTasks(filters), fetchProjects(workspaceId ?? undefined), ]); if (ac.signal.aborted) return; setTasks(taskData); setProjects(projectData); } catch (err: unknown) { console.error("[Kanban] Failed to fetch tasks:", err); if (ac.signal.aborted) return; setError( err instanceof Error ? err.message : "Something went wrong loading tasks. You could try again when ready." ); } finally { if (!ac.signal.aborted) { setIsLoading(false); } } } void load(); return (): void => { ac.abort(); }; }, [workspaceId]); /* --- apply client-side filters --- */ const filteredTasks = useMemo(() => { let result = tasks; if (filterProject) { result = result.filter((t) => t.projectId === filterProject); } if (filterPriority) { result = result.filter((t) => t.priority === (filterPriority as TaskPriority)); } if (filterSearch) { const q = filterSearch.toLowerCase(); result = result.filter( (t) => t.title.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q) ); } if (filterMyTasks) { // "My Tasks" filters to tasks assigned to the current user. // Since we don't have the current userId readily available, // filter by assigneeId being non-null (assigned tasks). // A proper implementation would compare against the logged-in user's ID. result = result.filter((t) => t.assigneeId !== null); } return result; }, [tasks, filterProject, filterPriority, filterSearch, filterMyTasks]); /* --- 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(filteredTasks); /* --- 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); } /* --- add task handler --- */ const handleAddTask = useCallback( async (status: TaskStatus, title: string) => { try { const wsId = workspaceId ?? undefined; const newTask = await createTask({ title, status }, wsId); // Optimistically add to local state setTasks((prev) => [...prev, newTask]); } catch (err: unknown) { console.error("[Kanban] Failed to create task:", err); // Re-fetch on error to get consistent state void loadTasks(workspaceId); } }, [workspaceId, loadTasks] ); /* --- render --- */ return (
{/* Page header */}

Kanban Board

Visualize and manage task progress across stages

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

{error}

) : filteredTasks.length === 0 && tasks.length > 0 ? ( /* No results (filtered) */

No tasks match your filters.

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

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

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