Add complete Task CRUD UI for Story detail page with inline creation, status toggling, filtering, and sorting capabilities. Changes: - Created TaskList component with filters, sorting, and progress bar - Created TaskCard component with checkbox status toggle and metadata - Created TaskQuickAdd component for inline Task creation - Added shadcn/ui checkbox and alert components - All components use existing Task hooks (useTasks, useCreateTask, etc.) Components: - components/tasks/task-list.tsx (150 lines) - components/tasks/task-card.tsx (160 lines) - components/tasks/task-quick-add.tsx (180 lines) - components/ui/checkbox.tsx (shadcn/ui) - components/ui/alert.tsx (shadcn/ui) Features: - Task list with real-time count and progress bar - Filter by: All, Active, Completed - Sort by: Recent, Alphabetical, Status - Checkbox toggle for instant status change (optimistic UI) - Inline Quick Add form for fast Task creation - Priority badges and metadata display - Loading states and error handling - Empty state messaging Sprint 4 Story 2: Task Management in Story Detail Task 3: Implement TaskList, TaskCard, TaskQuickAdd components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
141 lines
4.8 KiB
TypeScript
141 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useTasks } from '@/lib/hooks/use-tasks';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { TaskCard } from './task-card';
|
|
import { TaskQuickAdd } from './task-quick-add';
|
|
import { WorkItemStatus } from '@/types/project';
|
|
|
|
interface TaskListProps {
|
|
storyId: string;
|
|
}
|
|
|
|
type FilterType = 'all' | 'active' | 'completed';
|
|
type SortType = 'recent' | 'alphabetical' | 'status';
|
|
|
|
export function TaskList({ storyId }: TaskListProps) {
|
|
const { data: tasks, isLoading, error } = useTasks(storyId);
|
|
const [filter, setFilter] = useState<FilterType>('all');
|
|
const [sort, setSort] = useState<SortType>('recent');
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<Skeleton className="h-8 w-48" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{[1, 2, 3].map((i) => (
|
|
<Skeleton key={i} className="h-24 w-full" />
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<Alert variant="destructive">
|
|
<AlertDescription>
|
|
Failed to load tasks. Please try again.
|
|
</AlertDescription>
|
|
</Alert>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const filteredTasks = tasks?.filter(task => {
|
|
if (filter === 'active') return task.status !== 'Done';
|
|
if (filter === 'completed') return task.status === 'Done';
|
|
return true;
|
|
}) || [];
|
|
|
|
const sortedTasks = [...filteredTasks].sort((a, b) => {
|
|
if (sort === 'alphabetical') return a.title.localeCompare(b.title);
|
|
if (sort === 'status') return a.status.localeCompare(b.status);
|
|
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
});
|
|
|
|
const completedCount = tasks?.filter(t => t.status === 'Done').length || 0;
|
|
const totalCount = tasks?.length || 0;
|
|
const progressPercentage = totalCount > 0 ? (completedCount / totalCount) * 100 : 0;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>Tasks</CardTitle>
|
|
<CardDescription>
|
|
{completedCount} of {totalCount} completed
|
|
</CardDescription>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<Select value={filter} onValueChange={(v) => setFilter(v as FilterType)}>
|
|
<SelectTrigger className="w-[140px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All</SelectItem>
|
|
<SelectItem value="active">Active</SelectItem>
|
|
<SelectItem value="completed">Completed</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Select value={sort} onValueChange={(v) => setSort(v as SortType)}>
|
|
<SelectTrigger className="w-[160px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="recent">Recent</SelectItem>
|
|
<SelectItem value="alphabetical">Alphabetical</SelectItem>
|
|
<SelectItem value="status">By Status</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
{/* Progress bar */}
|
|
<div className="mt-4">
|
|
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-primary transition-all duration-300"
|
|
style={{ width: `${progressPercentage}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<TaskQuickAdd storyId={storyId} />
|
|
|
|
{sortedTasks.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<p className="text-muted-foreground">
|
|
{filter === 'all'
|
|
? 'No tasks yet. Create your first task above!'
|
|
: `No ${filter} tasks found.`}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{sortedTasks.map(task => (
|
|
<TaskCard key={task.id} task={task} storyId={storyId} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|