diff --git a/backend/app.py b/backend/app.py index e46aff9..9cfb87d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -41,7 +41,7 @@ from .data_loader import get_data_info as get_db_info from . import flags from .places import build_place_index, build_place_registry, places_for_urn from .schemas import METRIC_DEFINITIONS, PHASE_GROUPS, RANKING_COLUMNS, SCHOOL_COLUMNS -from .similar_schools import select_similar +from .nearby_schools import select_nearby from .utils import clean_for_json, convert_to_native # Values to exclude from filter dropdowns (empty strings, non-applicable labels) @@ -266,10 +266,10 @@ def _places_payload(urn: int) -> list[dict]: return payload -def _similar_schools_payload(urn: int) -> list[dict]: +def _nearby_schools_payload(urn: int) -> list[dict]: """The nearest eligible schools this page may offer, closest first. - Phase and reach are read from the school's own row inside select_similar, + Phase and reach are read from the school's own row inside select_nearby, so nothing here can hand it a phase that disagrees with the data. Wrapped: a failure in selection must never 500 a page that is otherwise @@ -277,12 +277,12 @@ def _similar_schools_payload(urn: int) -> list[dict]: section simply does not render. """ try: - return select_similar(load_latest_school_data(), int(urn)) + return select_nearby(load_latest_school_data(), int(urn)) except Exception: import logging logging.getLogger(__name__).exception( - "Similar schools selection failed for urn=%s", urn + "Nearby schools selection failed for urn=%s", urn ) return [] @@ -1000,7 +1000,7 @@ async def get_school_details(request: Request, urn: int): # The nearest eligible schools, closest first. Always present on a # build with this code; the frontend treats absent and empty # identically, which is what lets the two images deploy independently. - "similar_schools": _similar_schools_payload(urn), + "nearby_schools": _nearby_schools_payload(urn), "yearly_data": clean_for_json(school_data), # Supplementary data (null if not yet populated by Kestra) "ofsted": supplementary.get("ofsted"), diff --git a/backend/similar_schools.py b/backend/nearby_schools.py similarity index 99% rename from backend/similar_schools.py rename to backend/nearby_schools.py index 08a5966..c4f22bd 100644 --- a/backend/similar_schools.py +++ b/backend/nearby_schools.py @@ -183,7 +183,7 @@ def _shared(subject: pd.Series, candidate: pd.Series, is_secondary: bool) -> lis return shared -def select_similar(frame: pd.DataFrame, urn: int) -> list[dict]: +def select_nearby(frame: pd.DataFrame, urn: int) -> list[dict]: """The nearest eligible schools, closest first — at most MAX_SCHOOLS, and none at all below MINIMUM. diff --git a/backend/schemas.py b/backend/schemas.py index 8d49c3c..5dbec05 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -536,7 +536,7 @@ RANKING_COLUMNS = [ # include. All-through schools appear in both primary and secondary results, # which is why this is a set per phase rather than a single string comparison. # -# Lives here rather than in app.py because similar_schools.py needs it too, and +# Lives here rather than in app.py because nearby_schools.py needs it too, and # importing app from there would be a cycle. PHASE_GROUPS: dict[str, set[str]] = { "primary": {"primary", "middle deemed primary", "all-through"}, diff --git a/backend/tests/test_similar_schools.py b/backend/tests/test_nearby_schools.py similarity index 89% rename from backend/tests/test_similar_schools.py rename to backend/tests/test_nearby_schools.py index 874ed4b..a096d9b 100644 --- a/backend/tests/test_similar_schools.py +++ b/backend/tests/test_nearby_schools.py @@ -15,10 +15,10 @@ card and never reorders the row. import numpy as np import pandas as pd -from backend.similar_schools import ( +from backend.nearby_schools import ( is_secondary_phase, radius_miles, - select_similar, + select_nearby, ) BASE_LAT, BASE_LON = 51.5000, -0.1000 @@ -66,7 +66,7 @@ def test_returns_nearest_first(): _row(100003, "Near", latitude=_at(0.4)), _row(100004, "Far", latitude=_at(1.8)), ) - result = select_similar(frame, 100001) + result = select_nearby(frame, 100001) assert [s["urn"] for s in result] == [100003, 100002, 100004] assert result[0]["distance_miles"] == 0.4 @@ -81,7 +81,7 @@ def test_a_faith_match_never_outranks_a_closer_school(): _row(100004, "Sacred Heart RC", religious_denomination="Roman Catholic", latitude=_at(1.2)), _row(100005, "St Peter's RC", religious_denomination="Roman Catholic", latitude=_at(1.6)), ) - result = select_similar(frame, 100001) + result = select_nearby(frame, 100001) assert result[0]["urn"] == 100002, "the nearest school leads, whatever its intake" assert [s["distance_miles"] for s in result] == sorted(s["distance_miles"] for s in result) @@ -98,7 +98,7 @@ def test_the_nearest_eligible_school_is_always_shown(): for n in range(6) ], ) - assert select_similar(frame, 100001)[0]["urn"] == 100002 + assert select_nearby(frame, 100001)[0]["urn"] == 100002 def test_caps_at_six_taking_the_nearest(): @@ -106,7 +106,7 @@ def test_caps_at_six_taking_the_nearest(): _row(100001, "Subject"), *[_row(100010 + n, f"Peer {n}", latitude=_at(0.1 * (n + 1))) for n in range(7)], ) - result = select_similar(frame, 100001) + result = select_nearby(frame, 100001) assert len(result) == 6 assert 100016 not in {s["urn"] for s in result}, "the seventh-nearest is the one dropped" @@ -116,7 +116,7 @@ def test_fewer_than_two_matches_returns_empty(): _row(100001, "Subject"), _row(100002, "Only neighbour", latitude=_at(0.5)), ) - assert select_similar(frame, 100001) == [] + assert select_nearby(frame, 100001) == [] def test_excludes_the_subject_school(): @@ -125,7 +125,7 @@ def test_excludes_the_subject_school(): _row(100002, "A", latitude=_at(0.5)), _row(100003, "B", latitude=_at(0.6)), ) - assert 100001 not in {s["urn"] for s in select_similar(frame, 100001)} + assert 100001 not in {s["urn"] for s in select_nearby(frame, 100001)} def test_a_school_is_never_listed_twice(): @@ -134,7 +134,7 @@ def test_a_school_is_never_listed_twice(): _row(100002, "A", latitude=_at(0.5)), _row(100003, "B", latitude=_at(0.6)), ) - result = select_similar(frame, 100001) + result = select_nearby(frame, 100001) assert len(result) == len({s["urn"] for s in result}) @@ -151,7 +151,7 @@ def test_primary_does_not_reach_past_two_miles(): ) # One inside the cap is below the minimum, so nothing renders at all — # a primary with nothing within two miles has no nearby schools. - assert select_similar(frame, 100001) == [] + assert select_nearby(frame, 100001) == [] def test_secondary_reaches_further_than_primary(): @@ -160,7 +160,7 @@ def test_secondary_reaches_further_than_primary(): _row(100002, "A", phase="Secondary", latitude=_at(3.0)), _row(100003, "B", phase="Secondary", latitude=_at(5.5)), ) - assert {s["urn"] for s in select_similar(frame, 100001)} == {100002, 100003} + assert {s["urn"] for s in select_nearby(frame, 100001)} == {100002, 100003} def test_the_cap_follows_the_phase(): @@ -183,8 +183,8 @@ def test_selective_never_meets_non_selective(): _row(100002, "Comp A", phase="Secondary", admissions_policy="Non-selective", latitude=_at(0.5)), _row(100003, "Comp B", phase="Secondary", admissions_policy="Non-selective", latitude=_at(0.6)), ) - assert select_similar(frame, 100001) == [] - assert 100001 not in {s["urn"] for s in select_similar(frame, 100002)} + assert select_nearby(frame, 100001) == [] + assert 100001 not in {s["urn"] for s in select_nearby(frame, 100002)} def test_special_schools_match_only_each_other(): @@ -193,8 +193,8 @@ def test_special_schools_match_only_each_other(): _row(100002, "Mainstream A", latitude=_at(0.5)), _row(100003, "Mainstream B", latitude=_at(0.6)), ) - assert select_similar(frame, 100001) == [] - assert select_similar(frame, 100002) == [] + assert select_nearby(frame, 100001) == [] + assert select_nearby(frame, 100002) == [] def test_boys_never_meets_girls(): @@ -204,7 +204,7 @@ def test_boys_never_meets_girls(): _row(100003, "Mixed School", gender="Mixed", latitude=_at(0.6)), _row(100004, "Another Mixed", gender="Mixed", latitude=_at(0.7)), ) - urns = {s["urn"] for s in select_similar(frame, 100001)} + urns = {s["urn"] for s in select_nearby(frame, 100001)} assert 100002 not in urns assert urns == {100003, 100004} @@ -217,7 +217,7 @@ def test_closed_schools_and_missing_coordinates_are_dropped(): _row(100004, "Good A", latitude=_at(0.6)), _row(100005, "Good B", latitude=_at(0.7)), ) - assert {s["urn"] for s in select_similar(frame, 100001)} == {100004, 100005} + assert {s["urn"] for s in select_nearby(frame, 100001)} == {100004, 100005} def test_all_through_is_offered_on_both_phase_sides(): @@ -226,14 +226,14 @@ def test_all_through_is_offered_on_both_phase_sides(): _row(100002, "All through", phase="All-through", latitude=_at(0.5)), _row(100003, "Primary peer", phase="Primary", latitude=_at(0.6)), ) - assert 100002 in {s["urn"] for s in select_similar(frame, 100001)} + assert 100002 in {s["urn"] for s in select_nearby(frame, 100001)} secondary = _frame( _row(100010, "Secondary subject", phase="Secondary"), _row(100002, "All through", phase="All-through", latitude=_at(0.5)), _row(100011, "Secondary peer", phase="Secondary", latitude=_at(0.6)), ) - assert 100002 in {s["urn"] for s in select_similar(secondary, 100010)} + assert 100002 in {s["urn"] for s in select_nearby(secondary, 100010)} def test_sixteen_plus_is_matched_against_secondary_not_primary(): @@ -248,7 +248,7 @@ def test_sixteen_plus_is_matched_against_secondary_not_primary(): _row(100003, "Nearby College", phase="16 plus", latitude=_at(0.6)), _row(100004, "Nearby Primary", phase="Primary", latitude=_at(0.1)), ) - result = select_similar(frame, 100001) + result = select_nearby(frame, 100001) urns = {s["urn"] for s in result} assert 100004 not in urns, "a primary school is not a peer for a sixth form" assert urns == {100002, 100003} @@ -278,7 +278,7 @@ def test_shared_lists_only_what_is_actually_shared(): _row(100003, "Faith differs", phase="Secondary", gender="Mixed", religious_denomination="Church of England", admissions_policy="Non-selective", latitude=_at(0.6)), ) - by_urn = {s["urn"]: s for s in select_similar(frame, 100001)} + by_urn = {s["urn"]: s for s in select_nearby(frame, 100001)} assert by_urn[100002]["shared"] == ["Mixed", "Non-selective", "No religious character"] assert by_urn[100003]["shared"] == ["Mixed", "Non-selective"] @@ -289,7 +289,7 @@ def test_a_shared_faith_is_named(): _row(100002, "Also RC", religious_denomination="Roman Catholic", latitude=_at(0.4)), _row(100003, "Secular", religious_denomination="None", latitude=_at(0.5)), ) - by_urn = {s["urn"]: s for s in select_similar(frame, 100001)} + by_urn = {s["urn"]: s for s in select_nearby(frame, 100001)} assert "Roman Catholic" in by_urn[100002]["shared"] assert by_urn[100003]["shared"] == ["Mixed"] @@ -300,7 +300,7 @@ def test_shared_is_empty_when_nothing_is_shared(): _row(100002, "A", gender="Mixed", religious_denomination="None", latitude=_at(0.4)), _row(100003, "B", gender="Mixed", religious_denomination="Church of England", latitude=_at(0.5)), ) - assert all(s["shared"] == [] for s in select_similar(frame, 100001)) + assert all(s["shared"] == [] for s in select_nearby(frame, 100001)) def test_no_tier_is_reported_because_there_are_no_tiers(): @@ -309,7 +309,7 @@ def test_no_tier_is_reported_because_there_are_no_tiers(): _row(100002, "A", latitude=_at(0.4)), _row(100003, "B", latitude=_at(0.5)), ) - assert all("tier" not in s for s in select_similar(frame, 100001)) + assert all("tier" not in s for s in select_nearby(frame, 100001)) def test_metric_follows_the_phase_side_not_the_neighbour(): @@ -318,7 +318,7 @@ def test_metric_follows_the_phase_side_not_the_neighbour(): _row(100002, "A", phase="Secondary", attainment_8_score=52.8, latitude=_at(0.5)), _row(100003, "B", phase="Secondary", attainment_8_score=np.nan, latitude=_at(0.6)), ) - by_urn = {s["urn"]: s for s in select_similar(frame, 100001)} + by_urn = {s["urn"]: s for s in select_nearby(frame, 100001)} assert by_urn[100002]["metric_key"] == "attainment_8_score" assert by_urn[100002]["metric_value"] == 52.8 assert by_urn[100002]["metric_year"] == 202425 @@ -331,7 +331,7 @@ def test_values_are_json_safe_native_types(): _row(100002, "A", latitude=_at(0.5)), _row(100003, "B", latitude=_at(0.6)), ) - for school in select_similar(frame, 100001): + for school in select_nearby(frame, 100001): assert isinstance(school["urn"], int) assert isinstance(school["distance_miles"], float) assert not isinstance(school["metric_value"], np.generic) @@ -366,7 +366,7 @@ def client(monkeypatch): def test_detail_payload_carries_nearby_schools(client): resp = client.get("/api/schools/100001") assert resp.status_code == 200, resp.text - similar = resp.json()["similar_schools"] + similar = resp.json()["nearby_schools"] assert [s["school_name"] for s in similar] == ["Neighbour A", "Neighbour B"] assert similar[0]["metric_key"] == "rwm_expected_pct" @@ -377,7 +377,7 @@ def test_a_failure_in_selection_does_not_break_the_page(client, monkeypatch): def _explode(*args, **kwargs): raise ValueError("selection blew up") - monkeypatch.setattr(app_module, "select_similar", _explode) + monkeypatch.setattr(app_module, "select_nearby", _explode) resp = client.get("/api/schools/100001") assert resp.status_code == 200, resp.text - assert resp.json()["similar_schools"] == [] + assert resp.json()["nearby_schools"] == [] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e5ca4cc..6d72959 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -73,12 +73,12 @@ There is no SWR dependency. Leaflet maps are loaded through dynamic wrappers; Chart.js renders performance and comparison charts. `components/school/` contains detail sections, with section decisions and data -preparation in `lib/schoolSections.ts`. The similar-schools section is selected -in `backend/similar_schools.py` — hard filters that never relax (phase, -provision, selectivity, gender) and soft preferences that do (religious -character, then gender exactness) — and served on `/api/schools/{urn}`. Its -rules are presentation logic, deliberately kept out of `marts.*` so they can be -tuned by deploy rather than by pipeline run. `lib/types.ts` contains manually maintained +preparation in `lib/schoolSections.ts`. The nearby-schools section is selected in +`backend/nearby_schools.py` — hard filters decide eligibility (phase, provision, +selectivity, gender) and distance alone decides the order, capped per phase — +and served on `/api/schools/{urn}`. Its rules are presentation logic, +deliberately kept out of `marts.*` so they can be tuned by deploy rather than by +pipeline run. `lib/types.ts` contains manually maintained API types. `payload-types.ts` and the Payload import map are generated artifacts. ## Publication and caching today diff --git a/nextjs-app/__tests__/components/SimilarSchoolsSection.test.tsx b/nextjs-app/__tests__/components/NearbySchoolsSection.test.tsx similarity index 87% rename from nextjs-app/__tests__/components/SimilarSchoolsSection.test.tsx rename to nextjs-app/__tests__/components/NearbySchoolsSection.test.tsx index 3e381b3..0f042d8 100644 --- a/nextjs-app/__tests__/components/SimilarSchoolsSection.test.tsx +++ b/nextjs-app/__tests__/components/NearbySchoolsSection.test.tsx @@ -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 }) => ( ), })); -jest.mock('@/components/school/SimilarSchoolsCompareBar', () => ({ - SimilarSchoolsCompareBar: ({ thisUrn }: { thisUrn: number }) => ( +jest.mock('@/components/school/NearbySchoolsCompareBar', () => ({ + NearbySchoolsCompareBar: ({ thisUrn }: { thisUrn: number }) => (
bar for {thisUrn}
), })); -function school(overrides: Partial = {}): SimilarSchool { +function school(overrides: Partial = {}): NearbySchool { return { urn: 100002, school_name: 'Willow Lane Primary School', @@ -41,14 +41,14 @@ function school(overrides: Partial = {}): SimilarSchool { }; } -function renderSection(similar: SimilarSchool[]) { +function renderSection(nearby: NearbySchool[]) { return render( - , ); } @@ -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( - , ); // 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( - , ); expect(screen.getByText(/Other schools and colleges near Barnet Sixth Form College/)).toBeInTheDocument(); diff --git a/nextjs-app/app/(frontend)/school/[slug]/page.tsx b/nextjs-app/app/(frontend)/school/[slug]/page.tsx index 1a2f040..838ab67 100644 --- a/nextjs-app/app/(frontend)/school/[slug]/page.tsx +++ b/nextjs-app/app/(frontend)/school/[slug]/page.tsx @@ -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} /> @@ -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} /> diff --git a/nextjs-app/components/school/AddToCompareButton.tsx b/nextjs-app/components/school/AddToCompareButton.tsx index b263969..dd519ea 100644 --- a/nextjs-app/components/school/AddToCompareButton.tsx +++ b/nextjs-app/components/school/AddToCompareButton.tsx @@ -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); diff --git a/nextjs-app/components/school/SimilarSchools.module.css b/nextjs-app/components/school/NearbySchools.module.css similarity index 100% rename from nextjs-app/components/school/SimilarSchools.module.css rename to nextjs-app/components/school/NearbySchools.module.css diff --git a/nextjs-app/components/school/SimilarSchoolsCarousel.tsx b/nextjs-app/components/school/NearbySchoolsCarousel.tsx similarity index 97% rename from nextjs-app/components/school/SimilarSchoolsCarousel.tsx rename to nextjs-app/components/school/NearbySchoolsCarousel.tsx index 8b748a4..ccc59fd 100644 --- a/nextjs-app/components/school/SimilarSchoolsCarousel.tsx +++ b/nextjs-app/components/school/NearbySchoolsCarousel.tsx @@ -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, diff --git a/nextjs-app/components/school/SimilarSchoolsCompareBar.tsx b/nextjs-app/components/school/NearbySchoolsCompareBar.tsx similarity index 90% rename from nextjs-app/components/school/SimilarSchoolsCompareBar.tsx rename to nextjs-app/components/school/NearbySchoolsCompareBar.tsx index 8e0050b..ff99478 100644 --- a/nextjs-app/components/school/SimilarSchoolsCompareBar.tsx +++ b/nextjs-app/components/school/NearbySchoolsCompareBar.tsx @@ -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(); diff --git a/nextjs-app/components/school/SimilarSchoolsSection.tsx b/nextjs-app/components/school/NearbySchoolsSection.tsx similarity index 88% rename from nextjs-app/components/school/SimilarSchoolsSection.tsx rename to nextjs-app/components/school/NearbySchoolsSection.tsx index ec0334e..07d4bba 100644 --- a/nextjs-app/components/school/SimilarSchoolsSection.tsx +++ b/nextjs-app/components/school/NearbySchoolsSection.tsx @@ -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 (
- ))} - + - + {/* 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 diff --git a/nextjs-app/components/school/PrimarySchoolSections.tsx b/nextjs-app/components/school/PrimarySchoolSections.tsx index 18bf76c..b7dd319 100644 --- a/nextjs-app/components/school/PrimarySchoolSections.tsx +++ b/nextjs-app/components/school/PrimarySchoolSections.tsx @@ -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 && } {/* 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 a660e1f..697dfaa 100644 --- a/nextjs-app/components/school/SecondarySchoolSections.tsx +++ b/nextjs-app/components/school/SecondarySchoolSections.tsx @@ -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. */} - ); diff --git a/nextjs-app/lib/types.ts b/nextjs-app/lib/types.ts index 425369a..d2fdc97 100644 --- a/nextjs-app/lib/types.ts +++ b/nextjs-app/lib/types.ts @@ -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)