feat(frontend): add Dashboard page and components for #53

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>
This commit is contained in:
2026-01-01 17:19:59 +01:00
parent 0ceee8545e
commit 6f5dd58b54
9 changed files with 960 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
/**
* 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>
);
}