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')) {
|
if (url.includes('/public/roadmap/stats')) {
|
||||||
return route.fulfill({ json: 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')) {
|
if (url.includes('/public/roadmap')) {
|
||||||
return route.fulfill({
|
return route.fulfill({
|
||||||
json: { items: ROADMAP_ITEMS, total: ROADMAP_ITEMS.length, limit: 100, offset: 0 },
|
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();
|
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 }) => {
|
test('has no serious accessibility violations', async ({ page }) => {
|
||||||
await page.goto('/roadmap');
|
await page.goto('/roadmap');
|
||||||
await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible();
|
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