feat(frontend): Create Sprint 4 Stories and Tasks for Story Management
Created comprehensive Story and Task files for Sprint 4 frontend implementation: Story 1: Story Detail Page Foundation (P0 Critical - 3 days) - 6 tasks: route creation, header, sidebar, data loading, Edit/Delete, responsive design - Fixes critical 404 error when clicking Story cards - Two-column layout consistent with Epic detail page Story 2: Task Management in Story Detail (P0 Critical - 2 days) - 6 tasks: API verification, hooks, TaskList, TaskCard, TaskForm, integration - Complete Task CRUD with checkbox status toggle - Filters, sorting, and optimistic UI updates Story 3: Enhanced Story Form (P1 High - 2 days) - 6 tasks: acceptance criteria, assignee selector, tags, story points, integration - Aligns with UX design specification - Backward compatible with existing Stories Story 4: Quick Add Story Workflow (P1 High - 2 days) - 5 tasks: inline form, keyboard shortcuts, batch creation, navigation - Rapid Story creation with minimal fields - Keyboard shortcut (Cmd/Ctrl + N) Story 5: Story Card Component (P2 Medium - 1 day) - 4 tasks: component variants, visual states, Task count, optimization - Reusable component with list/kanban/compact variants - React.memo optimization Story 6: Kanban Story Creation Enhancement (P2 Optional - 2 days) - 4 tasks: Epic card enhancement, inline form, animation, real-time updates - Contextual Story creation from Kanban - Stretch goal - implement only if ahead of schedule Total: 6 Stories, 31 Tasks, 12 days estimated Priority breakdown: P0 (2), P1 (2), P2 (2 optional) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
298
docs/plans/sprint_4_story_1_task_3.md
Normal file
298
docs/plans/sprint_4_story_1_task_3.md
Normal file
@@ -0,0 +1,298 @@
|
||||
---
|
||||
task_id: sprint_4_story_1_task_3
|
||||
story_id: sprint_4_story_1
|
||||
sprint_id: sprint_4
|
||||
status: not_started
|
||||
type: frontend
|
||||
assignee: Frontend Developer 1
|
||||
created_date: 2025-11-05
|
||||
estimated_hours: 4
|
||||
---
|
||||
|
||||
# Task 3: Implement Story Metadata Sidebar Component
|
||||
|
||||
## Description
|
||||
|
||||
Create the metadata sidebar component that displays Story status, priority, assignee, time tracking, dates, and a parent Epic card. This sidebar provides quick access to Story metadata and context.
|
||||
|
||||
## What to Do
|
||||
|
||||
1. Create `components/projects/story-metadata-sidebar.tsx`
|
||||
2. Display Status dropdown (quick status change)
|
||||
3. Display Priority dropdown (quick priority change)
|
||||
4. Display Assignee with avatar and change button
|
||||
5. Display Time Tracking (Estimated vs Actual hours, progress bar)
|
||||
6. Display Dates (Created, Updated with relative time)
|
||||
7. Display Parent Epic card (title, status, progress, link)
|
||||
8. Add responsive design (sidebar moves to top on mobile)
|
||||
9. Integrate with Story update hooks
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
- `components/projects/story-metadata-sidebar.tsx` (new, ~150-200 lines)
|
||||
- `components/projects/epic-card-compact.tsx` (optional, for parent Epic display)
|
||||
- `app/(dashboard)/stories/[id]/page.tsx` (modify, integrate sidebar)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
```typescript
|
||||
// components/projects/story-metadata-sidebar.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Calendar, Clock, User } from 'lucide-react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import type { Story, Epic } from '@/types/project';
|
||||
import { useChangeStoryStatus, useAssignStory } from '@/lib/hooks/use-stories';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
interface StoryMetadataSidebarProps {
|
||||
story: Story;
|
||||
parentEpic?: Epic;
|
||||
}
|
||||
|
||||
export function StoryMetadataSidebar({ story, parentEpic }: StoryMetadataSidebarProps) {
|
||||
const changeStatus = useChangeStoryStatus();
|
||||
const assignStory = useAssignStory();
|
||||
|
||||
const handleStatusChange = async (newStatus: string) => {
|
||||
await changeStatus.mutateAsync({ storyId: story.id, status: newStatus });
|
||||
};
|
||||
|
||||
const completionPercentage = story.actualHours && story.estimatedHours
|
||||
? Math.min(100, (story.actualHours / story.estimatedHours) * 100)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Status Section */}
|
||||
<Card className="p-4">
|
||||
<h3 className="text-sm font-medium mb-3">Status</h3>
|
||||
<Select
|
||||
value={story.status}
|
||||
onValueChange={handleStatusChange}
|
||||
disabled={changeStatus.isPending}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Backlog">Backlog</SelectItem>
|
||||
<SelectItem value="Todo">Todo</SelectItem>
|
||||
<SelectItem value="InProgress">In Progress</SelectItem>
|
||||
<SelectItem value="Done">Done</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Card>
|
||||
|
||||
{/* Priority Section */}
|
||||
<Card className="p-4">
|
||||
<h3 className="text-sm font-medium mb-3">Priority</h3>
|
||||
<Badge variant={getPriorityVariant(story.priority)} className="w-full justify-center">
|
||||
{story.priority}
|
||||
</Badge>
|
||||
</Card>
|
||||
|
||||
{/* Assignee Section */}
|
||||
<Card className="p-4">
|
||||
<h3 className="text-sm font-medium mb-3">Assignee</h3>
|
||||
{story.assigneeId ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={`/avatars/${story.assigneeId}.png`} />
|
||||
<AvatarFallback>
|
||||
<User className="h-4 w-4" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Assignee Name</p>
|
||||
<Button variant="link" size="sm" className="h-auto p-0 text-xs">
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
<User className="h-4 w-4 mr-2" />
|
||||
Assign
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Time Tracking Section */}
|
||||
<Card className="p-4">
|
||||
<h3 className="text-sm font-medium mb-3">Time Tracking</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Estimated</span>
|
||||
<span className="font-medium">{story.estimatedHours || 0}h</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Actual</span>
|
||||
<span className="font-medium">{story.actualHours || 0}h</span>
|
||||
</div>
|
||||
<Progress value={completionPercentage} className="h-2" />
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
{Math.round(completionPercentage)}% complete
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Dates Section */}
|
||||
<Card className="p-4">
|
||||
<h3 className="text-sm font-medium mb-3">Dates</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>Created {formatDistanceToNow(new Date(story.createdAt), { addSuffix: true })}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>Updated {formatDistanceToNow(new Date(story.updatedAt), { addSuffix: true })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Parent Epic Section */}
|
||||
{parentEpic && (
|
||||
<Card className="p-4">
|
||||
<h3 className="text-sm font-medium mb-3">Parent Epic</h3>
|
||||
<Link
|
||||
href={`/epics/${parentEpic.id}`}
|
||||
className="block p-3 rounded-lg border hover:bg-accent transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h4 className="font-medium line-clamp-1">{parentEpic.title}</h4>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{parentEpic.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
View Epic Details →
|
||||
</p>
|
||||
</Link>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getPriorityVariant(priority: string) {
|
||||
const variants = {
|
||||
Low: 'default',
|
||||
Medium: 'warning',
|
||||
High: 'destructive',
|
||||
Critical: 'destructive',
|
||||
};
|
||||
return variants[priority] || 'default';
|
||||
}
|
||||
```
|
||||
|
||||
**Integration in page.tsx**:
|
||||
```typescript
|
||||
// app/(dashboard)/stories/[id]/page.tsx
|
||||
import { StoryMetadataSidebar } from '@/components/projects/story-metadata-sidebar';
|
||||
import { useEpic } from '@/lib/hooks/use-epics';
|
||||
|
||||
// Inside component
|
||||
const { data: story } = useStory(params.id);
|
||||
const { data: parentEpic } = useEpic(story?.epicId);
|
||||
|
||||
<StoryMetadataSidebar story={story} parentEpic={parentEpic} />
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Sidebar displays all metadata sections (Status, Priority, Assignee, Time, Dates, Parent Epic)
|
||||
- [ ] Status dropdown allows quick status changes
|
||||
- [ ] Status changes update immediately (optimistic UI)
|
||||
- [ ] Priority badge shows correct color
|
||||
- [ ] Assignee section shows avatar and name (if assigned)
|
||||
- [ ] Time tracking shows estimated/actual hours and progress bar
|
||||
- [ ] Progress bar correctly calculates percentage
|
||||
- [ ] Dates display in relative format ("2 hours ago", "3 days ago")
|
||||
- [ ] Parent Epic card is clickable and navigates to Epic detail
|
||||
- [ ] Parent Epic card shows Epic status and title
|
||||
- [ ] Responsive: Sidebar moves above content on mobile
|
||||
- [ ] Loading states show skeleton loaders
|
||||
|
||||
## Testing
|
||||
|
||||
**Manual Testing**:
|
||||
1. View Story detail page
|
||||
2. Verify all metadata sections display correctly
|
||||
3. Change status via dropdown → Verify updates immediately
|
||||
4. Verify priority badge color matches priority
|
||||
5. Verify assignee displays correctly (if assigned)
|
||||
6. Verify time tracking shows correct hours and percentage
|
||||
7. Verify dates are in relative format
|
||||
8. Click parent Epic card → Navigates to Epic detail page
|
||||
9. Test on mobile → Sidebar moves above main content
|
||||
|
||||
**Unit Test**:
|
||||
```typescript
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { StoryMetadataSidebar } from './story-metadata-sidebar';
|
||||
|
||||
describe('StoryMetadataSidebar', () => {
|
||||
const mockStory = {
|
||||
id: '123',
|
||||
status: 'InProgress',
|
||||
priority: 'High',
|
||||
assigneeId: 'user-1',
|
||||
estimatedHours: 16,
|
||||
actualHours: 8,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const mockEpic = {
|
||||
id: 'epic-1',
|
||||
title: 'Parent Epic',
|
||||
status: 'InProgress',
|
||||
};
|
||||
|
||||
it('renders all metadata sections', () => {
|
||||
render(<StoryMetadataSidebar story={mockStory} parentEpic={mockEpic} />);
|
||||
expect(screen.getByText('Status')).toBeInTheDocument();
|
||||
expect(screen.getByText('Priority')).toBeInTheDocument();
|
||||
expect(screen.getByText('Assignee')).toBeInTheDocument();
|
||||
expect(screen.getByText('Time Tracking')).toBeInTheDocument();
|
||||
expect(screen.getByText('Dates')).toBeInTheDocument();
|
||||
expect(screen.getByText('Parent Epic')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calculates time tracking percentage correctly', () => {
|
||||
render(<StoryMetadataSidebar story={mockStory} parentEpic={mockEpic} />);
|
||||
expect(screen.getByText('50% complete')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Prerequisites**:
|
||||
- Task 1 (page structure must exist)
|
||||
- ✅ `useChangeStoryStatus()` hook (already exists)
|
||||
- ✅ `useAssignStory()` hook (already exists)
|
||||
- ✅ shadcn/ui Card, Badge, Select, Avatar, Progress components
|
||||
- ✅ date-fns for relative time formatting
|
||||
|
||||
**Blocks**:
|
||||
- None (independent component)
|
||||
|
||||
## Estimated Time
|
||||
|
||||
4 hours
|
||||
|
||||
## Notes
|
||||
|
||||
**Code Reuse**: Copy sidebar structure from Epic detail page if similar metadata sidebar exists. The parent Epic card is similar to showing a parent Project card in Epic sidebar - reuse that pattern.
|
||||
|
||||
**Future Enhancement**: Assignee selector will be enhanced in Story 3 (Enhanced Story Form).
|
||||
Reference in New Issue
Block a user