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:
Tudor
2026-08-02 21:33:28 +01:00
co-authored by Claude Opus 5
parent 2f786787b9
commit a7f6ff4035
14 changed files with 1645 additions and 0 deletions
@@ -0,0 +1,249 @@
/**
* GcseSection — KS4 headline results. Secondary pages. Server component.
*/
import type { School, SchoolResult } from '@/lib/types';
import { formatPercentage, formatProgress, formatAcademicYear } from '@/lib/utils';
import { MetricTooltip } from '../MetricTooltip';
import { DeltaChip } from '../DeltaChip';
import { SpecialSchoolNote } from '../SpecialSchoolNote';
import { Section, sectionStyles as styles, progressClass } from './sectionShared';
export function GcseSection({
latestResults, schoolInfo, secondaryAvg, p8Suspended, suppressComparison,
}: {
latestResults: SchoolResult;
schoolInfo: School;
secondaryAvg: Record<string, number>;
p8Suspended: boolean;
suppressComparison: boolean;
}) {
return (
<section id="gcse" className={styles.card}>
<h2 className={styles.sectionTitle}>
GCSE Results ({formatAcademicYear(latestResults.year)})
</h2>
<p className={styles.sectionSubtitle}>
GCSE results for Year 11 pupils.{!suppressComparison && ' England averages shown for comparison.'}
</p>
<SpecialSchoolNote school={schoolInfo} />
{p8Suspended && (
<div className={styles.p8Banner}>
Progress 8 isn&apos;t published for 2024/25: this GCSE year group sat no KS2 tests
(COVID), so DfE has no starting point to measure their progress from.
</div>
)}
{/* Hero stat cards — top GCSE metrics */}
<div className={styles.heroStatGrid}>
{latestResults.attainment_8_score != null && (
<div className={styles.heroStatCard}>
<div className={styles.heroStatLabel}>
Attainment 8 score
<MetricTooltip metricKey="attainment_8_score" />
</div>
<div className={styles.heroStatValue}>
{latestResults.attainment_8_score.toFixed(1)}
{!suppressComparison && secondaryAvg.attainment_8_score != null && (
<DeltaChip
value={latestResults.attainment_8_score}
baseline={secondaryAvg.attainment_8_score}
unit="pts"
size="sm"
/>
)}
</div>
{!suppressComparison && secondaryAvg.attainment_8_score != null && (
<div className={styles.heroStatHint}>England avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
)}
</div>
)}
{latestResults.progress_8_score != null && (
<div className={styles.heroStatCard}>
<div className={styles.heroStatLabel}>
Progress 8 score
<MetricTooltip metricKey="progress_8_score" />
</div>
<div className={`${styles.heroStatValue} ${progressClass(latestResults.progress_8_score)}`}>
{formatProgress(latestResults.progress_8_score)}
</div>
{(latestResults.progress_8_lower_ci != null && latestResults.progress_8_upper_ci != null) ? (
<div className={styles.heroStatHint}>
CI: {latestResults.progress_8_lower_ci.toFixed(2)} to {latestResults.progress_8_upper_ci.toFixed(2)}
</div>
) : (
<div className={styles.heroStatHint}>National baseline: 0.0</div>
)}
</div>
)}
{latestResults.english_maths_strong_pass_pct != null && (
<div className={styles.heroStatCard}>
<div className={styles.heroStatLabel}>
English &amp; Maths Grade 5+
<MetricTooltip metricKey="english_maths_strong_pass_pct" />
</div>
<div className={styles.heroStatValue}>
{formatPercentage(latestResults.english_maths_strong_pass_pct)}
{!suppressComparison && secondaryAvg.english_maths_strong_pass_pct != null && (
<DeltaChip
value={latestResults.english_maths_strong_pass_pct}
baseline={secondaryAvg.english_maths_strong_pass_pct}
unit="pts"
size="sm"
/>
)}
</div>
{!suppressComparison && secondaryAvg.english_maths_strong_pass_pct != null && (
<div className={styles.heroStatHint}>England avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
)}
</div>
)}
{latestResults.english_maths_standard_pass_pct != null && (
<div className={styles.heroStatCard}>
<div className={styles.heroStatLabel}>
English &amp; Maths Grade 4+
<MetricTooltip metricKey="english_maths_standard_pass_pct" />
</div>
<div className={styles.heroStatValue}>
{formatPercentage(latestResults.english_maths_standard_pass_pct)}
{!suppressComparison && secondaryAvg.english_maths_standard_pass_pct != null && (
<DeltaChip
value={latestResults.english_maths_standard_pass_pct}
baseline={secondaryAvg.english_maths_standard_pass_pct}
unit="pts"
size="sm"
/>
)}
</div>
{!suppressComparison && secondaryAvg.english_maths_standard_pass_pct != null && (
<div className={styles.heroStatHint}>England avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
)}
</div>
)}
</div>
{/* Attainment 8 visual bar (080 scale). This viz is explicitly
"school vs national", so it's dropped for special schools where
that comparison isn't meaningful. */}
{!suppressComparison && latestResults.attainment_8_score != null && (
<div className={styles.att8Viz}>
<div className={styles.att8VizLabel}>Attainment 8 school vs national</div>
<div className={styles.att8VizTrack}>
<div
className={styles.att8VizFill}
style={{ width: `${Math.min((latestResults.attainment_8_score / 80) * 100, 100)}%` }}
/>
{secondaryAvg.attainment_8_score != null && (
<div
className={styles.att8VizNatLine}
style={{ left: `${(secondaryAvg.attainment_8_score / 80) * 100}%` }}
>
<div className={styles.att8VizNatPill}>
Nat avg {secondaryAvg.attainment_8_score.toFixed(1)}
</div>
</div>
)}
</div>
<div className={styles.att8VizTicks}>
<span>0</span><span>20</span><span>40</span><span>60</span><span>80</span>
</div>
</div>
)}
{/* Progress 8 number line with CI */}
{latestResults.progress_8_score != null && !p8Suspended && (
<div className={styles.p8Viz}>
<div className={styles.p8VizLabel}>Progress 8 relative to national baseline (0)</div>
{(() => {
const p8 = latestResults.progress_8_score!;
const lo = latestResults.progress_8_lower_ci ?? p8;
const hi = latestResults.progress_8_upper_ci ?? p8;
const range = 6; // 3 to +3
const toX = (v: number) => `${Math.min(Math.max(((v + 3) / range) * 100, 0), 100)}%`;
return (
<div className={styles.p8VizTrack}>
{/* CI band */}
<div
className={styles.p8VizCi}
style={{ left: toX(lo), width: `calc(${toX(hi)} - ${toX(lo)})` }}
/>
{/* Zero line */}
<div className={styles.p8VizZero} style={{ left: toX(0) }} />
{/* Score dot */}
<div
className={`${styles.p8VizDot} ${p8 < 0 ? styles.p8VizDotNeg : ''}`}
style={{ left: toX(p8) }}
/>
</div>
);
})()}
<div className={styles.p8VizTicks}>
<span>3</span><span>2</span><span>1</span><span>0</span><span>+1</span><span>+2</span><span>+3</span>
</div>
</div>
)}
{/* Progress 8 component breakdown */}
{(latestResults.progress_8_english != null || latestResults.progress_8_maths != null ||
latestResults.progress_8_ebacc != null || latestResults.progress_8_open != null) && (
<>
<h3 className={styles.subSectionTitle}>Attainment 8 Components (Progress 8 contribution)</h3>
<div className={styles.metricTable}>
{[
{ label: 'English', val: latestResults.progress_8_english },
{ label: 'Maths', val: latestResults.progress_8_maths },
{ label: 'EBacc subjects', val: latestResults.progress_8_ebacc },
{ label: 'Open (other GCSEs)', val: latestResults.progress_8_open },
].filter(r => r.val != null).map(({ label, val }) => (
<div key={label} className={styles.metricRow}>
<span className={styles.metricName}>{label}</span>
<span className={`${styles.metricValue} ${progressClass(val)}`}>
{formatProgress(val!)}
</span>
</div>
))}
</div>
</>
)}
{/* EBacc */}
{(latestResults.ebacc_entry_pct != null || latestResults.ebacc_standard_pass_pct != null) && (
<>
<h3 className={styles.subSectionTitle} style={{ marginTop: '1rem' }}>
English Baccalaureate (EBacc)
<MetricTooltip metricKey="ebacc_entry_pct" />
</h3>
<div className={styles.metricTable}>
{latestResults.ebacc_entry_pct != null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Pupils entered for EBacc</span>
<span className={styles.metricValue}>{formatPercentage(latestResults.ebacc_entry_pct)}</span>
</div>
)}
{latestResults.ebacc_standard_pass_pct != null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>EBacc Grade 4+</span>
<span className={styles.metricValue}>{formatPercentage(latestResults.ebacc_standard_pass_pct)}</span>
</div>
)}
{latestResults.ebacc_strong_pass_pct != null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>EBacc Grade 5+</span>
<span className={styles.metricValue}>{formatPercentage(latestResults.ebacc_strong_pass_pct)}</span>
</div>
)}
{latestResults.ebacc_avg_score != null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>EBacc average point score</span>
<span className={styles.metricValue}>{latestResults.ebacc_avg_score.toFixed(2)}</span>
</div>
)}
</div>
</>
)}
</section>
);
}