feat(web): project list and mission dashboard views (#87)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #87.
This commit is contained in:
96
apps/web/src/app/(dashboard)/projects/page.tsx
Normal file
96
apps/web/src/app/(dashboard)/projects/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
apps/web/src/components/projects/project-card.tsx
Normal file
44
apps/web/src/components/projects/project-card.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
| P3-001 | done | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | #82 | #26 |
|
| 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-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-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-004 | done | Phase 3 | Task management — list view + kanban board | #86 | #29 |
|
||||||
| P3-005 | not-started | Phase 3 | Project & mission views — dashboard + PRD viewer | — | #30 |
|
| 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-006 | not-started | Phase 3 | Settings — provider config, profile, integrations | — | #31 |
|
||||||
| P3-007 | not-started | Phase 3 | Admin panel — user management, RBAC | — | #32 |
|
| 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 |
|
| P3-008 | not-started | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 |
|
||||||
|
|||||||
Reference in New Issue
Block a user