refactor(detail): extract sections as server components
Moves ~1,300 lines of section markup out of the two client views into components/school/, mirroring the components/compare/ layout. Twelve section components plus shared primitives, all server components. The only client file is AdmissionsViewToggle, which owns the hidden/aria-pressed state and receives both views as server-rendered children. JSX was extracted mechanically rather than retyped, so the markup the CSS modules depend on is verbatim. Sharing follows measured similarity, not assumption: - Finances (91%) shared. The secondary premises-cost card is gated behind a prop so primary pages are unchanged; enabling it is a one-line follow-up. - Ofsted (80%) shared, but behind a variant prop. The headline similarity hid a real fork: on a school with no overall grade the primary page shows a "Not rated" badge while the secondary shows a four-area OEIF panel, and the disclaimer copy differs. Both preserved exactly; reconciling them is a human decision, not a side effect of a move. - Admissions (14%) and History (40%) kept separate. Not yet wired up -- the old views still render. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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 dynamic from 'next/dynamic';
|
||||
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';
|
||||
|
||||
const AdmissionsTrendChart = dynamic(() => import('../AdmissionsTrendChart'), { ssr: false });
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user