From 6f5dd58b5437ed09334e2a486b7c0dfbf3473483 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Thu, 1 Jan 2026 17:19:59 +0100 Subject: [PATCH] feat(frontend): add Dashboard page and components for #53 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/app/[locale]/(authenticated)/page.tsx | 23 ++ .../src/components/dashboard/Dashboard.tsx | 131 +++++++++ .../dashboard/DashboardQuickStats.tsx | 63 +++++ .../src/components/dashboard/EmptyState.tsx | 61 +++++ .../components/dashboard/PendingApprovals.tsx | 195 +++++++++++++ .../components/dashboard/RecentProjects.tsx | 152 +++++++++++ .../components/dashboard/WelcomeHeader.tsx | 55 ++++ frontend/src/components/dashboard/index.ts | 23 ++ frontend/src/lib/api/hooks/useDashboard.ts | 257 ++++++++++++++++++ 9 files changed, 960 insertions(+) create mode 100644 frontend/src/app/[locale]/(authenticated)/page.tsx create mode 100644 frontend/src/components/dashboard/Dashboard.tsx create mode 100644 frontend/src/components/dashboard/DashboardQuickStats.tsx create mode 100644 frontend/src/components/dashboard/EmptyState.tsx create mode 100644 frontend/src/components/dashboard/PendingApprovals.tsx create mode 100644 frontend/src/components/dashboard/RecentProjects.tsx create mode 100644 frontend/src/components/dashboard/WelcomeHeader.tsx create mode 100644 frontend/src/components/dashboard/index.ts create mode 100644 frontend/src/lib/api/hooks/useDashboard.ts diff --git a/frontend/src/app/[locale]/(authenticated)/page.tsx b/frontend/src/app/[locale]/(authenticated)/page.tsx new file mode 100644 index 0000000..43e0755 --- /dev/null +++ b/frontend/src/app/[locale]/(authenticated)/page.tsx @@ -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 ; +} diff --git a/frontend/src/components/dashboard/Dashboard.tsx b/frontend/src/components/dashboard/Dashboard.tsx new file mode 100644 index 0000000..5c130ab --- /dev/null +++ b/frontend/src/components/dashboard/Dashboard.tsx @@ -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 ( +
+
+ {/* Welcome Header - always shown */} + + + {hasNoProjects ? ( + // Empty state for new users + + ) : ( + // Main dashboard layout +
+ {/* Main Content */} +
+ {/* Quick Stats */} + + + {/* Recent Projects */} + + + {/* Pending Approvals */} + +
+ + {/* Activity Feed Sidebar */} +
+ + + +
+
+ )} +
+
+ ); +} diff --git a/frontend/src/components/dashboard/DashboardQuickStats.tsx b/frontend/src/components/dashboard/DashboardQuickStats.tsx new file mode 100644 index 0000000..20ea65e --- /dev/null +++ b/frontend/src/components/dashboard/DashboardQuickStats.tsx @@ -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 ( +
+
+ + + + +
+
+ ); +} diff --git a/frontend/src/components/dashboard/EmptyState.tsx b/frontend/src/components/dashboard/EmptyState.tsx new file mode 100644 index 0000000..58e7cb5 --- /dev/null +++ b/frontend/src/components/dashboard/EmptyState.tsx @@ -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 ( + + +
+ +
+ +

Welcome to Syndarix, {userName}!

+

+ Get started by creating your first project. Our AI agents will help you + turn your ideas into reality. +

+ + + +
+ + + Set up AI agent types + + + + Configure your account + +
+
+
+ ); +} diff --git a/frontend/src/components/dashboard/PendingApprovals.tsx b/frontend/src/components/dashboard/PendingApprovals.tsx new file mode 100644 index 0000000..91fbc23 --- /dev/null +++ b/frontend/src/components/dashboard/PendingApprovals.tsx @@ -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 = { + low: { label: 'Low', variant: 'outline' }, + medium: { label: 'Medium', variant: 'secondary' }, + high: { label: 'High', variant: 'default' }, + critical: { label: 'Critical', variant: 'destructive' }, +}; + +function ApprovalSkeleton() { + return ( +
+ +
+ + + +
+
+ + +
+
+ ); +} + +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 ( +
+
+ +
+ +
+
+

{approval.title}

+ + {priority.label} + +
+

{approval.description}

+
+ + {approval.projectName} + + - + Requested by {approval.requestedBy} + - + {timeAgo} +
+
+ +
+ + +
+
+ ); +} + +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 ( + + +
+ + Pending Approvals + {approvals && approvals.length > 0 && ( + {approvals.length} + )} +
+
+ + {isLoading ? ( + <> + + + + ) : ( + approvals?.map((approval) => ( + onApprove?.(approval)} + onReject={() => onReject?.(approval)} + /> + )) + )} + +
+ ); +} diff --git a/frontend/src/components/dashboard/RecentProjects.tsx b/frontend/src/components/dashboard/RecentProjects.tsx new file mode 100644 index 0000000..0185b98 --- /dev/null +++ b/frontend/src/components/dashboard/RecentProjects.tsx @@ -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 ( + + +
+ + +
+ +
+ + + +
+ + +
+
+
+ ); +} + +interface ProjectCardProps { + project: DashboardProject; +} + +function ProjectCard({ project }: ProjectCardProps) { + return ( + + + +
+ + {project.currentSprint && ( + + {project.currentSprint} + + )} +
+ {project.name} +
+ + {project.description && ( +

{project.description}

+ )} + + + +
+
+ + + {project.activeAgents} agents + + + + {project.openIssues} issues + +
+ + + {project.lastActivity} + +
+
+
+ + ); +} + +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 ( +
+
+

Recent Projects

+ +
+ + {isLoading ? ( +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( +
3 && 'hidden lg:block')} + > + +
+ ))} +
+ ) : displayProjects.length === 0 ? ( + + +

No projects yet

+ +
+
+ ) : ( +
+ {displayProjects.map((project, index) => ( +
= 3 && 'hidden lg:block')} + > + +
+ ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/dashboard/WelcomeHeader.tsx b/frontend/src/components/dashboard/WelcomeHeader.tsx new file mode 100644 index 0000000..420df36 --- /dev/null +++ b/frontend/src/components/dashboard/WelcomeHeader.tsx @@ -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 ( +
+
+
+

+ {getGreeting()}, {firstName} +

+

+ Here's what's happening with your projects today. +

+
+ +
+
+ ); +} diff --git a/frontend/src/components/dashboard/index.ts b/frontend/src/components/dashboard/index.ts new file mode 100644 index 0000000..4a7b9e6 --- /dev/null +++ b/frontend/src/components/dashboard/index.ts @@ -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'; diff --git a/frontend/src/lib/api/hooks/useDashboard.ts b/frontend/src/lib/api/hooks/useDashboard.ts new file mode 100644 index 0000000..79a55b7 --- /dev/null +++ b/frontend/src/lib/api/hooks/useDashboard.ts @@ -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({ + 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({ + 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({ + 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({ + queryKey: ['dashboard', 'pendingApprovals'], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 300)); + return mockApprovals; + }, + staleTime: 30000, + }); +}