feat(search): align the map view with the mockup and open postcode searches on it #158

Merged
tudor merged 2 commits from feat/results-map-view into main 2026-09-30 20:58:40 +00:00
Owner

Follow-up to #157. On staging the map view looked nothing like mockup B (https://claude.ai/artifact/EDhJuBTH9L1iaEtZW7zLrv): the map sat on the left with a narrow 340px list on the right, it used Leaflet's default blue markers, and a school opened in Leaflet's default popup. This PR aligns it with the mockup and makes the map the default on desktop.

Map view (desktop)

  • The list pane is on the left, under the result count and sort. The map fills the rest, down to 1rem above the bottom of the screen. The pinned toolbar's height is measured with a ResizeObserver, so the layout still fits when the controls wrap.
  • Pins are brand-teal dots and the selected one is coral. Your postcode is an ink dot, and the search radius is a dashed circle labelled with its distance. The map zooms to fit that circle. Tiles are muted (saturate(.45)), and the zoom buttons sit under the fullscreen button.
  • Choosing a school on the map or in the list opens its card on the map, with View and + Compare. The + Compare button reflects the compare basket. The school's list card gets a highlight ring and scrolls into view.
  • The list cards show the full name, Ofsted rating and school type, the headline figure and the pupil count.
  • The map cards now follow the list-row rules. Special schools, PRUs and AP get no England comparison, and a placeholder all-zero RWM is hidden. On staging, Greenmead showed "0% RWM −62 pts vs national". That rule is now a shared helper, listRwmValue, which SchoolRow also uses.

Phones keep the full-screen map with the bottom sheet, restyled with the same card.

Map as the default for postcode searches (desktop and phones, on trial)

  • Every postcode search opens on the map. Name searches have no map and still open on the list. The default is DEFAULT_LOCATION_VIEW in HomeView, so reverting it is one line. results_view_changed shows how often readers switch to the list.
  • The view is derived: it is the reader's choice once they make one, and the default for the kind of search until then. A hero search reaches the results by client-side navigation, which passes the same HomeView new props instead of mounting a fresh one, and the derived view handles that without any state syncing.
  • Phones hide the map view's list pane, so they no longer build up to 500 hidden cards. That decision is made after mount, so hydration still matches.
  • An earlier revision of this PR chose the default from the user agent. It was removed when phones got the map too, and that settled two review points: the HTML no longer needs Vary: User-Agent, and there is no narrow-window fallback left to flash the map before the list.

Review fix

  • LeafletMapInner reopened the card only when the selection or the school list changed. A rebuild caused by the center, reference point or radius changing closed the card for good while the page still had the school selected. The selection effect now also depends on a counter that goes up every time the pins are rebuilt. The regression test fails without the fix.
  • SchoolMap memoises its fallback center. It was a fresh array on every render, which made the map refit and rebuild every pin each time.

Tests

  • Unit: 513/513 pass, tsc is clean, and next build succeeds with DATABASE_URL unset.
    • New LeafletMapInner.test.tsx runs real Leaflet in jsdom. It covers the pins and radius, selection and the card, adding to compare from the card, closing the card versus replacing it, keeping the selection through a data reload, and no card on narrow screens.
    • New ResultsMapView.test.tsx covers the map default and the list default for name searches, the hero-search case, the reader's choice outlasting later searches, choosing a school in either direction, the special-school and placeholder rules, and no list cards on phones.
    • darkThemeSafety: the guard against passing vacuously now also recognises class-styled L.popup cards.
  • E2E:
    • New: at 1280px, a desktop postcode search opens on the map with the list beside it, the split reaches the bottom of the screen, and choosing a card highlights its pin and opens its card on the map.
    • Changed: the pinned-toolbar test now starts from the map and switches to the list before scrolling.
    • Changed: at 360, 390 and 430px, a phone opens on the map with "Show list" clear of the tab bar. A pin opens the bottom sheet between the button and the bar. The overflow and 44px checks are kept. After switching to the list, the toolbar and button stay pinned while scrolling.
    • Changed: the iOS fullscreen test opens fullscreen straight from the default map.

Not verified

None of this has been seen rendered. Per CLAUDE.md there was no local server, and jsdom does no layout. On staging, check:

  • The desktop split at 1280 and 1440px.
  • The radius label's position.
  • The card on the map in dark mode.

🤖 Generated with Claude Code

