CUB-121: build React pages with real API integration
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m11s
Dev Build / build-test (push) Successful in 2m18s

- HubPage: agent summary stats, cards, status badges, progress bars, refresh
- LogsPage: activity feed from tasks, status filter, loading skeleton
- ProjectsPage: project cards with status badges and agent counts
- SessionsPage: responsive table/card view with model/token info
- SettingsPage: dark mode toggle, gateway URL, refresh interval persist
- ThemeProvider with dark/light mode via CSS custom properties
- useLocalStorage hook for settings persistence
- Loading/error/empty states across all pages
- npm run build passes cleanly
This commit is contained in:
2026-05-08 19:53:21 -04:00
parent 4a2e660a4a
commit 8b8cb8210c
9 changed files with 885 additions and 79 deletions

View File

@@ -1,8 +1,117 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { listProjects } from '../services/api'
import { AlertCircle, RefreshCw, FolderKanban, Users } from 'lucide-react'
const STATUS_COLORS: Record<string, string> = {
planned: 'bg-purple-500',
active: 'bg-green-500',
paused: 'bg-yellow-500',
completed: 'bg-blue-400',
}
export default function ProjectsPage() {
const queryClient = useQueryClient()
const { data, isLoading, error } = useQuery({
queryKey: ['projects'],
queryFn: listProjects,
})
const projects = data?.data ?? []
if (isLoading) return <ProjectsSkeleton />
if (error) {
return (
<div className="flex flex-col items-center justify-center h-96 gap-4">
<AlertCircle size={48} className="text-danger" />
<p className="text-danger text-lg">Failed to load projects</p>
<button
onClick={() => queryClient.invalidateQueries({ queryKey: ['projects'] })}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
>
<RefreshCw size={16} /> Retry
</button>
</div>
)
}
return (
<div>
<h1 className="text-2xl font-bold mb-4">Projects</h1>
<p className="text-on-surface-variant">Tracked projects will appear here.</p>
<div className="space-y-6">
<header>
<h1 className="text-2xl font-bold">Projects</h1>
<p className="text-on-surface-variant">Tracked projects and initiatives</p>
</header>
{projects.length === 0 ? (
<div className="flex flex-col items-center justify-center h-48 gap-3 border border-dashed border-surface-light rounded-xl">
<FolderKanban size={40} className="text-on-surface-muted" />
<p className="text-on-surface-muted text-lg">No projects tracked</p>
<p className="text-on-surface-muted text-sm">Projects synced from Linear will appear here.</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{projects.map((project) => (
<div
key={project.id}
className="p-5 rounded-xl border border-surface-light bg-surface-dark hover:border-surface-lighter transition-colors flex flex-col"
>
<div className="flex items-start justify-between mb-3">
<FolderKanban size={20} className="text-on-surface-variant shrink-0 mt-0.5" />
<ProjectStatusBadge status={project.status} />
</div>
<h3 className="font-semibold mb-1">{project.name}</h3>
{project.description && (
<p className="text-sm text-on-surface-variant mb-4 line-clamp-2 flex-1">
{project.description}
</p>
)}
<div className="flex items-center gap-2 text-xs text-on-surface-muted pt-3 border-t border-surface-light">
<Users size={14} />
<span>{project.agentIds?.length ?? 0} agent{(project.agentIds?.length ?? 0) !== 1 ? 's' : ''} assigned</span>
</div>
</div>
))}
</div>
)}
</div>
)
}
function ProjectStatusBadge({ status }: { status: string }) {
return (
<div className="flex items-center gap-1.5 px-2 py-1 rounded-full bg-surface-light/50">
<div className={`w-2 h-2 rounded-full ${STATUS_COLORS[status] ?? 'bg-gray-500'}`} />
<span className="text-xs capitalize text-on-surface-variant">{status}</span>
</div>
)
}
function ProjectsSkeleton() {
return (
<div className="space-y-6 animate-pulse">
<div>
<div className="h-8 w-40 skeleton mb-2" />
<div className="h-4 w-56 skeleton" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="p-5 rounded-xl border border-surface-light bg-surface-dark">
<div className="flex justify-between mb-3">
<div className="h-5 w-5 rounded skeleton" />
<div className="h-6 w-20 rounded-full skeleton" />
</div>
<div className="h-5 w-3/4 skeleton mb-2" />
<div className="h-4 w-full skeleton mb-2" />
<div className="h-4 w-2/3 skeleton mb-4" />
<div className="pt-3 border-t border-surface-light">
<div className="h-3 w-32 skeleton" />
</div>
</div>
))}
</div>
</div>
)
}