From ccfa44389ec4a5addddde0b255e7bab97acefb50 Mon Sep 17 00:00:00 2001 From: Tudor Date: Sat, 3 Oct 2026 21:10:36 +0100 Subject: [PATCH] fix(school): drop the nearby section's lede, which repeated its heading "Other schools nearby" was followed by "Other primary schools near .", which says the same thing again. The heading now stands alone. nearbyNoun() and the phase and schoolName props existed only to build that line, so they go with it. Its bottom margin was the only gap between the heading and the cards, so the header row carries that gap now, and centres the heading against the carousel arrows now that it is a single line. Co-Authored-By: Claude Opus 5.5 --- .../components/NearbySchoolsSection.test.tsx | 53 ++----------------- .../school/NearbySchools.module.css | 11 ++-- .../school/NearbySchoolsCarousel.tsx | 2 +- .../school/NearbySchoolsSection.tsx | 46 +++------------- .../school/PrimarySchoolSections.tsx | 2 - .../school/SecondarySchoolSections.tsx | 2 - 6 files changed, 18 insertions(+), 98 deletions(-) diff --git a/nextjs-app/__tests__/components/NearbySchoolsSection.test.tsx b/nextjs-app/__tests__/components/NearbySchoolsSection.test.tsx index 0f042d8..02baef2 100644 --- a/nextjs-app/__tests__/components/NearbySchoolsSection.test.tsx +++ b/nextjs-app/__tests__/components/NearbySchoolsSection.test.tsx @@ -8,7 +8,6 @@ import { render, screen } from '@testing-library/react'; import { - nearbyNoun, NearbySchoolsSection, shouldRenderNearby, } from '@/components/school/NearbySchoolsSection'; @@ -43,13 +42,7 @@ function school(overrides: Partial = {}): NearbySchool { function renderSection(nearby: NearbySchool[]) { return render( - , + , ); } @@ -90,15 +83,10 @@ describe('what the section claims', () => { }); it('shows no chips at all when nothing is shared, rather than inventing one', () => { - const { container } = render( - , - ); + const { container } = renderSection([ + school({ shared: [] }), + school({ urn: 100003, shared: [] }), + ]); // The card still carries its distance, name, type and figure — just no // claim of likeness. expect(container.querySelectorAll('li ul').length).toBe(0); @@ -106,37 +94,6 @@ describe('what the section claims', () => { }); }); -describe('what the lede calls the set', () => { - it.each([ - ['Primary', 'primary schools'], - ['Middle deemed primary', 'primary schools'], - ['Secondary', 'secondary schools'], - ['Middle deemed secondary', 'secondary schools'], - ['All-through', 'all-through schools'], - // GIAS phase 6. Its candidates span the whole secondary group, so no - // single noun fits and it takes the honest general one. - ['16 plus', 'schools and colleges'], - ['', 'schools'], - [null, 'schools'], - ])('calls a %s school\'s neighbours "%s"', (phase, expected) => { - expect(nearbyNoun(phase)).toBe(expected); - }); - - it('never calls a sixth form college\'s neighbours primary schools', () => { - render( - , - ); - expect(screen.getByText(/Other schools and colleges near Barnet Sixth Form College/)).toBeInTheDocument(); - expect(screen.queryByText(/primary schools/)).not.toBeInTheDocument(); - }); -}); - describe('cards', () => { it('links each school to its canonical slug', () => { renderSection([school(), school({ urn: 100003, school_name: 'Oakfield Primary School' })]); diff --git a/nextjs-app/components/school/NearbySchools.module.css b/nextjs-app/components/school/NearbySchools.module.css index 82bad54..06c7e6f 100644 --- a/nextjs-app/components/school/NearbySchools.module.css +++ b/nextjs-app/components/school/NearbySchools.module.css @@ -1,8 +1,7 @@ .heading { font-family: var(--font-display); font-size: 1.4rem; letter-spacing: -0.4px; margin: 0; } -.lede { margin: 0.5rem 0 1.25rem; color: var(--text-secondary); max-width: 64ch; } .caption { margin: 1rem 0 0; font-size: 0.72rem; color: var(--text-muted); } -.top { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; } +.top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem; } .arrows { display: flex; gap: 0.5rem; flex: none; } .arrow { width: 44px; height: 44px; display: grid; place-items: center; cursor: pointer; border: 1px solid var(--border-strong); border-radius: 999px; background: var(--bg-card); color: var(--brand); } .arrow:hover:not(:disabled) { border-color: var(--brand); background: var(--brand-bg); } @@ -15,10 +14,10 @@ .scroller { display: grid; grid-auto-flow: column; grid-auto-columns: calc((100% - 1.8rem) / 3); gap: 0.9rem; overflow-x: auto; scroll-snap-type: x mandatory; padding: 2px; margin: -2px; list-style: none; scrollbar-width: none; -ms-overflow-style: none; } .scroller::-webkit-scrollbar { display: none; } @media (max-width: 820px) { .scroller { grid-auto-columns: calc((100% - 0.9rem) / 2); } } -/* Touch widths (MOBILE.md): the arrows would take 96px from a 328px card and - crush the lede into four lines, for a control swiping already provides. They - go, and the documented right-edge fade carries the affordance — lifting at - the end of the travel, where there is nothing more to hint at. */ +/* Touch widths (MOBILE.md): the arrows would take 96px from a 328px card, for + a control swiping already provides. They go, and the documented right-edge + fade carries the affordance — lifting at the end of the travel, where there + is nothing more to hint at. */ @media (max-width: 640px) { .top { display: block; } .arrows { display: none; } diff --git a/nextjs-app/components/school/NearbySchoolsCarousel.tsx b/nextjs-app/components/school/NearbySchoolsCarousel.tsx index ccc59fd..e40ee6c 100644 --- a/nextjs-app/components/school/NearbySchoolsCarousel.tsx +++ b/nextjs-app/components/school/NearbySchoolsCarousel.tsx @@ -4,7 +4,7 @@ * The scroller and its arrows. * * `children` are the server-rendered cards and `header` the server-rendered - * heading and lede: both stay server components, passed through, so this file + * heading: both stay server components, passed through, so this file * owns a DOM ref and nothing else. That is what keeps all six links in the * initial HTML — a carousel that mounted cards on click would put four of the * six beyond a crawler and beyond a reader with no JavaScript. diff --git a/nextjs-app/components/school/NearbySchoolsSection.tsx b/nextjs-app/components/school/NearbySchoolsSection.tsx index 07d4bba..c2882df 100644 --- a/nextjs-app/components/school/NearbySchoolsSection.tsx +++ b/nextjs-app/components/school/NearbySchoolsSection.tsx @@ -13,8 +13,10 @@ * reached on their behalf. * * There is deliberately no "how these are chosen" panel: the method is already - * visible in the lede, the chips and the distances. The single caption line is - * not a method note — it is the one thing a card cannot self-correct. + * visible in the chips and the distances. The single caption line is not a + * method note — it is the one thing a card cannot self-correct. + * + * Nor is there a lede: "Other primary schools near X" only restated the heading. */ import Link from 'next/link'; @@ -32,28 +34,6 @@ export function shouldRenderNearby(nearby?: NearbySchool[] | null): boolean { return (nearby?.length ?? 0) >= MINIMUM; } -/** - * What the lede calls the set of schools it is showing. - * - * Derived from the school's own GIAS phase rather than the template it renders - * with, because those disagree for "16 plus" (GIAS phase 6): a sixth-form - * college renders the primary template — computeSchoolFlags tests for the - * substring "secondary" — while the backend correctly matches it against the - * secondary group. Taking the noun from the template would print "Other primary - * schools near " above a row of secondaries. - * - * A 16-plus school's candidates span the whole secondary group, so no single - * noun fits and it gets the honest general one. - */ -export function nearbyNoun(phase: string | null | undefined): string { - const text = (phase ?? '').trim().toLowerCase(); - if (text === 'all-through') return 'all-through schools'; - if (text === '16 plus') return 'schools and colleges'; - if (text.includes('secondary')) return 'secondary schools'; - if (text.includes('primary')) return 'primary schools'; - return 'schools'; -} - function metricLabel(key: string): string { return key === 'attainment_8_score' ? 'Attainment 8' : 'Reading, writing & maths'; } @@ -65,25 +45,16 @@ function formatMetric(value: number | null, key: string): string { export function NearbySchoolsSection({ urn, - schoolName, - phase, thisMetricValue, nearby, }: { urn: number; - schoolName: string; - /** The school's own GIAS phase, not the template it renders with. */ - phase: string | null | undefined; thisMetricValue: number | null; nearby?: NearbySchool[] | null; }) { if (!shouldRenderNearby(nearby)) return null; const schools = nearby as NearbySchool[]; - - // One card matched on phase alone, so the section may not claim the set - // shares an intake with this school. const metricKey = schools[0].metric_key; - const noun = nearbyNoun(phase); return (
@@ -91,12 +62,9 @@ export function NearbySchoolsSection({ count={schools.length} labelledBy="nearby-schools-heading" header={ -
-

- Other schools nearby -

-

{`Other ${noun} near ${schoolName}.`}

-
+

+ Other schools nearby +

} > {schools.map((school) => ( diff --git a/nextjs-app/components/school/PrimarySchoolSections.tsx b/nextjs-app/components/school/PrimarySchoolSections.tsx index b7dd319..319ee86 100644 --- a/nextjs-app/components/school/PrimarySchoolSections.tsx +++ b/nextjs-app/components/school/PrimarySchoolSections.tsx @@ -154,8 +154,6 @@ export function PrimarySchoolSections({ {/* Last: it is where the reader goes next, not part of this school. */} diff --git a/nextjs-app/components/school/SecondarySchoolSections.tsx b/nextjs-app/components/school/SecondarySchoolSections.tsx index 697dfaa..7fcbe98 100644 --- a/nextjs-app/components/school/SecondarySchoolSections.tsx +++ b/nextjs-app/components/school/SecondarySchoolSections.tsx @@ -148,8 +148,6 @@ export function SecondarySchoolSections({ {/* Last: it is where the reader goes next, not part of this school. */}