Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15da060e4a | ||
|
|
a5b2296240 | ||
|
|
af23fbf27e | ||
|
|
22e9eb2d48 | ||
|
|
13253116c2 | ||
|
|
1b8adb91ee | ||
|
|
ca4fc647a5 |
@@ -438,6 +438,18 @@ test('compare chart on mobile shows school chips with tap-to-focus', async ({ pa
|
||||
await expect(chips.first()).toHaveAttribute('aria-pressed', 'false');
|
||||
});
|
||||
|
||||
test('admissions guide renders its key milestones', async ({ page }) => {
|
||||
// Static content page — assert the guide loads and the load-bearing
|
||||
// milestones parents rely on are present (dates are statutory, so these
|
||||
// strings are stable invariants, not data-refresh-sensitive).
|
||||
await page.goto('/admissions');
|
||||
await expect(page.getByRole('heading', { name: /School Admissions Guide/i })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: /Primary school admissions/i })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: /Secondary school admissions/i })).toBeVisible();
|
||||
// National Offer Day is the milestone the whole guide builds toward.
|
||||
await expect(page.getByText(/National Offer Day/i).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('rankings page loads a populated table', async ({ page }) => {
|
||||
await page.goto('/rankings');
|
||||
await expect(page.getByRole('heading', { name: /rankings/i }).first()).toBeVisible();
|
||||
|
||||
@@ -442,6 +442,58 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ─── Easy-to-miss callouts ──────────────────────────── */
|
||||
|
||||
.calloutGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
|
||||
.callout {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
background: var(--accent-gold-bg, rgba(201, 162, 39, 0.12));
|
||||
border: 1px solid rgba(201, 162, 39, 0.28);
|
||||
border-left: 3px solid var(--accent-gold, #c9a227);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.calloutIcon {
|
||||
flex-shrink: 0;
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
margin-top: 0.1rem;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-gold, #c9a227);
|
||||
color: #fff;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.05rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calloutText {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.calloutTitle {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-gold-text, #7a6800);
|
||||
line-height: 1.3;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.calloutBody {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-secondary, #5c564d);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ─── Tips ───────────────────────────────────────────── */
|
||||
|
||||
.tips {
|
||||
|
||||
@@ -54,6 +54,9 @@ interface Step {
|
||||
title: string;
|
||||
body: string;
|
||||
highlight?: 'deadline' | 'offer';
|
||||
/* Easy-to-miss, deadline-sensitive warnings surfaced as callout boxes
|
||||
rather than buried in the body prose. */
|
||||
callouts?: { title: string; body: string }[];
|
||||
/* How SchoolCompare helps at this stage of the journey */
|
||||
tool?: { label: string; href: string };
|
||||
}
|
||||
@@ -62,6 +65,16 @@ const SECONDARY_STEPS: Step[] = [
|
||||
{
|
||||
title: 'Check entry criteria',
|
||||
body: 'Look at each school\'s admissions policy — catchment areas, faith criteria, sibling priority, and aptitude tests vary widely.',
|
||||
callouts: [
|
||||
{
|
||||
title: 'Selective schools register earlier',
|
||||
body: 'Grammar and other selective schools need separate entrance-test registration months earlier — often by September, well before the 31 October deadline.',
|
||||
},
|
||||
{
|
||||
title: 'Faith schools may need an extra form',
|
||||
body: 'Many faith schools require a supplementary information form (SIF) sent directly to the school, on top of the council application.',
|
||||
},
|
||||
],
|
||||
tool: { label: 'Find schools & view their admissions history', href: '/' },
|
||||
},
|
||||
{
|
||||
@@ -73,14 +86,14 @@ const SECONDARY_STEPS: Step[] = [
|
||||
{
|
||||
date: '31 October',
|
||||
title: 'Application deadline',
|
||||
body: 'Submit your ranked list of up to six schools. Councils treat all preferences equally — list schools in the genuine order you want them, not strategically.',
|
||||
body: 'Submit your ranked list of schools — most councils allow three to six preferences (London boroughs typically six). Councils treat all preferences equally, so list schools in the genuine order you want them, not strategically.',
|
||||
highlight: 'deadline',
|
||||
tool: { label: 'Compare performance to order your preferences', href: '/rankings' },
|
||||
},
|
||||
{
|
||||
date: '1 March',
|
||||
title: 'National Offer Day',
|
||||
body: 'Results are published online, usually from 12:01 am. You\'ll receive an email or letter with your allocated school.',
|
||||
body: 'Your allocated school appears on the council\'s online admissions portal and is sent by email. Release times are set by each council — often from late afternoon, though some publish overnight — so check your council\'s page rather than refreshing at midnight.',
|
||||
highlight: 'offer',
|
||||
tool: { label: 'Look up your allocated school', href: '/' },
|
||||
},
|
||||
@@ -101,6 +114,12 @@ const PRIMARY_STEPS: Step[] = [
|
||||
{
|
||||
title: 'Research entry criteria',
|
||||
body: 'Faith schools, language units, and distance-based catchments differ by school. Start by reading each school\'s admissions policy on their website or the council\'s website.',
|
||||
callouts: [
|
||||
{
|
||||
title: 'Faith schools may need an extra form',
|
||||
body: 'Many faith schools require a supplementary information form (SIF) sent directly to the school, in addition to the council application.',
|
||||
},
|
||||
],
|
||||
tool: { label: 'Find schools & view their admissions history', href: '/' },
|
||||
},
|
||||
{
|
||||
@@ -144,7 +163,7 @@ interface Tip {
|
||||
const TIPS: Tip[] = [
|
||||
{
|
||||
heading: 'Equal preference rule',
|
||||
body: 'Councils rank offers by your eligibility for each school, not by the order you listed them. You cannot game the system — put schools in the order you actually want them.',
|
||||
body: 'Councils consider every school you list before making any offer, then give you the highest-ranked one you qualify for. Your order only decides which qualifying school you get — you can\'t game it, so list schools in the order you genuinely want them.',
|
||||
},
|
||||
{
|
||||
heading: 'Late applications go to the back',
|
||||
@@ -307,6 +326,19 @@ export function AdmissionsView() {
|
||||
{step.date && <div className={styles.stepDate}>{step.date}</div>}
|
||||
<div className={styles.stepTitle}>{step.title}</div>
|
||||
<p className={styles.stepBody}>{step.body}</p>
|
||||
{step.callouts && step.callouts.length > 0 && (
|
||||
<div className={styles.calloutGroup}>
|
||||
{step.callouts.map((c) => (
|
||||
<div key={c.title} className={styles.callout}>
|
||||
<span className={styles.calloutIcon} aria-hidden="true">!</span>
|
||||
<div className={styles.calloutText}>
|
||||
<div className={styles.calloutTitle}>{c.title}</div>
|
||||
<p className={styles.calloutBody}>{c.body}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{step.tool && (
|
||||
<a
|
||||
href={step.tool.href}
|
||||
@@ -355,6 +387,19 @@ export function AdmissionsView() {
|
||||
{step.date && <div className={styles.stepDate}>{step.date}</div>}
|
||||
<div className={styles.stepTitle}>{step.title}</div>
|
||||
<p className={styles.stepBody}>{step.body}</p>
|
||||
{step.callouts && step.callouts.length > 0 && (
|
||||
<div className={styles.calloutGroup}>
|
||||
{step.callouts.map((c) => (
|
||||
<div key={c.title} className={styles.callout}>
|
||||
<span className={styles.calloutIcon} aria-hidden="true">!</span>
|
||||
<div className={styles.calloutText}>
|
||||
<div className={styles.calloutTitle}>{c.title}</div>
|
||||
<p className={styles.calloutBody}>{c.body}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{step.tool && (
|
||||
<a
|
||||
href={step.tool.href}
|
||||
|
||||
@@ -13,17 +13,17 @@ export function EditorialSection({ totalSchools, localAuthorityCount }: Editoria
|
||||
<div className={styles.editorialGrid}>
|
||||
<div className={styles.editorialText}>
|
||||
<div className={styles.editorialKicker}>About school data</div>
|
||||
<h2 className={styles.editorialHeading}>Making UK school performance data actually readable</h2>
|
||||
<h2 className={styles.editorialHeading}>Making England's school performance data actually readable</h2>
|
||||
<p>
|
||||
School performance data in England is rich but fragmented. The Department for Education publishes
|
||||
Key Stage 2 SATs, GCSE attainment, Ofsted outcomes, progress scores, admissions figures and
|
||||
demographics — each in its own table, each with its own jargon.
|
||||
School performance data in England is rich but fragmented. The Department for Education and Ofsted
|
||||
publish Key Stage 2 SATs, GCSE attainment, inspection outcomes, progress scores, admissions figures
|
||||
and demographics — each in its own table, each with its own jargon.
|
||||
</p>
|
||||
<p>
|
||||
SchoolCompare brings it all into one place. Every school page shows performance against the national
|
||||
average, explains what the numbers mean, and lets you shortlist schools side by side. Built for
|
||||
parents, governors, journalists, and anyone who wants to understand a school without reading a
|
||||
40-page inspection report.
|
||||
full inspection report.
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.factbox}>
|
||||
|
||||
@@ -720,10 +720,13 @@
|
||||
.miniRowHead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.48rem;
|
||||
align-items: baseline;
|
||||
gap: 0.2rem;
|
||||
font-size: 0.44rem;
|
||||
color: var(--text-muted, #6d685f);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.miniRowHead strong {
|
||||
@@ -869,28 +872,77 @@
|
||||
color: var(--text-primary, #1a1612);
|
||||
}
|
||||
|
||||
.ofstedBadge {
|
||||
.ofstedFramework {
|
||||
align-self: flex-start;
|
||||
padding: 0.2rem 0.55rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(45, 125, 125, 0.12);
|
||||
color: var(--accent-teal, #2d7d7d);
|
||||
font-size: 0.55rem;
|
||||
background: rgba(90, 58, 110, 0.12);
|
||||
color: #5a3a6e;
|
||||
font-size: 0.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ofstedVerdict {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-size: 1.05rem;
|
||||
.rcList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.rcRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.rcArea {
|
||||
font-size: 0.62rem;
|
||||
color: var(--text-secondary, #4a453d);
|
||||
}
|
||||
|
||||
.rcChip {
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.52rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: var(--text-primary, #1a1612);
|
||||
letter-spacing: 0.03em;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ofstedVerdict em {
|
||||
.rcChipGood {
|
||||
background: rgba(45, 125, 125, 0.12);
|
||||
color: var(--accent-teal, #2d7d7d);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.rcChipNeutral {
|
||||
background: var(--accent-gold-bg, rgba(180, 150, 40, 0.12));
|
||||
color: var(--accent-gold-text, #7a6800);
|
||||
}
|
||||
|
||||
/* Safeguarding is a separate binary met/not-met judgement, not a graded area,
|
||||
so it sits below the five-point list behind a divider with its own chip. */
|
||||
.rcSafeguarding {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.05rem;
|
||||
padding-top: 0.35rem;
|
||||
border-top: 1px dashed var(--border-color, #e5dfd5);
|
||||
}
|
||||
|
||||
.rcSep {
|
||||
color: var(--text-muted, #8a847a);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.rcChipMet {
|
||||
background: transparent;
|
||||
color: var(--accent-teal, #2d7d7d);
|
||||
border: 1px solid rgba(45, 125, 125, 0.4);
|
||||
}
|
||||
|
||||
.ofstedMeta {
|
||||
|
||||
@@ -261,7 +261,7 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
|
||||
<div className={styles.heroSection}>
|
||||
<span className={styles.heroEyebrow}>
|
||||
<span className={styles.heroEyebrowDot} aria-hidden="true" />
|
||||
Updated with 2026/2027 admissions results
|
||||
Updated for the 2026/27 admissions round
|
||||
</span>
|
||||
<h1 className={styles.heroTitle}>
|
||||
Every school in England, <em className={styles.heroEmph}>compared.</em>
|
||||
|
||||
@@ -5,15 +5,26 @@
|
||||
import styles from './HomeView.module.css';
|
||||
|
||||
export function HowItWorksSection() {
|
||||
// "exc" is the school's above-expected result. DfE names this differently
|
||||
// by subject: "higher standard" for the reading and maths tests, "greater
|
||||
// depth" for teacher-assessed writing. "nat" is the national expected %.
|
||||
const miniCascade = [
|
||||
{ subj: 'Reading', exp: 96, exc: 73, nat: 75 },
|
||||
{ subj: 'Writing', exp: 81, exc: 15, nat: 72 },
|
||||
{ subj: 'Maths', exp: 85, exc: 47, nat: 74 },
|
||||
{ subj: 'Reading', exp: 96, exc: 44, nat: 75, excLabel: 'Higher std' },
|
||||
{ subj: 'Writing', exp: 81, exc: 26, nat: 72, excLabel: 'Greater depth' },
|
||||
{ subj: 'Maths', exp: 85, exc: 41, nat: 74, excLabel: 'Higher std' },
|
||||
];
|
||||
const compareRows = [
|
||||
{ label: 'Reading, Writing & Maths', a: '70%', b: '64%', aHi: true },
|
||||
{ label: 'Higher standard (RWM)', a: '13%', b: '6%', aHi: true },
|
||||
{ label: 'Ofsted', a: 'Outstanding', b: 'Good', aHi: true },
|
||||
{ label: 'Reading progress', a: '+2.1', b: '+0.4', aHi: true },
|
||||
];
|
||||
// Report card (Ofsted's framework from Nov 2025): one rating per area of
|
||||
// school life on a five-point scale. Safeguarding is judged separately, as a
|
||||
// binary met/not-met outcome — kept out of this list and shown on its own.
|
||||
const reportCard: Array<{ area: string; grade: string; good: boolean }> = [
|
||||
{ area: 'Achievement', grade: 'Strong', good: true },
|
||||
{ area: 'Curriculum & teaching', grade: 'Expected standard', good: false },
|
||||
{ area: 'Attendance & behaviour', grade: 'Strong', good: true },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -29,7 +40,7 @@ export function HowItWorksSection() {
|
||||
<div className={styles.hiwPhaseBlock}>
|
||||
<div className={styles.hiwPhaseLabel}>Primary · Year 6 · <strong>Key Stage 2 SATs</strong></div>
|
||||
<div className={styles.miniCascade}>
|
||||
{miniCascade.map(({ subj, exp, exc, nat }) => (
|
||||
{miniCascade.map(({ subj, exp, exc, nat, excLabel }) => (
|
||||
<div key={subj} className={styles.miniCascadeCol}>
|
||||
<div className={styles.miniSubj}>{subj}</div>
|
||||
<div className={styles.miniRowHead}><span>Expected</span><strong>{exp}%</strong></div>
|
||||
@@ -37,7 +48,7 @@ export function HowItWorksSection() {
|
||||
<div className={styles.miniNatPill} style={{ left: `${nat}%` }}>{nat}%</div>
|
||||
<div className={styles.miniBarExp} style={{ width: `${exp}%` }} />
|
||||
</div>
|
||||
<div className={styles.miniRowHead}><span>Exceeding</span><strong>{exc}%</strong></div>
|
||||
<div className={styles.miniRowHead}><span>{excLabel}</span><strong>{exc}%</strong></div>
|
||||
<div className={styles.miniTrack}>
|
||||
<div className={styles.miniBarExc} style={{ width: `${exc}%` }} />
|
||||
</div>
|
||||
@@ -49,15 +60,15 @@ export function HowItWorksSection() {
|
||||
<div className={styles.hiwPhaseLabel}>Secondary · Year 11 · <strong>GCSE Attainment 8</strong></div>
|
||||
<div className={styles.att8Row}>
|
||||
<div className={styles.att8BarWrap}>
|
||||
<div className={styles.att8BarHead}><span>This school</span><span>National avg 50.2</span></div>
|
||||
<div className={styles.att8BarHead}><span>This school</span><span>National avg 45.9</span></div>
|
||||
<div className={styles.att8Track}>
|
||||
<div className={styles.att8Fill} style={{ width: '62%' }} />
|
||||
<div className={styles.att8NatLine} style={{ left: '50%' }} />
|
||||
<div className={styles.att8Fill} style={{ width: '54.8%' }} />
|
||||
<div className={styles.att8NatLine} style={{ left: '45.9%' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.att8Score}>
|
||||
<div className={styles.att8Value}>62.4</div>
|
||||
<div className={styles.att8Delta}>+12.2 vs national</div>
|
||||
<div className={styles.att8Value}>54.8</div>
|
||||
<div className={styles.att8Delta}>+8.9 vs national</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,15 +88,26 @@ export function HowItWorksSection() {
|
||||
<span className={styles.ofstedBullet} />
|
||||
<span className={styles.ofstedTitle}>Latest Ofsted inspection</span>
|
||||
</div>
|
||||
<span className={styles.ofstedBadge}>OUTSTANDING</span>
|
||||
<div className={styles.ofstedVerdict}>Rated <em>Outstanding</em> at last inspection.</div>
|
||||
<div className={styles.ofstedMeta}>Full inspection · March 2024</div>
|
||||
<span className={styles.ofstedFramework}>Report card · five-point scale</span>
|
||||
<div className={styles.rcList}>
|
||||
{reportCard.map(({ area, grade, good }) => (
|
||||
<div key={area} className={styles.rcRow}>
|
||||
<span className={styles.rcArea}>{area}</span>
|
||||
<span className={`${styles.rcChip} ${good ? styles.rcChipGood : styles.rcChipNeutral}`}>{grade}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.rcSafeguarding}>
|
||||
<span className={styles.rcArea}>Safeguarding <span className={styles.rcSep}>· separate check</span></span>
|
||||
<span className={`${styles.rcChip} ${styles.rcChipMet}`}>Met</span>
|
||||
</div>
|
||||
<div className={styles.ofstedMeta}>Full inspection · March 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hiwCardBody}>
|
||||
<div className={styles.hiwStep}>Judgement</div>
|
||||
<div className={styles.hiwTitle}>Ofsted at a glance</div>
|
||||
<p className={styles.hiwDesc}>Current grade, inspection date, and a plain-English headline — without opening a 40-page report.</p>
|
||||
<p className={styles.hiwDesc}>Since November 2025, Ofsted rates each area of school life on a five-point scale rather than one overall word. We show every area and the inspection date — and, for schools not yet reinspected, the older overall grade they still carry.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ export function RankingsView({
|
||||
<header className={styles.header}>
|
||||
<h1>School Rankings</h1>
|
||||
<p className={styles.subtitle}>
|
||||
Top-performing schools by {metricLabel.toLowerCase()}
|
||||
Schools ranked by {metricLabel.toLowerCase()}
|
||||
{!selectedArea && rankings.length > 0 && <span className={styles.limitNote}> — showing top {rankings.length}</span>}
|
||||
</p>
|
||||
</header>
|
||||
@@ -143,7 +143,10 @@ export function RankingsView({
|
||||
<p className={styles.metricDescription}>{currentMetricDef.description}</p>
|
||||
)}
|
||||
{isProgressScore && (
|
||||
<p className={styles.progressHint}>Progress scores: 0 = national average. Positive = above average.</p>
|
||||
<p className={styles.progressHint}>
|
||||
Progress scores: 0 = national average. Positive = above average.
|
||||
{isPrimary && ' KS2 progress isn’t published for 2023/24 or 2024/25 (there’s no key stage 1 baseline) — pick an earlier year to rank by it.'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
@@ -218,7 +221,11 @@ export function RankingsView({
|
||||
{rankings.length === 0 ? (
|
||||
<EmptyState
|
||||
title="No rankings found"
|
||||
message="Try selecting a different metric, area, or year."
|
||||
message={
|
||||
isPrimary && isProgressScore
|
||||
? 'KS2 progress scores aren’t published for the most recent years (2023/24 and 2024/25) because there’s no key stage 1 baseline. Select an earlier year to see progress rankings.'
|
||||
: 'Try selecting a different metric, area, or year.'
|
||||
}
|
||||
action={{
|
||||
label: 'Clear filters',
|
||||
onClick: () => router.push(`${pathname}?phase=${selectedPhase}`),
|
||||
|
||||
Reference in New Issue
Block a user