PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m20s
Rewrites every visitor-facing string that used an em dash, choosing the punctuation for what the dash was doing: a colon before a list or explanation, a comma for an aside, a full stop between two thoughts, parentheses for an aside mid-sentence. Covers page titles and meta descriptions, the home and admissions guide copy, school page headings and notes, the compare page, metric labels and tooltips. Two rewrites also fix the sentence around them: the closure banner no longer repeats "proposed for closure", and the cut-off caveat's list of priorities now parses. A lone dash marking a missing value in a table cell stays: it is a data convention, not prose. A Jest guard walks the source with the TypeScript parser and fails on any other em dash in a string or JSX text node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
216 lines
8.0 KiB
TypeScript
216 lines
8.0 KiB
TypeScript
/**
|
|
* Who goes there — the school's community from the latest census plus GIAS
|
|
* facts. Benchmark chips use the computed state-school averages and must
|
|
* carry their provenance wording (never "England average" for computed
|
|
* figures). Copy verbatim from the reviewed mockups.
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { verdict } from '@/lib/compareLogic';
|
|
import type { Benchmarks, ComparisonData, School } from '@/lib/types';
|
|
import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
|
|
|
function pctSplit(part: number | null | undefined, total: number | null | undefined): string | null {
|
|
if (part == null || total == null || total === 0) return null;
|
|
return `${Math.round((part / total) * 100)}%`;
|
|
}
|
|
|
|
export function CompareCommunity({
|
|
schools,
|
|
data,
|
|
benchmarks,
|
|
isSecondary: propIsSecondary,
|
|
}: {
|
|
schools: School[];
|
|
data: Record<string, ComparisonData>;
|
|
benchmarks?: Benchmarks;
|
|
isSecondary?: boolean;
|
|
}) {
|
|
const isSecondary = propIsSecondary !== undefined ? propIsSecondary : schools.some(
|
|
(school) => data[String(school.urn)]?.school_info?.attainment_8_score != null,
|
|
);
|
|
const bench = isSecondary ? benchmarks?.secondary : benchmarks?.primary;
|
|
|
|
const fsmChip = (value: number | null) => {
|
|
// FSM is anchored only against a real FSM benchmark (census-sourced,
|
|
// pupil-weighted). disadvantaged_pct is a different measure (FSM6+CLA)
|
|
// — never fall back across definitions; no anchor means no chip.
|
|
const anchor = bench?.fsm_pct ?? null;
|
|
if (value == null || anchor == null) return null;
|
|
const v = verdict(value, anchor, 3);
|
|
return (
|
|
<Chip tone="neutral">
|
|
{v === 'above' && `Above the state-school average (${Math.round(anchor)}%)`}
|
|
{v === 'close' && `About the state-school average (${Math.round(anchor)}%)`}
|
|
{v === 'below' && `Below the state-school average (${Math.round(anchor)}%)`}
|
|
</Chip>
|
|
);
|
|
};
|
|
|
|
const anyAllThrough = schools.some((school) => /all.?through/i.test(school.phase ?? ''));
|
|
|
|
return (
|
|
<Section
|
|
title="Who goes there"
|
|
how={
|
|
<>
|
|
The school's community, from the latest school census. State-school averages are
|
|
computed from our dataset and shown for context. There's no “right”
|
|
number here.
|
|
{anyAllThrough && (
|
|
<>
|
|
{' '}
|
|
For all-through schools these figures cover the whole school and all ages, not just
|
|
the {isSecondary ? 'secondary' : 'primary'} phase.
|
|
</>
|
|
)}
|
|
</>
|
|
}
|
|
>
|
|
<SectionGrid schools={schools}>
|
|
<Measure label="Pupils on roll">
|
|
{schools.map((school, i) => {
|
|
const info = data[String(school.urn)]?.school_info as (School & { gias_total_pupils?: number | null; capacity?: number | null }) | undefined;
|
|
const census = data[String(school.urn)]?.census;
|
|
const pupils = census?.total_pupils ?? info?.gias_total_pupils ?? null;
|
|
const capacity = info?.capacity ?? null;
|
|
let capNote: string | null = null;
|
|
if (pupils != null && capacity != null && capacity > 0) {
|
|
capNote =
|
|
pupils >= capacity
|
|
? `${capacity.toLocaleString('en-GB')} places (at or above capacity)`
|
|
: `of ${capacity.toLocaleString('en-GB')} places (${Math.round((pupils / capacity) * 100)}% full)`;
|
|
}
|
|
return (
|
|
<Cell key={school.urn} school={school} index={i}>
|
|
{pupils != null ? (
|
|
<>
|
|
{pupils.toLocaleString('en-GB')}
|
|
{capNote && <span className={s.small}>{capNote}</span>}
|
|
</>
|
|
) : (
|
|
<span className={s.small}>No data</span>
|
|
)}
|
|
</Cell>
|
|
);
|
|
})}
|
|
</Measure>
|
|
|
|
<Measure label="Girls / boys">
|
|
{schools.map((school, i) => {
|
|
const census = data[String(school.urn)]?.census;
|
|
const girls = pctSplit(census?.female_pupils, census?.total_pupils);
|
|
const boys = pctSplit(census?.male_pupils, census?.total_pupils);
|
|
return (
|
|
<Cell key={school.urn} school={school} index={i}>
|
|
{girls && boys ? `${girls} / ${boys}` : <span className={s.small}>No data</span>}
|
|
</Cell>
|
|
);
|
|
})}
|
|
</Measure>
|
|
|
|
<Measure
|
|
tip="% of pupils eligible for free school meals, a common measure of how many pupils come from lower-income families. Benchmark computed across state schools in our dataset."
|
|
label="Free school meals"
|
|
>
|
|
{schools.map((school, i) => {
|
|
const fsm = data[String(school.urn)]?.census?.fsm_pct ?? null;
|
|
return (
|
|
<Cell key={school.urn} school={school} index={i}>
|
|
{fsm != null ? (
|
|
<>
|
|
{Math.round(fsm)}% {fsmChip(fsm)}
|
|
</>
|
|
) : (
|
|
<span className={s.small}>No data</span>
|
|
)}
|
|
</Cell>
|
|
);
|
|
})}
|
|
</Measure>
|
|
|
|
<Measure
|
|
tip="% of pupils whose first language is known or believed to be other than English. State-school average computed from our dataset."
|
|
label="English as an additional language"
|
|
>
|
|
{schools.map((school, i) => {
|
|
const eal = data[String(school.urn)]?.census?.eal_pct ?? null;
|
|
return (
|
|
<Cell key={school.urn} school={school} index={i}>
|
|
{eal != null ? `${Math.round(eal)}%` : <span className={s.small}>No data</span>}
|
|
</Cell>
|
|
);
|
|
})}
|
|
</Measure>
|
|
|
|
<Measure
|
|
tip="% of pupils receiving SEN support (not including EHC plans). A high figure can mean the school hosts specialist provision, which is often a strength rather than a warning sign. State-school average computed from our dataset."
|
|
label="Extra learning support (SEN)"
|
|
>
|
|
{schools.map((school, i) => {
|
|
const rows = data[String(school.urn)]?.yearly_data ?? [];
|
|
let sen: number | null = null;
|
|
for (let r = rows.length - 1; r >= 0; r--) {
|
|
if (rows[r].sen_support_pct != null) {
|
|
sen = rows[r].sen_support_pct;
|
|
break;
|
|
}
|
|
}
|
|
const high =
|
|
sen != null && bench?.sen_support_pct != null && sen >= bench.sen_support_pct * 1.75;
|
|
return (
|
|
<Cell key={school.urn} school={school} index={i}>
|
|
{sen != null ? (
|
|
<>
|
|
{Math.round(sen)}% {high && <Chip tone="neutral">Well above average</Chip>}
|
|
</>
|
|
) : (
|
|
<span className={s.small}>No data</span>
|
|
)}
|
|
</Cell>
|
|
);
|
|
})}
|
|
</Measure>
|
|
|
|
<Measure label="Faith character">
|
|
{schools.map((school, i) => {
|
|
const info = data[String(school.urn)]?.school_info;
|
|
const faith = info?.religious_denomination;
|
|
const none = !faith || faith === 'Does not apply' || faith === 'None';
|
|
return (
|
|
<Cell key={school.urn} school={school} index={i}>
|
|
{none ? 'None' : faith}
|
|
</Cell>
|
|
);
|
|
})}
|
|
</Measure>
|
|
|
|
<Measure label="Ages">
|
|
{schools.map((school, i) => {
|
|
const info = data[String(school.urn)]?.school_info;
|
|
return (
|
|
<Cell key={school.urn} school={school} index={i}>
|
|
{info?.age_range || <span className={s.small}>No data</span>}
|
|
</Cell>
|
|
);
|
|
})}
|
|
</Measure>
|
|
|
|
<Measure label="Run by">
|
|
{schools.map((school, i) => {
|
|
const info = data[String(school.urn)]?.school_info;
|
|
const trust = info?.trust_name;
|
|
const la = info?.local_authority ?? school.local_authority;
|
|
return (
|
|
<Cell key={school.urn} school={school} index={i}>
|
|
{trust ? trust : la ? `${la} council` : <span className={s.small}>No data</span>}
|
|
</Cell>
|
|
);
|
|
})}
|
|
</Measure>
|
|
</SectionGrid>
|
|
</Section>
|
|
);
|
|
}
|