From 29ce1cce2a64508b78f96217886368d7db21e5a6 Mon Sep 17 00:00:00 2001
From: OpenClaw Bot
Date: Thu, 19 Feb 2026 17:23:54 -0600
Subject: [PATCH] Add Sprint Board UI component
- Created SprintBoard component with:
- Sprint selector dropdown
- Sprint creation form
- Sprint board with To Do/In Progress/Review/Done columns
- Drag-and-drop ready task cards
- Sprint goal display
- Date range and status badges
- Added view toggle (Kanban/Sprint) to main page
- Sprint data persisted to server
---
src/app/page.tsx | 50 +++++-
src/components/SprintBoard.tsx | 274 +++++++++++++++++++++++++++++++++
2 files changed, 317 insertions(+), 7 deletions(-)
create mode 100644 src/components/SprintBoard.tsx
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 6a861f1..bcac201 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -8,7 +8,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
import { Textarea } from "@/components/ui/textarea"
import { Label } from "@/components/ui/label"
import { useTaskStore, Task, TaskType, TaskStatus, Priority, Project } from "@/stores/useTaskStore"
-import { Plus, MessageSquare, Calendar, Tag, Trash2, Edit2, X, Check, MoreHorizontal } from "lucide-react"
+import { SprintBoard } from "@/components/SprintBoard"
+import { Plus, MessageSquare, Calendar, Tag, Trash2, Edit2, X, Check, MoreHorizontal, LayoutGrid, Flag } from "lucide-react"
const typeColors: Record = {
idea: "bg-purple-500",
@@ -68,6 +69,7 @@ export default function Home() {
})
const [newComment, setNewComment] = useState("")
const [editingTask, setEditingTask] = useState(null)
+ const [viewMode, setViewMode] = useState<'kanban' | 'sprint'>('kanban')
// Sync from server on mount
useEffect(() => {
@@ -225,7 +227,7 @@ export default function Home() {
- {/* Main Content - Kanban Board */}
+ {/* Main Content */}
{selectedProject ? (
<>
@@ -238,13 +240,45 @@ export default function Home() {
{projectTasks.length} tasks · {projectTasks.filter((t) => t.status === "done").length} done
-
+
+ {/* View Toggle */}
+
+
+
+
+
+
- {/* Kanban Columns */}
+ {/* View Content */}
+ {viewMode === 'sprint' ? (
+
+ ) : (
+ <>
+ {/* Kanban Columns */}
{statusColumns.map((status) => {
const columnTasks = projectTasks.filter((t) => t.status === status)
@@ -343,6 +377,8 @@ export default function Home() {
)
})}
+ >
+ )}
>
) : (
diff --git a/src/components/SprintBoard.tsx b/src/components/SprintBoard.tsx
new file mode 100644
index 0000000..bebd01c
--- /dev/null
+++ b/src/components/SprintBoard.tsx
@@ -0,0 +1,274 @@
+"use client"
+
+import { useState } from "react"
+import { useTaskStore, Task, Sprint } from "@/stores/useTaskStore"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Plus, Calendar, Flag } from "lucide-react"
+import { format, parseISO } from "date-fns"
+
+const statusColumns = ["backlog", "in-progress", "review", "done"] as const
+
+const statusLabels: Record
= {
+ backlog: "To Do",
+ "in-progress": "In Progress",
+ review: "Review",
+ done: "Done",
+}
+
+const statusColors: Record = {
+ backlog: "bg-zinc-700",
+ "in-progress": "bg-blue-600",
+ review: "bg-yellow-600",
+ done: "bg-green-600",
+}
+
+export function SprintBoard() {
+ const {
+ tasks,
+ sprints,
+ selectedSprintId,
+ selectSprint,
+ selectedProjectId,
+ updateTask,
+ } = useTaskStore()
+
+ const [isCreatingSprint, setIsCreatingSprint] = useState(false)
+ const [newSprint, setNewSprint] = useState({
+ name: "",
+ goal: "",
+ startDate: "",
+ endDate: "",
+ })
+
+ // Get sprints for selected project
+ const projectSprints = sprints.filter(
+ (s) => s.projectId === selectedProjectId
+ )
+
+ // Get current sprint
+ const currentSprint = sprints.find((s) => s.id === selectedSprintId)
+
+ // Get tasks for current sprint
+ const sprintTasks = tasks.filter((t) => t.sprintId === selectedSprintId)
+
+ // Group tasks by status
+ const tasksByStatus = statusColumns.reduce((acc, status) => {
+ acc[status] = sprintTasks.filter((t) => t.status === status)
+ return acc
+ }, {} as Record)
+
+ const handleCreateSprint = () => {
+ if (!newSprint.name || !selectedProjectId) return
+
+ const sprint: Omit = {
+ name: newSprint.name,
+ goal: newSprint.goal,
+ startDate: newSprint.startDate || new Date().toISOString(),
+ endDate: newSprint.endDate || new Date().toISOString(),
+ status: "planning",
+ 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 })
+ }
+
+ 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"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Sprint Header */}
+
+
+
+
+
+
+ {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}
+
+
+
+ {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
+
+ )}
+
+ )
+}