Implement the main dashboard homepage with: - WelcomeHeader: Personalized greeting with user name - DashboardQuickStats: Stats cards for projects, agents, issues, approvals - RecentProjects: Dynamic grid showing 3-6 recent projects - PendingApprovals: Action-required approvals section - EmptyState: Onboarding experience for new users - useDashboard hook: Mock data fetching with React Query The dashboard serves as the authenticated homepage at /(authenticated)/ and provides quick access to all project management features. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
/**
|
|
* RecentProjects Component
|
|
*
|
|
* Displays recent projects in a responsive grid with a "View all" link.
|
|
* Shows 3 projects on mobile, 6 on desktop.
|
|
*
|
|
* @see Issue #53
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { ArrowRight, Bot, CircleDot, Clock } from 'lucide-react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { Link } from '@/lib/i18n/routing';
|
|
import { cn } from '@/lib/utils';
|
|
import { ProjectStatusBadge } from '@/components/projects/StatusBadge';
|
|
import { ProgressBar } from '@/components/projects/ProgressBar';
|
|
import type { DashboardProject } from '@/lib/api/hooks/useDashboard';
|
|
|
|
export interface RecentProjectsProps {
|
|
/** Projects to display */
|
|
projects?: DashboardProject[];
|
|
/** Whether data is loading */
|
|
isLoading?: boolean;
|
|
/** Additional CSS classes */
|
|
className?: string;
|
|
}
|
|
|
|
function ProjectCardSkeleton() {
|
|
return (
|
|
<Card className="animate-pulse">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<Skeleton className="h-5 w-20" />
|
|
<Skeleton className="h-5 w-16" />
|
|
</div>
|
|
<Skeleton className="mt-2 h-6 w-3/4" />
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<Skeleton className="h-4 w-full" />
|
|
<Skeleton className="h-2 w-full" />
|
|
<div className="flex justify-between">
|
|
<Skeleton className="h-4 w-20" />
|
|
<Skeleton className="h-4 w-16" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
interface ProjectCardProps {
|
|
project: DashboardProject;
|
|
}
|
|
|
|
function ProjectCard({ project }: ProjectCardProps) {
|
|
return (
|
|
<Link href={`/projects/${project.id}`} className="block">
|
|
<Card className="h-full cursor-pointer transition-all hover:border-primary hover:shadow-md">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<ProjectStatusBadge status={project.status} />
|
|
{project.currentSprint && (
|
|
<Badge variant="outline" className="text-xs">
|
|
{project.currentSprint}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<CardTitle className="mt-2 line-clamp-1 text-lg">{project.name}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{project.description && (
|
|
<p className="line-clamp-2 text-sm text-muted-foreground">{project.description}</p>
|
|
)}
|
|
|
|
<ProgressBar value={project.progress} size="sm" showLabel />
|
|
|
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
<div className="flex items-center gap-3">
|
|
<span className="flex items-center gap-1">
|
|
<Bot className="h-3 w-3" />
|
|
{project.activeAgents} agents
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<CircleDot className="h-3 w-3" />
|
|
{project.openIssues} issues
|
|
</span>
|
|
</div>
|
|
<span className="flex items-center gap-1">
|
|
<Clock className="h-3 w-3" />
|
|
{project.lastActivity}
|
|
</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
export function RecentProjects({ projects, isLoading = false, className }: RecentProjectsProps) {
|
|
// Show first 3 on mobile (hidden beyond), 6 on desktop
|
|
const displayProjects = projects?.slice(0, 6) ?? [];
|
|
|
|
return (
|
|
<div className={className}>
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h2 className="text-xl font-semibold">Recent Projects</h2>
|
|
<Button variant="ghost" size="sm" asChild>
|
|
<Link href="/projects" className="gap-1">
|
|
View all
|
|
<ArrowRight className="h-4 w-4" />
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
|
<div
|
|
key={i}
|
|
className={cn(i > 3 && 'hidden lg:block')}
|
|
>
|
|
<ProjectCardSkeleton />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : displayProjects.length === 0 ? (
|
|
<Card className="py-8 text-center">
|
|
<CardContent>
|
|
<p className="text-muted-foreground">No projects yet</p>
|
|
<Button asChild className="mt-4">
|
|
<Link href="/projects/new">Create your first project</Link>
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{displayProjects.map((project, index) => (
|
|
<div
|
|
key={project.id}
|
|
className={cn(index >= 3 && 'hidden lg:block')}
|
|
>
|
|
<ProjectCard project={project} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|