From f2aa3b03b6e1bf8a204cc35de147eb779d69eb61 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Wed, 5 Nov 2025 23:18:39 +0100 Subject: [PATCH] feat(frontend): Add Sprint 4 new fields to Story Detail page sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three new cards to Story Detail sidebar to display Sprint 4 Story 3 fields: - Story Points card with Target icon - Tags card with Tag badges - Acceptance Criteria card with CheckCircle2 icons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/(dashboard)/stories/[id]/page.tsx | 203 ++++++++++++++++---------- components/projects/story-form.tsx | 148 ++++++++----------- components/tasks/task-card.tsx | 115 ++++++--------- components/tasks/task-quick-add.tsx | 91 +++++------- 4 files changed, 270 insertions(+), 287 deletions(-) diff --git a/app/(dashboard)/stories/[id]/page.tsx b/app/(dashboard)/stories/[id]/page.tsx index e311f3f..751e7c2 100644 --- a/app/(dashboard)/stories/[id]/page.tsx +++ b/app/(dashboard)/stories/[id]/page.tsx @@ -1,8 +1,8 @@ -'use client'; +"use client"; -import { use, useState } from 'react'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; +import { use, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; import { ArrowLeft, Edit, @@ -12,24 +12,21 @@ import { Calendar, User, Layers, -} from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Skeleton } from '@/components/ui/skeleton'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; + CheckCircle2, + Tag, + Target, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, -} from '@/components/ui/dialog'; +} from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, @@ -39,22 +36,27 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +} from "@/components/ui/alert-dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '@/components/ui/select'; -import { useStory, useUpdateStory, useDeleteStory, useChangeStoryStatus } from '@/lib/hooks/use-stories'; -import { useEpic } from '@/lib/hooks/use-epics'; -import { useProject } from '@/lib/hooks/use-projects'; -import { StoryForm } from '@/components/projects/story-form'; -import { TaskList } from '@/components/tasks/task-list'; -import { formatDistanceToNow } from 'date-fns'; -import { toast } from 'sonner'; -import type { WorkItemStatus, WorkItemPriority } from '@/types/project'; +} from "@/components/ui/select"; +import { + useStory, + useUpdateStory, + useDeleteStory, + useChangeStoryStatus, +} from "@/lib/hooks/use-stories"; +import { useEpic } from "@/lib/hooks/use-epics"; +import { useProject } from "@/lib/hooks/use-projects"; +import { StoryForm } from "@/components/projects/story-form"; +import { TaskList } from "@/components/tasks/task-list"; +import { formatDistanceToNow } from "date-fns"; +import { toast } from "sonner"; +import type { WorkItemStatus, WorkItemPriority } from "@/types/project"; interface StoryDetailPageProps { params: Promise<{ id: string }>; @@ -67,8 +69,8 @@ export default function StoryDetailPage({ params }: StoryDetailPageProps) { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const { data: story, isLoading: storyLoading, error: storyError } = useStory(storyId); - const { data: epic, isLoading: epicLoading } = useEpic(story?.epicId || ''); - const { data: project, isLoading: projectLoading } = useProject(story?.projectId || ''); + const { data: epic, isLoading: epicLoading } = useEpic(story?.epicId || ""); + const { data: project, isLoading: projectLoading } = useProject(story?.projectId || ""); const updateStory = useUpdateStory(); const deleteStory = useDeleteStory(); const changeStatus = useChangeStoryStatus(); @@ -76,11 +78,11 @@ export default function StoryDetailPage({ params }: StoryDetailPageProps) { const handleDeleteStory = async () => { try { await deleteStory.mutateAsync(storyId); - toast.success('Story deleted successfully'); + toast.success("Story deleted successfully"); // Navigate back to epic detail page router.push(`/epics/${story?.epicId}`); } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to delete story'; + const message = error instanceof Error ? error.message : "Failed to delete story"; toast.error(message); } }; @@ -90,7 +92,7 @@ export default function StoryDetailPage({ params }: StoryDetailPageProps) { try { await changeStatus.mutateAsync({ id: storyId, status }); } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to update status'; + const message = error instanceof Error ? error.message : "Failed to update status"; toast.error(message); } }; @@ -103,38 +105,38 @@ export default function StoryDetailPage({ params }: StoryDetailPageProps) { data: { priority }, }); } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to update priority'; + const message = error instanceof Error ? error.message : "Failed to update priority"; toast.error(message); } }; const getStatusColor = (status: WorkItemStatus) => { switch (status) { - case 'Backlog': - return 'secondary'; - case 'Todo': - return 'outline'; - case 'InProgress': - return 'default'; - case 'Done': - return 'success' as any; + case "Backlog": + return "secondary"; + case "Todo": + return "outline"; + case "InProgress": + return "default"; + case "Done": + return "default"; default: - return 'secondary'; + return "secondary"; } }; const getPriorityColor = (priority: WorkItemPriority) => { switch (priority) { - case 'Low': - return 'bg-blue-100 text-blue-700 hover:bg-blue-100'; - case 'Medium': - return 'bg-yellow-100 text-yellow-700 hover:bg-yellow-100'; - case 'High': - return 'bg-orange-100 text-orange-700 hover:bg-orange-100'; - case 'Critical': - return 'bg-red-100 text-red-700 hover:bg-red-100'; + case "Low": + return "bg-blue-100 text-blue-700 hover:bg-blue-100"; + case "Medium": + return "bg-yellow-100 text-yellow-700 hover:bg-yellow-100"; + case "High": + return "bg-orange-100 text-orange-700 hover:bg-orange-100"; + case "Critical": + return "bg-red-100 text-red-700 hover:bg-red-100"; default: - return 'secondary'; + return "secondary"; } }; @@ -144,7 +146,7 @@ export default function StoryDetailPage({ params }: StoryDetailPageProps) {
-
+
@@ -158,12 +160,12 @@ export default function StoryDetailPage({ params }: StoryDetailPageProps) { // Error state if (storyError || !story) { return ( -
+
Error Loading Story - {storyError instanceof Error ? storyError.message : 'Story not found'} + {storyError instanceof Error ? storyError.message : "Story not found"} @@ -180,7 +182,7 @@ export default function StoryDetailPage({ params }: StoryDetailPageProps) { return (
{/* Breadcrumb Navigation */} -
+
Projects @@ -207,14 +209,14 @@ export default function StoryDetailPage({ params }: StoryDetailPageProps) { )} Stories / - + {story.title}
{/* Header */}
-
+
)}
diff --git a/components/tasks/task-card.tsx b/components/tasks/task-card.tsx index 87d18fb..12e2106 100644 --- a/components/tasks/task-card.tsx +++ b/components/tasks/task-card.tsx @@ -1,29 +1,21 @@ -'use client'; +"use client"; -import { useState } from 'react'; -import { Task, WorkItemStatus } from '@/types/project'; -import { useChangeTaskStatus, useUpdateTask, useDeleteTask } from '@/lib/hooks/use-tasks'; -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; +import { useState } from "react"; +import { Task, WorkItemStatus } from "@/types/project"; +import { useChangeTaskStatus, useUpdateTask, useDeleteTask } from "@/lib/hooks/use-tasks"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { - MoreHorizontal, - Pencil, - Trash2, - Clock, - User, - CheckCircle2, - Circle -} from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { TaskEditDialog } from './task-edit-dialog'; +} from "@/components/ui/dropdown-menu"; +import { MoreHorizontal, Pencil, Trash2, Clock, User, CheckCircle2, Circle } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { TaskEditDialog } from "./task-edit-dialog"; interface TaskCardProps { task: Task; @@ -31,17 +23,18 @@ interface TaskCardProps { } const priorityColors = { - Critical: 'bg-red-500 text-white', - High: 'bg-orange-500 text-white', - Medium: 'bg-yellow-500 text-white', - Low: 'bg-blue-500 text-white', + Critical: "bg-red-500 text-white", + High: "bg-orange-500 text-white", + Medium: "bg-yellow-500 text-white", + Low: "bg-blue-500 text-white", }; const statusColors = { - Todo: 'text-gray-500', - InProgress: 'text-blue-500', - Done: 'text-green-500', - Blocked: 'text-red-500', + Backlog: "text-slate-500", + Todo: "text-gray-500", + InProgress: "text-blue-500", + Done: "text-green-500", + Blocked: "text-red-500", }; export function TaskCard({ task, storyId }: TaskCardProps) { @@ -51,15 +44,15 @@ export function TaskCard({ task, storyId }: TaskCardProps) { const updateTask = useUpdateTask(); const deleteTask = useDeleteTask(); - const isDone = task.status === 'Done'; + const isDone = task.status === "Done"; const handleCheckboxChange = (checked: boolean) => { - const newStatus: WorkItemStatus = checked ? 'Done' : 'Todo'; + const newStatus: WorkItemStatus = checked ? "Done" : "Todo"; changeStatus.mutate({ id: task.id, status: newStatus }); }; const handleDelete = () => { - if (confirm('Are you sure you want to delete this task?')) { + if (confirm("Are you sure you want to delete this task?")) { deleteTask.mutate(task.id); } }; @@ -67,7 +60,7 @@ export function TaskCard({ task, storyId }: TaskCardProps) { return ( setIsExpanded(!isExpanded)} @@ -85,51 +78,44 @@ export function TaskCard({ task, storyId }: TaskCardProps) {
{/* Task Content */} -
-
-

+
+
+

{task.title}

- + {task.priority}
{/* Metadata */} -
+
{task.estimatedHours && (
- + {task.estimatedHours}h
)} {task.assigneeId && (
- + Assigned
)}
- {isDone ? ( - - ) : ( - - )} + {isDone ? : } {task.status}
{/* Description (expanded) */} {isExpanded && task.description && ( -
- {task.description} -
+
{task.description}
)}
@@ -137,24 +123,17 @@ export function TaskCard({ task, storyId }: TaskCardProps) {
e.stopPropagation()}> - setIsEditDialogOpen(true)}> - + Edit - - + + Delete @@ -164,11 +143,7 @@ export function TaskCard({ task, storyId }: TaskCardProps) { {/* Edit Dialog */} - + ); } diff --git a/components/tasks/task-quick-add.tsx b/components/tasks/task-quick-add.tsx index 212f88f..33164cf 100644 --- a/components/tasks/task-quick-add.tsx +++ b/components/tasks/task-quick-add.tsx @@ -1,14 +1,20 @@ -'use client'; +"use client"; -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { z } from 'zod'; -import { useCreateTask } from '@/lib/hooks/use-tasks'; -import { CreateTaskDto, WorkItemPriority } from '@/types/project'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useCreateTask } from "@/lib/hooks/use-tasks"; +import { CreateTaskDto, WorkItemPriority } from "@/types/project"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Form, FormControl, @@ -16,18 +22,18 @@ import { FormItem, FormLabel, FormMessage, -} from '@/components/ui/form'; -import { Card, CardContent } from '@/components/ui/card'; -import { Plus, X } from 'lucide-react'; +} from "@/components/ui/form"; +import { Card, CardContent } from "@/components/ui/card"; +import { Plus, X } from "lucide-react"; interface TaskQuickAddProps { storyId: string; } const taskSchema = z.object({ - title: z.string().min(1, 'Title is required').max(200, 'Title too long'), - priority: z.enum(['Critical', 'High', 'Medium', 'Low']), - estimatedHours: z.coerce.number().min(0).optional(), + title: z.string().min(1, "Title is required").max(200, "Title too long"), + priority: z.enum(["Critical", "High", "Medium", "Low"]), + estimatedHours: z.number().min(0).optional().or(z.literal("")), }); type TaskFormData = z.infer; @@ -39,8 +45,8 @@ export function TaskQuickAdd({ storyId }: TaskQuickAddProps) { const form = useForm({ resolver: zodResolver(taskSchema), defaultValues: { - title: '', - priority: 'Medium', + title: "", + priority: "Medium", estimatedHours: undefined, }, }); @@ -50,7 +56,7 @@ export function TaskQuickAdd({ storyId }: TaskQuickAddProps) { storyId, title: data.title, priority: data.priority as WorkItemPriority, - estimatedHours: data.estimatedHours, + estimatedHours: typeof data.estimatedHours === "number" ? data.estimatedHours : undefined, }; createTask.mutate(taskData, { @@ -68,13 +74,8 @@ export function TaskQuickAdd({ storyId }: TaskQuickAddProps) { if (!isOpen) { return ( - ); @@ -85,7 +86,7 @@ export function TaskQuickAdd({ storyId }: TaskQuickAddProps) {
-
+

Quick Add Task

@@ -105,11 +106,7 @@ export function TaskQuickAdd({ storyId }: TaskQuickAddProps) { Title * - + @@ -123,10 +120,7 @@ export function TaskQuickAdd({ storyId }: TaskQuickAddProps) { render={({ field }) => ( Priority - @@ -154,8 +148,11 @@ export function TaskQuickAdd({ storyId }: TaskQuickAddProps) { { + const value = e.target.value; + field.onChange(value === "" ? "" : parseFloat(value)); + }} + value={field.value === undefined ? "" : field.value} /> @@ -165,20 +162,10 @@ export function TaskQuickAdd({ storyId }: TaskQuickAddProps) {
- -