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>
151 lines
6.8 KiB
TypeScript
151 lines
6.8 KiB
TypeScript
/**
|
|
* HistorySection — results over time (chart plus historical table).
|
|
* Primary pages; the secondary equivalent is SecondaryHistorySection (the two
|
|
* were only 40% similar). Server component.
|
|
*/
|
|
|
|
import type { School, SchoolResult, NationalAverages } from '@/lib/types';
|
|
import { formatPercentage, formatProgress, formatAcademicYear } from '@/lib/utils';
|
|
import { Section, sectionStyles as styles } from './sectionShared';
|
|
import { PerformanceChart, SatsChart } from './charts';
|
|
|
|
export function HistorySection({
|
|
yearlyData, schoolInfo, nationalAvg, primaryAvg, secondaryAvg,
|
|
isAllThrough, isPrimary, isSecondary, hasKS2Results, hasKS4Results,
|
|
suppressKs2Comparison, suppressKs4Comparison,
|
|
}: {
|
|
yearlyData: SchoolResult[];
|
|
schoolInfo: School;
|
|
nationalAvg: NationalAverages | null;
|
|
primaryAvg: Record<string, number>;
|
|
secondaryAvg: Record<string, number>;
|
|
isAllThrough: boolean;
|
|
isPrimary: boolean;
|
|
isSecondary: boolean;
|
|
hasKS2Results: boolean;
|
|
hasKS4Results: boolean;
|
|
suppressKs2Comparison: boolean;
|
|
suppressKs4Comparison: boolean;
|
|
}) {
|
|
return (
|
|
<section id="history" className={styles.card}>
|
|
<h2 className={styles.sectionTitle}>Results Over Time</h2>
|
|
{isAllThrough ? (
|
|
// All-through: KS2 and KS4 trends are on different scales and have
|
|
// different gap stories, so render them as two stacked charts
|
|
// rather than crowding 8+ series onto one axis.
|
|
<>
|
|
{hasKS2Results && (
|
|
<>
|
|
<h3 className={styles.subSectionTitle}>Primary — KS2 SATs</h3>
|
|
<div className={styles.chartContainer}>
|
|
<PerformanceChart
|
|
data={yearlyData}
|
|
schoolName={schoolInfo.school_name}
|
|
isSecondary={false}
|
|
nationalRwmAvg={suppressKs2Comparison ? null : (primaryAvg.rwm_expected_pct ?? null)}
|
|
nationalByYear={suppressKs2Comparison ? undefined : nationalAvg?.by_year}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
{hasKS4Results && (
|
|
<>
|
|
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.5rem' }}>Secondary — GCSEs</h3>
|
|
<div className={styles.chartContainer}>
|
|
<PerformanceChart
|
|
data={yearlyData}
|
|
schoolName={schoolInfo.school_name}
|
|
isSecondary={true}
|
|
nationalAtt8Avg={suppressKs4Comparison ? null : (secondaryAvg.attainment_8_score ?? null)}
|
|
nationalByYear={suppressKs4Comparison ? undefined : nationalAvg?.by_year}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className={styles.chartContainer}>
|
|
<PerformanceChart
|
|
data={yearlyData}
|
|
schoolName={schoolInfo.school_name}
|
|
isSecondary={isSecondary}
|
|
nationalRwmAvg={isPrimary && !suppressKs2Comparison ? (primaryAvg.rwm_expected_pct ?? null) : null}
|
|
nationalAtt8Avg={isSecondary && !suppressKs4Comparison ? (secondaryAvg.attainment_8_score ?? null) : null}
|
|
nationalByYear={(isPrimary ? suppressKs2Comparison : suppressKs4Comparison) ? undefined : nationalAvg?.by_year}
|
|
/>
|
|
</div>
|
|
)}
|
|
{yearlyData.length > 1 && (
|
|
<details className={styles.historyDisclosure}>
|
|
<summary className={styles.historyToggle}>View raw year-by-year data</summary>
|
|
<div className={styles.tableWrapper}>
|
|
<table className={styles.dataTable}>
|
|
<thead>
|
|
<tr>
|
|
<th>Year</th>
|
|
{isAllThrough ? (
|
|
<>
|
|
<th>RWM (expected %)</th>
|
|
<th>Exceeding (%)</th>
|
|
<th>Attainment 8</th>
|
|
<th>Progress 8</th>
|
|
<th>English & Maths Grade 4+</th>
|
|
</>
|
|
) : isSecondary ? (
|
|
<>
|
|
<th>Attainment 8</th>
|
|
<th>Progress 8</th>
|
|
<th>English & Maths Grade 4+</th>
|
|
<th>English & Maths Grade 5+</th>
|
|
</>
|
|
) : (
|
|
<>
|
|
<th>Reading, Writing & Maths (expected %)</th>
|
|
<th>Exceeding expected (%)</th>
|
|
<th>Reading Progress</th>
|
|
<th>Writing Progress</th>
|
|
<th>Maths Progress</th>
|
|
</>
|
|
)}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{yearlyData.map((result) => (
|
|
<tr key={result.year}>
|
|
<td className={styles.yearCell}>{formatAcademicYear(result.year)}</td>
|
|
{isAllThrough ? (
|
|
<>
|
|
<td>{result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'}</td>
|
|
<td>{result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'}</td>
|
|
<td>{result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'}</td>
|
|
<td>{result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'}</td>
|
|
<td>{result.english_maths_standard_pass_pct !== null ? formatPercentage(result.english_maths_standard_pass_pct) : '-'}</td>
|
|
</>
|
|
) : isSecondary ? (
|
|
<>
|
|
<td>{result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'}</td>
|
|
<td>{result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'}</td>
|
|
<td>{result.english_maths_standard_pass_pct !== null ? formatPercentage(result.english_maths_standard_pass_pct) : '-'}</td>
|
|
<td>{result.english_maths_strong_pass_pct !== null ? formatPercentage(result.english_maths_strong_pass_pct) : '-'}</td>
|
|
</>
|
|
) : (
|
|
<>
|
|
<td>{result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'}</td>
|
|
<td>{result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'}</td>
|
|
<td>{result.reading_progress !== null ? formatProgress(result.reading_progress) : '-'}</td>
|
|
<td>{result.writing_progress !== null ? formatProgress(result.writing_progress) : '-'}</td>
|
|
<td>{result.maths_progress !== null ? formatProgress(result.maths_progress) : '-'}</td>
|
|
</>
|
|
)}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</details>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|