feat(web): add bgm viewer navigation

This commit is contained in:
2026-08-09 20:02:03 +08:00
parent 64c8bb60b3
commit 4e18ab5bc8
12 changed files with 403 additions and 66 deletions
+44 -66
View File
@@ -1,36 +1,47 @@
import { Link, useParams } from 'react-router-dom';
import packages from 'virtual:bgm/packages';
import Breadcrumbs from '../components/Breadcrumbs';
import PackageMissing from '../components/PackageMissing';
import { findPackage } from './bgm';
/**
* Detail view for a single bgm package, looked up by id from the
* `virtual:bgm/packages` module's list of all discovered packages.
* Summary view for a single bgm package: its metadata and counts of parts,
* surfaces, and setups, each linking to its collection page.
*/
export default function BgmPackagePage() {
const { id } = useParams<{ id: string }>();
const pkg = packages.find((p) => p.meta.id === id);
const pkg = findPackage(id);
if (!pkg) return <PackageMissing id={id ?? ''} />;
if (!pkg) {
return (
<div className="space-y-4">
<p className="text-sm text-zinc-400">No package {id}.</p>
<Link to="/bgm" className="text-sm text-zinc-300 underline">
Back to all packages
</Link>
</div>
);
}
const partCount = Object.keys(pkg.parts).length;
const surfaceCount = Object.keys(pkg.surfaces).length;
const setupCount = Object.keys(pkg.setups).length;
const parts = Object.values(pkg.parts);
const surfaces = Object.values(pkg.surfaces);
const setups = Object.values(pkg.setups);
const sections = [
{
label: 'Parts',
count: partCount,
to: `/bgm/${pkg.meta.id}/parts`,
},
{
label: 'Surfaces',
count: surfaceCount,
to: `/bgm/${pkg.meta.id}/surfaces`,
},
{
label: 'Setups',
count: setupCount,
to: `/bgm/${pkg.meta.id}/setups`,
},
];
return (
<div className="space-y-6">
<Breadcrumbs
crumbs={[{ label: 'Board games', to: '/bgm' }, { label: pkg.meta.title ?? pkg.meta.id }]}
/>
<div>
<Link to="/bgm" className="text-sm text-zinc-400 hover:text-zinc-200">
All packages
</Link>
<h1 className="mt-1 text-2xl font-semibold">{pkg.meta.title ?? pkg.meta.id}</h1>
<h1 className="text-2xl font-semibold">{pkg.meta.title ?? pkg.meta.id}</h1>
<p className="mt-1 text-sm text-zinc-400">
{pkg.meta.designer ? `${pkg.meta.designer} · ` : ''}
{pkg.meta.players ? `${pkg.meta.players} players · ` : ''}
@@ -38,52 +49,19 @@ export default function BgmPackagePage() {
</p>
</div>
<section className="space-y-2">
<h2 className="text-lg font-semibold">Parts</h2>
<ul className="grid gap-2 sm:grid-cols-2">
{parts.map((part) => (
<li key={`${part.type}#${part.id}`} className="rounded-lg border border-zinc-800 bg-zinc-900 p-3">
<div className="font-medium">
{part.type}#{part.id}
</div>
<div className="mt-1 text-xs text-zinc-400">
{part.size ? `size ${part.size.join('×')}mm` : ''}
{part.fillet ? ` · fillet ${part.fillet}mm` : ''}
</div>
</li>
))}
</ul>
</section>
<section className="space-y-2">
<h2 className="text-lg font-semibold">Surfaces</h2>
<ul className="grid gap-2 sm:grid-cols-2">
{surfaces.map((surface) => (
<li key={`${surface.type}#${surface.id}`} className="rounded-lg border border-zinc-800 bg-zinc-900 p-3">
<div className="font-medium">
{surface.type}#{surface.id}
</div>
<div className="mt-1 text-xs text-zinc-400">
{surface.size ? `size ${surface.size.join('×')}mm` : ''} · {surface.layout.length} routes
</div>
</li>
))}
</ul>
</section>
<section className="space-y-2">
<h2 className="text-lg font-semibold">Setups</h2>
{setups.map((setup) => (
<div key={`${setup.type}#${setup.id}`} className="rounded-lg border border-zinc-800 bg-zinc-900 p-3">
<div className="font-medium">
{setup.type}#{setup.id}
</div>
<pre className="mt-2 overflow-x-auto text-xs text-zinc-400">
{JSON.stringify(setup.setup, null, 2)}
</pre>
</div>
<ul className="grid gap-2 sm:grid-cols-3">
{sections.map((section) => (
<li key={section.label}>
<Link
to={section.to}
className="block rounded-lg border border-zinc-800 bg-zinc-900 p-3 hover:border-zinc-600"
>
<div className="font-medium">{section.label}</div>
<div className="mt-1 text-sm text-zinc-400">{section.count}</div>
</Link>
</li>
))}
</section>
</ul>
</div>
);
}