Skip to content

Commit b32a7ee

Browse files
authored
Merge pull request #1247 from WatWowMap/feat/shell-and-ia
feat: 2.0 shell, routes, and per-user shell selection
2 parents 0909e0c + 2ca22d9 commit b32a7ee

33 files changed

Lines changed: 1729 additions & 21 deletions

app/App.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { createBrowserRouter, RouterProvider } from 'react-router'
2+
import { ROUTER_ROUTES } from './routes'
3+
4+
const router = createBrowserRouter(ROUTER_ROUTES)
5+
16
export function App() {
2-
return (
3-
<main className="grid min-h-dvh place-items-center bg-white font-sans">
4-
<p className="text-lg text-neutral-500">ReactMap 2.0</p>
5-
</main>
6-
)
7+
return <RouterProvider router={router} />
78
}

app/layout/BottomNav.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { afterAll, afterEach, beforeAll, expect, test } from 'bun:test'
2+
import { cleanup, render, within } from '@testing-library/react'
3+
import { MemoryRouter } from 'react-router'
4+
import { setupDom, teardownDom } from '../test-setup'
5+
import { BottomNav } from './BottomNav'
6+
7+
// `@testing-library/dom`'s `screen` singleton snapshots `document` the
8+
// moment the module is first imported (dist/screen.js), so it only works
9+
// when a global document exists before any test file's imports run — which
10+
// means registering it process-wide via bunfig's preload, for every
11+
// workspace in this monorepo. That broke unrelated suites elsewhere (see
12+
// test-setup.ts). Using the queries `render` returns needs the DOM only once
13+
// the test body actually runs, so registering it here in beforeAll, scoped to
14+
// this file, is enough.
15+
beforeAll(setupDom)
16+
afterAll(teardownDom)
17+
18+
// Every render is appended to the same document and stays there, so without
19+
// this each test sees the leftovers of the ones before it.
20+
afterEach(cleanup)
21+
22+
// Queries are scoped to the container this render owns rather than the whole
23+
// body. The hub renders a link labelled Filters too, so a document-wide query
24+
// finds more than one and getByRole throws for being ambiguous. Whether that
25+
// happened depended on which files had already run, which is why this passed
26+
// locally and failed in CI.
27+
test('shows the four primary destinations in order', () => {
28+
const { container } = render(
29+
<MemoryRouter initialEntries={['/map']}>
30+
<BottomNav />
31+
</MemoryRouter>,
32+
)
33+
const labels = within(container)
34+
.getAllByRole('link')
35+
.map((link) => link.textContent)
36+
expect(labels).toEqual(['Map', 'Filters', 'Alerts', 'Me'])
37+
})
38+
39+
test('marks the active destination for assistive tech', () => {
40+
const { container } = render(
41+
<MemoryRouter initialEntries={['/filters']}>
42+
<BottomNav />
43+
</MemoryRouter>,
44+
)
45+
const active = within(container).getByRole('link', { name: 'Filters' })
46+
expect(active.getAttribute('aria-current')).toBe('page')
47+
})

app/layout/BottomNav.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { NavLink } from 'react-router'
2+
3+
const DESTINATIONS = [
4+
{ to: '/map', label: 'Map' },
5+
{ to: '/filters', label: 'Filters' },
6+
{ to: '/alerts', label: 'Alerts' },
7+
{ to: '/profile', label: 'Me' },
8+
] as const
9+
10+
export function BottomNav() {
11+
return (
12+
<nav className="fixed inset-x-0 bottom-0 grid grid-cols-4 border-t border-neutral-200 bg-white pb-[env(safe-area-inset-bottom)]">
13+
{DESTINATIONS.map(({ to, label }) => (
14+
<NavLink
15+
key={to}
16+
to={to}
17+
className={({ isActive }) =>
18+
`py-3 text-center text-sm ${isActive ? 'text-violet-600' : 'text-neutral-500'}`
19+
}
20+
>
21+
{label}
22+
</NavLink>
23+
))}
24+
</nav>
25+
)
26+
}

app/layout/Shell.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Outlet } from 'react-router'
2+
import { BottomNav } from './BottomNav'
3+
4+
export function Shell() {
5+
return (
6+
<div className="min-h-dvh bg-white font-sans">
7+
<main className="pb-16">
8+
<Outlet />
9+
</main>
10+
<BottomNav />
11+
</div>
12+
)
13+
}

app/pages/AlertsPage.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function AlertsPage() {
2+
return (
3+
<section className="p-6">
4+
<h1 className="text-2xl font-semibold">Alerts</h1>
5+
<p className="mt-2 text-neutral-500">Alerts arrive in a later plan.</p>
6+
</section>
7+
)
8+
}

app/pages/FiltersPage.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function FiltersPage() {
2+
return (
3+
<section className="p-6">
4+
<h1 className="text-2xl font-semibold">Filters</h1>
5+
<p className="mt-2 text-neutral-500">Filters arrive in a later plan.</p>
6+
</section>
7+
)
8+
}

app/pages/Hub.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { afterAll, afterEach, beforeAll, expect, test } from 'bun:test'
2+
import { cleanup, render, within } from '@testing-library/react'
3+
import { MemoryRouter } from 'react-router'
4+
import { setupDom, teardownDom } from '../test-setup'
5+
import { Hub } from './Hub'
6+
7+
beforeAll(setupDom)
8+
afterAll(teardownDom)
9+
10+
// Every render is appended to the same document and stays there, so without
11+
// this each test sees the leftovers of the ones before it. The bottom nav
12+
// renders a link labelled Filters as well, so a stale render from either file
13+
// can make the other's query ambiguous.
14+
afterEach(cleanup)
15+
16+
test('links to the four primary surfaces without a session', () => {
17+
const { container } = render(
18+
<MemoryRouter>
19+
<Hub />
20+
</MemoryRouter>,
21+
)
22+
const hrefs = within(container)
23+
.getAllByRole('link')
24+
.map((link) => link.getAttribute('href'))
25+
expect(hrefs).toEqual(['/map', '/filters', '/alerts', '/profile'])
26+
})

app/pages/Hub.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Link } from 'react-router'
2+
3+
const DESTINATIONS = [
4+
{ to: '/map', label: 'Map' },
5+
{ to: '/filters', label: 'Filters' },
6+
{ to: '/alerts', label: 'Alerts' },
7+
{ to: '/profile', label: 'Profile' },
8+
] as const
9+
10+
export function Hub() {
11+
return (
12+
<section className="p-6">
13+
<h1 className="text-2xl font-semibold">Hub</h1>
14+
<nav className="mt-4 grid grid-cols-2 gap-3">
15+
{DESTINATIONS.map(({ to, label }) => (
16+
<Link
17+
key={to}
18+
to={to}
19+
className="rounded-lg border border-neutral-200 p-4 text-center text-sm font-medium"
20+
>
21+
{label}
22+
</Link>
23+
))}
24+
</nav>
25+
</section>
26+
)
27+
}

app/pages/Locales.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function Locales() {
2+
return (
3+
<section className="p-6">
4+
<h1 className="text-2xl font-semibold">Locales</h1>
5+
<p className="mt-2 text-neutral-500">Locales arrive in a later plan.</p>
6+
</section>
7+
)
8+
}

app/pages/MapPage.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function MapPage() {
2+
return (
3+
<section className="p-6">
4+
<h1 className="text-2xl font-semibold">Map</h1>
5+
<p className="mt-2 text-neutral-500">The map arrives in a later plan.</p>
6+
</section>
7+
)
8+
}

0 commit comments

Comments
 (0)