2026-05-08 19:53:21 -04:00
|
|
|
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',
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 20:15:30 -04:00
|
|
|
export default function ProjectsPage() {
|
2026-05-08 19:53:21 -04:00
|
|
|
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>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 20:15:30 -04:00
|
|
|
return (
|
2026-05-08 19:53:21 -04:00
|
|
|
<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>
|
2026-05-07 20:15:30 -04:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|