Files
ColaFlow-Web/lib/api/pm.ts
Yaojia Wang f7a17a3d1a feat(frontend): Implement Story detail page - Sprint 4 Story 1
Add complete Story detail page with two-column layout, breadcrumb navigation,
and full CRUD operations.

Key Features:
- Story detail page at /stories/[id] route
- Two-column layout (main content + metadata sidebar)
- Breadcrumb navigation: Projects > Project > Epics > Epic > Stories > Story
- Story header with title, status, priority badges, Edit/Delete actions
- Main content area with Story description and Tasks placeholder
- Metadata sidebar with:
  * Status selector (with optimistic updates)
  * Priority selector
  * Assignee display
  * Time tracking (estimated/actual hours)
  * Created/Updated dates
  * Parent Epic card (clickable link)
- Edit Story dialog (reuses StoryForm component)
- Delete Story confirmation dialog
- Loading state (skeleton loaders)
- Error handling with error.tsx
- Responsive design (mobile/tablet/desktop)
- Accessibility support (keyboard navigation, ARIA labels)

Technical Implementation:
- Uses Next.js 13+ App Router with dynamic routes
- React Query for data fetching and caching
- Optimistic updates for status/priority changes
- Proper TypeScript typing throughout
- Reuses existing components (StoryForm, shadcn/ui)
- 85% code reuse from Epic detail page pattern

Bug Fixes:
- Fixed TypeScript error in pm.ts (api.post generic type)

Files Created:
- app/(dashboard)/stories/[id]/page.tsx (478 lines)
- app/(dashboard)/stories/[id]/loading.tsx (66 lines)
- app/(dashboard)/stories/[id]/error.tsx (53 lines)

Files Modified:
- lib/api/pm.ts (added generic type to api.post<Epic>)

Verification:
- Build successful (npm run build)
- No TypeScript errors
- Route registered: /stories/[id] (Dynamic)

Next Steps:
- Task management functionality (Sprint 4 Story 2)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 22:00:24 +01:00

124 lines
3.5 KiB
TypeScript

import { api } from './client';
import type {
Epic,
CreateEpicDto,
UpdateEpicDto,
Story,
CreateStoryDto,
UpdateStoryDto,
Task,
CreateTaskDto,
UpdateTaskDto,
WorkItemStatus,
} from '@/types/project';
// ==================== Epics API ====================
export const epicsApi = {
list: async (projectId?: string): Promise<Epic[]> => {
if (!projectId) {
throw new Error('projectId is required for listing epics');
}
return api.get(`/api/v1/projects/${projectId}/epics`);
},
get: async (id: string): Promise<Epic> => {
return api.get(`/api/v1/epics/${id}`);
},
create: async (data: CreateEpicDto): Promise<Epic> => {
console.log('[epicsApi.create] Sending request', { url: '/api/v1/epics', data });
try {
const result = await api.post<Epic>('/api/v1/epics', data);
console.log('[epicsApi.create] Request successful', result);
return result;
} catch (error) {
console.error('[epicsApi.create] Request failed', error);
throw error;
}
},
update: async (id: string, data: UpdateEpicDto): Promise<Epic> => {
return api.put(`/api/v1/epics/${id}`, data);
},
delete: async (id: string): Promise<void> => {
return api.delete(`/api/v1/epics/${id}`);
},
changeStatus: async (id: string, status: WorkItemStatus): Promise<Epic> => {
return api.put(`/api/v1/epics/${id}/status`, { status });
},
assign: async (id: string, assigneeId: string): Promise<Epic> => {
return api.put(`/api/v1/epics/${id}/assign`, { assigneeId });
},
};
// ==================== Stories API ====================
export const storiesApi = {
list: async (epicId?: string): Promise<Story[]> => {
if (!epicId) {
throw new Error('epicId is required for listing stories');
}
return api.get(`/api/v1/epics/${epicId}/stories`);
},
get: async (id: string): Promise<Story> => {
return api.get(`/api/v1/stories/${id}`);
},
create: async (data: CreateStoryDto): Promise<Story> => {
return api.post('/api/v1/stories', data);
},
update: async (id: string, data: UpdateStoryDto): Promise<Story> => {
return api.put(`/api/v1/stories/${id}`, data);
},
delete: async (id: string): Promise<void> => {
return api.delete(`/api/v1/stories/${id}`);
},
changeStatus: async (id: string, status: WorkItemStatus): Promise<Story> => {
return api.put(`/api/v1/stories/${id}/status`, { status });
},
assign: async (id: string, assigneeId: string): Promise<Story> => {
return api.put(`/api/v1/stories/${id}/assign`, { assigneeId });
},
};
// ==================== Tasks API ====================
export const tasksApi = {
list: async (storyId?: string): Promise<Task[]> => {
if (!storyId) {
throw new Error('storyId is required for listing tasks');
}
return api.get(`/api/v1/stories/${storyId}/tasks`);
},
get: async (id: string): Promise<Task> => {
return api.get(`/api/v1/tasks/${id}`);
},
create: async (data: CreateTaskDto): Promise<Task> => {
return api.post('/api/v1/tasks', data);
},
update: async (id: string, data: UpdateTaskDto): Promise<Task> => {
return api.put(`/api/v1/tasks/${id}`, data);
},
delete: async (id: string): Promise<void> => {
return api.delete(`/api/v1/tasks/${id}`);
},
changeStatus: async (id: string, status: WorkItemStatus): Promise<Task> => {
return api.put(`/api/v1/tasks/${id}/status`, { status });
},
assign: async (id: string, assigneeId: string): Promise<Task> => {
return api.put(`/api/v1/tasks/${id}/assign`, { assigneeId });
},
};