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,119 @@
/**
* ProjectsGrid Component
*
* Displays projects in either grid or list view with
* loading and empty states.
*
* @see Issue #54
*/
'use client';
import { Folder, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Link } from '@/lib/i18n/routing';
import { cn } from '@/lib/utils';
import { ProjectCard, ProjectCardSkeleton } from './ProjectCard';
import type { ProjectListItem } from '@/lib/api/hooks/useProjects';
import type { ViewMode } from './ProjectFilters';
export interface ProjectsGridProps {
/** Projects to display */
projects: ProjectListItem[];
/** Whether data is loading */
isLoading?: boolean;
/** Current view mode */
viewMode?: ViewMode;
/** Called when a project card is clicked */
onProjectClick?: (project: ProjectListItem) => void;
/** Called when a project action is selected */
onProjectAction?: (project: ProjectListItem, action: 'archive' | 'pause' | 'resume' | 'delete') => void;
/** Whether filters are currently applied (affects empty state message) */
hasFilters?: boolean;
/** Additional CSS classes */
className?: string;
}
/**
* Empty state component
*/
function EmptyState({ hasFilters }: { hasFilters: boolean }) {
return (
<div className="py-16 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted">
<Folder className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold">No projects found</h3>
<p className="mt-1 text-muted-foreground">
{hasFilters
? 'Try adjusting your filters or search query'
: 'Get started by creating your first project'}
</p>
{!hasFilters && (
<Button asChild className="mt-4">
<Link href="/projects/new">
<Plus className="mr-2 h-4 w-4" />
Create Project
</Link>
</Button>
)}
</div>
);
}
/**
* Loading skeleton grid
*/
function LoadingSkeleton({ viewMode }: { viewMode: ViewMode }) {
return (
<div
className={cn(
viewMode === 'grid'
? 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'
: 'space-y-4'
)}
>
{[1, 2, 3, 4, 5, 6].map((i) => (
<ProjectCardSkeleton key={i} />
))}
</div>
);
}
export function ProjectsGrid({
projects,
isLoading = false,
viewMode = 'grid',
onProjectClick,
onProjectAction,
hasFilters = false,
className,
}: ProjectsGridProps) {
if (isLoading) {
return <LoadingSkeleton viewMode={viewMode} />;
}
if (projects.length === 0) {
return <EmptyState hasFilters={hasFilters} />;
}
return (
<div
className={cn(
viewMode === 'grid'
? 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'
: 'space-y-4',
className
)}
>
{projects.map((project) => (
<ProjectCard
key={project.id}
project={project}
onClick={() => onProjectClick?.(project)}
onAction={onProjectAction ? (action) => onProjectAction(project, action) : undefined}
/>
))}
</div>
);
}