feat(web): project list and mission dashboard views #87

Merged
jason.woltje merged 1 commits from feat/p3-005-projects-missions into main 2026-03-13 13:30:11 +00:00
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 |