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" + /> +
+
+ +