"use client" import { useState } from "react" import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, PointerSensor, useSensor, useSensors, closestCorners, } from "@dnd-kit/core" import { SortableContext, verticalListSortingStrategy, useSortable, } from "@dnd-kit/sortable" import { CSS } from "@dnd-kit/utilities" import { useTaskStore, Task, Sprint } from "@/stores/useTaskStore" import { Card, CardContent } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Plus, Calendar, Flag, GripVertical } from "lucide-react" import { format, isValid } from "date-fns" import { inferSprintStatusForDateRange, parseSprintEnd, parseSprintStart, toLocalDateInputValue, } from "@/lib/utils" const statusColumns = ["backlog", "in-progress", "review", "done"] as const type SprintColumnStatus = typeof statusColumns[number] const statusLabels: Record = { backlog: "To Do", "in-progress": "In Progress", review: "Review", done: "Done", } const statusColors: Record = { backlog: "bg-slate-700", "in-progress": "bg-blue-600", review: "bg-yellow-600", done: "bg-green-600", } const priorityColors: Record = { low: "bg-slate-600", medium: "bg-blue-600", high: "bg-orange-600", urgent: "bg-red-600", } function formatSprintDateRange(startDate?: string, endDate?: string): string { if (!startDate || !endDate) return "No dates" const start = parseSprintStart(startDate) const end = parseSprintEnd(endDate) if (!isValid(start) || !isValid(end)) return "Invalid dates" return `${format(start, "MMM d")} - ${format(end, "MMM d, yyyy")}` } // Sortable Task Card Component function SortableTaskCard({ task, onClick, }: { task: Task onClick: () => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: task.id }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, } return (

{task.title}

{task.priority} {task.type} {task.comments && task.comments.length > 0 && ( 💬 {task.comments.length} )}
) } // Drag Overlay Task Card (shown while dragging) function DragOverlayTaskCard({ task }: { task: Task }) { return (

{task.title}

{task.priority} {task.type}
) } export function SprintBoard() { const { tasks, sprints, selectedSprintId, selectSprint, updateTask, selectTask, addSprint, } = useTaskStore() const [isCreatingSprint, setIsCreatingSprint] = useState(false) const [newSprint, setNewSprint] = useState({ name: "", goal: "", startDate: "", endDate: "", }) const [activeId, setActiveId] = useState(null) // Sensors for drag detection const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }) ) const projectSprints = sprints // Get current sprint const currentSprint = sprints.find((s) => s.id === selectedSprintId) // Get tasks for current sprint, sorted by updatedAt descending (latest first) const sprintTasks = tasks .filter((t) => t.sprintId === selectedSprintId) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) // Group tasks by status const tasksByStatus = statusColumns.reduce((acc, status) => { acc[status] = sprintTasks.filter((t) => t.status === status) return acc }, {} as Record) // Get active task for drag overlay const activeTask = activeId ? tasks.find((t) => t.id === activeId) : null const handleCreateSprint = () => { if (!newSprint.name) return const startDate = newSprint.startDate || toLocalDateInputValue() const endDate = newSprint.endDate || toLocalDateInputValue() const sprint: Omit = { name: newSprint.name, goal: newSprint.goal, startDate, endDate, status: inferSprintStatusForDateRange(startDate, endDate), } addSprint(sprint) setIsCreatingSprint(false) setNewSprint({ name: "", goal: "", startDate: "", endDate: "" }) } const handleDragStart = (event: DragStartEvent) => { setActiveId(event.active.id as string) } const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event setActiveId(null) if (!over) return const taskId = active.id as string const overId = over.id as string // Check if dropped over a column if (statusColumns.includes(overId as SprintColumnStatus)) { updateTask(taskId, { status: overId as Task["status"] }) return } // Check if dropped over another task const overTask = tasks.find((t) => t.id === overId) if (overTask && overTask.status !== tasks.find((t) => t.id === taskId)?.status) { updateTask(taskId, { status: overTask.status }) } } if (projectSprints.length === 0 && !isCreatingSprint) { return (

No Sprints Yet

Create your first sprint to start organizing work

) } if (isCreatingSprint) { return (

Create New Sprint

setNewSprint({ ...newSprint, name: e.target.value })} placeholder="e.g., Sprint 1 - Foundation" className="w-full mt-1 bg-slate-800 border border-slate-700 rounded px-3 py-2 text-slate-100" />