69 lines
2.6 KiB
TypeScript
69 lines
2.6 KiB
TypeScript
import { type ReactElement } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { ProjectCard } from '@/components/projects/project-card';
|
|
import { StaleDataNotice, UnavailableDataNotice } from '@/components/freshness/freshness-notices';
|
|
import { api } from '@/lib/api';
|
|
import type { Project } from '@/lib/types';
|
|
import { useFreshCollection, describeFailure } from '@/lib/freshness/use-fresh-collection';
|
|
import { validateProjectCollection } from '@/lib/freshness/validators';
|
|
|
|
export function ProjectsPage(): ReactElement {
|
|
const navigate = useNavigate();
|
|
const projects = useFreshCollection<Project[]>({
|
|
source: 'gateway:/api/projects',
|
|
fetcher: (signal) => api<unknown>('/api/projects', { signal }),
|
|
validate: validateProjectCollection,
|
|
// Projects carry workspace identity (userId) that is only knowable from
|
|
// the payload itself, so a restored entry cannot be scope-checked before
|
|
// display. Conservative choice: no last-known restore for this surface;
|
|
// cross-workspace switching is still invalidated at verification time.
|
|
});
|
|
const retry = (): void => {
|
|
void projects.revalidate();
|
|
};
|
|
|
|
return (
|
|
<div
|
|
data-freshness={projects.freshness}
|
|
className="flex min-h-screen flex-col px-4 py-6 sm:px-6"
|
|
>
|
|
<header className="mb-6 border-b px-1 pb-3">
|
|
<h1 className="text-2xl font-semibold">Projects</h1>
|
|
</header>
|
|
|
|
{projects.freshness === 'stale' && projects.snapshot ? (
|
|
<div className="mb-6">
|
|
<StaleDataNotice label={projects.snapshot} onRetry={retry} />
|
|
</div>
|
|
) : null}
|
|
|
|
{projects.freshness === 'unknown' ? (
|
|
<p className="py-8 text-center text-sm text-text-muted">Loading projects...</p>
|
|
) : projects.freshness === 'unavailable' ? (
|
|
<UnavailableDataNotice
|
|
title="Projects"
|
|
detail={describeFailure(projects.failure)}
|
|
onRetry={retry}
|
|
/>
|
|
) : projects.data !== null && projects.data.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.data ?? []).map((project) => (
|
|
<ProjectCard
|
|
key={project.id}
|
|
project={project}
|
|
onClick={(selectedProject) => navigate(`/projects/${selectedProject.id}`)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|