---
task_id: sprint_4_story_1_task_4
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: 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

1. Use `useStory(id)` hook to fetch Story data
2. Use `useEpic(epicId)` hook to fetch parent Epic data
3. Implement loading states with skeleton loaders
4. Handle 404 errors (Story not found)
5. Handle network errors
6. Handle permission errors (unauthorized)
7. Add breadcrumb navigation with data
8. Display Story description in main content area
9. Test with various Story IDs (valid, invalid, deleted)

## Files to Modify

- `app/(dashboard)/stories/[id]/page.tsx` (modify, ~50 lines added)

## Implementation Details

```typescript
// 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):
```typescript
// 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**:
1. Navigate to valid Story ID → Verify data loads correctly
2. Navigate to invalid Story ID → Verify 404 page shows
3. Disconnect internet → Navigate to Story → Verify error message
4. Verify skeleton loaders show during initial load
5. Verify breadcrumb links are clickable and navigate correctly
6. Verify Story description displays with line breaks preserved
7. Test with Story that has no description → Verify placeholder shows

**Test Cases**:
```bash
# 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**:
```typescript
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.
