feat(frontend): add Projects list page and components for #54

Implement the projects CRUD page with:
- ProjectCard: Card component with status badge, progress, metrics, actions
- ProjectFilters: Search, status filter, complexity, sort controls
- ProjectsGrid: Grid/list view toggle with loading and empty states
- useProjects hook: Mock data with filtering, sorting, pagination

Features include:
- Debounced search (300ms)
- Quick filters (status) and extended filters (complexity, sort)
- Grid and list view toggle
- Click navigation to project detail

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-01 17:20:17 +01:00
parent 6f5dd58b54
commit 50b865b23b
5 changed files with 1006 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
/**
* Projects List Page
*
* Displays all projects with filtering, sorting, and search.
* Supports grid and list view modes.
*
* @see Issue #54
*/
'use client';
import { useState, useCallback, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Link } from '@/lib/i18n/routing';
import { ProjectFilters, ProjectsGrid } from '@/components/projects';
import type { ViewMode, SortBy, SortOrder, Complexity } from '@/components/projects';
import type { ProjectStatus } from '@/components/projects/types';
import { useProjects, type ProjectListItem } from '@/lib/api/hooks/useProjects';
import { useDebounce } from '@/lib/hooks/useDebounce';
export default function ProjectsPage() {
const router = useRouter();
// Filter state
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<ProjectStatus | 'all'>('all');
const [complexityFilter, setComplexityFilter] = useState<Complexity>('all');
const [sortBy, setSortBy] = useState<SortBy>('recent');
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
const [viewMode, setViewMode] = useState<ViewMode>('grid');
// Debounce search for API calls
const debouncedSearch = useDebounce(searchQuery, 300);
// Fetch projects
const { data, isLoading, error } = useProjects({
search: debouncedSearch || undefined,
status: statusFilter,
complexity: complexityFilter !== 'all' ? complexityFilter : undefined,
sortBy,
sortOrder,
page: 1,
limit: 50,
});
// Check if any filters are active (for empty state message)
const hasFilters = useMemo(() => {
return searchQuery !== '' || statusFilter !== 'all' || complexityFilter !== 'all';
}, [searchQuery, statusFilter, complexityFilter]);
// Handle project card click
const handleProjectClick = useCallback(
(project: ProjectListItem) => {
router.push(`/projects/${project.id}`);
},
[router]
);
// Handle project action
const handleProjectAction = useCallback(
(project: ProjectListItem, action: 'archive' | 'pause' | 'resume' | 'delete') => {
// TODO: Implement actual API calls
switch (action) {
case 'archive':
toast.success(`Archived: ${project.name}`);
break;
case 'pause':
toast.info(`Paused: ${project.name}`);
break;
case 'resume':
toast.success(`Resumed: ${project.name}`);
break;
case 'delete':
toast.error(`Deleted: ${project.name}`);
break;
}
},
[]
);
// Show error toast if fetch fails
if (error) {
toast.error('Failed to load projects', {
description: 'Please try again later',
});
}
return (
<div className="container mx-auto px-4 py-6">
{/* Header */}
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-3xl font-bold">Projects</h1>
<p className="text-muted-foreground">Manage and monitor your projects</p>
</div>
<Button asChild>
<Link href="/projects/new">
<Plus className="mr-2 h-4 w-4" />
Create Project
</Link>
</Button>
</div>
{/* Filters */}
<ProjectFilters
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
complexityFilter={complexityFilter}
onComplexityFilterChange={setComplexityFilter}
sortBy={sortBy}
onSortByChange={setSortBy}
sortOrder={sortOrder}
onSortOrderChange={setSortOrder}
viewMode={viewMode}
onViewModeChange={setViewMode}
className="mb-6"
/>
{/* Projects Grid */}
<ProjectsGrid
projects={data?.data ?? []}
isLoading={isLoading}
viewMode={viewMode}
onProjectClick={handleProjectClick}
onProjectAction={handleProjectAction}
hasFilters={hasFilters}
/>
{/* Pagination - TODO: Add when more than 50 projects */}
{data && data.pagination.totalPages > 1 && (
<div className="mt-6 flex items-center justify-between text-sm text-muted-foreground">
<span>
Showing {data.data.length} of {data.pagination.total} projects
</span>
{/* TODO: Add pagination controls */}
</div>
)}
</div>
);
}