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>
12 KiB
12 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_2_task_2 | sprint_4_story_2 | sprint_4 | not_started | frontend | Frontend Developer 2 | 2025-11-05 | 3 |
Task 2: Create Task API Client and React Query Hooks
Description
Create the Task API client module and React Query hooks for Task CRUD operations. This establishes the data layer for Task management with optimistic updates, caching, and error handling.
What to Do
- Add Task API client methods to
lib/api/pm.ts - Create
lib/hooks/use-tasks.tswith React Query hooks - Implement optimistic updates for instant UI feedback
- Add cache invalidation strategies
- Add error handling with toast notifications
- Add logger integration for debugging
- Test all hooks with real API data
- Document hook usage and examples
Files to Create/Modify
lib/api/pm.ts(modify, add Task methods ~100 lines)lib/hooks/use-tasks.ts(new, ~150-200 lines)
Implementation Details
Task API Client (lib/api/pm.ts)
// lib/api/pm.ts
// Add these Task API methods
import { apiClient } from './client';
import type {
Task,
CreateTaskDto,
UpdateTaskDto,
ChangeTaskStatusDto,
AssignTaskDto,
} from '@/types/project';
export const tasksApi = {
/**
* Get all Tasks for a Story
*/
list: async (storyId: string): Promise<Task[]> => {
const response = await apiClient.get(`/api/v1/stories/${storyId}/tasks`);
return response.data;
},
/**
* Get single Task by ID
*/
get: async (id: string): Promise<Task> => {
const response = await apiClient.get(`/api/v1/tasks/${id}`);
return response.data;
},
/**
* Create new Task
*/
create: async (data: CreateTaskDto): Promise<Task> => {
const response = await apiClient.post('/api/v1/tasks', data);
return response.data;
},
/**
* Update Task
*/
update: async (id: string, data: UpdateTaskDto): Promise<Task> => {
const response = await apiClient.put(`/api/v1/tasks/${id}`, data);
return response.data;
},
/**
* Delete Task
*/
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/api/v1/tasks/${id}`);
},
/**
* Change Task status
*/
changeStatus: async (id: string, status: string): Promise<Task> => {
const response = await apiClient.put(`/api/v1/tasks/${id}/status`, { status });
return response.data;
},
/**
* Assign Task to user
*/
assign: async (id: string, assigneeId: string): Promise<Task> => {
const response = await apiClient.put(`/api/v1/tasks/${id}/assign`, { assigneeId });
return response.data;
},
};
React Query Hooks (lib/hooks/use-tasks.ts)
// lib/hooks/use-tasks.ts
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { tasksApi } from '@/lib/api/pm';
import { toast } from 'sonner';
import { logger } from '@/lib/utils/logger';
import type { Task, CreateTaskDto, UpdateTaskDto } from '@/types/project';
/**
* Get all Tasks for a Story
*/
export function useTasks(storyId: string | undefined) {
return useQuery({
queryKey: ['tasks', storyId],
queryFn: () => {
if (!storyId) throw new Error('Story ID required');
return tasksApi.list(storyId);
},
enabled: !!storyId,
staleTime: 60_000, // Consider fresh for 1 minute
});
}
/**
* Get single Task by ID
*/
export function useTask(id: string | undefined) {
return useQuery({
queryKey: ['tasks', id],
queryFn: () => {
if (!id) throw new Error('Task ID required');
return tasksApi.get(id);
},
enabled: !!id,
});
}
/**
* Create new Task with optimistic update
*/
export function useCreateTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: CreateTaskDto) => {
logger.info('Creating task:', data);
return tasksApi.create(data);
},
onMutate: async (newTask) => {
// Cancel outgoing queries
await queryClient.cancelQueries({ queryKey: ['tasks', newTask.storyId] });
// Snapshot previous value
const previousTasks = queryClient.getQueryData<Task[]>(['tasks', newTask.storyId]);
// Optimistically update (optional for creates)
// Usually we just wait for server response
return { previousTasks };
},
onSuccess: (newTask, variables) => {
logger.info('Task created successfully:', newTask);
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['tasks', variables.storyId] });
queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] }); // Update Story task count
toast.success('Task created successfully');
},
onError: (error, variables, context) => {
logger.error('Failed to create task:', error);
// Restore previous state if optimistic update was used
if (context?.previousTasks) {
queryClient.setQueryData(['tasks', variables.storyId], context.previousTasks);
}
toast.error('Failed to create Task');
},
});
}
/**
* Update Task with optimistic update
*/
export function useUpdateTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, data }: { id: string; data: UpdateTaskDto }) => {
logger.info('Updating task:', id, data);
return tasksApi.update(id, data);
},
onMutate: async ({ id, data }) => {
// Cancel queries
await queryClient.cancelQueries({ queryKey: ['tasks', id] });
// Snapshot previous
const previousTask = queryClient.getQueryData<Task>(['tasks', id]);
// Optimistically update
queryClient.setQueryData<Task>(['tasks', id], (old) => {
if (!old) return old;
return { ...old, ...data };
});
return { previousTask };
},
onSuccess: (updatedTask) => {
logger.info('Task updated successfully:', updatedTask);
// Invalidate queries
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.id] });
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.storyId] });
toast.success('Task updated successfully');
},
onError: (error, variables, context) => {
logger.error('Failed to update task:', error);
// Revert optimistic update
if (context?.previousTask) {
queryClient.setQueryData(['tasks', variables.id], context.previousTask);
}
toast.error('Failed to update Task');
},
});
}
/**
* Delete Task
*/
export function useDeleteTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
logger.info('Deleting task:', id);
return tasksApi.delete(id);
},
onSuccess: (_, deletedId) => {
logger.info('Task deleted successfully:', deletedId);
// Remove from cache
queryClient.removeQueries({ queryKey: ['tasks', deletedId] });
// Invalidate lists
queryClient.invalidateQueries({ queryKey: ['tasks'] });
toast.success('Task deleted successfully');
},
onError: (error) => {
logger.error('Failed to delete task:', error);
toast.error('Failed to delete Task');
},
});
}
/**
* Change Task status (for checkbox toggle)
*/
export function useChangeTaskStatus() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ taskId, status }: { taskId: string; status: string }) => {
logger.info('Changing task status:', taskId, status);
return tasksApi.changeStatus(taskId, status);
},
onMutate: async ({ taskId, status }) => {
// Cancel queries
await queryClient.cancelQueries({ queryKey: ['tasks', taskId] });
// Snapshot previous
const previousTask = queryClient.getQueryData<Task>(['tasks', taskId]);
// Optimistically update status
queryClient.setQueryData<Task>(['tasks', taskId], (old) => {
if (!old) return old;
return { ...old, status };
});
// Also update in list
const storyId = previousTask?.storyId;
if (storyId) {
queryClient.setQueryData<Task[]>(['tasks', storyId], (old) => {
if (!old) return old;
return old.map((task) =>
task.id === taskId ? { ...task, status } : task
);
});
}
return { previousTask };
},
onSuccess: (updatedTask) => {
logger.info('Task status changed successfully:', updatedTask);
// Invalidate queries
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.id] });
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.storyId] });
queryClient.invalidateQueries({ queryKey: ['stories', updatedTask.storyId] }); // Update Story progress
toast.success(`Task marked as ${updatedTask.status}`);
},
onError: (error, variables, context) => {
logger.error('Failed to change task status:', error);
// Revert optimistic update
if (context?.previousTask) {
queryClient.setQueryData(['tasks', variables.taskId], context.previousTask);
}
toast.error('Failed to update Task status');
},
});
}
/**
* Assign Task to user
*/
export function useAssignTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ taskId, assigneeId }: { taskId: string; assigneeId: string }) => {
logger.info('Assigning task:', taskId, assigneeId);
return tasksApi.assign(taskId, assigneeId);
},
onSuccess: (updatedTask) => {
logger.info('Task assigned successfully:', updatedTask);
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.id] });
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.storyId] });
toast.success('Task assigned successfully');
},
onError: (error) => {
logger.error('Failed to assign task:', error);
toast.error('Failed to assign Task');
},
});
}
Acceptance Criteria
- Task API client methods added to
lib/api/pm.ts - All CRUD operations implemented (list, get, create, update, delete)
use-tasks.tshooks file createduseTasks(storyId)hook returns Task list for StoryuseCreateTask()hook creates Task with optimistic updateuseUpdateTask()hook updates Task with optimistic updateuseDeleteTask()hook deletes TaskuseChangeTaskStatus()hook changes status with optimistic update- All hooks include error handling with toast notifications
- All hooks include logger integration
- Cache invalidation strategies implemented correctly
- Optimistic updates provide instant UI feedback
- Hooks tested with real API data
Testing
Manual Testing:
// Test in a React component
function TestTaskHooks() {
const { data: tasks, isLoading } = useTasks('story-123');
const createTask = useCreateTask();
const handleCreate = () => {
createTask.mutate({
storyId: 'story-123',
title: 'Test Task',
priority: 'High',
estimatedHours: 8,
createdBy: 'user-123',
});
};
return (
<div>
<button onClick={handleCreate}>Create Task</button>
{isLoading && <p>Loading...</p>}
{tasks?.map((task) => (
<div key={task.id}>{task.title}</div>
))}
</div>
);
}
Test Cases:
- Create Task → Verify appears in list immediately (optimistic update)
- Update Task → Verify updates instantly
- Delete Task → Verify removes from list
- Change status → Verify checkbox updates instantly
- Error handling → Disconnect internet → Verify error toast shows
- Cache invalidation → Create Task → Verify Story task count updates
Dependencies
Prerequisites:
- Task 1 (API endpoints verified, types created)
- ✅ React Query configured
- ✅ apiClient ready (
lib/api/client.ts) - ✅ logger utility (
lib/utils/logger.ts) - ✅ sonner toast library
Blocks:
- Task 3, 4, 5 (components depend on hooks)
Estimated Time
3 hours
Notes
Optimistic Updates: Provide instant UI feedback by updating cache immediately, then reverting on error. This makes the app feel fast and responsive.
Cache Invalidation: When creating/updating/deleting Tasks, also invalidate the parent Story query to update task counts and progress indicators.
Code Reuse: Copy patterns from use-stories.ts hook - Task hooks are very similar to Story hooks.