/** * AdmissionsSection — primary detail pages. * * Not shared with the secondary page: the two versions were only 14% similar * (this one carries the year/trend toggle and the offer-rate chart; the * secondary one is a much simpler panel). See SecondaryAdmissionsSection. * * Server component. The year/trend toggle is delegated to the small * AdmissionsViewToggle client island, which receives both views as * server-rendered children. When there is only one year of offer data no * toggle renders at all, so such pages ship zero admissions JavaScript. * * The two views are stacked in one grid cell so switching never shifts layout, * which means the card is sized by the TALLER of them. Keep any view added * here close to the tile grid in height: a cut-off-distance view carrying a * chart, a table and a map was tried, measured 1402px against the tiles' 316px, * and left the default view as four tiles adrift in blank card. It now lives in * DistanceSection. */ import type { ReactNode } from 'react'; import type { SchoolAdmissions, SchoolAdmissionDistance } from '@/lib/types'; import { formatAcademicYear, formatPercentage } from '@/lib/utils'; import { summariseAdmissions } from '@/lib/compareLogic'; import { Section, sectionStyles as styles } from './sectionShared'; import { AdmissionsViewToggle, type AdmissionsView } from './AdmissionsViewToggle'; import { AdmissionsTrendChart } from './charts'; import { describeCutoff, CUTOFF_NOTE, CUTOFF_MEASUREMENT_NOTE } from './lastDistanceOffered'; export function AdmissionsSection({ admissions, admissionsHistory, admissionDistance, isAllThrough, }: { /* Nullable: the two data sources are independent. A minority of schools have a published cut-off distance but no EES admissions row (3% of the schools that render, sampled against staging), and for those this section exists to carry the distance alone. */ admissions: SchoolAdmissions | null; admissionsHistory: SchoolAdmissions[]; admissionDistance: SchoolAdmissionDistance | null; isAllThrough: boolean; }) { const cutoff = describeCutoff(admissionDistance); // Trend toggle only appears with ≥2 years carrying an offer rate. const admissionsOfferYears = admissionsHistory.filter((h) => h.first_preference_offer_pct != null).length; const showAdmissionsTrend = admissions != null && admissionsOfferYears >= 2; // Banded interpretation of the first-choice offer rate ("More than half of // first choices missed out" etc.) — the same banding the compare screen // uses, so a low offer rate reads as how severe it actually is. const admissionsSummary = admissions ? summariseAdmissions(admissions) : null; const title = ( <>Admissions{!showAdmissionsTrend && admissions && ` (${formatAcademicYear(admissions.year)})`} ); {/* All-through admissions data covers a single entry point (usually the Year 7 secondary intake), not reception — say so, or a parent could read these as the whole-school figures. */} const subtitle: ReactNode = isAllThrough && admissions?.school_phase ? (

These figures are for {admissions.school_phase.toLowerCase()} entry {/secondary/i.test(admissions.school_phase) ? ' (Year 7)' : /primary/i.test(admissions.school_phase) ? ' (Reception)' : ''}.

) : null; {/* Spans both columns rather than taking a half-width cell. This is the figure parents come to the page for, and at tile width the two-line "0.31 miles / September 2025" pairing wraps badly. */} {/* The {' '} between the figure and its metric support is not decoration. The two are flex children, so the gap is drawn by CSS and the text layer had nothing between them: textContent read "0.88 miles1.4 km", which is what a screen reader announces and what any text matcher sees. Whitespace text nodes are not rendered as flex items, so this changes the reading without changing the layout. */} const distanceTile = cutoff && (
{cutoff.primary}{' '} {cutoff.secondary}
Last distance offered · {cutoff.entryYear}
); const distanceNote = cutoff && (

{CUTOFF_NOTE} {CUTOFF_MEASUREMENT_NOTE} {cutoff.routeNote && <> {cutoff.routeNote}}

); /* * Which row template the tile grid needs. * * The grid reserves two equal rows so the year view matches the height of the * trend chart beside it. With a cut-off and no EES admissions there are no * metric tiles to fill them, and the reserved rows render as a bare block of * the grid's own gap colour under the distance tile. */ const hasMetricTiles = admissions != null && ( admissions.places_offered != null || admissions.first_preference_applications != null || admissions.first_preference_offer_pct != null || admissions.total_applications != null ); const tilesClass = [ styles.admissionsTiles, cutoff && hasMetricTiles ? styles.admissionsTilesWithDistance : '', cutoff && !hasMetricTiles ? styles.admissionsTilesDistanceOnly : '', ].filter(Boolean).join(' '); const yearView = ( <>
{admissions?.places_offered != null && (
{admissions.places_offered}
Places offered
)} {admissions?.first_preference_applications != null && (
{admissions.first_preference_applications}
Wanted it first
)} {admissions?.first_preference_offer_pct != null && (
{admissions.first_preference_offers != null && admissions.first_preference_applications != null ? ( <> {admissions.first_preference_offers} of {admissions.first_preference_applications} · {formatPercentage(admissions.first_preference_offer_pct)} ) : ( formatPercentage(admissions.first_preference_offer_pct) )}
Got their first choice
)} {admissions?.total_applications != null && (
{admissions.total_applications.toLocaleString()}
Applied in total
)} {distanceTile}
{admissionsSummary?.chip && (

{admissionsSummary.chip.text}

)} {distanceNote} ); {/* Guarded rather than asserted: showAdmissionsTrend already requires admissions, and tying the two together in one expression keeps that invariant checked by the compiler instead of assumed. */} const trendView = admissions && ( <>
First-choice offer rate

This year ({formatAcademicYear(admissions.year)}),{' '} {admissions.first_preference_applications != null && ( <>{admissions.first_preference_applications} families put it first for )} {admissions.places_offered != null && <>{admissions.places_offered} places} {admissions.total_applications != null && ` — ${admissions.total_applications.toLocaleString()} applications in total`}.

); const views: AdmissionsView[] = [ { id: 'year', label: 'This year', content: yearView, className: styles.admissionsViewYear }, ]; if (showAdmissionsTrend) { views.push({ id: 'trend', label: `${admissionsHistory.length}-year trend`, content: trendView, className: styles.admissionsViewTrend, }); } return (
{views.length > 1 ? ( ) : ( /* One view — render statically, with no client component at all. */ <>

{title}

{subtitle}
{yearView}
)}
); }