feat(web): project list and mission dashboard views

Add projects page with grid layout of project cards showing status,
description, and creation date. Include active mission status section
that fetches from the coord API with stat cards for mission ID, phase,
task progress, and status.

Components: ProjectCard, MissionStatus, StatCard

Refs #30

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 08:29:44 -05:00
parent a1a1976b38
commit 080144d606
3 changed files with 142 additions and 2 deletions

View File

@@ -0,0 +1,96 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { api } from '@/lib/api';
import type { Project } from '@/lib/types';
import { ProjectCard } from '@/components/projects/project-card';
export default function ProjectsPage(): React.ReactElement {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
api<Project[]>('/api/projects')
.then(setProjects)
.catch(() => {})
.finally(() => setLoading(false));
}, []);
const handleProjectClick = useCallback((project: Project) => {
console.log('Project clicked:', project.id);
}, []);
return (
<div>
<div className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-semibold">Projects</h1>
</div>
{loading ? (
<p className="py-8 text-center text-sm text-text-muted">Loading projects...</p>
) : projects.length === 0 ? (
<div className="py-12 text-center">
<h2 className="text-lg font-medium text-text-secondary">No projects yet</h2>
<p className="mt-1 text-sm text-text-muted">
Projects will appear here when created via the gateway API
</p>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => (
<ProjectCard key={project.id} project={project} onClick={handleProjectClick} />
))}
</div>
)}
{/* Mission status section */}
<MissionStatus />
</div>
);
}
function MissionStatus(): React.ReactElement {
const [mission, setMission] = useState<Record<string, unknown> | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
api<Record<string, unknown>>('/api/coord/status')
.then(setMission)
.catch(() => setMission(null))
.finally(() => setLoading(false));
}, []);
return (
<section className="mt-8">
<h2 className="mb-4 text-lg font-semibold">Active Mission</h2>
{loading ? (
<p className="text-sm text-text-muted">Loading mission status...</p>
) : !mission ? (
<div className="rounded-lg border border-surface-border bg-surface-card p-6 text-center">
<p className="text-sm text-text-muted">No active mission detected</p>
</div>
) : (
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatCard label="Mission" value={String(mission['missionId'] ?? 'Unknown')} />
<StatCard label="Phase" value={String(mission['currentPhase'] ?? '—')} />
<StatCard
label="Tasks"
value={`${mission['completedTasks'] ?? 0} / ${mission['totalTasks'] ?? 0}`}
/>
<StatCard label="Status" value={String(mission['status'] ?? '—')} />
</div>
</div>
)}
</section>
);
}
function StatCard({ label, value }: { label: string; value: string }): React.ReactElement {
return (
<div className="rounded-lg bg-surface-elevated p-3">
<p className="text-xs text-text-muted">{label}</p>
<p className="mt-1 text-sm font-medium text-text-primary">{value}</p>
</div>
);
}

View File

@@ -0,0 +1,44 @@
'use client';
import { cn } from '@/lib/cn';
import type { Project } from '@/lib/types';
interface ProjectCardProps {
project: Project;
onClick: (project: Project) => void;
}
const statusColors: Record<string, string> = {
active: 'bg-success/20 text-success',
paused: 'bg-warning/20 text-warning',
completed: 'bg-blue-600/20 text-blue-400',
archived: 'bg-gray-600/20 text-gray-400',
};
export function ProjectCard({ project, onClick }: ProjectCardProps): React.ReactElement {
return (
<button
type="button"
onClick={() => onClick(project)}
className="w-full rounded-lg border border-surface-border bg-surface-card p-4 text-left transition-colors hover:border-gray-500"
>
<div className="flex items-start justify-between">
<h3 className="text-sm font-medium text-text-primary">{project.name}</h3>
<span
className={cn(
'rounded-full px-2 py-0.5 text-xs',
statusColors[project.status] ?? 'bg-gray-600/20 text-gray-400',
)}
>
{project.status}
</span>
</div>
{project.description && (
<p className="mt-2 line-clamp-2 text-xs text-text-muted">{project.description}</p>
)}
<p className="mt-3 text-xs text-text-muted">
Created {new Date(project.createdAt).toLocaleDateString()}
</p>
</button>
);
}

View File

@@ -32,8 +32,8 @@
| P3-001 | done | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | #82 | #26 |
| P3-002 | done | Phase 3 | Auth pages — login, registration, SSO redirect | #83 | #27 |
| P3-003 | done | Phase 3 | Chat UI — conversations, messages, streaming | #84 | #28 |
| P3-004 | in-progress | Phase 3 | Task management — list view + kanban board | | #29 |
| P3-005 | not-started | Phase 3 | Project & mission views — dashboard + PRD viewer | — | #30 |
| P3-004 | done | Phase 3 | Task management — list view + kanban board | #86 | #29 |
| P3-005 | in-progress | Phase 3 | Project & mission views — dashboard + PRD viewer | — | #30 |
| P3-006 | not-started | Phase 3 | Settings — provider config, profile, integrations | — | #31 |
| P3-007 | not-started | Phase 3 | Admin panel — user management, RBAC | — | #32 |
| P3-008 | not-started | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 |