feat(frontend): Add main dashboard prototype for #47

- Create interactive main dashboard / projects list page prototype
- Add grid and list view modes for projects with toggle
- Implement real-time activity feed with simulated SSE events
- Add project status badges (Active, Paused, Completed, Archived)
- Add complexity indicator (3-dot system)
- Include quick stats cards (active projects, agents, issues, approvals)
- Add filter by status and sort controls
- Implement empty state for new users (with toggle for demo)
- Add notifications dropdown with pending approvals
- Add user menu dropdown
- Include performance summary sidebar card
- Responsive layout (4-col desktop, 3-col tablet, 1-col mobile)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 19:05:16 +01:00
parent 29309e5cfd
commit e41ceafaef
3 changed files with 1169 additions and 0 deletions

View File

@@ -0,0 +1,225 @@
# Main Dashboard / Projects List - Design Prototype
## Overview
The Main Dashboard is the landing page users see after logging in to Syndarix. It provides a comprehensive overview of all their projects, real-time activity across the platform, and quick access to create new projects. This is the central hub from which users navigate to individual project dashboards.
## User Stories
- As a user, I want to see all my projects at a glance so I can quickly assess overall status
- As a user, I want to see real-time activity from my agents so I stay informed of progress
- As a user, I want to quickly create a new project without leaving the dashboard
- As a user, I want to filter and sort my projects to find specific ones easily
- As a user, I want to switch between grid and list views based on my preference
- As a new user, I want a clear call-to-action to create my first project
## Key Screens
### 1. Navigation Header
- Syndarix logo with link to prototypes index
- Global search bar (placeholder) for searching projects, issues, and agents
- Notifications bell with badge count for pending approvals
- User menu dropdown with profile, settings, and logout options
### 2. Welcome Section
- Personalized welcome message with user name
- Quick summary stats (active projects, working agents)
- Prominent "New Project" button
### 3. Quick Stats Cards
- Active Projects count with icon
- Agents Active count
- Open Issues total across all projects
- Pending Approvals requiring user action
### 4. Projects Section
- **Grid View**: Card-based layout showing project details
- Project icon with status-based coloring
- Status badge (Active, Paused, Completed, Archived)
- Complexity indicator (3-dot system)
- Quick stats grid (issues, agents, current sprint)
- Progress bar with percentage
- Last activity timestamp
- **List View**: Compact row-based layout for data-dense viewing
- All key information in a single scannable row
- Responsive - hides some columns on smaller screens
- **Controls**: Filter by status, sort options, view toggle
- **Empty State**: For new users with no projects (toggle available for demo)
### 5. Activity Feed (Right Sidebar)
- Real-time connection indicator (Live/Reconnecting)
- Chronological list of events across all projects
- Event types: agent messages, issue updates, sprint events, approvals
- Project attribution for each event
- Action buttons for pending approvals
- "View All Activity" link
### 6. Performance Summary
- Average sprint velocity
- Issues resolved (7-day window)
- Agent uptime percentage
- Average approval response time
## User Flow
1. User logs in and lands on the main dashboard
2. User scans quick stats to get an overview of platform status
3. User reviews the projects list to see current work
4. User optionally switches between grid/list views or applies filters
5. User monitors activity feed for real-time updates
6. If notifications badge shows pending items, user reviews approvals
7. User clicks a project card to navigate to the project dashboard
8. Alternatively, user clicks "New Project" to start a new project
## Design Decisions
### Layout
- Responsive 4-column grid on desktop: 3 columns for main content, 1 for sidebar
- Sidebar collapses below main content on mobile
- Sticky navigation header for easy access to search and notifications
### Project Cards (Grid View)
- Fixed height cards for visual consistency in grid
- Three-column quick stats (issues, agents, sprint) for rapid scanning
- Progress bar as the main visual indicator of project health
- Complexity indicator uses a 3-dot system (low/medium/high)
### Project Rows (List View)
- All critical information visible without scrolling
- Progressive disclosure: less info on smaller screens
- Clear click affordance with hover states and chevron
### Status Colors
- Active: Green (positive, working)
- Paused: Yellow (attention, on hold)
- Completed: Blue (success, finished)
- Archived: Gray (inactive, historical)
### Activity Feed
- Live connection indicator gives confidence in real-time updates
- Events grouped by time (relative timestamps)
- Action items (approvals) highlighted with yellow background
- Limited to 8 items with "View All" for full history
### Empty State
- Friendly message for new users
- Clear call-to-action to create first project
- Dashed border to indicate "fill me" visually
## States
### Loading
- Skeleton cards in grid view
- Skeleton rows in list view
- Activity feed shows loading placeholders
- Stats cards show placeholder numbers
### Empty
- "No projects yet" message with illustration
- Encouragement text explaining the value proposition
- Prominent "Create Your First Project" button
- Toggle available in prototype to switch between empty/populated
### Error
- Toast notification for failed operations
- Retry buttons where applicable
- Activity feed shows "Connection lost" with reconnection status
### Real-time Updates
- Activity feed updates every 30 seconds (simulated in prototype)
- Connection status indicator shows Live/Reconnecting
- New events animate into the feed at the top
## Responsive Breakpoints
### Desktop (lg: 1024px+)
- 4-column layout (3 main + 1 sidebar)
- Grid view shows 3 project cards per row
- Full activity feed visible
- All stats and filters inline
### Tablet (md: 768px)
- 3-column layout (main takes full width, sidebar below)
- Grid view shows 2 project cards per row
- Search visible in header
- Some list view columns hidden
### Mobile (< 768px)
- Single column, vertical stack
- Grid view shows 1 project card per row
- Search hidden (accessible via icon)
- Compact quick stats (2x2 grid)
- Activity feed collapsible
## Accessibility Notes
- All interactive elements are keyboard navigable
- Status badges use both color AND text labels
- Complexity indicator has title attribute for screen readers
- Activity feed connection status announced to screen readers
- Focus states visible on all buttons and cards
- Color contrast meets WCAG AA standards
- View toggle buttons have aria-labels
## Components Used
- Card, CardHeader, CardTitle, CardContent, CardDescription
- Button (default, outline, ghost, secondary variants)
- Badge (default, secondary, outline variants)
- Input (for search)
- Avatar, AvatarFallback
- DropdownMenu components
- Select, SelectContent, SelectItem, SelectTrigger, SelectValue
- Separator
- Lucide icons for visual indicators
## Interactive Features (Prototype)
1. **View Toggle**: Switch between grid and list views
2. **Filter Dropdown**: Filter projects by status (all, active, paused, completed, archived)
3. **Sort Dropdown**: Sort by recent, name, progress, or issues
4. **Empty State Toggle**: Button to switch between empty and populated states for demo
5. **Simulated Activity Feed**: New events appear every 30 seconds
6. **Connection Status**: Randomly disconnects/reconnects to show the indicator
7. **Notifications Dropdown**: Shows pending approval items
8. **User Menu**: Dropdown with profile options
## Questions for Review
1. Is the 4-column layout optimal or should the activity feed take more/less space?
2. Should project cards show more or fewer quick stats?
3. Is the complexity indicator (3 dots) intuitive enough?
4. Should the activity feed show events from ALL projects or have a filter?
5. Is the empty state message compelling enough for new users?
6. Should there be a "pinned projects" feature for frequent access?
7. Is the search bar placement optimal or should it be more prominent?
## How to View
Navigate to: `/prototypes/main-dashboard`
## Mock Data Summary
### Projects (5 total)
- E-Commerce Platform Redesign (Active, High complexity)
- Mobile Banking App (Active, High complexity)
- Internal HR Portal (Paused, Medium complexity)
- API Gateway Modernization (Completed, High complexity)
- Customer Support Chatbot (Archived, Low complexity)
### Activity Events (10 initial)
- Mix of agent messages, issue updates, sprint events, and approval requests
- Timestamps range from 2 minutes ago to 5 hours ago
- One pending approval request highlighted
## Next Steps
After approval:
1. Connect to real user authentication for personalized welcome
2. Implement actual project list API integration
3. Connect to SSE/WebSocket for real-time activity feed
4. Add project creation modal/wizard
5. Implement global search functionality
6. Add loading skeletons for all data sections
7. Implement persistent view preference (grid/list)
8. Add project quick actions (pause, archive) from dashboard

