CUB-121: build React pages with real API integration
- 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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user