Follow-up to #157. On staging the map view looked nothing like mockup B (https://claude.ai/artifact/EDhJuBTH9L1iaEtZW7zLrv): the map sat on the left with a narrow 340px list on the right, it used Leaflet's default blue markers, and a school opened in Leaflet's default popup. This PR aligns it with the mockup and makes the map the default on desktop. ## Map view (desktop) - The list pane is on the left, under the result count and sort. The map fills the rest, down to 1rem above the bottom of the screen. The pinned toolbar's height is measured with a `ResizeObserver`, so the layout still fits when the controls wrap. - Pins are brand-teal dots and the selected one is coral. Your postcode is an ink dot, and the search radius is a dashed circle labelled with its distance. The map zooms to fit that circle. Tiles are muted (`saturate(.45)`), and the zoom buttons sit under the fullscreen button. - Choosing a school on the map or in the list opens its card on the map, with View and + Compare. The + Compare button reflects the compare basket. The school's list card gets a highlight ring and scrolls into view. - The list cards show the full name, Ofsted rating and school type, the headline figure and the pupil count. - The map cards now follow the list-row rules. Special schools, PRUs and AP get no England comparison, and a placeholder all-zero RWM is hidden. On staging, Greenmead showed "0% RWM −62 pts vs national". That rule is now a shared helper, `listRwmValue`, which `SchoolRow` also uses. Phones keep the full-screen map with the bottom sheet, restyled with the same card. ## Map as the default for postcode searches (desktop and phones, on trial) - Every postcode search opens on the map. Name searches have no map and still open on the list. The default is `DEFAULT_LOCATION_VIEW` in HomeView, so reverting it is one line. `results_view_changed` shows how often readers switch to the list. - The view is derived: it is the reader's choice once they make one, and the default for the kind of search until then. A hero search reaches the results by client-side navigation, which passes the same HomeView new props instead of mounting a fresh one, and the derived view handles that without any state syncing. - Phones hide the map view's list pane, so they no longer build up to 500 hidden cards. That decision is made after mount, so hydration still matches. - An earlier revision of this PR chose the default from the user agent. It was removed when phones got the map too, and that settled two review points: the HTML no longer needs `Vary: User-Agent`, and there is no narrow-window fallback left to flash the map before the list. ## Review fix - `LeafletMapInner` reopened the card only when the selection or the school list changed. A rebuild caused by the center, reference point or radius changing closed the card for good while the page still had the school selected. The selection effect now also depends on a counter that goes up every time the pins are rebuilt. The regression test fails without the fix. - `SchoolMap` memoises its fallback center. It was a fresh array on every render, which made the map refit and rebuild every pin each time. ## Tests - Unit: 513/513 pass, `tsc` is clean, and `next build` succeeds with `DATABASE_URL` unset. - New `LeafletMapInner.test.tsx` runs real Leaflet in jsdom. It covers the pins and radius, selection and the card, adding to compare from the card, closing the card versus replacing it, keeping the selection through a data reload, and no card on narrow screens. - New `ResultsMapView.test.tsx` covers the map default and the list default for name searches, the hero-search case, the reader's choice outlasting later searches, choosing a school in either direction, the special-school and placeholder rules, and no list cards on phones. - `darkThemeSafety`: the guard against passing vacuously now also recognises class-styled `L.popup` cards. - E2E: - New: at 1280px, a desktop postcode search opens on the map with the list beside it, the split reaches the bottom of the screen, and choosing a card highlights its pin and opens its card on the map. - Changed: the pinned-toolbar test now starts from the map and switches to the list before scrolling. - Changed: at 360, 390 and 430px, a phone opens on the map with "Show list" clear of the tab bar. A pin opens the bottom sheet between the button and the bar. The overflow and 44px checks are kept. After switching to the list, the toolbar and button stay pinned while scrolling. - Changed: the iOS fullscreen test opens fullscreen straight from the default map. ## Not verified None of this has been seen rendered. Per CLAUDE.md there was no local server, and jsdom does no layout. On staging, check: - The desktop split at 1280 and 1440px. - The radius label's position. - The card on the map in dark mode. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
tudor added 1 commit 2026-09-30 20:30:51 +00:00
feat(search): align the map view with the mockup and open it on desktop
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m20s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 26s
4e4b30e812
The map view now follows option B of the results-controls mockups:

- The list sits in a pane on the left, under the result count and sort,
  and the map fills the rest of the screen below the pinned toolbar. The
  toolbar's height is measured, so the split ends 1rem above the bottom
  of the screen however the controls wrap.
- Pins are brand-teal dots and the selected one is coral. The search
  location is an ink dot, the search radius a dashed circle with its
  distance, and the view fits that circle. Tiles are muted and the zoom
  sits under the fullscreen button.
- A school picked on the map or in the list opens a card on the map
  (View, + Compare, following the basket), and its list card is ringed
  and scrolled into view. Phones keep the bottom sheet.
- The list cards show the full name, Ofsted and school type, the
  headline figure and pupils.

The map cards now follow the list rows: no England benchmark for
special schools, PRUs and AP, and no placeholder all-zero RWM (Greenmead
showed "0% RWM -62 pts vs national"). That rule moves to a shared
listRwmValue helper.

A postcode search opens on the map for desktop browsers, chosen on the
server from the user agent so the list never paints first. The client
falls back to the list below 1024px, and follows the default through
client-side navigation from the hero until the reader picks a view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

