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:
119
frontend/src/components/projects/ProjectsGrid.tsx
Normal file
119
frontend/src/components/projects/ProjectsGrid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user