diff --git a/nextjs-app/__tests__/lib/schoolSections.test.ts b/nextjs-app/__tests__/lib/schoolSections.test.ts index acaec4e..fdd807a 100644 --- a/nextjs-app/__tests__/lib/schoolSections.test.ts +++ b/nextjs-app/__tests__/lib/schoolSections.test.ts @@ -172,3 +172,39 @@ describe('buildSecondaryNavItems', () => { expect(ids).not.toContain('history'); }); }); + +describe('the similar-schools nav item', () => { + const navInput = { + ofsted: null, admissions: null, admissionDistance: null, + hasLocation: true, yearlyDataLength: 1, + }; + + it('appears on both templates when the section renders', () => { + const primary = computeSchoolFlags(primaryFixture); + const secondary = computeSecondaryFlags(secondaryFixture); + const input = { ...navInput, hasSimilarSchools: true }; + + expect(buildNavItems(primary, input).map((i) => i.id)).toContain('similar'); + expect(buildSecondaryNavItems(secondary, input).map((i) => i.id)).toContain('similar'); + }); + + it('is absent when the section does not render', () => { + const primary = computeSchoolFlags(primaryFixture); + const secondary = computeSecondaryFlags(secondaryFixture); + const input = { ...navInput, hasSimilarSchools: false }; + + expect(buildNavItems(primary, input).map((i) => i.id)).not.toContain('similar'); + expect(buildSecondaryNavItems(secondary, input).map((i) => i.id)).not.toContain('similar'); + }); + + it('is absent when nothing says either way', () => { + const primary = computeSchoolFlags(primaryFixture); + expect(buildNavItems(primary, navInput).map((i) => i.id)).not.toContain('similar'); + }); + + it('comes last, because the section renders last', () => { + const primary = computeSchoolFlags(primaryFixture); + const ids = buildNavItems(primary, { ...navInput, hasSimilarSchools: true }).map((i) => i.id); + expect(ids[ids.length - 1]).toBe('similar'); + }); +}); diff --git a/nextjs-app/app/(frontend)/school/[slug]/page.tsx b/nextjs-app/app/(frontend)/school/[slug]/page.tsx index fe0f4af..23b98e8 100644 --- a/nextjs-app/app/(frontend)/school/[slug]/page.tsx +++ b/nextjs-app/app/(frontend)/school/[slug]/page.tsx @@ -8,6 +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 { schoolBreadcrumbJsonLd, type SchoolPlace } from '@/lib/jsonld'; import { PrimarySchoolSections } from '@/components/school/PrimarySchoolSections'; import { SecondarySchoolSections } from '@/components/school/SecondarySchoolSections'; @@ -155,6 +156,8 @@ export default async function SchoolPage({ params }: SchoolPageProps) { // nothing rather than throwing, which is how this shipped without a // 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 ?? []; // Redirect bare URN to canonical slug URL const canonicalSlug = schoolUrl(urn, school_info.school_name).replace('/school/', ''); @@ -186,6 +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, + hasSimilarSchools: shouldRenderSimilar(similarSchools), yearlyDataLength: yearly_data.length, }; const primaryNavItems = buildNavItems(primaryFlags, navInput); @@ -262,6 +266,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) { finance={finance ?? null} nationalAvg={nationalAvg} destinations={destinations ?? null} + similarSchools={similarSchools} flags={secondaryFlags} /> @@ -284,6 +289,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) { deprivation={deprivation ?? null} finance={finance ?? null} nationalAvg={nationalAvg} + similarSchools={similarSchools} flags={primaryFlags} /> diff --git a/nextjs-app/components/school/PrimarySchoolSections.tsx b/nextjs-app/components/school/PrimarySchoolSections.tsx index 1eeabf5..4898c78 100644 --- a/nextjs-app/components/school/PrimarySchoolSections.tsx +++ b/nextjs-app/components/school/PrimarySchoolSections.tsx @@ -14,6 +14,7 @@ import type { School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus, SchoolAdmissions, SchoolAdmissionDistance, SchoolDeprivation, SchoolFinance, NationalAverages, + SimilarSchool, } from '@/lib/types'; import { ofstedLegacyAreas } from '@/lib/utils'; import type { SchoolFlags } from '@/lib/schoolSections'; @@ -26,6 +27,7 @@ import { HistorySection } from './HistorySection'; import { SchoolLifeSection } from './SchoolLifeSection'; import { LocalAreaSection } from './LocalAreaSection'; import { FinancesSection } from './FinancesSection'; +import { SimilarSchoolsSection } from './SimilarSchoolsSection'; export interface PrimarySchoolSectionsProps { schoolInfo: School; @@ -39,13 +41,15 @@ export interface PrimarySchoolSectionsProps { deprivation: SchoolDeprivation | null; finance: SchoolFinance | null; nationalAvg: NationalAverages | null; + /** Nearby schools of a comparable intake. Absent on an older API build. */ + similarSchools?: SimilarSchool[]; flags: SchoolFlags; } export function PrimarySchoolSections({ schoolInfo, yearlyData, absenceData, ofsted, census, admissions, admissionsHistory, admissionDistance, - deprivation, finance, nationalAvg, flags, + deprivation, finance, nationalAvg, similarSchools, flags, }: PrimarySchoolSectionsProps) { const primaryAvg = nationalAvg?.primary ?? {}; const secondaryAvg = nationalAvg?.secondary ?? {}; @@ -146,6 +150,15 @@ 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 68f8ba9..6cc2a25 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, + SchoolDestinations, SimilarSchool, } from '@/lib/types'; import { ofstedLegacyAreas } from '@/lib/utils'; import type { SecondaryFlags } from '@/lib/schoolSections'; @@ -27,6 +27,7 @@ import { DistanceSection } from './DistanceSection'; import { SecondaryHistorySection } from './SecondaryHistorySection'; import { WellbeingSection } from './WellbeingSection'; import { FinancesSection } from './FinancesSection'; +import { SimilarSchoolsSection } from './SimilarSchoolsSection'; import styles from './schoolSections.module.css'; export interface SecondarySchoolSectionsProps { @@ -47,13 +48,15 @@ export interface SecondarySchoolSectionsProps { finance: SchoolFinance | null; nationalAvg: NationalAverages | null; destinations: SchoolDestinations | null; + /** Nearby schools of a comparable intake. Absent on an older API build. */ + similarSchools?: SimilarSchool[]; flags: SecondaryFlags; } export function SecondarySchoolSections({ schoolInfo, yearlyData, ofsted, census, admissions, admissionsHistory, admissionDistance, - deprivation, finance, nationalAvg, destinations, flags, + deprivation, finance, nationalAvg, destinations, similarSchools, flags, }: SecondarySchoolSectionsProps) { const secondaryAvg = nationalAvg?.secondary ?? {}; @@ -141,6 +144,15 @@ export function SecondarySchoolSections({ {flags.hasFinance && finance && ( )} + + {/* Last: it is where the reader goes next, not part of this school. */} + ); } diff --git a/nextjs-app/lib/schoolSections.ts b/nextjs-app/lib/schoolSections.ts index eeec922..fedc0be 100644 --- a/nextjs-app/lib/schoolSections.ts +++ b/nextjs-app/lib/schoolSections.ts @@ -128,6 +128,10 @@ export interface NavItemsInput { * measure a postcode, so the nav must gate on them too or it will link to an * anchor that was never rendered. */ hasLocation?: boolean; + /** Whether the similar-schools section will render. Optional for the same + * reason hasLocation is: the nav must never link to an anchor that was not + * rendered, and absent has to mean "no section". */ + hasSimilarSchools?: boolean; yearlyDataLength: number; } @@ -142,7 +146,10 @@ export interface NavItemsInput { */ export function buildNavItems( flags: SchoolFlags, - { ofsted, admissions, admissionDistance, hasLocation, yearlyDataLength }: NavItemsInput, + { + ofsted, admissions, admissionDistance, hasLocation, + hasSimilarSchools, yearlyDataLength, + }: NavItemsInput, ): NavItem[] { const navItems: NavItem[] = []; if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' }); @@ -161,6 +168,8 @@ export function buildNavItems( if (flags.hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' }); if (flags.hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' }); if (flags.hasFinance) navItems.push({ id: 'finances', label: 'Finances' }); + // Last, because the section renders last — the scroll-spy reads this order. + if (hasSimilarSchools) navItems.push({ id: 'similar', label: 'Similar schools' }); return navItems; } @@ -239,7 +248,10 @@ export function computeSecondaryFlags({ */ export function buildSecondaryNavItems( flags: SecondaryFlags, - { ofsted, admissions, admissionDistance, hasLocation, yearlyDataLength }: NavItemsInput, + { + ofsted, admissions, admissionDistance, hasLocation, + hasSimilarSchools, yearlyDataLength, + }: NavItemsInput, ): NavItem[] { const navItems: NavItem[] = []; if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' }); @@ -257,5 +269,7 @@ export function buildSecondaryNavItems( if (yearlyDataLength > 1) navItems.push({ id: 'history', label: 'History' }); if (flags.hasWellbeing) navItems.push({ id: 'wellbeing', label: 'Wellbeing' }); if (flags.hasFinance) navItems.push({ id: 'finances', label: 'Finances' }); + // Last, because the section renders last — the scroll-spy reads this order. + if (hasSimilarSchools) navItems.push({ id: 'similar', label: 'Similar schools' }); return navItems; }