diff --git a/package-lock.json b/package-lock.json
index 113346d..1e2dd8c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,9 @@
"version": "0.1.0",
"license": "ISC",
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
@@ -296,6 +299,59 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@dnd-kit/accessibility": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
+ "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/core": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
+ "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@dnd-kit/accessibility": "^3.1.1",
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/sortable": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
+ "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@dnd-kit/core": "^6.3.0",
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/utilities": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
+ "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
diff --git a/package.json b/package.json
index 3b6983e..36d8a40 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,9 @@
"lint": "eslint"
},
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
diff --git a/src/app/page.tsx b/src/app/page.tsx
index bcac201..310aaec 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -40,8 +40,10 @@ export default function Home() {
const {
projects,
tasks,
+ sprints,
selectedProjectId,
selectedTaskId,
+ selectedSprintId,
selectProject,
addProject,
deleteProject,
@@ -416,7 +418,7 @@ export default function Home() {
rows={3}
/>
-
+
+
+
+
+
+
@@ -563,6 +580,21 @@ export default function Home() {
+ {/* Sprint */}
+
+
+
+
+
{/* Comments Section */}
diff --git a/src/components/SprintBoard.tsx b/src/components/SprintBoard.tsx
index bebd01c..2d3569a 100644
--- a/src/components/SprintBoard.tsx
+++ b/src/components/SprintBoard.tsx
@@ -1,11 +1,27 @@
"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, CardHeader, CardTitle } from "@/components/ui/card"
+import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
-import { Plus, Calendar, Flag } from "lucide-react"
+import { Plus, Calendar, Flag, GripVertical } from "lucide-react"
import { format, parseISO } from "date-fns"
const statusColumns = ["backlog", "in-progress", "review", "done"] as const
@@ -18,12 +34,114 @@ const statusLabels: Record = {
}
const statusColors: Record = {
- backlog: "bg-zinc-700",
+ 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",
+}
+
+// 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.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,
@@ -31,7 +149,10 @@ export function SprintBoard() {
selectedSprintId,
selectSprint,
selectedProjectId,
+ selectedTaskId,
updateTask,
+ selectTask,
+ addSprint,
} = useTaskStore()
const [isCreatingSprint, setIsCreatingSprint] = useState(false)
@@ -41,6 +162,16 @@ export function SprintBoard() {
startDate: "",
endDate: "",
})
+ const [activeId, setActiveId] = useState(null)
+
+ // Sensors for drag detection
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 8,
+ },
+ })
+ )
// Get sprints for selected project
const projectSprints = sprints.filter(
@@ -59,6 +190,9 @@ export function SprintBoard() {
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 || !selectedProjectId) return
@@ -71,16 +205,35 @@ export function SprintBoard() {
projectId: selectedProjectId,
}
- // Use the store's addSprint action
- const { addSprint } = useTaskStore.getState()
addSprint(sprint)
-
setIsCreatingSprint(false)
setNewSprint({ name: "", goal: "", startDate: "", endDate: "" })
}
- const handleMoveTask = (taskId: string, newStatus: string) => {
- updateTask(taskId, { status: newStatus as any })
+ 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 any)) {
+ 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) {
@@ -101,10 +254,8 @@ export function SprintBoard() {
return (
-
- Create New Sprint
-
-
+
+ Create New Sprint
- {/* Sprint Header */}
-
-
-
-
+
+
+ {/* Sprint Header */}
+
+
+
+
+
+
+ {currentSprint && (
+
+
+
+
+ {format(parseISO(currentSprint.startDate), "MMM d")} -{" "}
+ {format(parseISO(currentSprint.endDate), "MMM d, yyyy")}
+
+
+
+ {currentSprint.status}
+
+
+ )}
- {currentSprint && (
-
-
-
-
- {format(parseISO(currentSprint.startDate), "MMM d")} -{" "}
- {format(parseISO(currentSprint.endDate), "MMM d, yyyy")}
-
-
-
- {currentSprint.status}
-
+ {/* Sprint Goal */}
+ {currentSprint?.goal && (
+
+ Goal: {currentSprint.goal}
+
+ )}
+
+ {/* Sprint Board */}
+ {selectedSprintId ? (
+
+ {statusColumns.map((status) => (
+
+
+
+
+
{statusLabels[status]}
+
+
+ {tasksByStatus[status]?.length || 0}
+
+
+
t.id) || []}
+ strategy={verticalListSortingStrategy}
+ >
+
+ {tasksByStatus[status]?.map((task) => (
+ selectTask(task.id)}
+ />
+ ))}
+
+
+
+ ))}
+
+ ) : (
+
+
Select a sprint to view the sprint board
)}
- {/* Sprint Goal */}
- {currentSprint?.goal && (
-
- Goal: {currentSprint.goal}
-
- )}
-
- {/* Sprint Board */}
- {selectedSprintId ? (
-
- {statusColumns.map((status) => (
-
-
-
{statusLabels[status]}
-
- {tasksByStatus[status]?.length || 0}
-
-
-
- {tasksByStatus[status]?.map((task) => (
-
{}}
- >
-
-
-
- {task.title}
-
-
- {task.priority}
-
-
-
-
- {task.type}
-
- {task.comments.length > 0 && (
-
- 💬 {task.comments.length}
-
- )}
-
-
-
- ))}
-
-
- ))}
-
- ) : (
-
-
Select a sprint to view the sprint board
-
- )}
-
+
+ {activeTask ? : null}
+
+
)
}