feat(web): add bgm package browser

This commit is contained in:
2026-08-09 19:44:27 +08:00
parent c86d7444b8
commit 64c8bb60b3
7 changed files with 222 additions and 2 deletions
+1
View File
@@ -21,6 +21,7 @@
"@tts/extract": "workspace:*",
"@tts/mesh": "workspace:*",
"@tts/shared": "workspace:*",
"@tts/bgm": "workspace:*",
"bson": "^7.3.1",
"react": "^19.2.8",
"react-dom": "^19.2.8",
+7
View File
@@ -2,6 +2,8 @@ import { Link, Route, Routes } from 'react-router-dom';
import SearchPage from './pages/SearchPage';
import ModPage from './pages/ModPage';
import FullSetupPage from './pages/FullSetupPage';
import BgmPage from './pages/BgmPage';
import BgmPackagePage from './pages/BgmPackagePage';
export default function App() {
return (
@@ -15,6 +17,9 @@ export default function App() {
<Link to="/" className="hover:text-zinc-100">
Search
</Link>
<Link to="/bgm" className="hover:text-zinc-100">
BGM
</Link>
</nav>
</div>
</header>
@@ -23,6 +28,8 @@ export default function App() {
<Route path="/" element={<SearchPage />} />
<Route path="/mod/:id" element={<ModPage />} />
<Route path="/mod/:id/setup" element={<FullSetupPage />} />
<Route path="/bgm" element={<BgmPage />} />
<Route path="/bgm/:id" element={<BgmPackagePage />} />
</Routes>
</main>
</div>
+89
View File
@@ -0,0 +1,89 @@
import { Link, useParams } from 'react-router-dom';
import packages from 'virtual:bgm/packages';
/**
* Detail view for a single bgm package, looked up by id from the
* `virtual:bgm/packages` module's list of all discovered packages.
*/
export default function BgmPackagePage() {
const { id } = useParams<{ id: string }>();
const pkg = packages.find((p) => p.meta.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 parts = Object.values(pkg.parts);
const surfaces = Object.values(pkg.surfaces);
const setups = Object.values(pkg.setups);
return (
<div className="space-y-6">
<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>
<p className="mt-1 text-sm text-zinc-400">
{pkg.meta.designer ? `${pkg.meta.designer} · ` : ''}
{pkg.meta.players ? `${pkg.meta.players} players · ` : ''}
{pkg.meta.language}
</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>
))}
</section>
</div>
);
}
+42
View File
@@ -0,0 +1,42 @@
import { Link } from 'react-router-dom';
import packages from 'virtual:bgm/packages';
/**
* Lists every discovered bgm package. The `virtual:bgm/packages` module's
* default export is an array of all packages the loader found in the games
* root.
*/
export default function BgmPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Board games</h1>
<p className="mt-1 text-sm text-zinc-400">
{packages.length} package{packages.length === 1 ? '' : 's'} discovered.
</p>
</div>
<ul className="grid gap-2 sm:grid-cols-2">
{packages.map((pkg) => (
<li key={pkg.meta.id}>
<Link
to={`/bgm/${pkg.meta.id}`}
className="block rounded-lg border border-zinc-800 bg-zinc-900 p-3 hover:border-zinc-600"
>
<div className="font-medium">{pkg.meta.title ?? pkg.meta.id}</div>
<div className="mt-1 text-xs text-zinc-400">
{pkg.meta.designer ? `${pkg.meta.designer} · ` : ''}
{pkg.meta.players ? `${pkg.meta.players} players · ` : ''}
{pkg.meta.language}
</div>
<div className="mt-1 text-xs text-zinc-500">
{Object.keys(pkg.parts).length} parts · {Object.keys(pkg.surfaces).length} surfaces ·{' '}
{Object.keys(pkg.setups).length} setups
</div>
</Link>
</li>
))}
</ul>
</div>
);
}
+19 -1
View File
@@ -1 +1,19 @@
/// <reference types="vite/client" />
/// <reference types="vite/client" />
/**
* Ambient types for the virtual `bgm` modules served by the bgm vite plugin.
*
* - `virtual:bgm/packages` — every discovered package.
* - `virtual:bgm/package/<id>` — a single package's assembled JSON.
*/
declare module 'virtual:bgm/packages' {
import type { SerializedPackage } from '@tts/bgm';
const packages: SerializedPackage[];
export default packages;
}
declare module 'virtual:bgm/package/*' {
import type { SerializedPackage } from '@tts/bgm';
const pkg: SerializedPackage;
export default pkg;
}
+7 -1
View File
@@ -1,9 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import { fileURLToPath } from 'node:url';
import { bgm } from '@tts/bgm';
export default defineConfig({
plugins: [react(), tailwindcss()],
plugins: [
react(),
tailwindcss(),
bgm({ root: fileURLToPath(new URL('../../games', import.meta.url)) }),
],
server: {
// Proxy API calls to the Hono backend during development.
proxy: {