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 package “{id}”.

- - Back to all packages - -
- ); - } + 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 (
+ +
- - ← All packages - -

{pkg.meta.title ?? pkg.meta.id}

+

{pkg.meta.title ?? pkg.meta.id}

{pkg.meta.designer ? `${pkg.meta.designer} · ` : ''} {pkg.meta.players ? `${pkg.meta.players} players · ` : ''} @@ -38,52 +49,19 @@ export default function BgmPackagePage() {

-
-

Parts

-
    - {parts.map((part) => ( -
  • -
    - {part.type}#{part.id} -
    -
    - {part.size ? `size ${part.size.join('×')}mm` : ''} - {part.fillet ? ` · fillet ${part.fillet}mm` : ''} -
    -
  • - ))} -
-
- -
-

Surfaces

-
    - {surfaces.map((surface) => ( -
  • -
    - {surface.type}#{surface.id} -
    -
    - {surface.size ? `size ${surface.size.join('×')}mm` : ''} · {surface.layout.length} routes -
    -
  • - ))} -
-
- -
-

Setups

- {setups.map((setup) => ( -
-
- {setup.type}#{setup.id} -
-
-              {JSON.stringify(setup.setup, null, 2)}
-            
-
+
    + {sections.map((section) => ( +
  • + +
    {section.label}
    +
    {section.count}
    + +
  • ))} -
+
); } \ 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 ( +

+ + + {!found ? ( +

No part “{type}#{part}”.

+ ) : ( +
+
+

+ {found.type}#{found.id} +

+

+ {found.size ? `size ${found.size.join('×')}mm` : ''} + {found.fillet ? ` · fillet ${found.fillet}mm` : ''} +

+
+
+            {JSON.stringify(found, null, 2)}
+          
+
+ )} +
+ ); +} \ 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 ( +
+ + +
+

Parts

+

{parts.length} part{parts.length === 1 ? '' : 's'}.

+
+ + {[...byType.entries()].map(([type, list]) => ( +
+

{type}

+
    + {list.map((part) => ( +
  • + + {part.id} + +
    + {part.size ? `size ${part.size.join('×')}mm` : ''} + {part.fillet ? ` · fillet ${part.fillet}mm` : ''} +
    +
  • + ))} +
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/pages/SetupPage.tsx b/apps/web/src/pages/SetupPage.tsx new file mode 100644 index 0000000..4f6f273 --- /dev/null +++ b/apps/web/src/pages/SetupPage.tsx @@ -0,0 +1,41 @@ +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 setup within a package. */ +export default function SetupPage() { + const { id, type, setup } = useParams<{ id: string; type: string; setup: string }>(); + const pkg = findPackage(id); + if (!pkg) return ; + + const found = pkg.setups[`${type}#${setup}`]; + + return ( +
+ + + {!found ? ( +

No setup “{type}#{setup}”.

+ ) : ( +
+
+

+ {found.type}#{found.id} +

+
+
+            {JSON.stringify(found.setup, null, 2)}
+          
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/pages/SetupsPage.tsx b/apps/web/src/pages/SetupsPage.tsx new file mode 100644 index 0000000..af41925 --- /dev/null +++ b/apps/web/src/pages/SetupsPage.tsx @@ -0,0 +1,48 @@ +import { Link, useParams } from 'react-router-dom'; +import Breadcrumbs from '../components/Breadcrumbs'; +import PackageMissing from '../components/PackageMissing'; +import { findPackage } from './bgm'; + +/** + * All setups of a package. Each setup links to its + * `/bgm/:id/setups/:type/:setup` detail page. + */ +export default function SetupsPage() { + const { id } = useParams<{ id: string }>(); + const pkg = findPackage(id); + if (!pkg) return ; + + const setups = Object.values(pkg.setups); + + return ( +
+ + +
+

Setups

+

+ {setups.length} setup{setups.length === 1 ? '' : 's'}. +

+
+ +
    + {setups.map((setup) => ( +
  • + + {setup.id} + +
  • + ))} +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/pages/SurfacePage.tsx b/apps/web/src/pages/SurfacePage.tsx new file mode 100644 index 0000000..3425633 --- /dev/null +++ b/apps/web/src/pages/SurfacePage.tsx @@ -0,0 +1,44 @@ +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 surface within a package. */ +export default function SurfacePage() { + const { id, type, surface } = useParams<{ id: string; type: string; surface: string }>(); + const pkg = findPackage(id); + if (!pkg) return ; + + const found = pkg.surfaces[`${type}#${surface}`]; + + return ( +
+ + + {!found ? ( +

No surface “{type}#{surface}”.

+ ) : ( +
+
+

+ {found.type}#{found.id} +

+

+ {found.size ? `size ${found.size.join('×')}mm` : ''} · {found.layout.length} routes +

+
+
+            {JSON.stringify(found, null, 2)}
+          
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/pages/SurfacesPage.tsx b/apps/web/src/pages/SurfacesPage.tsx new file mode 100644 index 0000000..769141d --- /dev/null +++ b/apps/web/src/pages/SurfacesPage.tsx @@ -0,0 +1,51 @@ +import { Link, useParams } from 'react-router-dom'; +import Breadcrumbs from '../components/Breadcrumbs'; +import PackageMissing from '../components/PackageMissing'; +import { findPackage } from './bgm'; + +/** + * All surfaces of a package. Each surface links to its + * `/bgm/:id/surfaces/:type/:surface` detail page. + */ +export default function SurfacesPage() { + const { id } = useParams<{ id: string }>(); + const pkg = findPackage(id); + if (!pkg) return ; + + const surfaces = Object.values(pkg.surfaces); + + return ( +
+ + +
+

Surfaces

+

+ {surfaces.length} surface{surfaces.length === 1 ? '' : 's'}. +

+
+ +
    + {surfaces.map((surface) => ( +
  • + + {surface.id} + +
    + {surface.size ? `size ${surface.size.join('×')}mm` : ''} · {surface.layout.length} routes +
    +
  • + ))} +
+
+ ); +} \ 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