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>
71 lines
2.7 KiB
TypeScript
71 lines
2.7 KiB
TypeScript
/**
|
|
* SecondaryHistorySection — results over time. Secondary pages.
|
|
* 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 } from './charts';
|
|
|
|
|
|
export function SecondaryHistorySection({
|
|
yearlyData, schoolInfo, nationalAvg, secondaryAvg, suppressComparison,
|
|
}: {
|
|
yearlyData: SchoolResult[];
|
|
schoolInfo: School;
|
|
nationalAvg: NationalAverages | null;
|
|
secondaryAvg: Record<string, number>;
|
|
suppressComparison: boolean;
|
|
}) {
|
|
// National Attainment 8 baseline for the "Results Over Time" chart.
|
|
const heroAtt8Nat = secondaryAvg.attainment_8_score ?? null;
|
|
|
|
return (
|
|
<section id="history" className={styles.card}>
|
|
<h2 className={styles.sectionTitle}>Historical Results</h2>
|
|
{yearlyData.length > 0 && (
|
|
<>
|
|
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.25rem' }}>Results Over Time</h3>
|
|
<div className={styles.chartContainer}>
|
|
<PerformanceChart
|
|
data={yearlyData}
|
|
schoolName={schoolInfo.school_name}
|
|
isSecondary={true}
|
|
nationalAtt8Avg={suppressComparison ? null : heroAtt8Nat}
|
|
nationalByYear={suppressComparison ? undefined : nationalAvg?.by_year}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
<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>
|
|
<th>Attainment 8</th>
|
|
<th>Progress 8</th>
|
|
<th>Eng & Maths 4+</th>
|
|
<th>EBacc entry %</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{yearlyData.map((result) => (
|
|
<tr key={result.year}>
|
|
<td className={styles.yearCell}>{formatAcademicYear(result.year)}</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>
|
|
<td>{result.ebacc_entry_pct != null ? formatPercentage(result.ebacc_entry_pct) : '-'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</details>
|
|
</section>
|
|
);
|
|
}
|