feat(tracker-web): add public submission status page
This commit is contained in:
parent
5606ccf1f7
commit
3a6ed3a5f8
@ -109,6 +109,13 @@ async function mockRoadmap(page: Page): Promise<void> {
|
||||
if (url.includes('/public/roadmap/stats')) {
|
||||
return route.fulfill({ json: ROADMAP_STATS });
|
||||
}
|
||||
if (url.includes('/public/items/')) {
|
||||
const id = url.split('/public/items/')[1]?.split(/[?#]/)[0];
|
||||
const item = ROADMAP_ITEMS.find(entry => entry.id === id);
|
||||
return item
|
||||
? route.fulfill({ json: item })
|
||||
: route.fulfill({ status: 404, json: { error: 'Item not found' } });
|
||||
}
|
||||
if (url.includes('/public/roadmap')) {
|
||||
return route.fulfill({
|
||||
json: { items: ROADMAP_ITEMS, total: ROADMAP_ITEMS.length, limit: 100, offset: 0 },
|
||||
@ -329,6 +336,23 @@ test.describe('Tracker — Public Roadmap', () => {
|
||||
await expect(page.getByRole('heading', { name: /enter your email to vote/i })).toBeVisible();
|
||||
});
|
||||
|
||||
test('links submitted users to a public status page for an item', async ({ page }) => {
|
||||
await page.goto('/status/1');
|
||||
await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible();
|
||||
await expect(page.getByText('Submission status')).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('We received this submission and it is waiting for triage.')
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('12 votes')).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: /back to roadmap/i })).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows a helpful message for an unknown public status item', async ({ page }) => {
|
||||
await page.goto('/status/missing');
|
||||
await expect(page.getByRole('heading', { name: /submission not found/i })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: /view roadmap/i })).toBeVisible();
|
||||
});
|
||||
|
||||
test('has no serious accessibility violations', async ({ page }) => {
|
||||
await page.goto('/roadmap');
|
||||
await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible();
|
||||
|
||||
167
dashboards/tracker-web/src/app/status/[id]/page.tsx
Normal file
167
dashboards/tracker-web/src/app/status/[id]/page.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getPublicItem, type TrackerItem } from '@/lib/tracker-client';
|
||||
|
||||
const STATUS_COPY: Record<string, { label: string; description: string }> = {
|
||||
open: {
|
||||
label: 'Open',
|
||||
description: 'We received this submission and it is waiting for triage.',
|
||||
},
|
||||
in_progress: {
|
||||
label: 'In Progress',
|
||||
description: 'This item is actively being worked on.',
|
||||
},
|
||||
done: {
|
||||
label: 'Complete',
|
||||
description: 'This item has been shipped or otherwise completed.',
|
||||
},
|
||||
closed: {
|
||||
label: 'Closed',
|
||||
description: 'This item has been closed.',
|
||||
},
|
||||
wont_fix: {
|
||||
label: "Won't Fix",
|
||||
description: 'This item was reviewed but is not currently planned.',
|
||||
},
|
||||
};
|
||||
|
||||
function formatStatus(status: string) {
|
||||
return STATUS_COPY[status]?.label ?? status.replaceAll('_', ' ');
|
||||
}
|
||||
|
||||
function formatDate(value: string) {
|
||||
return new Intl.DateTimeFormat('en', { month: 'short', day: 'numeric', year: 'numeric' }).format(
|
||||
new Date(value)
|
||||
);
|
||||
}
|
||||
|
||||
export default function SubmissionStatusPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const id = params?.id;
|
||||
const [item, setItem] = useState<TrackerItem | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function loadItem() {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const nextItem = await getPublicItem(id);
|
||||
if (!cancelled) setItem(nextItem);
|
||||
} catch (_err) {
|
||||
if (!cancelled) setError('not_found');
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void loadItem();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<main className="min-h-screen bg-slate-950 px-6 py-12 text-slate-100">
|
||||
<div className="mx-auto max-w-3xl rounded-3xl border border-slate-800 bg-slate-900/70 p-8 shadow-2xl shadow-black/30">
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-cyan-300">Submission status</p>
|
||||
<h1 className="mt-3 text-3xl font-bold">Loading submission…</h1>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !item) {
|
||||
return (
|
||||
<main className="min-h-screen bg-slate-950 px-6 py-12 text-slate-100">
|
||||
<div className="mx-auto max-w-3xl rounded-3xl border border-slate-800 bg-slate-900/70 p-8 shadow-2xl shadow-black/30">
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-cyan-300">Submission status</p>
|
||||
<h1 className="mt-3 text-3xl font-bold">Submission not found</h1>
|
||||
<p className="mt-3 text-slate-300">
|
||||
We could not find a public roadmap item for this status link. It may have been made
|
||||
internal, merged into another item, or removed.
|
||||
</p>
|
||||
<Link
|
||||
href="/roadmap"
|
||||
className="mt-6 inline-flex items-center gap-2 rounded-full bg-cyan-400 px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-cyan-300"
|
||||
>
|
||||
View roadmap
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const status = STATUS_COPY[item.status] ?? {
|
||||
label: formatStatus(item.status),
|
||||
description: 'This submission is being tracked by the ByteLyst roadmap team.',
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-slate-950 px-6 py-12 text-slate-100">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<Link
|
||||
href="/roadmap"
|
||||
className="inline-flex items-center gap-2 text-sm text-cyan-300 hover:text-cyan-200"
|
||||
>
|
||||
Back to roadmap
|
||||
</Link>
|
||||
|
||||
<section className="mt-6 rounded-3xl border border-slate-800 bg-slate-900/75 p-8 shadow-2xl shadow-black/30">
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-cyan-300">Submission status</p>
|
||||
<div className="mt-4 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">{item.title}</h1>
|
||||
<p className="mt-3 max-w-2xl text-slate-300">{item.description}</p>
|
||||
</div>
|
||||
<span className="inline-flex w-fit items-center gap-2 rounded-full border border-cyan-300/40 bg-cyan-300/10 px-4 py-2 text-sm font-semibold text-cyan-100">
|
||||
{status.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-2xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<p className="text-xs uppercase tracking-[0.16em] text-slate-500">Status</p>
|
||||
<p className="mt-2 text-lg font-semibold">{status.label}</p>
|
||||
<p className="mt-1 text-sm text-slate-400">{status.description}</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<p className="flex items-center gap-2 text-xs uppercase tracking-[0.16em] text-slate-500">
|
||||
Votes
|
||||
</p>
|
||||
<p className="mt-2 text-lg font-semibold">{item.voteCount} votes</p>
|
||||
<p className="mt-1 text-sm text-slate-400">Community interest signal.</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-slate-800 bg-slate-950/60 p-4">
|
||||
<p className="flex items-center gap-2 text-xs uppercase tracking-[0.16em] text-slate-500">
|
||||
Activity
|
||||
</p>
|
||||
<p className="mt-2 text-lg font-semibold">{item.commentCount} comments</p>
|
||||
<p className="mt-1 text-sm text-slate-400">
|
||||
Last updated {formatDate(item.updatedAt)}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 rounded-2xl border border-slate-800 bg-slate-950/40 p-5">
|
||||
<p className="flex items-center gap-2 text-sm font-semibold text-slate-200">
|
||||
What happens next?
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">
|
||||
Keep this link to check progress. Public roadmap status updates, votes, and completion
|
||||
state will appear here without requiring an account.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user