View File

@@ -0,0 +1,927 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Plus,
Search,
Bell,
LayoutGrid,
List,
ChevronRight,
Bot,
CircleDot,
Clock,
Activity,
Settings,
User,
LogOut,
GitBranch,
MessageSquare,
PlayCircle,
CheckCircle2,
AlertTriangle,
Folder,
FolderOpen,
Zap,
Gauge,
Archive,
Pause,
Filter,
ArrowUpDown,
} from 'lucide-react';
// Mock data for projects
const mockProjects = [
{
id: 'proj-001',
name: 'E-Commerce Platform Redesign',
description: 'Complete redesign of the e-commerce platform with modern UI/UX',
status: 'active',
complexity: 'high',
openIssues: 12,
activeAgents: 4,
currentSprint: 'Sprint 3',
lastActivity: '2 min ago',
progress: 67,
},
{
id: 'proj-002',
name: 'Mobile Banking App',
description: 'Native mobile app for banking services with biometric authentication',
status: 'active',
complexity: 'high',
openIssues: 8,
activeAgents: 5,
currentSprint: 'Sprint 2',
lastActivity: '15 min ago',
progress: 45,
},
{
id: 'proj-003',
name: 'Internal HR Portal',
description: 'Employee self-service portal for HR operations',
status: 'paused',
complexity: 'medium',
openIssues: 5,
activeAgents: 0,
currentSprint: 'Sprint 1',
lastActivity: '2 days ago',
progress: 23,
},
{
id: 'proj-004',
name: 'API Gateway Modernization',
description: 'Migrate legacy API gateway to modern microservices architecture',
status: 'completed',
complexity: 'high',
openIssues: 0,
activeAgents: 0,
currentSprint: 'Sprint 5',
lastActivity: '1 week ago',
progress: 100,
},
{
id: 'proj-005',
name: 'Customer Support Chatbot',
description: 'AI-powered chatbot for customer support automation',
status: 'archived',
complexity: 'low',
openIssues: 0,
activeAgents: 0,
currentSprint: 'Sprint 3',
lastActivity: '1 month ago',
progress: 100,
},
];
// Mock activity events (simulated real-time)
const mockActivityEvents = [
{
id: 'evt-001',
type: 'agent_message',
project: 'E-Commerce Platform Redesign',
projectId: 'proj-001',
agent: 'Product Owner',
message: 'Approved user story #42: Cart checkout flow',
timestamp: new Date(Date.now() - 2 * 60 * 1000),
icon: MessageSquare,
},
{
id: 'evt-002',
type: 'issue_update',
project: 'Mobile Banking App',
projectId: 'proj-002',
agent: 'Backend Engineer',
message: 'Moved issue #38 to "In Review"',
timestamp: new Date(Date.now() - 8 * 60 * 1000),
icon: GitBranch,
},
{
id: 'evt-003',
type: 'agent_status',
project: 'E-Commerce Platform Redesign',
projectId: 'proj-001',
agent: 'Frontend Engineer',
message: 'Started working on issue #45',
timestamp: new Date(Date.now() - 15 * 60 * 1000),
icon: PlayCircle,
},
{
id: 'evt-004',
type: 'sprint_event',
project: 'Mobile Banking App',
projectId: 'proj-002',
agent: 'System',
message: 'Sprint 2 daily standup completed',
timestamp: new Date(Date.now() - 45 * 60 * 1000),
icon: CheckCircle2,
},
{
id: 'evt-005',
type: 'approval_request',
project: 'E-Commerce Platform Redesign',
projectId: 'proj-001',
agent: 'Architect',
message: 'Requesting approval for API design document',
timestamp: new Date(Date.now() - 60 * 60 * 1000),
icon: AlertTriangle,
requiresAction: true,
},
{
id: 'evt-006',
type: 'agent_message',
project: 'Mobile Banking App',
projectId: 'proj-002',
agent: 'QA Engineer',
message: 'Test suite passed: 142 tests, 0 failures',
timestamp: new Date(Date.now() - 90 * 60 * 1000),
icon: CheckCircle2,
},
{
id: 'evt-007',
type: 'issue_update',
project: 'E-Commerce Platform Redesign',
projectId: 'proj-001',
agent: 'Backend Engineer',
message: 'Created PR #78 for payment integration',
timestamp: new Date(Date.now() - 120 * 60 * 1000),
icon: GitBranch,
},
{
id: 'evt-008',
type: 'sprint_event',
project: 'E-Commerce Platform Redesign',
projectId: 'proj-001',
agent: 'System',
message: 'Sprint 3 started with 15 issues',
timestamp: new Date(Date.now() - 180 * 60 * 1000),
icon: PlayCircle,
},
{
id: 'evt-009',
type: 'agent_status',
project: 'Mobile Banking App',
projectId: 'proj-002',
agent: 'Architect',
message: 'Completed security audit review',
timestamp: new Date(Date.now() - 240 * 60 * 1000),
icon: CheckCircle2,
},
{
id: 'evt-010',
type: 'issue_update',
project: 'Mobile Banking App',
projectId: 'proj-002',
agent: 'Frontend Engineer',
message: 'Resolved issue #32: Login screen layout',
timestamp: new Date(Date.now() - 300 * 60 * 1000),
icon: CheckCircle2,
},
];
// Helper function to format relative time
function formatRelativeTime(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins} min ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
}
// Project status badge component
function ProjectStatusBadge({ status }: { status: string }) {
const variants: Record<string, { label: string; className: string; icon: React.ElementType }> = {
active: {
label: 'Active',
className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
icon: PlayCircle,
},
paused: {
label: 'Paused',
className: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
icon: Pause,
},
completed: {
label: 'Completed',
className: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
icon: CheckCircle2,
},
archived: {
label: 'Archived',
className: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300',
icon: Archive,
},
};
const variant = variants[status] || variants.active;
const Icon = variant.icon;
return (
<Badge className={`gap-1 ${variant.className}`} variant="outline">
<Icon className="h-3 w-3" />
{variant.label}
</Badge>
);
}
// Complexity indicator component
function ComplexityIndicator({ complexity }: { complexity: string }) {
const variants: Record<string, { label: string; dots: number; color: string }> = {
low: { label: 'Low', dots: 1, color: 'bg-green-500' },
medium: { label: 'Medium', dots: 2, color: 'bg-yellow-500' },
high: { label: 'High', dots: 3, color: 'bg-red-500' },
};
const variant = variants[complexity] || variants.medium;
return (
<div className="flex items-center gap-1" title={`Complexity: ${variant.label}`}>
{[1, 2, 3].map((i) => (
<div
key={i}
className={`h-2 w-2 rounded-full ${i <= variant.dots ? variant.color : 'bg-muted'}`}
/>
))}
</div>
);
}
// Progress bar component
function ProgressBar({ value, size = 'default' }: { value: number; size?: 'default' | 'sm' }) {
const height = size === 'sm' ? 'h-1.5' : 'h-2';
return (
<div className={`w-full rounded-full bg-muted ${height}`}>
<div
className={`rounded-full bg-primary transition-all ${height}`}
style={{ width: `${Math.min(100, Math.max(0, value))}%` }}
/>
</div>
);
}
// Project card component (Grid view)
function ProjectCardGrid({
project,
}: {
project: (typeof mockProjects)[0];
}) {
return (
<Link href={`/prototypes/project-dashboard`}>
<Card className="h-full cursor-pointer transition-all hover:border-primary hover:shadow-lg">
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
{project.status === 'archived' ? (
<Archive className="h-5 w-5 text-muted-foreground" />
) : project.status === 'completed' ? (
<FolderOpen className="h-5 w-5 text-primary" />
) : (
<Folder className="h-5 w-5 text-primary" />
)}
</div>
<div className="flex flex-col items-end gap-1">
<ProjectStatusBadge status={project.status} />
<ComplexityIndicator complexity={project.complexity} />
</div>
</div>
<CardTitle className="mt-3 line-clamp-1">{project.name}</CardTitle>
<CardDescription className="line-clamp-2">{project.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Quick stats */}
<div className="grid grid-cols-3 gap-2 text-center">
<div className="rounded-md bg-muted/50 p-2">
<div className="flex items-center justify-center gap-1 text-lg font-semibold">
<CircleDot className="h-4 w-4 text-blue-500" />
{project.openIssues}
</div>
<div className="text-xs text-muted-foreground">Issues</div>
</div>
<div className="rounded-md bg-muted/50 p-2">
<div className="flex items-center justify-center gap-1 text-lg font-semibold">
<Bot className="h-4 w-4 text-green-500" />
{project.activeAgents}
</div>
<div className="text-xs text-muted-foreground">Agents</div>
</div>
<div className="rounded-md bg-muted/50 p-2">
<div className="text-xs font-medium text-primary">{project.currentSprint}</div>
<div className="text-xs text-muted-foreground">Current</div>
</div>
</div>
{/* Progress */}
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Progress</span>
<span className="font-medium">{project.progress}%</span>
</div>
<ProgressBar value={project.progress} />
</div>
{/* Last activity */}
<div className="flex items-center justify-between text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
Last activity
</div>
<span>{project.lastActivity}</span>
</div>
</div>
</CardContent>
</Card>
</Link>
);
}
// Project row component (List view)
function ProjectRowList({
project,
}: {
project: (typeof mockProjects)[0];
}) {
return (
<Link href={`/prototypes/project-dashboard`}>
<div className="flex items-center gap-4 rounded-lg border p-4 transition-all hover:border-primary hover:bg-muted/50">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
{project.status === 'archived' ? (
<Archive className="h-5 w-5 text-muted-foreground" />
) : project.status === 'completed' ? (
<FolderOpen className="h-5 w-5 text-primary" />
) : (
<Folder className="h-5 w-5 text-primary" />
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<h3 className="font-medium">{project.name}</h3>
<ProjectStatusBadge status={project.status} />
</div>
<p className="mt-0.5 line-clamp-1 text-sm text-muted-foreground">
{project.description}
</p>
</div>
<div className="hidden items-center gap-6 md:flex">
<div className="flex items-center gap-1 text-sm">
<CircleDot className="h-4 w-4 text-blue-500" />
<span className="font-medium">{project.openIssues}</span>
<span className="text-muted-foreground">issues</span>
</div>
<div className="flex items-center gap-1 text-sm">
<Bot className="h-4 w-4 text-green-500" />
<span className="font-medium">{project.activeAgents}</span>
<span className="text-muted-foreground">agents</span>
</div>
<div className="w-24">
<div className="text-right text-xs text-muted-foreground">{project.progress}%</div>
<ProgressBar value={project.progress} size="sm" />
</div>
</div>
<div className="hidden flex-col items-end gap-1 lg:flex">
<ComplexityIndicator complexity={project.complexity} />
<span className="text-xs text-muted-foreground">{project.lastActivity}</span>
</div>
<ChevronRight className="h-5 w-5 shrink-0 text-muted-foreground" />
</div>
</Link>
);
}
// Empty state component
function EmptyState() {
return (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16">
<div className="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="mt-4 text-lg font-semibold">No projects yet</h3>
<p className="mt-2 max-w-sm text-center text-sm text-muted-foreground">
Get started by creating your first project. Syndarix will help you assemble a team of AI
agents to build your software.
</p>
<Button className="mt-6">
<Plus className="mr-2 h-4 w-4" />
Create Your First Project
</Button>
</CardContent>
</Card>
);
}
// Activity event component
function ActivityEvent({
event,
}: {
event: (typeof mockActivityEvents)[0];
}) {
const Icon = event.icon;
return (
<div className="flex gap-3 py-2">
<div
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${
event.requiresAction
? 'bg-yellow-100 text-yellow-600 dark:bg-yellow-900 dark:text-yellow-400'
: 'bg-muted text-muted-foreground'
}`}
>
<Icon className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1">
<p className="text-sm">
<span className="font-medium">{event.agent}</span>{' '}
<span className="text-muted-foreground">{event.message}</span>
</p>
<div className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
<span>{event.project}</span>
<span>-</span>
<span>{formatRelativeTime(event.timestamp)}</span>
</div>
{event.requiresAction && (
<Button variant="outline" size="sm" className="mt-2 h-7 text-xs">
Review Request
</Button>
)}
</div>
</div>
);
}
export default function MainDashboardPrototype() {
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [showEmptyState, setShowEmptyState] = useState(false);
const [statusFilter, setStatusFilter] = useState<string>('all');
const [sortBy, setSortBy] = useState<string>('recent');
const [activityEvents, setActivityEvents] = useState(mockActivityEvents);
const [isConnected, setIsConnected] = useState(true);
// Simulate real-time activity feed updates
useEffect(() => {
const interval = setInterval(() => {
// Randomly add a new event every 30 seconds
const eventTypes = ['agent_message', 'issue_update', 'agent_status'];
const agents = ['Product Owner', 'Backend Engineer', 'Frontend Engineer', 'QA Engineer'];
const messages = [
'Updated documentation for module #12',
'Completed code review for PR #89',
'Started implementing feature #34',
'Resolved merge conflict in main branch',
'Added unit tests for auth module',
];
const projects = mockProjects.filter((p) => p.status === 'active');
if (projects.length > 0 && Math.random() > 0.7) {
const newEvent = {
id: `evt-${Date.now()}`,
type: eventTypes[Math.floor(Math.random() * eventTypes.length)],
project: projects[Math.floor(Math.random() * projects.length)].name,
projectId: projects[Math.floor(Math.random() * projects.length)].id,
agent: agents[Math.floor(Math.random() * agents.length)],
message: messages[Math.floor(Math.random() * messages.length)],
timestamp: new Date(),
icon: MessageSquare,
requiresAction: false,
};
setActivityEvents((prev) => [newEvent, ...prev.slice(0, 9)]);
}
}, 30000);
return () => clearInterval(interval);
}, []);
// Simulate connection status
useEffect(() => {
const interval = setInterval(() => {
// Randomly disconnect for demo purposes (rare)
if (Math.random() > 0.95) {
setIsConnected(false);
setTimeout(() => setIsConnected(true), 3000);
}
}, 10000);
return () => clearInterval(interval);
}, []);
// Filter and sort projects
const filteredProjects = mockProjects
.filter((project) => {
if (statusFilter === 'all') return true;
return project.status === statusFilter;
})
.sort((a, b) => {
switch (sortBy) {
case 'name':
return a.name.localeCompare(b.name);
case 'progress':
return b.progress - a.progress;
case 'issues':
return b.openIssues - a.openIssues;
default: // recent
return 0; // Keep original order (already sorted by recent)
}
});
const activeProjectsCount = mockProjects.filter((p) => p.status === 'active').length;
const totalAgentsActive = mockProjects.reduce((sum, p) => sum + p.activeAgents, 0);
const pendingApprovals = activityEvents.filter((e) => e.requiresAction).length;
return (
<div className="min-h-screen bg-background">
{/* Navigation Header */}
<nav className="sticky top-0 z-50 border-b bg-card">
<div className="container mx-auto flex h-14 items-center justify-between px-4">
<div className="flex items-center gap-4">
<Link href="/prototypes" className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary">
<Zap className="h-5 w-5 text-primary-foreground" />
</div>
<span className="font-semibold text-primary">Syndarix</span>
</Link>
</div>
{/* Global Search */}
<div className="hidden max-w-md flex-1 px-8 md:block">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search projects, issues, agents..."
className="w-full pl-9"
/>
</div>
</div>
{/* Right side actions */}
<div className="flex items-center gap-2">
{/* Notifications */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<Bell className="h-5 w-5" />
{pendingApprovals > 0 && (
<span className="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-destructive text-[10px] font-medium text-destructive-foreground">
{pendingApprovals}
</span>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80">
<DropdownMenuLabel>Notifications</DropdownMenuLabel>
<DropdownMenuSeparator />
{activityEvents
.filter((e) => e.requiresAction)
.slice(0, 3)
.map((event) => (
<DropdownMenuItem key={event.id} className="flex-col items-start p-3">
<div className="font-medium">{event.agent}</div>
<div className="text-sm text-muted-foreground">{event.message}</div>
<div className="mt-1 text-xs text-muted-foreground">
{formatRelativeTime(event.timestamp)}
</div>
</DropdownMenuItem>
))}
{pendingApprovals === 0 && (
<div className="p-4 text-center text-sm text-muted-foreground">
No pending notifications
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
<Separator orientation="vertical" className="mx-2 h-6" />
{/* User Menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="gap-2">
<Avatar className="h-8 w-8">
<AvatarFallback>JD</AvatarFallback>
</Avatar>
<span className="hidden md:inline">John Doe</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<User className="mr-2 h-4 w-4" />
Profile
</DropdownMenuItem>
<DropdownMenuItem>
<Settings className="mr-2 h-4 w-4" />
Settings
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">
<LogOut className="mr-2 h-4 w-4" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</nav>
<div className="container mx-auto px-4 py-6">
<div className="grid gap-6 lg:grid-cols-4">
{/* Main Content */}
<div className="space-y-6 lg:col-span-3">
{/* Welcome Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold">Welcome back, John</h1>
<p className="text-muted-foreground">
{activeProjectsCount} active project{activeProjectsCount !== 1 ? 's' : ''},{' '}
{totalAgentsActive} agent{totalAgentsActive !== 1 ? 's' : ''} working
</p>
</div>
<Button>
<Plus className="mr-2 h-4 w-4" />
New Project
</Button>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<Card>
<CardContent className="flex items-center gap-3 p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-100 dark:bg-green-900">
<PlayCircle className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
<div>
<div className="text-2xl font-bold">{activeProjectsCount}</div>
<div className="text-xs text-muted-foreground">Active Projects</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900">
<Bot className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<div className="text-2xl font-bold">{totalAgentsActive}</div>
<div className="text-xs text-muted-foreground">Agents Active</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900">
<CircleDot className="h-5 w-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<div className="text-2xl font-bold">
{mockProjects.reduce((sum, p) => sum + p.openIssues, 0)}
</div>
<div className="text-xs text-muted-foreground">Open Issues</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100 dark:bg-yellow-900">
<AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
</div>
<div>
<div className="text-2xl font-bold">{pendingApprovals}</div>
<div className="text-xs text-muted-foreground">Pending Approvals</div>
</div>
</CardContent>
</Card>
</div>
{/* Projects Section */}
<Card>
<CardHeader>
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardTitle>Projects</CardTitle>
<CardDescription>
Manage your software projects and track AI agent progress
</CardDescription>
</div>
<div className="flex flex-wrap items-center gap-2">
{/* Demo toggle for empty state */}
<Button
variant="outline"
size="sm"
onClick={() => setShowEmptyState(!showEmptyState)}
className="text-xs"
>
{showEmptyState ? 'Show Projects' : 'Show Empty State'}
</Button>
<Separator orientation="vertical" className="hidden h-6 sm:block" />
{/* Filter */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-32">
<Filter className="mr-2 h-4 w-4" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="paused">Paused</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="archived">Archived</SelectItem>
</SelectContent>
</Select>
{/* Sort */}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-32">
<ArrowUpDown className="mr-2 h-4 w-4" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="recent">Recent</SelectItem>
<SelectItem value="name">Name</SelectItem>
<SelectItem value="progress">Progress</SelectItem>
<SelectItem value="issues">Issues</SelectItem>
</SelectContent>
</Select>
{/* View Toggle */}
<div className="flex rounded-md border">
<Button
variant={viewMode === 'grid' ? 'secondary' : 'ghost'}
size="sm"
className="rounded-r-none"
onClick={() => setViewMode('grid')}
aria-label="Grid view"
>
<LayoutGrid className="h-4 w-4" />
</Button>
<Button
variant={viewMode === 'list' ? 'secondary' : 'ghost'}
size="sm"
className="rounded-l-none"
onClick={() => setViewMode('list')}
aria-label="List view"
>
<List className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</CardHeader>
<CardContent>
{showEmptyState ? (
<EmptyState />
) : viewMode === 'grid' ? (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
{filteredProjects.map((project) => (
<ProjectCardGrid key={project.id} project={project} />
))}
</div>
) : (
<div className="space-y-2">
{filteredProjects.map((project) => (
<ProjectRowList key={project.id} project={project} />
))}
</div>
)}
{filteredProjects.length === 0 && !showEmptyState && (
<div className="py-8 text-center text-muted-foreground">
No projects match the selected filters
</div>
)}
</CardContent>
</Card>
</div>
{/* Right Sidebar - Activity Feed */}
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2 text-lg">
<Activity className="h-5 w-5" />
Activity Feed
</CardTitle>
<div className="flex items-center gap-2">
{isConnected ? (
<Badge variant="outline" className="gap-1 text-xs">
<span className="h-2 w-2 animate-pulse rounded-full bg-green-500" />
Live
</Badge>
) : (
<Badge variant="outline" className="gap-1 text-xs text-muted-foreground">
<span className="h-2 w-2 rounded-full bg-gray-400" />
Reconnecting...
</Badge>
)}
</div>
</div>
<CardDescription>Real-time updates from all projects</CardDescription>
</CardHeader>
<CardContent>
<div className="divide-y">
{activityEvents.slice(0, 8).map((event) => (
<ActivityEvent key={event.id} event={event} />
))}
</div>
<Button variant="ghost" className="mt-4 w-full text-sm">
View All Activity
<ChevronRight className="ml-1 h-4 w-4" />
</Button>
</CardContent>
</Card>
{/* Quick Performance Stats */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg">
<Gauge className="h-5 w-5" />
Performance
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Avg. Sprint Velocity</span>
<span className="font-medium">24 pts</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Issues Resolved (7d)</span>
<span className="font-medium">42</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Agent Uptime</span>
<span className="font-medium text-green-600">99.8%</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Approval Response</span>
<span className="font-medium">2.3 hrs avg</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
);
}

View File

@@ -15,9 +15,26 @@ import {
CircleDot,
Activity,
ChevronRight,
Home,
} from 'lucide-react';
const prototypes = [
{
id: 'main-dashboard',
title: 'Main Dashboard',
description: 'Landing page with projects list, activity feed, and quick stats overview',
icon: Home,
issue: '#47',
status: 'ready',
features: [
'Projects grid/list view with status badges',
'Real-time activity feed',
'Quick stats overview',
'Filter and sort controls',
'Empty state for new users',
'Responsive layout',
],
},
{
id: 'project-dashboard',
title: 'Project Dashboard',