/** * SimilarSchoolsSection — nearby schools of the same phase and a comparable * intake. Server component; only the carousel, the compare bar and the * add-to-compare button are client-side. * * The section is allowed to say exactly what the backend matched and no more. * The lede only claims a similar intake when no card came from tier 3, and a * card's chips list what that school actually shares rather than a match it * did not earn. * * 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. */ import Link from 'next/link'; import type { SimilarSchool } from '@/lib/types'; import { schoolUrl } from '@/lib/utils'; import { AddToCompareButton } from './AddToCompareButton'; import { SimilarSchoolsCarousel } from './SimilarSchoolsCarousel'; import { SimilarSchoolsCompareBar } from './SimilarSchoolsCompareBar'; import { Section } from './sectionShared'; import styles from './SimilarSchools.module.css'; const MINIMUM = 2; export function shouldRenderSimilar(similar?: SimilarSchool[] | null): boolean { return (similar?.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'; } function formatMetric(value: number | null, key: string): string { if (value == null) return 'Not published'; return key === 'attainment_8_score' ? value.toFixed(1) : `${Math.round(value)}%`; } export function SimilarSchoolsSection({ urn, schoolName, phase, thisMetricValue, similar, }: { 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; }) { if (!shouldRenderSimilar(similar)) return null; const schools = similar as SimilarSchool[]; // One card matched on phase alone, so the section may not claim the set // shares an intake with this school. const loosest = Math.max(...schools.map((s) => s.tier)); const metricKey = schools[0].metric_key; const noun = nearbyNoun(phase); return (

Similar schools nearby

{loosest >= 3 ? `Other ${noun} near ${schoolName}.` : `Other ${noun} near ${schoolName}, with a similar intake.`}

} > {schools.map((school) => (
  • {school.distance_miles} miles away

    {school.school_name}

    {[school.school_type, school.age_range && `Ages ${school.age_range}`] .filter(Boolean) .join(' · ')}

      {school.shared.map((label) => (
    • = 3 ? styles.chipLoose : styles.chip}> {label}
    • ))}

    {formatMetric(school.metric_value, school.metric_key)}

    {metricLabel(school.metric_key)}

    {thisMetricValue != null && (

    {formatMetric(thisMetricValue, metricKey)} at this school

    )}
  • ))}
    {/* 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 corrects that. */}

    Distances are straight-line from this school, not road distance.

    ); }