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>
10 KiB
10 KiB
task_id, story_id, sprint_id, status, type, assignee, created_date, estimated_hours
| task_id | story_id | sprint_id | status | type | assignee | created_date | estimated_hours |
|---|---|---|---|---|---|---|---|
| sprint_4_story_1_task_4 | sprint_4_story_1 | sprint_4 | not_started | frontend | Frontend Developer 1 | 2025-11-05 | 3 |
Task 4: Integrate Story API Data Loading and Error Handling
Description
Connect the Story detail page to the backend API using React Query hooks. Implement data loading, error handling, 404 detection, and loading states with skeleton loaders.
What to Do
- Use
useStory(id)hook to fetch Story data - Use
useEpic(epicId)hook to fetch parent Epic data - Implement loading states with skeleton loaders
- Handle 404 errors (Story not found)
- Handle network errors
- Handle permission errors (unauthorized)
- Add breadcrumb navigation with data
- Display Story description in main content area
- Test with various Story IDs (valid, invalid, deleted)
Files to Modify
app/(dashboard)/stories/[id]/page.tsx(modify, ~50 lines added)
Implementation Details
// app/(dashboard)/stories/[id]/page.tsx
'use client';
import { use } from 'react';
import { notFound } from 'next/navigation';
import { useStory } from '@/lib/hooks/use-stories';
import { useEpic } from '@/lib/hooks/use-epics';
import { StoryHeader } from '@/components/projects/story-header';
import { StoryMetadataSidebar } from '@/components/projects/story-metadata-sidebar';
import { Card } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { AlertCircle } from 'lucide-react';
interface StoryDetailPageProps {
params: Promise<{ id: string }>;
}
export default function StoryDetailPage({ params }: StoryDetailPageProps) {
const { id } = use(params);
// Fetch Story data
const {
data: story,
isLoading: storyLoading,
error: storyError,
} = useStory(id);
// Fetch parent Epic data
const {
data: parentEpic,
isLoading: epicLoading,
} = useEpic(story?.epicId, {
enabled: !!story?.epicId, // Only fetch if Story loaded
});
// Handle loading state
if (storyLoading || epicLoading) {
return <StoryDetailPageSkeleton />;
}
// Handle 404 - Story not found
if (storyError?.response?.status === 404) {
notFound();
}
// Handle other errors (network, permission, etc.)
if (storyError || !story) {
return (
<div className="container mx-auto px-4 py-6 max-w-7xl">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{storyError?.message || 'Failed to load Story. Please try again.'}
</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="container mx-auto px-4 py-6 max-w-7xl">
{/* Breadcrumb Navigation */}
<nav className="mb-4 text-sm text-muted-foreground" aria-label="Breadcrumb">
<ol className="flex items-center gap-2">
<li><a href="/projects" className="hover:text-foreground">Projects</a></li>
<li>/</li>
<li><a href={`/projects/${story.projectId}`} className="hover:text-foreground">Project</a></li>
<li>/</li>
<li><a href="/epics" className="hover:text-foreground">Epics</a></li>
<li>/</li>
{parentEpic && (
<>
<li>
<a href={`/epics/${parentEpic.id}`} className="hover:text-foreground">
{parentEpic.title}
</a>
</li>
<li>/</li>
</>
)}
<li><a href={`/epics/${story.epicId}/stories`} className="hover:text-foreground">Stories</a></li>
<li>/</li>
<li className="font-medium text-foreground" aria-current="page">{story.title}</li>
</ol>
</nav>
{/* Story Header */}
<StoryHeader
story={story}
onEdit={() => setIsEditDialogOpen(true)}
onDelete={() => setIsDeleteDialogOpen(true)}
/>
{/* Two-column layout */}
<div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6 mt-6">
{/* Main Content Column */}
<div className="space-y-6">
{/* Story Description Card */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Description</h2>
{story.description ? (
<div className="prose prose-sm max-w-none">
<p className="whitespace-pre-wrap">{story.description}</p>
</div>
) : (
<p className="text-sm text-muted-foreground italic">No description provided.</p>
)}
</Card>
{/* Acceptance Criteria Card - Story 3 */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Acceptance Criteria</h2>
<p className="text-sm text-muted-foreground italic">
Acceptance criteria will be added in Story 3.
</p>
</Card>
{/* Tasks Section - Story 2 */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Tasks</h2>
<p className="text-sm text-muted-foreground italic">
Task list will be added in Story 2.
</p>
</Card>
</div>
{/* Metadata Sidebar Column */}
<aside>
<StoryMetadataSidebar story={story} parentEpic={parentEpic} />
</aside>
</div>
</div>
);
}
// Loading skeleton
function StoryDetailPageSkeleton() {
return (
<div className="container mx-auto px-4 py-6 max-w-7xl">
<Skeleton className="h-4 w-64 mb-4" />
<Skeleton className="h-12 w-96 mb-6" />
<div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6">
<div className="space-y-6">
<Card className="p-6">
<Skeleton className="h-6 w-32 mb-4" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-3/4" />
</Card>
<Card className="p-6">
<Skeleton className="h-6 w-48 mb-4" />
<Skeleton className="h-20 w-full" />
</Card>
<Card className="p-6">
<Skeleton className="h-6 w-24 mb-4" />
<Skeleton className="h-32 w-full" />
</Card>
</div>
<aside className="space-y-6">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-32 w-full" />
<Skeleton className="h-24 w-full" />
</aside>
</div>
</div>
);
}
404 Page (optional, create if doesn't exist):
// app/(dashboard)/stories/[id]/not-found.tsx
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { FileQuestion } from 'lucide-react';
export default function StoryNotFound() {
return (
<div className="container mx-auto px-4 py-6 max-w-7xl">
<Card className="p-12 text-center">
<FileQuestion className="h-16 w-16 mx-auto mb-4 text-muted-foreground" />
<h1 className="text-2xl font-bold mb-2">Story Not Found</h1>
<p className="text-muted-foreground mb-6">
The Story you're looking for doesn't exist or you don't have permission to view it.
</p>
<div className="flex justify-center gap-4">
<Button variant="outline" asChild>
<Link href="/epics">View All Epics</Link>
</Button>
<Button asChild>
<Link href="/projects">Go to Projects</Link>
</Button>
</div>
</Card>
</div>
);
}
Acceptance Criteria
- Page fetches Story data using
useStory(id)hook - Page fetches parent Epic data using
useEpic(epicId)hook - Loading state displays skeleton loaders
- 404 error handled (Story not found → shows custom 404 page)
- Network errors display error message with retry option
- Permission errors display appropriate message
- Breadcrumb navigation shows full hierarchy with clickable links
- Story description displays correctly (supports line breaks)
- Empty description shows placeholder message
- All data populates components (header, sidebar)
- No console errors during data fetching
Testing
Manual Testing:
- Navigate to valid Story ID → Verify data loads correctly
- Navigate to invalid Story ID → Verify 404 page shows
- Disconnect internet → Navigate to Story → Verify error message
- Verify skeleton loaders show during initial load
- Verify breadcrumb links are clickable and navigate correctly
- Verify Story description displays with line breaks preserved
- Test with Story that has no description → Verify placeholder shows
Test Cases:
# Valid Story
/stories/[valid-story-id] → Should load Story details
# Invalid Story
/stories/invalid-id → Should show 404 page
# Deleted Story
/stories/[deleted-story-id] → Should show 404 page
# No permission
/stories/[other-tenant-story-id] → Should show error message
E2E Test:
test('story detail page loads data correctly', async ({ page }) => {
await page.goto('/stories/story-123');
// Wait for data to load
await expect(page.locator('h1')).toContainText('Story Title');
// Verify metadata displays
await expect(page.locator('[data-testid="story-status"]')).toBeVisible();
await expect(page.locator('[data-testid="story-priority"]')).toBeVisible();
// Verify description
await expect(page.getByText('Description')).toBeVisible();
});
test('handles 404 for invalid story', async ({ page }) => {
await page.goto('/stories/invalid-id');
await expect(page.getByText('Story Not Found')).toBeVisible();
});
Dependencies
Prerequisites:
- Task 1 (page structure must exist)
- Task 2 (header component must exist)
- Task 3 (sidebar component must exist)
- ✅
useStory(id)hook (already exists) - ✅
useEpic(id)hook (already exists)
Blocks:
- Task 5 (Edit/Delete actions need data loaded)
Estimated Time
3 hours
Notes
Error Handling Strategy:
- 404 (Not Found) → Custom 404 page with helpful navigation
- 403 (Forbidden) → Error message with permission context
- 500 (Server Error) → Error message with retry button
- Network Error → Error message with offline indicator
Performance: Use React Query's built-in caching. Subsequent visits to the same Story load instantly from cache.