diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 79ce2f8..622998f 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -4,6 +4,12 @@ import ModPage from './pages/ModPage';
import FullSetupPage from './pages/FullSetupPage';
import BgmPage from './pages/BgmPage';
import BgmPackagePage from './pages/BgmPackagePage';
+import PartsPage from './pages/PartsPage';
+import PartPage from './pages/PartPage';
+import SurfacesPage from './pages/SurfacesPage';
+import SurfacePage from './pages/SurfacePage';
+import SetupsPage from './pages/SetupsPage';
+import SetupPage from './pages/SetupPage';
export default function App() {
return (
@@ -30,6 +36,12 @@ export default function App() {
} />
} />
} />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
diff --git a/apps/web/src/components/Breadcrumbs.tsx b/apps/web/src/components/Breadcrumbs.tsx
new file mode 100644
index 0000000..93b9d0d
--- /dev/null
+++ b/apps/web/src/components/Breadcrumbs.tsx
@@ -0,0 +1,33 @@
+import { Link } from 'react-router-dom';
+
+export interface Crumb {
+ label: string;
+ /** Where the crumb links; omit for the current (last) crumb. */
+ to?: string;
+}
+
+/**
+ * A breadcrumb navigation trail. The last crumb is the current page and is
+ * rendered as plain text; earlier crumbs link to their routes.
+ */
+export default function Breadcrumbs({ crumbs }: { crumbs: Crumb[] }) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/components/PackageMissing.tsx b/apps/web/src/components/PackageMissing.tsx
new file mode 100644
index 0000000..0a30dff
--- /dev/null
+++ b/apps/web/src/components/PackageMissing.tsx
@@ -0,0 +1,15 @@
+import { Link } from 'react-router-dom';
+import Breadcrumbs from './Breadcrumbs';
+
+/** Shown when a bgm package id doesn't resolve to a known package. */
+export default function PackageMissing({ id }: { id: string }) {
+ return (
+
+
+
No package “{id}”.
+
+ Back to all packages
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/pages/BgmPackagePage.tsx b/apps/web/src/pages/BgmPackagePage.tsx
index 40dcc97..e76100d 100644
--- a/apps/web/src/pages/BgmPackagePage.tsx
+++ b/apps/web/src/pages/BgmPackagePage.tsx
@@ -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 ;
- if (!pkg) {
- return (
-
);
}
\ No newline at end of file
diff --git a/apps/web/src/pages/BgmPage.tsx b/apps/web/src/pages/BgmPage.tsx
index 1f50609..c4969be 100644
--- a/apps/web/src/pages/BgmPage.tsx
+++ b/apps/web/src/pages/BgmPage.tsx
@@ -1,5 +1,6 @@
import { Link } from 'react-router-dom';
import packages from 'virtual:bgm/packages';
+import Breadcrumbs from '../components/Breadcrumbs';
/**
* Lists every discovered bgm package. The `virtual:bgm/packages` module's
@@ -9,6 +10,7 @@ import packages from 'virtual:bgm/packages';
export default function BgmPage() {
return (
+
Board games
diff --git a/apps/web/src/pages/PartPage.tsx b/apps/web/src/pages/PartPage.tsx
new file mode 100644
index 0000000..e523748
--- /dev/null
+++ b/apps/web/src/pages/PartPage.tsx
@@ -0,0 +1,45 @@
+import { useParams } from 'react-router-dom';
+import Breadcrumbs from '../components/Breadcrumbs';
+import PackageMissing from '../components/PackageMissing';
+import { findPackage } from './bgm';
+
+/** Detail view for a single part within a package. */
+export default function PartPage() {
+ const { id, type, part } = useParams<{ id: string; type: string; part: string }>();
+ const pkg = findPackage(id);
+ if (!pkg) return ;
+
+ const found = pkg.parts[`${type}#${part}`];
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/pages/PartsPage.tsx b/apps/web/src/pages/PartsPage.tsx
new file mode 100644
index 0000000..a90f9ef
--- /dev/null
+++ b/apps/web/src/pages/PartsPage.tsx
@@ -0,0 +1,61 @@
+import { Link, useParams } from 'react-router-dom';
+import Breadcrumbs from '../components/Breadcrumbs';
+import PackageMissing from '../components/PackageMissing';
+import { findPackage } from './bgm';
+
+/**
+ * All parts of a package, grouped by part type. Each part links to its
+ * `/bgm/:id/parts/:type/:part` detail page.
+ */
+export default function PartsPage() {
+ const { id } = useParams<{ id: string }>();
+ const pkg = findPackage(id);
+ if (!pkg) return ;
+
+ const parts = Object.values(pkg.parts);
+ const byType = new Map();
+ for (const part of parts) {
+ const list = byType.get(part.type) ?? [];
+ list.push(part);
+ byType.set(part.type, list);
+ }
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/pages/bgm.ts b/apps/web/src/pages/bgm.ts
new file mode 100644
index 0000000..be6a48b
--- /dev/null
+++ b/apps/web/src/pages/bgm.ts
@@ -0,0 +1,7 @@
+import packages from 'virtual:bgm/packages';
+import type { SerializedPackage } from '@tts/bgm';
+
+/** Look up a bgm package by id from the `virtual:bgm/packages` module. */
+export function findPackage(id: string | undefined): SerializedPackage | undefined {
+ return packages.find((p) => p.meta.id === id);
+}
\ No newline at end of file