🤖 AI Code Review (Claude Code)

Adds a desktop map-first results view (list pane beside the map, with a selected-school card on the map). The default view is chosen server-side from the user agent, with a client-side width fallback. It also moves the map popup to class-based styling, adds a listRwmValue helper, and adds tests. The change looks healthy, with no severe problems and a few minor edge cases.

🟡 Minor

  • nextjs-app/components/LeafletMapInner.tsx: The pins/overlay effect depends on center, referencePoint and radiusMiles, but the selection effect that reopens the card depends only on selectedUrn and schools. If only those props change (for example a new array identity for the reference point, or a radius change with the same schools array), the open card is closed and not reopened while the page still considers the school selected. If SchoolMap passes a fresh default center array on every render, the map also refits and rebuilds every pin on each render.
  • nextjs-app/app/(frontend)/page.tsx: The initial view now varies by User-Agent. If a CDN or proxy caches this HTML without Vary: User-Agent, a phone could receive the map-default render (or a desktop the list). The client-side width fallback limits the damage, but the header should still be declared.
  • nextjs-app/components/HomeView.tsx: A narrow window rendered with a server-chosen 'map' view briefly paints the map layout before the mount effect switches to the list. This is one extra render and a layout flash, not a functional break.
## 🤖 AI Code Review (Claude Code) Adds a desktop map-first results view (list pane beside the map, with a selected-school card on the map). The default view is chosen server-side from the user agent, with a client-side width fallback. It also moves the map popup to class-based styling, adds a listRwmValue helper, and adds tests. The change looks healthy, with no severe problems and a few minor edge cases. ### 🟡 Minor - **nextjs-app/components/LeafletMapInner.tsx**: The pins/overlay effect depends on center, referencePoint and radiusMiles, but the selection effect that reopens the card depends only on selectedUrn and schools. If only those props change (for example a new array identity for the reference point, or a radius change with the same schools array), the open card is closed and not reopened while the page still considers the school selected. If SchoolMap passes a fresh default center array on every render, the map also refits and rebuilds every pin on each render. - **nextjs-app/app/(frontend)/page.tsx**: The initial view now varies by User-Agent. If a CDN or proxy caches this HTML without `Vary: User-Agent`, a phone could receive the map-default render (or a desktop the list). The client-side width fallback limits the damage, but the header should still be declared. - **nextjs-app/components/HomeView.tsx**: A narrow window rendered with a server-chosen 'map' view briefly paints the map layout before the mount effect switches to the list. This is one extra render and a layout flash, not a functional break.
tudor added 1 commit 2026-09-30 20:50:34 +00:00
feat(search): open postcode searches on the map on phones too
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 21s
1bb3e0360f
Trial the map as the default for every postcode search, not only on
desktop (DEFAULT_LOCATION_VIEW in HomeView; results_view_changed shows
how often readers switch away). The view is now derived: the reader's
choice once made, else the default for the kind of search. That follows
a hero search through client-side navigation without syncing state.

With nothing left varying by device, the user-agent default goes
(lib/device.ts, the headers() read in page.tsx). That settles two review
points: the HTML no longer needs Vary: User-Agent, and there is no
narrow-window fallback left to flash the map before the list.

Review fix in LeafletMapInner: the card reopened only when the selection
or the school list changed, so a rebuild caused by the centre, reference
point or radius closed it for good while the page still had the school
selected. The selection effect now follows a pins-rebuilt counter.
SchoolMap memoises its fallback centre, which was a fresh array on every
render and would have rebuilt every pin each time.

Phones hide the map view's list pane, so it no longer builds up to 500
hidden cards there (decided after mount, so hydration still matches).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tudor changed title from feat(search): align the map view with the mockup and open it on desktop to feat(search): align the map view with the mockup and open postcode searches on it 2026-09-30 20:50:49 +00:00

🤖 AI Code Review (Claude Code)

Makes the map the default view for postcode searches on every screen size. On desktop it shows a list pane beside the map, and on phones a bottom sheet. Leaflet pins become styled divIcons with a radius circle and a selected-school card. The Leaflet layer is refactored to rebuild overlays and reopen the card after a rebuild, and the RWM placeholder check moves into a shared listRwmValue helper. I found no correctness, security or deploy problems, and the new unit and e2e tests cover the main behaviours.

✅ No issues found.

## 🤖 AI Code Review (Claude Code) Makes the map the default view for postcode searches on every screen size. On desktop it shows a list pane beside the map, and on phones a bottom sheet. Leaflet pins become styled divIcons with a radius circle and a selected-school card. The Leaflet layer is refactored to rebuild overlays and reopen the card after a rebuild, and the RWM placeholder check moves into a shared `listRwmValue` helper. I found no correctness, security or deploy problems, and the new unit and e2e tests cover the main behaviours. ✅ No issues found.
tudor merged commit cff3854e63 into main 2026-09-30 20:58:40 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tudor/school_compare#158