refactor: rename similar → nearby, so the code says what the section does

The section ranks on distance and is headed "Other schools nearby", but every
identifier still called it "similar" — the exact drift that leaves a later
reader trusting a name over the behaviour.

Mechanical: files, the module, the payload key, the type, the components, the
prop. No behaviour change; the suites are unchanged in count and still green.
Free to do now because #150 has not merged, so the payload key rename needs no
lockstep deploy. Uses of "similar" that are ordinary English — progress
measures compared to similar pupils, and unrelated comments — are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
TudorandClaude Opus 5 committed 2026-09-22 11:59:27 +01:00
1 parent 179b6fec94
commit b650df8d93
15 files changed
+102 -102

No files matched your search

@@ -9,24 +9,24 @@ import { render, screen } from '@testing-library/react';
import {
nearbyNoun,
SimilarSchoolsSection,
shouldRenderSimilar,
} from '@/components/school/SimilarSchoolsSection';
import type { SimilarSchool } from '@/lib/types';
NearbySchoolsSection,
shouldRenderNearby,
} from '@/components/school/NearbySchoolsSection';
import type { NearbySchool } from '@/lib/types';
jest.mock('@/components/school/AddToCompareButton', () => ({
AddToCompareButton: ({ school }: { school: SimilarSchool }) => (
AddToCompareButton: ({ school }: { school: NearbySchool }) => (
<button type="button">Add {school.school_name} to compare</button>
),
}));
jest.mock('@/components/school/SimilarSchoolsCompareBar', () => ({
SimilarSchoolsCompareBar: ({ thisUrn }: { thisUrn: number }) => (
jest.mock('@/components/school/NearbySchoolsCompareBar', () => ({
NearbySchoolsCompareBar: ({ thisUrn }: { thisUrn: number }) => (
<div data-testid="compare-bar">bar for {thisUrn}</div>
),
}));
function school(overrides: Partial<SimilarSchool> = {}): SimilarSchool {
function school(overrides: Partial<NearbySchool> = {}): NearbySchool {
return {
urn: 100002,
school_name: 'Willow Lane Primary School',
@@ -41,14 +41,14 @@ function school(overrides: Partial<SimilarSchool> = {}): SimilarSchool {
};
}
function renderSection(similar: SimilarSchool[]) {
function renderSection(nearby: NearbySchool[]) {
return render(
<SimilarSchoolsSection
<NearbySchoolsSection
urn={100001}
schoolName="Meadowbrook Primary School"
phase="Primary"
thisMetricValue={72}
similar={similar}
nearby={nearby}
/>,
);
}
@@ -60,11 +60,11 @@ describe('render gates', () => {
['empty', []],
['a single school', [school()]],
])('renders nothing for %s', (_label, value) => {
expect(shouldRenderSimilar(value as SimilarSchool[] | null | undefined)).toBe(false);
expect(shouldRenderNearby(value as NearbySchool[] | null | undefined)).toBe(false);
});
it('renders for two or more schools', () => {
expect(shouldRenderSimilar([school(), school({ urn: 100003 })])).toBe(true);
expect(shouldRenderNearby([school(), school({ urn: 100003 })])).toBe(true);
});
it('returns null rather than an empty shell below the minimum', () => {
@@ -91,12 +91,12 @@ describe('what the section claims', () => {
it('shows no chips at all when nothing is shared, rather than inventing one', () => {
const { container } = render(
<SimilarSchoolsSection
<NearbySchoolsSection
urn={100001}
schoolName="Meadowbrook Primary School"
phase="Primary"
thisMetricValue={72}
similar={[school({ shared: [] }), school({ urn: 100003, shared: [] })]}
nearby={[school({ shared: [] }), school({ urn: 100003, shared: [] })]}
/>,
);
// The card still carries its distance, name, type and figure — just no
@@ -124,12 +124,12 @@ describe('what the lede calls the set', () => {
it('never calls a sixth form college\'s neighbours primary schools', () => {
render(
<SimilarSchoolsSection
<NearbySchoolsSection
urn={100001}
schoolName="Barnet Sixth Form College"
phase="16 plus"
thisMetricValue={null}
similar={[school(), school({ urn: 100003 })]}
nearby={[school(), school({ urn: 100003 })]}
/>,
);
expect(screen.getByText(/Other schools and colleges near Barnet Sixth Form College/)).toBeInTheDocument();
@@ -8,7 +8,7 @@ import { APIFetchError, fetchSchoolDetails, fetchSchools, fetchNationalAverages
import { notFound, redirect } from 'next/navigation';
import { SchoolDetailShell } from '@/components/school/SchoolDetailShell';
import { NearbyPlaces } from '@/components/school/NearbyPlaces';
import { shouldRenderSimilar } from '@/components/school/SimilarSchoolsSection';
import { shouldRenderNearby } from '@/components/school/NearbySchoolsSection';
import { schoolBreadcrumbJsonLd, type SchoolPlace } from '@/lib/jsonld';
import { PrimarySchoolSections } from '@/components/school/PrimarySchoolSections';
import { SecondarySchoolSections } from '@/components/school/SecondarySchoolSections';
@@ -157,7 +157,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
// lockstep deploy of the two images.
const places: SchoolPlace[] = data.places ?? [];
// Absent on an older API build, exactly like `places` above.
const similarSchools = data.similar_schools ?? [];
const nearbySchools = data.nearby_schools ?? [];
// Redirect bare URN to canonical slug URL
const canonicalSlug = schoolUrl(urn, school_info.school_name).replace('/school/', '');
@@ -189,7 +189,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
admissions: admissions ?? null,
admissionDistance: admission_distance ?? null,
hasLocation: school_info.latitude != null && school_info.longitude != null,
hasNearbySchools: shouldRenderSimilar(similarSchools),
hasNearbySchools: shouldRenderNearby(nearbySchools),
yearlyDataLength: yearly_data.length,
};
const primaryNavItems = buildNavItems(primaryFlags, navInput);
@@ -266,7 +266,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
finance={finance ?? null}
nationalAvg={nationalAvg}
destinations={destinations ?? null}
similarSchools={similarSchools}
nearbySchools={nearbySchools}
flags={secondaryFlags}
/>
</SchoolDetailShell>
@@ -289,7 +289,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
deprivation={deprivation ?? null}
finance={finance ?? null}
nationalAvg={nationalAvg}
similarSchools={similarSchools}
nearbySchools={nearbySchools}
flags={primaryFlags}
/>
</SchoolDetailShell>
@@ -9,10 +9,10 @@
*/
import { useComparisonContext } from '@/context/ComparisonContext';
import type { School, SimilarSchool } from '@/lib/types';
import styles from './SimilarSchools.module.css';
import type { School, NearbySchool } from '@/lib/types';
import styles from './NearbySchools.module.css';
export function AddToCompareButton({ school }: { school: SimilarSchool }) {
export function AddToCompareButton({ school }: { school: NearbySchool }) {
const { addSchool, removeSchool, selectedSchools } = useComparisonContext();
const selected = selectedSchools.some((s) => s.urn === school.urn);
@@ -14,7 +14,7 @@
*/
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
import styles from './SimilarSchools.module.css';
import styles from './NearbySchools.module.css';
/** Three cards fit the row, so fewer than four has nowhere to scroll to.
* Below 640px the arrows are not rendered at all — see the stylesheet. */
@@ -31,7 +31,7 @@ const VISIBLE = 3;
*/
const EDGE = 8;
export function SimilarSchoolsCarousel({
export function NearbySchoolsCarousel({
count,
labelledBy,
header,
@@ -11,15 +11,15 @@
import Link from 'next/link';
import { useComparisonContext } from '@/context/ComparisonContext';
import type { SimilarSchool } from '@/lib/types';
import styles from './SimilarSchools.module.css';
import type { NearbySchool } from '@/lib/types';
import styles from './NearbySchools.module.css';
export function SimilarSchoolsCompareBar({
export function NearbySchoolsCompareBar({
thisUrn,
candidates,
}: {
thisUrn: number;
candidates: SimilarSchool[];
candidates: NearbySchool[];
}) {
const { selectedSchools } = useComparisonContext();
@@ -18,18 +18,18 @@
*/
import Link from 'next/link';
import type { SimilarSchool } from '@/lib/types';
import type { NearbySchool } from '@/lib/types';
import { schoolUrl } from '@/lib/utils';
import { AddToCompareButton } from './AddToCompareButton';
import { SimilarSchoolsCarousel } from './SimilarSchoolsCarousel';
import { SimilarSchoolsCompareBar } from './SimilarSchoolsCompareBar';
import { NearbySchoolsCarousel } from './NearbySchoolsCarousel';
import { NearbySchoolsCompareBar } from './NearbySchoolsCompareBar';
import { Section } from './sectionShared';
import styles from './SimilarSchools.module.css';
import styles from './NearbySchools.module.css';
const MINIMUM = 2;
export function shouldRenderSimilar(similar?: SimilarSchool[] | null): boolean {
return (similar?.length ?? 0) >= MINIMUM;
export function shouldRenderNearby(nearby?: NearbySchool[] | null): boolean {
return (nearby?.length ?? 0) >= MINIMUM;
}
/**
@@ -63,22 +63,22 @@ function formatMetric(value: number | null, key: string): string {
return key === 'attainment_8_score' ? value.toFixed(1) : `${Math.round(value)}%`;
}
export function SimilarSchoolsSection({
export function NearbySchoolsSection({
urn,
schoolName,
phase,
thisMetricValue,
similar,
nearby,
}: {
urn: number;
schoolName: string;
/** The school's own GIAS phase, not the template it renders with. */
phase: string | null | undefined;
thisMetricValue: number | null;
similar?: SimilarSchool[] | null;
nearby?: NearbySchool[] | null;
}) {
if (!shouldRenderSimilar(similar)) return null;
const schools = similar as SimilarSchool[];
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.
@@ -87,7 +87,7 @@ export function SimilarSchoolsSection({
return (
<Section id="nearby">
<SimilarSchoolsCarousel
<NearbySchoolsCarousel
count={schools.length}
labelledBy="nearby-schools-heading"
header={
@@ -137,9 +137,9 @@ export function SimilarSchoolsSection({
<AddToCompareButton school={school} />
</li>
))}
</SimilarSchoolsCarousel>
</NearbySchoolsCarousel>
<SimilarSchoolsCompareBar thisUrn={urn} candidates={schools} />
<NearbySchoolsCompareBar thisUrn={urn} candidates={schools} />
{/* The one caveat the cards cannot make on their own: a reader who takes
"0.6 miles away" for the walk has been misled, and nothing else here
@@ -14,7 +14,7 @@
import type {
School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus,
SchoolAdmissions, SchoolAdmissionDistance, SchoolDeprivation, SchoolFinance, NationalAverages,
SimilarSchool,
NearbySchool,
} from '@/lib/types';
import { ofstedLegacyAreas } from '@/lib/utils';
import type { SchoolFlags } from '@/lib/schoolSections';
@@ -27,7 +27,7 @@ import { HistorySection } from './HistorySection';
import { SchoolLifeSection } from './SchoolLifeSection';
import { LocalAreaSection } from './LocalAreaSection';
import { FinancesSection } from './FinancesSection';
import { SimilarSchoolsSection } from './SimilarSchoolsSection';
import { NearbySchoolsSection } from './NearbySchoolsSection';
export interface PrimarySchoolSectionsProps {
schoolInfo: School;
@@ -42,14 +42,14 @@ export interface PrimarySchoolSectionsProps {
finance: SchoolFinance | null;
nationalAvg: NationalAverages | null;
/** Nearby schools of a comparable intake. Absent on an older API build. */
similarSchools?: SimilarSchool[];
nearbySchools?: NearbySchool[];
flags: SchoolFlags;
}
export function PrimarySchoolSections({
schoolInfo, yearlyData, absenceData, ofsted, census,
admissions, admissionsHistory, admissionDistance,
deprivation, finance, nationalAvg, similarSchools, flags,
deprivation, finance, nationalAvg, nearbySchools, flags,
}: PrimarySchoolSectionsProps) {
const primaryAvg = nationalAvg?.primary ?? {};
const secondaryAvg = nationalAvg?.secondary ?? {};
@@ -152,12 +152,12 @@ export function PrimarySchoolSections({
{flags.hasFinance && finance && <FinancesSection finance={finance} />}
{/* Last: it is where the reader goes next, not part of this school. */}
<SimilarSchoolsSection
<NearbySchoolsSection
urn={schoolInfo.urn}
schoolName={schoolInfo.school_name}
phase={schoolInfo.phase}
thisMetricValue={flags.latestResults?.rwm_expected_pct ?? null}
similar={similarSchools}
nearby={nearbySchools}
/>
</>
);
@@ -14,7 +14,7 @@
import type {
School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus,
SchoolAdmissions, SchoolAdmissionDistance, SchoolDeprivation, SchoolFinance, NationalAverages,
SchoolDestinations, SimilarSchool,
SchoolDestinations, NearbySchool,
} from '@/lib/types';
import { ofstedLegacyAreas } from '@/lib/utils';
import type { SecondaryFlags } from '@/lib/schoolSections';
@@ -27,7 +27,7 @@ import { DistanceSection } from './DistanceSection';
import { SecondaryHistorySection } from './SecondaryHistorySection';
import { WellbeingSection } from './WellbeingSection';
import { FinancesSection } from './FinancesSection';
import { SimilarSchoolsSection } from './SimilarSchoolsSection';
import { NearbySchoolsSection } from './NearbySchoolsSection';
import styles from './schoolSections.module.css';
export interface SecondarySchoolSectionsProps {
@@ -49,14 +49,14 @@ export interface SecondarySchoolSectionsProps {
nationalAvg: NationalAverages | null;
destinations: SchoolDestinations | null;
/** Nearby schools of a comparable intake. Absent on an older API build. */
similarSchools?: SimilarSchool[];
nearbySchools?: NearbySchool[];
flags: SecondaryFlags;
}
export function SecondarySchoolSections({
schoolInfo, yearlyData, ofsted, census,
admissions, admissionsHistory, admissionDistance,
deprivation, finance, nationalAvg, destinations, similarSchools, flags,
deprivation, finance, nationalAvg, destinations, nearbySchools, flags,
}: SecondarySchoolSectionsProps) {
const secondaryAvg = nationalAvg?.secondary ?? {};
@@ -146,12 +146,12 @@ export function SecondarySchoolSections({
)}
{/* Last: it is where the reader goes next, not part of this school. */}
<SimilarSchoolsSection
<NearbySchoolsSection
urn={schoolInfo.urn}
schoolName={schoolInfo.school_name}
phase={schoolInfo.phase}
thisMetricValue={flags.latestResults?.attainment_8_score ?? null}
similar={similarSchools}
nearby={nearbySchools}
/>
</div>
);
+2 -2
View File
@@ -354,7 +354,7 @@ export interface SchoolsResponse {
* version ordered by it and buried the school down the road under faith
* matches three times further away.
*/
export interface SimilarSchool {
export interface NearbySchool {
urn: number;
school_name: string;
distance_miles: number;
@@ -384,7 +384,7 @@ export interface SchoolDetailsResponse {
* API that serves this must render without it. Absent and empty mean the
* same thing here — no section.
*/
similar_schools?: SimilarSchool[];
nearby_schools?: NearbySchool[];
yearly_data: SchoolResult[];
absence_data: AbsenceData | null;
// Supplementary data (null until Kestra populates)