Files
TudorandClaude Opus 5 752eb07310 refactor(detail): render sections on the server behind a client shell
page.tsx now composes the sections and passes them through SchoolDetailShell
as children, so ~1,300 lines of static markup stop shipping as client
JavaScript. The shell keeps what is genuinely interactive: back link, header
reveal, hero map, compare CTA, sticky nav and scroll-spy.

The scroll-spy already located sections via document.getElementById, so it
works unchanged against server-rendered children.

Charts needed a client wrapper: next/dynamic with ssr:false is illegal in a
Server Component, so components/school/charts.tsx is the boundary that keeps
Chart.js (64 KB gz) lazy and browser-only.

Measured on this build:
- school route client chunk: 8 KB gz (33 KB raw)
- total static JS across all chunks: 380.6 -> 350.7 KB gz
- section markup is absent from every client chunk ("Got their first choice",
  "Ofsted reports", "Most deprived" etc. all return 0 hits); shell strings
  still present, as expected
- shared baseline unchanged at 172 KB gz -- out of scope, as designed

The 14 characterization tests pass byte-identical to the commit that
introduced them. Only the render helper changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 21:39:21 +01:00

137 lines
5.7 KiB
TypeScript

/**
* 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.
*/
import type { ReactNode } from 'react';
import type { SchoolAdmissions } from '@/lib/types';
import { formatAcademicYear, formatPercentage } from '@/lib/utils';
import { summariseAdmissions } from '@/lib/compareLogic';
import { Section, sectionStyles as styles } from './sectionShared';
import { AdmissionsViewToggle } from './AdmissionsViewToggle';
import { AdmissionsTrendChart } from './charts';
export function AdmissionsSection({
admissions,
admissionsHistory,
isAllThrough,
}: {
admissions: SchoolAdmissions;
admissionsHistory: SchoolAdmissions[];
isAllThrough: boolean;
}) {
// 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 = 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 = summariseAdmissions(admissions);
const title = <>Admissions{!showAdmissionsTrend && ` (${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 ? (
<p className={styles.sectionSubtitle}>
These figures are for {admissions.school_phase.toLowerCase()} entry
{/secondary/i.test(admissions.school_phase) ? ' (Year 7)' : /primary/i.test(admissions.school_phase) ? ' (Reception)' : ''}.
</p>
) : null;
const yearView = (
<>
<dl className={styles.admissionsTiles}>
{admissions.places_offered != null && (
<div className={styles.admissionsTile}>
<dd className={styles.admissionsTileNum}>{admissions.places_offered}</dd>
<dt className={styles.admissionsTileLabel}>Places offered</dt>
</div>
)}
{admissions.first_preference_applications != null && (
<div className={styles.admissionsTile}>
<dd className={styles.admissionsTileNum}>{admissions.first_preference_applications}</dd>
<dt className={styles.admissionsTileLabel}>Wanted it first</dt>
</div>
)}
{admissions.first_preference_offer_pct != null && (
<div className={`${styles.admissionsTile} ${styles.admissionsTileAccent}`}>
<dd className={styles.admissionsTileNum}>
{admissions.first_preference_offers != null && admissions.first_preference_applications != null ? (
<>
{admissions.first_preference_offers}
<span className={styles.admissionsTileSub}>
of {admissions.first_preference_applications} · {formatPercentage(admissions.first_preference_offer_pct)}
</span>
</>
) : (
formatPercentage(admissions.first_preference_offer_pct)
)}
</dd>
<dt className={styles.admissionsTileLabel}>Got their first choice</dt>
</div>
)}
{admissions.total_applications != null && (
<div className={styles.admissionsTile}>
<dd className={styles.admissionsTileNum}>{admissions.total_applications.toLocaleString()}</dd>
<dt className={styles.admissionsTileLabel}>Applied in total</dt>
</div>
)}
</dl>
{admissionsSummary.chip && (
<p className={styles.admissionsTrendSummary}>{admissionsSummary.chip.text}</p>
)}
</>
);
const trendView = (
<>
<div className={styles.admissionsChartCap}>First-choice offer rate</div>
<AdmissionsTrendChart history={admissionsHistory} />
<p className={styles.admissionsTrendSummary}>
This year ({formatAcademicYear(admissions.year)}),{' '}
{admissions.first_preference_applications != null && (
<><strong>{admissions.first_preference_applications}</strong> families put it first for </>
)}
{admissions.places_offered != null && <><strong>{admissions.places_offered}</strong> places</>}
{admissions.total_applications != null && ` — ${admissions.total_applications.toLocaleString()} applications in total`}.
</p>
</>
);
return (
<Section id="admissions">
{showAdmissionsTrend ? (
<AdmissionsViewToggle
title={title}
subtitle={subtitle}
trendLabel={`${admissionsHistory.length}-year trend`}
yearView={yearView}
trendView={trendView}
/>
) : (
/* No trend data — render statically, with no client component at all. */
<>
<div className={styles.admissionsHeader}>
<h2 className={styles.sectionTitle}>{title}</h2>
</div>
{subtitle}
<div className={styles.admissionsViewport}>
<div className={styles.admissionsViewYear}>{yearView}</div>
</div>
</>
)}
</Section>
);
}