forked from cardosofelipe/fast-next-template
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:
23
frontend/src/app/[locale]/(authenticated)/page.tsx
Normal file
23
frontend/src/app/[locale]/(authenticated)/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Dashboard Page
|
||||||
|
*
|
||||||
|
* Main authenticated homepage showing:
|
||||||
|
* - Quick stats overview
|
||||||
|
* - Recent projects
|
||||||
|
* - Pending approvals
|
||||||
|
* - Real-time activity feed
|
||||||
|
*
|
||||||
|
* @see Issue #53
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
import { Dashboard } from '@/components/dashboard';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Dashboard',
|
||||||
|
description: 'Overview of your projects, agents, and activity',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
return <Dashboard />;
|
||||||
|
}
|
||||||
131
frontend/src/components/dashboard/Dashboard.tsx
Normal file
131
frontend/src/components/dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
/**
|
||||||
|
* Dashboard Component
|
||||||
|
*
|
||||||
|
* Main dashboard layout orchestrator.
|
||||||
|
* Combines all dashboard sub-components into a cohesive layout.
|
||||||
|
*
|
||||||
|
* Layout:
|
||||||
|
* +------------------------------------------+------------------+
|
||||||
|
* | Welcome Header | ACTIVITY |
|
||||||
|
* +------------------------------------------+ FEED |
|
||||||
|
* | Quick Stats (4 cards) | SIDEBAR |
|
||||||
|
* +------------------------------------------+ |
|
||||||
|
* | Recent Projects (3-6 cards) | |
|
||||||
|
* +------------------------------------------+ |
|
||||||
|
* | Pending Approvals (if any) | |
|
||||||
|
* +------------------------------------------+------------------+
|
||||||
|
*
|
||||||
|
* @see Issue #53
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { ActivityFeed } from '@/components/activity/ActivityFeed';
|
||||||
|
import { WelcomeHeader } from './WelcomeHeader';
|
||||||
|
import { DashboardQuickStats } from './DashboardQuickStats';
|
||||||
|
import { RecentProjects } from './RecentProjects';
|
||||||
|
import { PendingApprovals } from './PendingApprovals';
|
||||||
|
import { EmptyState } from './EmptyState';
|
||||||
|
import { useDashboard, type PendingApproval } from '@/lib/api/hooks/useDashboard';
|
||||||
|
import { useAuth } from '@/lib/auth/AuthContext';
|
||||||
|
import { useProjectEvents } from '@/lib/hooks/useProjectEvents';
|
||||||
|
import { useProjectEventsFromStore } from '@/lib/stores/eventStore';
|
||||||
|
|
||||||
|
export interface DashboardProps {
|
||||||
|
/** Additional CSS classes */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Dashboard({ className }: DashboardProps) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { data, isLoading, error } = useDashboard();
|
||||||
|
|
||||||
|
// Real-time events - using a generic project ID for dashboard-wide events
|
||||||
|
// In production, this would be a dedicated dashboard events endpoint
|
||||||
|
const { connectionState } = useProjectEvents('dashboard', {
|
||||||
|
autoConnect: true,
|
||||||
|
});
|
||||||
|
const events = useProjectEventsFromStore('dashboard');
|
||||||
|
|
||||||
|
// Get user's first name for empty state
|
||||||
|
const firstName = user?.first_name || user?.email?.split('@')[0] || 'there';
|
||||||
|
|
||||||
|
// Handle approval actions
|
||||||
|
const handleApprove = useCallback((approval: PendingApproval) => {
|
||||||
|
// TODO: Implement actual approval API call
|
||||||
|
toast.success(`Approved: ${approval.title}`, {
|
||||||
|
description: `${approval.projectName}`,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleReject = useCallback((approval: PendingApproval) => {
|
||||||
|
// TODO: Implement actual rejection API call
|
||||||
|
toast.info(`Rejected: ${approval.title}`, {
|
||||||
|
description: `${approval.projectName}`,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Show error state
|
||||||
|
if (error) {
|
||||||
|
toast.error('Failed to load dashboard data', {
|
||||||
|
description: 'Please try refreshing the page',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has no projects (empty state)
|
||||||
|
const hasNoProjects = !isLoading && (!data?.recentProjects || data.recentProjects.length === 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="container mx-auto px-4 py-6">
|
||||||
|
{/* Welcome Header - always shown */}
|
||||||
|
<WelcomeHeader className="mb-6" />
|
||||||
|
|
||||||
|
{hasNoProjects ? (
|
||||||
|
// Empty state for new users
|
||||||
|
<EmptyState userName={firstName} />
|
||||||
|
) : (
|
||||||
|
// Main dashboard layout
|
||||||
|
<div className="grid gap-6 lg:grid-cols-[1fr_350px]">
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Quick Stats */}
|
||||||
|
<DashboardQuickStats stats={data?.stats} isLoading={isLoading} />
|
||||||
|
|
||||||
|
{/* Recent Projects */}
|
||||||
|
<RecentProjects projects={data?.recentProjects} isLoading={isLoading} />
|
||||||
|
|
||||||
|
{/* Pending Approvals */}
|
||||||
|
<PendingApprovals
|
||||||
|
approvals={data?.pendingApprovals}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onApprove={handleApprove}
|
||||||
|
onReject={handleReject}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Activity Feed Sidebar */}
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<Card className="sticky top-4">
|
||||||
|
<ActivityFeed
|
||||||
|
events={events}
|
||||||
|
connectionState={connectionState}
|
||||||
|
isLoading={isLoading}
|
||||||
|
maxHeight={600}
|
||||||
|
showHeader
|
||||||
|
title="Recent Activity"
|
||||||
|
enableFiltering={false}
|
||||||
|
enableSearch={false}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
frontend/src/components/dashboard/DashboardQuickStats.tsx
Normal file
63
frontend/src/components/dashboard/DashboardQuickStats.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* DashboardQuickStats Component
|
||||||
|
*
|
||||||
|
* Displays quick stats cards for the dashboard:
|
||||||
|
* - Active Projects
|
||||||
|
* - Running Agents
|
||||||
|
* - Open Issues
|
||||||
|
* - Pending Approvals
|
||||||
|
*
|
||||||
|
* @see Issue #53
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Folder, Bot, CircleDot, AlertCircle } from 'lucide-react';
|
||||||
|
import { StatCard } from '@/components/admin/StatCard';
|
||||||
|
import type { DashboardStats } from '@/lib/api/hooks/useDashboard';
|
||||||
|
|
||||||
|
export interface DashboardQuickStatsProps {
|
||||||
|
/** Stats data */
|
||||||
|
stats?: DashboardStats;
|
||||||
|
/** Whether data is loading */
|
||||||
|
isLoading?: boolean;
|
||||||
|
/** Additional CSS classes */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardQuickStats({ stats, isLoading = false, className }: DashboardQuickStatsProps) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<StatCard
|
||||||
|
title="Active Projects"
|
||||||
|
value={stats?.activeProjects ?? 0}
|
||||||
|
icon={Folder}
|
||||||
|
description="Currently in progress"
|
||||||
|
loading={isLoading}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Running Agents"
|
||||||
|
value={stats?.runningAgents ?? 0}
|
||||||
|
icon={Bot}
|
||||||
|
description="Working on tasks"
|
||||||
|
loading={isLoading}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Open Issues"
|
||||||
|
value={stats?.openIssues ?? 0}
|
||||||
|
icon={CircleDot}
|
||||||
|
description="Across all projects"
|
||||||
|
loading={isLoading}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Pending Approvals"
|
||||||
|
value={stats?.pendingApprovals ?? 0}
|
||||||
|
icon={AlertCircle}
|
||||||
|
description="Awaiting your review"
|
||||||
|
loading={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
frontend/src/components/dashboard/EmptyState.tsx
Normal file
61
frontend/src/components/dashboard/EmptyState.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* EmptyState Component
|
||||||
|
*
|
||||||
|
* Displays a welcome message for new users with no projects.
|
||||||
|
* Provides call-to-action to create first project.
|
||||||
|
*
|
||||||
|
* @see Issue #53
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Rocket, Bot, Settings } from 'lucide-react';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Link } from '@/lib/i18n/routing';
|
||||||
|
|
||||||
|
export interface EmptyStateProps {
|
||||||
|
/** User's first name for personalization */
|
||||||
|
userName?: string;
|
||||||
|
/** Additional CSS classes */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyState({ userName = 'there', className }: EmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<Card className={className}>
|
||||||
|
<CardContent className="py-12 text-center">
|
||||||
|
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
|
||||||
|
<Rocket className="h-8 w-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-bold">Welcome to Syndarix, {userName}!</h2>
|
||||||
|
<p className="mx-auto mt-2 max-w-md text-muted-foreground">
|
||||||
|
Get started by creating your first project. Our AI agents will help you
|
||||||
|
turn your ideas into reality.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button size="lg" asChild className="mt-6">
|
||||||
|
<Link href="/projects/new">Create Your First Project</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
||||||
|
<Link
|
||||||
|
href="/agents"
|
||||||
|
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<Bot className="h-4 w-4" />
|
||||||
|
Set up AI agent types
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/settings"
|
||||||
|
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
Configure your account
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
195
frontend/src/components/dashboard/PendingApprovals.tsx
Normal file
195
frontend/src/components/dashboard/PendingApprovals.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* PendingApprovals Component
|
||||||
|
*
|
||||||
|
* Displays pending approval requests that need user attention.
|
||||||
|
* Only renders when there are approvals to show.
|
||||||
|
*
|
||||||
|
* @see Issue #53
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
GitBranch,
|
||||||
|
Code2,
|
||||||
|
Building2,
|
||||||
|
Rocket,
|
||||||
|
} 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 type { PendingApproval } from '@/lib/api/hooks/useDashboard';
|
||||||
|
|
||||||
|
export interface PendingApprovalsProps {
|
||||||
|
/** Pending approvals to display */
|
||||||
|
approvals?: PendingApproval[];
|
||||||
|
/** Whether data is loading */
|
||||||
|
isLoading?: boolean;
|
||||||
|
/** Callback when approval is approved */
|
||||||
|
onApprove?: (approval: PendingApproval) => void;
|
||||||
|
/** Callback when approval is rejected */
|
||||||
|
onReject?: (approval: PendingApproval) => void;
|
||||||
|
/** Additional CSS classes */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeConfig: Record<
|
||||||
|
PendingApproval['type'],
|
||||||
|
{ icon: typeof GitBranch; label: string; color: string }
|
||||||
|
> = {
|
||||||
|
sprint_boundary: {
|
||||||
|
icon: GitBranch,
|
||||||
|
label: 'Sprint Boundary',
|
||||||
|
color: 'text-blue-500',
|
||||||
|
},
|
||||||
|
code_review: {
|
||||||
|
icon: Code2,
|
||||||
|
label: 'Code Review',
|
||||||
|
color: 'text-purple-500',
|
||||||
|
},
|
||||||
|
architecture_decision: {
|
||||||
|
icon: Building2,
|
||||||
|
label: 'Architecture',
|
||||||
|
color: 'text-orange-500',
|
||||||
|
},
|
||||||
|
deployment: {
|
||||||
|
icon: Rocket,
|
||||||
|
label: 'Deployment',
|
||||||
|
color: 'text-green-500',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const priorityConfig: Record<PendingApproval['priority'], { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline' }> = {
|
||||||
|
low: { label: 'Low', variant: 'outline' },
|
||||||
|
medium: { label: 'Medium', variant: 'secondary' },
|
||||||
|
high: { label: 'High', variant: 'default' },
|
||||||
|
critical: { label: 'Critical', variant: 'destructive' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function ApprovalSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-4 rounded-lg border p-4">
|
||||||
|
<Skeleton className="h-10 w-10 rounded-full" />
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<Skeleton className="h-5 w-3/4" />
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
<Skeleton className="h-4 w-1/2" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Skeleton className="h-9 w-20" />
|
||||||
|
<Skeleton className="h-9 w-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApprovalItemProps {
|
||||||
|
approval: PendingApproval;
|
||||||
|
onApprove?: () => void;
|
||||||
|
onReject?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApprovalItem({ approval, onApprove, onReject }: ApprovalItemProps) {
|
||||||
|
const config = typeConfig[approval.type];
|
||||||
|
const Icon = config.icon;
|
||||||
|
const priority = priorityConfig[approval.priority];
|
||||||
|
|
||||||
|
const timeAgo = formatDistanceToNow(new Date(approval.requestedAt), { addSuffix: true });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:items-start">
|
||||||
|
<div className={cn('flex h-10 w-10 items-center justify-center rounded-full bg-muted', config.color)}>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-1">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<h4 className="font-medium">{approval.title}</h4>
|
||||||
|
<Badge variant={priority.variant} className="text-xs">
|
||||||
|
{priority.label}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">{approval.description}</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Link
|
||||||
|
href={`/projects/${approval.projectId}`}
|
||||||
|
className="hover:text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{approval.projectName}
|
||||||
|
</Link>
|
||||||
|
<span>-</span>
|
||||||
|
<span>Requested by {approval.requestedBy}</span>
|
||||||
|
<span>-</span>
|
||||||
|
<span>{timeAgo}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||||
|
onClick={onReject}
|
||||||
|
>
|
||||||
|
<XCircle className="mr-1 h-4 w-4" />
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={onApprove}>
|
||||||
|
<CheckCircle2 className="mr-1 h-4 w-4" />
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PendingApprovals({
|
||||||
|
approvals,
|
||||||
|
isLoading = false,
|
||||||
|
onApprove,
|
||||||
|
onReject,
|
||||||
|
className,
|
||||||
|
}: PendingApprovalsProps) {
|
||||||
|
// Don't render if no approvals and not loading
|
||||||
|
if (!isLoading && (!approvals || approvals.length === 0)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={className}>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-amber-500" />
|
||||||
|
<CardTitle className="text-lg">Pending Approvals</CardTitle>
|
||||||
|
{approvals && approvals.length > 0 && (
|
||||||
|
<Badge variant="secondary">{approvals.length}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<ApprovalSkeleton />
|
||||||
|
<ApprovalSkeleton />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
approvals?.map((approval) => (
|
||||||
|
<ApprovalItem
|
||||||
|
key={approval.id}
|
||||||
|
approval={approval}
|
||||||
|
onApprove={() => onApprove?.(approval)}
|
||||||
|
onReject={() => onReject?.(approval)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
frontend/src/components/dashboard/RecentProjects.tsx
Normal file
152
frontend/src/components/dashboard/RecentProjects.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
frontend/src/components/dashboard/WelcomeHeader.tsx
Normal file
55
frontend/src/components/dashboard/WelcomeHeader.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* WelcomeHeader Component
|
||||||
|
*
|
||||||
|
* Displays a personalized welcome message for the dashboard.
|
||||||
|
*
|
||||||
|
* @see Issue #53
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Link } from '@/lib/i18n/routing';
|
||||||
|
import { useAuth } from '@/lib/auth/AuthContext';
|
||||||
|
|
||||||
|
export interface WelcomeHeaderProps {
|
||||||
|
/** Additional CSS classes */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WelcomeHeader({ className }: WelcomeHeaderProps) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
// Get first name for greeting
|
||||||
|
const firstName = user?.first_name || user?.email?.split('@')[0] || 'there';
|
||||||
|
|
||||||
|
// Get time-based greeting
|
||||||
|
const getGreeting = () => {
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
if (hour < 12) return 'Good morning';
|
||||||
|
if (hour < 18) return 'Good afternoon';
|
||||||
|
return 'Good evening';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
{getGreeting()}, {firstName}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-1 text-muted-foreground">
|
||||||
|
Here's what's happening with your projects today.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/projects/new">
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Create Project
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
frontend/src/components/dashboard/index.ts
Normal file
23
frontend/src/components/dashboard/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Dashboard Components
|
||||||
|
*
|
||||||
|
* Exports all dashboard-related components.
|
||||||
|
*
|
||||||
|
* @module components/dashboard
|
||||||
|
* @see Issue #53
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { Dashboard } from './Dashboard';
|
||||||
|
export { WelcomeHeader } from './WelcomeHeader';
|
||||||
|
export { DashboardQuickStats } from './DashboardQuickStats';
|
||||||
|
export { RecentProjects } from './RecentProjects';
|
||||||
|
export { PendingApprovals } from './PendingApprovals';
|
||||||
|
export { EmptyState } from './EmptyState';
|
||||||
|
|
||||||
|
// Re-export types
|
||||||
|
export type { DashboardProps } from './Dashboard';
|
||||||
|
export type { WelcomeHeaderProps } from './WelcomeHeader';
|
||||||
|
export type { DashboardQuickStatsProps } from './DashboardQuickStats';
|
||||||
|
export type { RecentProjectsProps } from './RecentProjects';
|
||||||
|
export type { PendingApprovalsProps } from './PendingApprovals';
|
||||||
|
export type { EmptyStateProps } from './EmptyState';
|
||||||
257
frontend/src/lib/api/hooks/useDashboard.ts
Normal file
257
frontend/src/lib/api/hooks/useDashboard.ts
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
/**
|
||||||
|
* Dashboard Data Hook
|
||||||
|
*
|
||||||
|
* Provides data for the main dashboard including:
|
||||||
|
* - Quick stats (projects, agents, issues, approvals)
|
||||||
|
* - Recent projects
|
||||||
|
* - Pending approvals
|
||||||
|
*
|
||||||
|
* Uses mock data until backend endpoints are available.
|
||||||
|
*
|
||||||
|
* @see Issue #53
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { Project, ProjectStatus } from '@/components/projects/types';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface DashboardStats {
|
||||||
|
activeProjects: number;
|
||||||
|
runningAgents: number;
|
||||||
|
openIssues: number;
|
||||||
|
pendingApprovals: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardProject extends Project {
|
||||||
|
progress: number;
|
||||||
|
openIssues: number;
|
||||||
|
activeAgents: number;
|
||||||
|
currentSprint?: string;
|
||||||
|
lastActivity: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PendingApproval {
|
||||||
|
id: string;
|
||||||
|
type: 'sprint_boundary' | 'code_review' | 'architecture_decision' | 'deployment';
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
projectId: string;
|
||||||
|
projectName: string;
|
||||||
|
requestedBy: string;
|
||||||
|
requestedAt: string;
|
||||||
|
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardData {
|
||||||
|
stats: DashboardStats;
|
||||||
|
recentProjects: DashboardProject[];
|
||||||
|
pendingApprovals: PendingApproval[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Mock Data
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const mockStats: DashboardStats = {
|
||||||
|
activeProjects: 3,
|
||||||
|
runningAgents: 8,
|
||||||
|
openIssues: 24,
|
||||||
|
pendingApprovals: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProjects: DashboardProject[] = [
|
||||||
|
{
|
||||||
|
id: 'proj-001',
|
||||||
|
name: 'E-Commerce Platform Redesign',
|
||||||
|
description: 'Complete redesign of the e-commerce platform with modern UI/UX',
|
||||||
|
status: 'active' as ProjectStatus,
|
||||||
|
autonomy_level: 'milestone',
|
||||||
|
created_at: '2025-11-15T10:00:00Z',
|
||||||
|
updated_at: '2025-12-30T14:30:00Z',
|
||||||
|
owner_id: 'user-001',
|
||||||
|
progress: 67,
|
||||||
|
openIssues: 12,
|
||||||
|
activeAgents: 4,
|
||||||
|
currentSprint: 'Sprint 3',
|
||||||
|
lastActivity: '2 minutes ago',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'proj-002',
|
||||||
|
name: 'Mobile Banking App',
|
||||||
|
description: 'Native mobile app for banking services with biometric authentication',
|
||||||
|
status: 'active' as ProjectStatus,
|
||||||
|
autonomy_level: 'autonomous',
|
||||||
|
created_at: '2025-11-20T09:00:00Z',
|
||||||
|
updated_at: '2025-12-30T12:00:00Z',
|
||||||
|
owner_id: 'user-001',
|
||||||
|
progress: 45,
|
||||||
|
openIssues: 8,
|
||||||
|
activeAgents: 5,
|
||||||
|
currentSprint: 'Sprint 2',
|
||||||
|
lastActivity: '15 minutes ago',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'proj-003',
|
||||||
|
name: 'Internal HR Portal',
|
||||||
|
description: 'Employee self-service portal for HR operations',
|
||||||
|
status: 'paused' as ProjectStatus,
|
||||||
|
autonomy_level: 'full_control',
|
||||||
|
created_at: '2025-10-01T08:00:00Z',
|
||||||
|
updated_at: '2025-12-28T16:00:00Z',
|
||||||
|
owner_id: 'user-001',
|
||||||
|
progress: 23,
|
||||||
|
openIssues: 5,
|
||||||
|
activeAgents: 0,
|
||||||
|
currentSprint: 'Sprint 1',
|
||||||
|
lastActivity: '2 days ago',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'proj-004',
|
||||||
|
name: 'API Gateway Modernization',
|
||||||
|
description: 'Migrate legacy API gateway to cloud-native architecture',
|
||||||
|
status: 'active' as ProjectStatus,
|
||||||
|
autonomy_level: 'milestone',
|
||||||
|
created_at: '2025-12-01T11:00:00Z',
|
||||||
|
updated_at: '2025-12-30T10:00:00Z',
|
||||||
|
owner_id: 'user-001',
|
||||||
|
progress: 82,
|
||||||
|
openIssues: 3,
|
||||||
|
activeAgents: 2,
|
||||||
|
currentSprint: 'Sprint 4',
|
||||||
|
lastActivity: '1 hour ago',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'proj-005',
|
||||||
|
name: 'Customer Analytics Dashboard',
|
||||||
|
description: 'Real-time analytics dashboard for customer behavior insights',
|
||||||
|
status: 'completed' as ProjectStatus,
|
||||||
|
autonomy_level: 'autonomous',
|
||||||
|
created_at: '2025-09-01T10:00:00Z',
|
||||||
|
updated_at: '2025-12-15T17:00:00Z',
|
||||||
|
owner_id: 'user-001',
|
||||||
|
progress: 100,
|
||||||
|
openIssues: 0,
|
||||||
|
activeAgents: 0,
|
||||||
|
lastActivity: '2 weeks ago',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'proj-006',
|
||||||
|
name: 'DevOps Pipeline Automation',
|
||||||
|
description: 'Automate CI/CD pipelines with AI-assisted deployments',
|
||||||
|
status: 'active' as ProjectStatus,
|
||||||
|
autonomy_level: 'milestone',
|
||||||
|
created_at: '2025-12-10T14:00:00Z',
|
||||||
|
updated_at: '2025-12-30T09:00:00Z',
|
||||||
|
owner_id: 'user-001',
|
||||||
|
progress: 35,
|
||||||
|
openIssues: 6,
|
||||||
|
activeAgents: 3,
|
||||||
|
currentSprint: 'Sprint 1',
|
||||||
|
lastActivity: '30 minutes ago',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockApprovals: PendingApproval[] = [
|
||||||
|
{
|
||||||
|
id: 'approval-001',
|
||||||
|
type: 'sprint_boundary',
|
||||||
|
title: 'Sprint 3 Completion Review',
|
||||||
|
description: 'Review sprint deliverables and approve transition to Sprint 4',
|
||||||
|
projectId: 'proj-001',
|
||||||
|
projectName: 'E-Commerce Platform Redesign',
|
||||||
|
requestedBy: 'Product Owner Agent',
|
||||||
|
requestedAt: '2025-12-30T14:00:00Z',
|
||||||
|
priority: 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'approval-002',
|
||||||
|
type: 'architecture_decision',
|
||||||
|
title: 'Database Migration Strategy',
|
||||||
|
description: 'Approve PostgreSQL to CockroachDB migration plan',
|
||||||
|
projectId: 'proj-004',
|
||||||
|
projectName: 'API Gateway Modernization',
|
||||||
|
requestedBy: 'Architect Agent',
|
||||||
|
requestedAt: '2025-12-30T10:30:00Z',
|
||||||
|
priority: 'medium',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Hook
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches dashboard data (stats, recent projects, pending approvals)
|
||||||
|
*
|
||||||
|
* @returns Query result with dashboard data
|
||||||
|
*/
|
||||||
|
export function useDashboard() {
|
||||||
|
return useQuery<DashboardData>({
|
||||||
|
queryKey: ['dashboard'],
|
||||||
|
queryFn: async () => {
|
||||||
|
// Simulate network delay
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// Return mock data
|
||||||
|
// TODO: Replace with actual API call when backend is ready
|
||||||
|
// const response = await apiClient.get('/api/v1/dashboard');
|
||||||
|
// return response.data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
stats: mockStats,
|
||||||
|
recentProjects: mockProjects,
|
||||||
|
pendingApprovals: mockApprovals,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
staleTime: 30000, // 30 seconds
|
||||||
|
refetchInterval: 60000, // Refetch every minute
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches only dashboard stats
|
||||||
|
*/
|
||||||
|
export function useDashboardStats() {
|
||||||
|
return useQuery<DashboardStats>({
|
||||||
|
queryKey: ['dashboard', 'stats'],
|
||||||
|
queryFn: async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
return mockStats;
|
||||||
|
},
|
||||||
|
staleTime: 30000,
|
||||||
|
refetchInterval: 60000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches recent projects for dashboard
|
||||||
|
*
|
||||||
|
* @param limit - Maximum number of projects to return
|
||||||
|
*/
|
||||||
|
export function useRecentProjects(limit: number = 6) {
|
||||||
|
return useQuery<DashboardProject[]>({
|
||||||
|
queryKey: ['dashboard', 'recentProjects', limit],
|
||||||
|
queryFn: async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||||
|
return mockProjects.slice(0, limit);
|
||||||
|
},
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches pending approvals
|
||||||
|
*/
|
||||||
|
export function usePendingApprovals() {
|
||||||
|
return useQuery<PendingApproval[]>({
|
||||||
|
queryKey: ['dashboard', 'pendingApprovals'],
|
||||||
|
queryFn: async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
return mockApprovals;
|
||||||
|
},
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user