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:
145
frontend/src/app/[locale]/(authenticated)/projects/page.tsx
Normal file
145
frontend/src/app/[locale]/(authenticated)/projects/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user