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>
124 lines
3.5 KiB
TypeScript
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 });
|
|
},
|
|
};
|