2026-02-02 20:34:35 +00:00
/**
* SchoolDetailView Component
* Displays comprehensive school information with performance charts
*/
'use client' ;
2026-05-18 15:35:17 +01:00
import { useEffect , useRef , useState } from 'react' ;
2026-03-23 21:31:28 +00:00
import { useRouter } from 'next/navigation' ;
2026-06-02 13:46:45 +01:00
import dynamic from 'next/dynamic' ;
2026-02-02 20:34:35 +00:00
import { useComparison } from '@/hooks/useComparison' ;
import { SchoolMap } from './SchoolMap' ;
2026-03-28 14:59:40 +00:00
import { MetricTooltip } from './MetricTooltip' ;
2026-03-24 11:44:04 +00:00
import type {
School , SchoolResult , AbsenceData ,
OfstedInspection , OfstedParentView , SchoolCensus ,
SchoolAdmissions , SenDetail , Phonics ,
2026-03-28 14:59:40 +00:00
SchoolDeprivation , SchoolFinance , NationalAverages ,
2026-03-24 11:44:04 +00:00
} from '@/lib/types' ;
2026-04-08 10:32:33 +01:00
import {
formatPercentage , formatProgress , formatAcademicYear ,
2026-04-08 15:27:03 +01:00
buildOfstedHeroChip ,
2026-04-08 10:32:33 +01:00
} from '@/lib/utils' ;
import { DeltaChip } from './DeltaChip' ;
2026-06-02 13:46:45 +01:00
const PerformanceChart = dynamic (
() => import ( './PerformanceChart' ). then (( m ) => m . PerformanceChart ),
{ ssr : false },
);
const SatsChart = dynamic (() => import ( './SatsChart' ), { ssr : false });
2026-06-19 18:59:57 +01:00
const AdmissionsTrendChart = dynamic (() => import ( './AdmissionsTrendChart' ), { ssr : false });
2026-05-19 22:04:22 +01:00
import { track , getNavigationSource } from '@/lib/analytics' ;
2026-02-02 20:34:35 +00:00
import styles from './SchoolDetailView.module.css' ;
2026-03-24 11:44:04 +00:00
const OFSTED_LABELS : Record < number , string > = {
1 : 'Outstanding' , 2 : 'Good' , 3 : 'Requires Improvement' , 4 : 'Inadequate' ,
};
2026-03-25 13:03:04 +00:00
const RC_LABELS : Record < number , string > = {
1 : 'Exceptional' , 2 : 'Strong' , 3 : 'Expected standard' , 4 : 'Needs attention' , 5 : 'Urgent improvement' ,
};
const RC_CATEGORIES = [
{ key : 'rc_inclusion' as const , label : 'Inclusion' },
{ key : 'rc_curriculum_teaching' as const , label : 'Curriculum & Teaching' },
{ key : 'rc_achievement' as const , label : 'Achievement' },
{ key : 'rc_attendance_behaviour' as const , label : 'Attendance & Behaviour' },
{ key : 'rc_personal_development' as const , label : 'Personal Development' },
{ key : 'rc_leadership_governance' as const , label : 'Leadership & Governance' },
{ key : 'rc_early_years' as const , label : 'Early Years' },
{ key : 'rc_sixth_form' as const , label : 'Sixth Form' },
];
2026-03-25 10:06:36 +00:00
function progressClass ( val : number | null | undefined ) : string {
if ( val == null ) return '' ;
2026-03-25 10:34:19 +00:00
if ( val > 0 ) return styles . progressPositive ;
if ( val < 0 ) return styles . progressNegative ;
2026-03-25 10:06:36 +00:00
return '' ;
}
2026-02-02 20:34:35 +00:00
interface SchoolDetailViewProps {
schoolInfo : School ;
yearlyData : SchoolResult [];
absenceData : AbsenceData | null ;
2026-03-24 11:44:04 +00:00
ofsted : OfstedInspection | null ;
parentView : OfstedParentView | null ;
census : SchoolCensus | null ;
admissions : SchoolAdmissions | null ;
2026-06-19 18:41:03 +01:00
admissionsHistory : SchoolAdmissions [];
2026-03-24 11:44:04 +00:00
senDetail : SenDetail | null ;
phonics : Phonics | null ;
deprivation : SchoolDeprivation | null ;
finance : SchoolFinance | null ;
2026-02-02 20:34:35 +00:00
}
2026-03-24 11:44:04 +00:00
export function SchoolDetailView ({
schoolInfo , yearlyData , absenceData ,
2026-06-19 18:41:03 +01:00
ofsted , parentView , census , admissions , admissionsHistory , senDetail , phonics , deprivation , finance ,
2026-03-24 11:44:04 +00:00
} : SchoolDetailViewProps ) {
2026-03-23 21:31:28 +00:00
const router = useRouter ();
2026-02-02 20:34:35 +00:00
const { addSchool , removeSchool , isSelected } = useComparison ();
const isInComparison = isSelected ( schoolInfo . urn );
2026-04-08 15:42:20 +01:00
const [ activeSection , setActiveSection ] = useState < string >( '' );
2026-06-19 18:41:03 +01:00
const [ admissionsView , setAdmissionsView ] = useState < 'year' | 'trend' > ( 'year' );
// Trend toggle only appears with ≥2 years carrying an offer rate.
const admissionsOfferYears = admissionsHistory . filter (( h ) => h . first_preference_offer_pct != null ). length ;
const showAdmissionsTrend = admissionsOfferYears >= 2 ;
2026-06-30 22:39:48 +01:00
// Only the section links scroll horizontally; Back and "All" stay pinned.
const sectionLinksRef = useRef < HTMLDivElement | null >( null );
2026-05-18 15:35:17 +01:00
const [ sectionNavAtEnd , setSectionNavAtEnd ] = useState ( false );
2026-06-30 22:39:48 +01:00
// Carry the "Add to Compare" CTA into the sticky bar once the hero's button leaves.
const heroActionsRef = useRef < HTMLDivElement | null >( null );
const [ heroCtaVisible , setHeroCtaVisible ] = useState ( true );
// "All ▾" jump menu listing every section.
const [ sectionsOpen , setSectionsOpen ] = useState ( false );
// Back returns to wherever the user came from; deep-links (no in-app history)
// fall back to search so the button never dead-ends or leaves the site.
const handleBack = () => {
if ( typeof window !== 'undefined' && window . history . length > 1 ) {
router . back ();
} else {
router . push ( '/search' );
}
};
2026-05-18 15:35:17 +01:00
useEffect (() => {
2026-06-30 22:39:48 +01:00
const el = sectionLinksRef . current ;
2026-05-18 15:35:17 +01:00
if ( ! el ) return ;
const update = () => {
const overflow = el . scrollWidth - el . clientWidth ;
// No overflow → treat as "at end" so the fade is hidden.
if ( overflow <= 1 ) {
setSectionNavAtEnd ( true );
return ;
}
setSectionNavAtEnd ( el . scrollLeft >= overflow - 2 );
};
update ();
el . addEventListener ( 'scroll' , update , { passive : true });
window . addEventListener ( 'resize' , update );
return () => {
el . removeEventListener ( 'scroll' , update );
window . removeEventListener ( 'resize' , update );
};
}, []);
2026-04-08 15:42:20 +01:00
2026-06-30 22:39:48 +01:00
// Track whether the hero's "Add to Compare" button is still on screen.
useEffect (() => {
const el = heroActionsRef . current ;
if ( ! el ) return ;
const obs = new IntersectionObserver (
([ entry ]) => setHeroCtaVisible ( entry . isIntersecting ),
{ rootMargin : '-64px 0px 0px 0px' },
);
obs . observe ( el );
return () => obs . disconnect ();
}, []);
// Close the "All ▾" menu on Escape.
useEffect (() => {
if ( ! sectionsOpen ) return ;
const onKey = ( e : KeyboardEvent ) => { if ( e . key === 'Escape' ) setSectionsOpen ( false ); };
window . addEventListener ( 'keydown' , onKey );
return () => window . removeEventListener ( 'keydown' , onKey );
}, [ sectionsOpen ]);
2026-02-02 20:34:35 +00:00
const latestResults = yearlyData . length > 0 ? yearlyData [ yearlyData . length - 1 ] : null ;
2026-03-28 14:59:40 +00:00
// Phase detection
const phase = schoolInfo . phase ?? '' ;
const isSecondary = phase . toLowerCase (). includes ( 'secondary' ) || phase . toLowerCase () === 'all-through' ;
const isPrimary = ! isSecondary ;
// National averages (fetched dynamically so they stay current)
const [ nationalAvg , setNationalAvg ] = useState < NationalAverages | null >( null );
useEffect (() => {
fetch ( '/api/national-averages' )
. then ( r => r . ok ? r . json () : null )
. then ( data => { if ( data ) setNationalAvg ( data ); })
. catch (() => {});
}, []);
const primaryAvg = nationalAvg ? . primary ?? {};
const secondaryAvg = nationalAvg ? . secondary ?? {};
2026-02-02 20:34:35 +00:00
const handleComparisonToggle = () => {
if ( isInComparison ) {
removeSchool ( schoolInfo . urn );
2026-05-19 22:04:22 +01:00
track ( 'compare_school_removed' , { urn : schoolInfo.urn , from : 'detail' });
2026-02-02 20:34:35 +00:00
} else {
addSchool ( schoolInfo );
2026-05-19 22:04:22 +01:00
track ( 'compare_school_added' , { urn : schoolInfo.urn , from : 'detail' });
2026-02-02 20:34:35 +00:00
}
};
2026-05-19 22:04:22 +01:00
// Page-view event with funnel attribution. Fires once per mount.
useEffect (() => {
track ( 'school_viewed' , {
urn : schoolInfo.urn ,
phase : phase || 'unknown' ,
local_authority : schoolInfo.local_authority || 'unknown' ,
from : getNavigationSource (),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ schoolInfo . urn ]);
2026-03-25 10:06:36 +00:00
const deprivationDesc = ( decile : number ) => {
if ( decile <= 3 ) return `This school is in one of England's most deprived areas (decile ${ decile } /10). Many pupils may face additional challenges at home.` ;
if ( decile <= 7 ) return `This school is in an area with average levels of deprivation (decile ${ decile } /10).` ;
return `This school is in one of England's less deprived areas (decile ${ decile } /10).` ;
};
2026-04-17 22:52:59 +01:00
// Gender split availability (only meaningful for Mixed schools with census data)
const isMixedSchool = schoolInfo . gender === 'Mixed' || schoolInfo . gender == null ;
const hasGenderSplit = isMixedSchool
&& census ? . female_pupils != null
&& census ? . male_pupils != null
&& ( census . female_pupils + census . male_pupils ) > 0 ;
2026-03-25 10:34:19 +00:00
// Guard for Pupils & Inclusion — only show if at least one metric is available
const hasInclusionData = ( latestResults ? . disadvantaged_pct != null )
|| ( latestResults ? . eal_pct != null )
|| ( latestResults ? . sen_support_pct != null )
2026-04-17 22:52:59 +01:00
|| senDetail != null
|| hasGenderSplit ;
2026-03-25 10:34:19 +00:00
const hasSchoolLife = absenceData != null || census ? . class_size_avg != null ;
const hasPhonics = phonics != null && phonics . year1_phonics_pct != null ;
const hasDeprivation = deprivation != null && deprivation . idaci_decile != null ;
const hasFinance = finance != null && finance . per_pupil_spend != null ;
const hasLocation = schoolInfo . latitude != null && schoolInfo . longitude != null ;
2026-03-28 14:59:40 +00:00
// Determine whether this school has KS2 or KS4 results to show
const hasKS2Results = latestResults != null && latestResults . rwm_expected_pct != null ;
const hasKS4Results = latestResults != null && latestResults . attainment_8_score != null ;
const hasAnyResults = hasKS2Results || hasKS4Results ;
2026-03-25 10:34:19 +00:00
// Build section nav items dynamically — only sections with data
const navItems : { id : string ; label : string }[] = [];
if ( ofsted ) navItems . push ({ id : 'ofsted' , label : 'Ofsted' });
if ( parentView && parentView . total_responses != null && parentView . total_responses > 0 )
navItems . push ({ id : 'parents' , label : 'Parents' });
2026-03-28 14:59:40 +00:00
if ( hasAnyResults ) navItems . push ({ id : 'results' , label : isSecondary ? 'GCSEs' : 'SATs' });
if ( hasPhonics && isPrimary ) navItems . push ({ id : 'phonics' , label : 'Phonics' });
2026-03-25 10:34:19 +00:00
if ( hasSchoolLife ) navItems . push ({ id : 'school-life' , label : 'School Life' });
if ( admissions ) navItems . push ({ id : 'admissions' , label : 'Admissions' });
if ( hasInclusionData ) navItems . push ({ id : 'inclusion' , label : 'Pupils' });
if ( hasLocation ) navItems . push ({ id : 'location' , label : 'Location' });
if ( hasDeprivation ) navItems . push ({ id : 'local-area' , label : 'Local Area' });
if ( hasFinance ) navItems . push ({ id : 'finances' , label : 'Finances' });
if ( yearlyData . length > 0 ) navItems . push ({ id : 'history' , label : 'History' });
2026-04-08 15:59:01 +01:00
// Track active section as user scrolls
useEffect (() => {
const ids = navItems . map ( n => n . id );
if ( ! ids . length ) return ;
const observers : IntersectionObserver [] = [];
const ratioMap : Record < string , number > = {};
const pickActive = () => {
const top = Object . entries ( ratioMap ). sort (( a , b ) => b [ 1 ] - a [ 1 ])[ 0 ];
setActiveSection ( top ? .[ 1 ] > 0 ? top [ 0 ] : '' );
};
ids . forEach ( id => {
const el = document . getElementById ( id );
if ( ! el ) return ;
ratioMap [ id ] = 0 ;
const obs = new IntersectionObserver (
([ entry ]) => {
ratioMap [ id ] = entry . intersectionRatio ;
pickActive ();
},
{ threshold : [ 0 , 0.1 , 0.25 , 0.5 , 0.75 , 1.0 ], rootMargin : '-56px 0px 0px 0px' },
);
obs . observe ( el );
observers . push ( obs );
});
return () => observers . forEach ( o => o . disconnect ());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ navItems . map ( n => n . id ). join ( ',' )]);
2026-04-08 15:13:13 +01:00
// ── Ofsted: detect if all OEIF sub-grades match the overall ───────────
const oeifAllSameGrade = (() => {
if ( ! ofsted || ofsted . framework === 'ReportCard' ) return false ;
const subs = [
ofsted . quality_of_education ,
ofsted . behaviour_attitudes ,
ofsted . personal_development ,
ofsted . leadership_management ,
...( ofsted . early_years_provision != null ? [ ofsted . early_years_provision ] : []),
]. filter (( v ) : v is number => v != null );
return subs . length >= 3 && subs . every ( v => v === ofsted . overall_effectiveness );
})();
2026-04-08 10:32:33 +01:00
// ── Hero: framework-aware signal chip + narrative summary ─────────────
const ofstedHeroChip = buildOfstedHeroChip ( ofsted );
// KS2 headline numbers for the at-a-glance row
const heroRwm = isPrimary ? latestResults ? . rwm_expected_pct ?? null : null ;
const heroRwmNat = primaryAvg . rwm_expected_pct ?? null ;
// KS4 headline number for secondary/all-through schools
const heroAtt8 = isSecondary ? latestResults ? . attainment_8_score ?? null : null ;
const heroAtt8Nat = secondaryAvg . attainment_8_score ?? null ;
const heroAcademicYear = latestResults ? formatAcademicYear ( latestResults . year ) : '' ;
2026-02-02 20:34:35 +00:00
return (
< div className = { styles . container }>
2026-03-25 10:06:36 +00:00
{ /* Header */ }
2026-02-02 20:34:35 +00:00
< header className = { styles . header }>
< div className = { styles . headerContent }>
< div className = { styles . titleSection }>
< h1 className = { styles . schoolName }>{ schoolInfo . school_name }</ h1 >
< div className = { styles . meta }>
{ schoolInfo . local_authority && (
2026-03-25 10:06:36 +00:00
< span className = { styles . metaItem }>{ schoolInfo . local_authority }</ span >
2026-02-02 20:34:35 +00:00
)}
{ schoolInfo . school_type && (
2026-03-25 10:06:36 +00:00
< span className = { styles . metaItem }>{ schoolInfo . school_type }</ span >
)}
{ schoolInfo . gender && schoolInfo . gender !== 'Mixed' && (
< span className = { styles . metaItem }>{ schoolInfo . gender } & apos ; s school </ span >
2026-02-02 20:34:35 +00:00
)}
</ div >
{ schoolInfo . address && (
< p className = { styles . address }>
2026-03-25 10:06:36 +00:00
{ schoolInfo . address }{ schoolInfo . postcode && `, ${ schoolInfo . postcode } ` }
2026-02-02 20:34:35 +00:00
</ p >
)}
2026-03-25 10:06:36 +00:00
< div className = { styles . headerDetails }>
{ schoolInfo . headteacher_name && (
< span className = { styles . headerDetail }>
< strong > Headteacher : </ strong > { schoolInfo . headteacher_name }
</ span >
)}
{ schoolInfo . website && (
< span className = { styles . headerDetail }>
2026-05-19 22:04:22 +01:00
< a
href = { /^ https ?: \ / \ //i.test(schoolInfo.website) ? schoolInfo.website : `https://${schoolInfo.website}`}
target = "_blank"
rel = "noopener noreferrer"
data - umami - event = "external_link_clicked"
data - umami - event - target = "school_website"
>
2026-03-25 10:06:36 +00:00
School website ↗
</ a >
</ span >
)}
2026-04-17 22:36:33 +01:00
{ (() = > {
2026-04-17 22:52:59 +01:00
const total = census ? . total_pupils ?? latestResults ? . total_pupils ?? null ;
if ( total == null ) return null ;
return (
< span className = { styles . headerDetail }>
< strong > Pupils : </ strong > { total . toLocaleString ()}
{ schoolInfo . capacity != null && ` (capacity: ${ schoolInfo . capacity } )` }
</ span >
);
2026-04-17 22:36:33 +01:00
})()}
2026-03-25 10:06:36 +00:00
{ schoolInfo . trust_name && (
< span className = { styles . headerDetail }>
Part of < strong >{ schoolInfo . trust_name }</ strong >
</ span >
)}
</ div >
2026-02-02 20:34:35 +00:00
</ div >
2026-06-30 22:39:48 +01:00
< div className = { styles . actions } ref = { heroActionsRef }>
2026-02-02 20:34:35 +00:00
< button
onClick = { handleComparisonToggle }
className = { isInComparison ? styles.btnRemove : styles.btnAdd }
>
{ isInComparison ? '✓ In Comparison' : '+ Add to Compare' }
</ button >
</ div >
</ div >
2026-04-08 10:32:33 +01:00
{ /* Hero signal chip strip */ }
< div className = { styles . heroChips }>
2026-05-19 09:38:48 +01:00
< div
className = { ` ${ styles . heroChip } ${ styles [ `tone- ${ ofstedHeroChip . tone } ` ] } ` }
data-ofsted-state = { ofstedHeroChip . state }
>
2026-04-08 10:32:33 +01:00
< div className = { styles . heroChipTitle }>{ ofstedHeroChip . title }</ div >
< div className = { styles . heroChipSub }>{ ofstedHeroChip . subtitle }</ div >
{ ofstedHeroChip . detail && (
< div className = { styles . heroChipDetail }>{ ofstedHeroChip . detail }</ div >
)}
</ div >
{ admissions ? . oversubscribed && (
2026-04-08 15:27:03 +01:00
< div className = { ` ${ styles . heroChip } ${ styles [ 'tone-coral' ] } ` }>
2026-04-08 10:32:33 +01:00
< div className = { styles . heroChipTitle }> Oversubscribed </ div >
< div className = { styles . heroChipSub }>
{ admissions . first_preference_offer_pct != null
2026-04-08 11:29:40 +01:00
? ` ${ Math . round ( admissions . first_preference_offer_pct ) } % of first-choice applicants offered a place`
2026-04-08 10:32:33 +01:00
: 'More applicants than places' }
</ div >
</ div >
)}
</ div >
{ /* At-a-glance stats row */ }
{ latestResults && (
< div className = { styles . heroStats }>
{ isPrimary && heroRwm != null && (
< div className = { styles . heroStat }>
< div className = { styles . heroStatNumber }>{ Math . round ( heroRwm )} % </ div >
< div className = { styles . heroStatLabel }> Reading , Writing & amp ; Maths </ div >
{ heroRwmNat != null && (
< DeltaChip value = { heroRwm } baseline = { heroRwmNat } unit = "pts" suffix = "vs national" />
)}
</ div >
)}
{ isSecondary && heroAtt8 != null && (
< div className = { styles . heroStat }>
< div className = { styles . heroStatNumber }>{ heroAtt8 . toFixed ( 1 )}</ div >
< div className = { styles . heroStatLabel }> Attainment 8 score </ div >
{ heroAtt8Nat != null && (
< DeltaChip value = { heroAtt8 } baseline = { heroAtt8Nat } unit = "pts" suffix = "vs national" />
)}
</ div >
)}
{ ofsted && (
< div className = { styles . heroStat }>
2026-04-08 10:44:37 +01:00
< div className = { ` ${ styles . heroStatNumberSerif } ${ styles [ `tone- ${ ofstedHeroChip . tone } ` ] } ` }>
2026-04-08 10:32:33 +01:00
{ ofstedHeroChip . state === 'oeif'
? ofstedHeroChip . title . replace ( /^Ofsted\s+/ , '' )
: ofstedHeroChip . state === 'reportCard'
? 'Report Card'
: '—' }
</ div >
< div className = { styles . heroStatLabel }>
{ ofstedHeroChip . subtitle }
</ div >
{ ofstedHeroChip . detail && (
< div className = { styles . heroStatFoot }>{ ofstedHeroChip . detail }</ div >
)}
</ div >
)}
{ admissions ? . first_preference_offer_pct != null && (
< div className = { styles . heroStat }>
< div className = { styles . heroStatNumber }>
{ Math . round ( admissions . first_preference_offer_pct )} %
</ div >
< div className = { styles . heroStatLabel }> First - choice offer rate </ div >
{ admissions . oversubscribed && (
< div className = { styles . heroStatFoot }> Oversubscribed </ div >
)}
</ div >
)}
</ div >
)}
{ heroAcademicYear && (
< p className = { styles . heroDataNote }>
Latest data : { heroAcademicYear }
</ p >
)}
2026-02-02 20:34:35 +00:00
</ header >
2026-06-30 22:39:48 +01:00
{ /* Sticky Section Navigation — docks under the global header */ }
< nav className = { styles . sectionNav } aria-label = "Page sections" >
< button onClick = { handleBack } className = { styles . sectionNavBack }> ← Back </ button >
< div
ref = { sectionLinksRef }
className = { ` ${ styles . sectionNavLinks }${ sectionNavAtEnd ? ` ${ styles . atEnd } ` : '' } ` }
>
2026-03-25 11:13:55 +00:00
{ navItems . map (({ id , label }) => (
2026-04-08 15:42:20 +01:00
< a
key = { id }
href = { `# ${ id } ` }
className = { ` ${ styles . sectionNavLink }${ activeSection === id ? ` ${ styles . sectionNavLinkActive } ` : '' } ` }
2026-05-19 22:04:22 +01:00
onClick = {() => track ( 'section_nav_used' , { section : id })}
2026-04-08 15:42:20 +01:00
>
{ label }
</ a >
2026-03-25 11:13:55 +00:00
))}
</ div >
2026-06-30 22:39:48 +01:00
{ /* The hero's Compare CTA, carried in once it scrolls out of view */ }
{ ! heroCtaVisible && (
< button
onClick = { handleComparisonToggle }
className = { ` ${ styles . sectionNavCompare }${ isInComparison ? ` ${ styles . sectionNavCompareIn } ` : '' } ` }
>
{ isInComparison ? '✓ Comparing' : '+ Compare' }
</ button >
)}
{ navItems . length > 0 && (
< div className = { styles . sectionNavAllWrap }>
< button
type = "button"
className = { styles . sectionNavAll }
aria-haspopup = "menu"
aria-expanded = { sectionsOpen }
onClick = {() => setSectionsOpen (( o ) => ! o )}
>
All < span aria-hidden = "true" > ▾ </ span >
</ button >
{ sectionsOpen && (
<>
< div className = { styles . sectionsBackdrop } onClick = {() => setSectionsOpen ( false )} />
< div className = { styles . sectionsPanel } role = "menu" aria-label = "Jump to section" >
< div className = { styles . sectionsPanelHead }> On this page </ div >
{ navItems . map (({ id , label }) => (
< a
key = { id }
href = { `# ${ id } ` }
role = "menuitem"
className = { ` ${ styles . sectionsItem }${ activeSection === id ? ` ${ styles . sectionsItemActive } ` : '' } ` }
onClick = {() => {
setSectionsOpen ( false );
track ( 'section_nav_used' , { section : id , via : 'all_menu' });
}}
>
< span >{ label }</ span >
{ activeSection === id && < span className = { styles . sectionsTick } aria-hidden = "true" > ✓ </ span >}
</ a >
))}
</ div >
</>
)}
</ div >
)}
2026-03-25 11:13:55 +00:00
</ nav >
2026-03-25 10:06:36 +00:00
2026-03-25 13:03:04 +00:00
{ /* Ofsted Rating / Report Card */ }
2026-03-25 10:06:36 +00:00
{ ofsted && (
2026-03-25 10:34:19 +00:00
< section id = "ofsted" className = { styles . card }>
2026-02-02 20:34:35 +00:00
< h2 className = { styles . sectionTitle }>
2026-03-25 13:03:04 +00:00
{ ofsted . framework === 'ReportCard' ? 'Ofsted Report Card' : 'Ofsted Rating' }
2026-03-25 10:06:36 +00:00
{ ofsted . inspection_date && (
< span className = { styles . ofstedDate }>
Inspected { new Date ( ofsted . inspection_date ). toLocaleDateString ( 'en-GB' , { day : 'numeric' , month : 'long' , year : 'numeric' })}
</ span >
)}
< a
href = { `https://reports.ofsted.gov.uk/provider/21/ ${ schoolInfo . urn } ` }
target = "_blank"
rel = "noopener noreferrer"
className = { styles . ofstedReportLink }
2026-05-19 22:04:22 +01:00
data-umami-event = "external_link_clicked"
data-umami-event-target = "ofsted"
2026-03-25 10:06:36 +00:00
>
Full report ↗
</ a >
2026-02-02 20:34:35 +00:00
</ h2 >
2026-03-25 13:03:04 +00:00
{ ofsted . framework === 'ReportCard' ? (
/* ── New Report Card layout ── */
<>
< p className = { styles . ofstedDisclaimer }>
From November 2025 , Ofsted replaced single overall grades with Report Cards rating schools across several areas .
</ p >
< div className = { styles . metricsGrid }>
2026-03-25 15:17:45 +00:00
{ ofsted . rc_safeguarding_met != null && (
< div className = { styles . metricCard }>
< div className = { styles . metricLabel }> Safeguarding </ div >
< div className = { ` ${ styles . metricValue } ${ ofsted . rc_safeguarding_met ? styles.safeguardingMet : styles.safeguardingNotMet } ` }>
{ ofsted . rc_safeguarding_met ? 'Met' : 'Not met' }
</ div >
</ div >
)}
2026-03-25 13:03:04 +00:00
{ RC_CATEGORIES . map (({ key , label }) => {
const value = ofsted [ key ] as number | null ;
return value != null ? (
< div key = { key } className = { styles . metricCard }>
< div className = { styles . metricLabel }>{ label }</ div >
< div className = { ` ${ styles . metricValue } ${ styles [ `rcGrade ${ value } ` ] } ` }>
{ RC_LABELS [ value ]}
</ div >
</ div >
) : null ;
})}
2026-03-25 10:06:36 +00:00
</ div >
2026-03-25 13:03:04 +00:00
{ parentView ? . q_recommend_pct != null && parentView . total_responses != null && parentView . total_responses > 0 && (
< p className = { styles . parentRecommendLine }>
< strong >{ Math . round ( parentView . q_recommend_pct )} % </ strong > of parents would recommend this school ({ parentView . total_responses . toLocaleString ()} responses )
</ p >
)}
</>
) : (
/* ── Old OEIF layout ── */
<>
< div className = { styles . ofstedHeader }>
< span className = { ` ${ styles . ofstedGrade } ${ styles [ `ofstedGrade ${ ofsted . overall_effectiveness } ` ] } ` }>
{ ofsted . overall_effectiveness ? OFSTED_LABELS [ ofsted . overall_effectiveness ] : 'Not rated' }
</ span >
{ ofsted . previous_overall != null &&
ofsted . previous_overall !== ofsted . overall_effectiveness && (
< span className = { styles . ofstedPrevious }>
Previously : { OFSTED_LABELS [ ofsted . previous_overall ]}
</ span >
)}
</ div >
< p className = { styles . ofstedDisclaimer }>
From September 2024 , Ofsted no longer makes an overall effectiveness judgement in inspections of state - funded schools .
</ p >
{ parentView ? . q_recommend_pct != null && parentView . total_responses != null && parentView . total_responses > 0 && (
< p className = { styles . parentRecommendLine }>
< strong >{ Math . round ( parentView . q_recommend_pct )} % </ strong > of parents would recommend this school ({ parentView . total_responses . toLocaleString ()} responses )
</ p >
)}
2026-04-08 15:13:13 +01:00
{ oeifAllSameGrade ? (
< p className = { styles . ofstedAllSame }>
Rated < strong >{ OFSTED_LABELS [ ofsted . overall_effectiveness ! ]}</ strong > across all inspected areas — Quality of Teaching , Behaviour , Pupils & apos ; Development and Leadership .
</ p >
) : (
< div className = { styles . metricsGrid }>
{[
{ label : 'Quality of Teaching' , value : ofsted.quality_of_education },
{ label : 'Behaviour in School' , value : ofsted.behaviour_attitudes },
{ label : 'Pupils\' Wider Development' , value : ofsted.personal_development },
{ label : 'School Leadership' , value : ofsted.leadership_management },
...( ofsted . early_years_provision != null
? [{ label : 'Early Years (Reception)' , value : ofsted.early_years_provision }]
: []),
]. map (({ label , value }) => value != null && (
< div key = { label } className = { styles . metricCard }>
< div className = { styles . metricLabel }>{ label }</ div >
< div className = { ` ${ styles . metricValue } ${ styles [ `ofstedGrade ${ value } ` ] } ` }>
{ OFSTED_LABELS [ value ]}
</ div >
2026-03-25 13:03:04 +00:00
</ div >
2026-04-08 15:13:13 +01:00
))}
</ div >
)}
2026-03-25 13:03:04 +00:00
</>
)}
2026-03-25 10:06:36 +00:00
</ section >
)}
{ /* What Parents Say */ }
{ parentView && parentView . total_responses != null && parentView . total_responses > 0 && (
2026-03-25 10:34:19 +00:00
< section id = "parents" className = { styles . card }>
2026-03-25 10:06:36 +00:00
< h2 className = { styles . sectionTitle }>
What Parents Say
< span className = { styles . responseBadge }>
{ parentView . total_responses . toLocaleString ()} responses
</ span >
</ h2 >
< p className = { styles . sectionSubtitle }>
From the Ofsted Parent View survey — parents share their experience of this school .
</ p >
< div className = { styles . parentViewGrid }>
{[
{ label : 'Would recommend this school' , pct : parentView.q_recommend_pct },
{ label : 'My child is happy here' , pct : parentView.q_happy_pct },
{ label : 'My child feels safe here' , pct : parentView.q_safe_pct },
{ label : 'Teaching is good' , pct : parentView.q_teaching_pct },
{ label : 'My child makes good progress' , pct : parentView.q_progress_pct },
{ label : 'School looks after pupils\' wellbeing' , pct : parentView.q_wellbeing_pct },
{ label : 'Behaviour is well managed' , pct : parentView.q_behaviour_pct },
{ label : 'School deals well with bullying' , pct : parentView.q_bullying_pct },
{ label : 'Communicates well with parents' , pct : parentView.q_communication_pct },
]. filter ( q => q . pct != null ). map (({ label , pct }) => (
< div key = { label } className = { styles . parentViewRow }>
< span className = { styles . parentViewLabel }>{ label }</ span >
< div className = { styles . parentViewBar }>
< div className = { styles . parentViewFill } style = {{ width : ` ${ pct } %` }} />
</ div >
< span className = { styles . parentViewPct }>{ Math . round ( pct ! )} % </ span >
</ div >
))}
</ div >
</ section >
)}
2026-03-28 14:59:40 +00:00
{ /* Results Section (SATs for primary, GCSEs for secondary) */ }
{ hasAnyResults && latestResults && (
< section id = "results" className = { styles . card }>
< h2 className = { styles . sectionTitle }>
{ isSecondary ? 'GCSE Results' : 'SATs Results' } ({ formatAcademicYear ( latestResults . year )})
</ h2 >
2026-03-25 10:06:36 +00:00
< p className = { styles . sectionSubtitle }>
2026-03-28 14:59:40 +00:00
{ isSecondary
? 'GCSE results for Year 11 pupils. National averages shown for comparison.'
: 'End-of-primary-school tests taken by Year 6 pupils. National averages shown for comparison.' }
2026-03-25 10:06:36 +00:00
</ p >
2026-03-25 10:34:19 +00:00
2026-03-28 14:59:40 +00:00
{ /* ── Primary / KS2 content ── */ }
{ hasKS2Results && (
<>
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatGrid }>
2026-03-28 14:59:40 +00:00
{ latestResults . rwm_expected_pct !== null && (
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }>
2026-03-28 14:59:40 +00:00
Reading , Writing & amp ; Maths combined
< MetricTooltip metricKey = "rwm_expected_pct" />
</ div >
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatValue }>
2026-04-08 10:32:33 +01:00
{ formatPercentage ( latestResults . rwm_expected_pct )}
{ primaryAvg . rwm_expected_pct != null && (
< DeltaChip
value = { latestResults . rwm_expected_pct }
baseline = { primaryAvg . rwm_expected_pct }
unit = "pts"
size = "sm"
/>
)}
</ div >
2026-03-28 14:59:40 +00:00
{ primaryAvg . rwm_expected_pct != null && (
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatHint }> National avg : { primaryAvg . rwm_expected_pct . toFixed ( 0 )} % </ div >
2026-03-28 14:59:40 +00:00
)}
</ div >
)}
{ latestResults . rwm_high_pct !== null && (
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }>
2026-03-28 14:59:40 +00:00
Exceeding expected level ( Reading , Writing & amp ; Maths )
< MetricTooltip metricKey = "rwm_high_pct" />
</ div >
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatValue }>
2026-04-08 10:32:33 +01:00
{ formatPercentage ( latestResults . rwm_high_pct )}
{ primaryAvg . rwm_high_pct != null && (
< DeltaChip
value = { latestResults . rwm_high_pct }
baseline = { primaryAvg . rwm_high_pct }
unit = "pts"
size = "sm"
/>
)}
</ div >
2026-03-28 14:59:40 +00:00
{ primaryAvg . rwm_high_pct != null && (
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatHint }> National avg : { primaryAvg . rwm_high_pct . toFixed ( 0 )} % </ div >
2026-03-28 14:59:40 +00:00
)}
</ div >
)}
2026-02-02 20:34:35 +00:00
</ div >
2026-03-23 21:31:28 +00:00
2026-04-14 13:30:11 +01:00
{ latestResults . rwm_expected_pct != null &&
latestResults . reading_expected_pct != null &&
latestResults . writing_expected_pct != null &&
latestResults . maths_expected_pct != null && (
< div className = { styles . rwmBridge }>
< span className = { styles . rwmBridgeIcon } aria-hidden = "true" > ? </ span >
< div className = { styles . rwmBridgeBody }>
< div className = { styles . rwmBridgeText }>
Why is combined lower ? A pupil is only counted if they met the bar in { ' ' }
< strong > all three </ strong > subjects . Some passed reading but not writing ; some passed writing but not maths .
</ div >
< div className = { styles . rwmBridgeMath }>
< span > Reading < strong >{ latestResults . reading_expected_pct . toFixed ( 0 )} % </ strong ></ span >
< span className = { styles . rwmBridgeMathSep }> · </ span >
< span > Writing < strong >{ latestResults . writing_expected_pct . toFixed ( 0 )} % </ strong ></ span >
< span className = { styles . rwmBridgeMathSep }> · </ span >
< span > Maths < strong >{ latestResults . maths_expected_pct . toFixed ( 0 )} % </ strong ></ span >
< span className = { styles . rwmBridgeMathSep }> → </ span >
< span > All three < strong >{ latestResults . rwm_expected_pct . toFixed ( 0 )} % </ strong ></ span >
</ div >
</ div >
</ div >
)}
2026-04-13 21:22:24 +01:00
< SatsChart
subjects = {[
{
name : 'Reading' ,
expectedPct : latestResults.reading_expected_pct ,
exceedingPct : latestResults.reading_high_pct ,
nationalExpectedPct : primaryAvg.reading_expected_pct ,
},
{
name : 'Writing' ,
expectedPct : latestResults.writing_expected_pct ,
exceedingPct : latestResults.writing_high_pct ,
nationalExpectedPct : primaryAvg.writing_expected_pct ,
},
{
name : 'Maths' ,
expectedPct : latestResults.maths_expected_pct ,
exceedingPct : latestResults.maths_high_pct ,
nationalExpectedPct : primaryAvg.maths_expected_pct ,
},
]}
/>
{ /* Progress scores row */ }
{( latestResults . reading_progress != null || latestResults . writing_progress != null || latestResults . maths_progress != null ) && (
< div className = { styles . progressScoresRow }>
< h3 className = { styles . subSectionTitle }> Progress Scores </ h3 >
< div className = { styles . progressScoresGrid }>
{ latestResults . reading_progress != null && (
< div className = { styles . progressScoreItem }>
< span className = { styles . progressScoreLabel }> Reading </ span >
< span className = { ` ${ styles . progressScoreValue } ${ progressClass ( latestResults . reading_progress ) } ` }>
2026-03-28 14:59:40 +00:00
{ formatProgress ( latestResults . reading_progress )}
</ span >
</ div >
)}
2026-04-13 21:22:24 +01:00
{ latestResults . writing_progress != null && (
< div className = { styles . progressScoreItem }>
< span className = { styles . progressScoreLabel }> Writing </ span >
< span className = { ` ${ styles . progressScoreValue } ${ progressClass ( latestResults . writing_progress ) } ` }>
2026-03-28 14:59:40 +00:00
{ formatProgress ( latestResults . writing_progress )}
</ span >
</ div >
)}
2026-04-13 21:22:24 +01:00
{ latestResults . maths_progress != null && (
< div className = { styles . progressScoreItem }>
< span className = { styles . progressScoreLabel }> Maths </ span >
< span className = { ` ${ styles . progressScoreValue } ${ progressClass ( latestResults . maths_progress ) } ` }>
2026-03-28 14:59:40 +00:00
{ formatProgress ( latestResults . maths_progress )}
</ span >
</ div >
)}
</ div >
</ div >
2026-04-13 21:22:24 +01:00
)}
2026-03-28 14:59:40 +00:00
{( latestResults . reading_progress !== null || latestResults . writing_progress !== null || latestResults . maths_progress !== null ) && (
< p className = { styles . progressNote }>
Progress scores measure how much pupils improved compared to similar schools nationally . Above 0 = better than average , below 0 = below average .
</ p >
)}
</>
)}
{ /* ── Secondary / KS4 content ── */ }
{ hasKS4Results && (
<>
< div className = { styles . metricsGrid }>
{ latestResults . attainment_8_score !== null && (
< div className = { styles . metricCard }>
< div className = { styles . metricLabel }>
Attainment 8
< MetricTooltip metricKey = "attainment_8_score" />
</ div >
< div className = { styles . metricValue }>{ latestResults . attainment_8_score . toFixed ( 1 )}</ div >
{ secondaryAvg . attainment_8_score != null && (
< div className = { styles . metricHint }> National avg : { secondaryAvg . attainment_8_score . toFixed ( 1 )}</ div >
)}
2026-03-25 10:06:36 +00:00
</ div >
)}
2026-03-28 14:59:40 +00:00
{ latestResults . progress_8_score !== null && (
< div className = { styles . metricCard }>
< div className = { styles . metricLabel }>
Progress 8
< MetricTooltip metricKey = "progress_8_score" />
</ div >
< div className = { ` ${ styles . metricValue } ${ progressClass ( latestResults . progress_8_score ) } ` }>
{ formatProgress ( latestResults . progress_8_score )}
</ div >
< div className = { styles . metricHint }> 0 = national average </ div >
2026-03-25 10:06:36 +00:00
</ div >
)}
2026-03-28 14:59:40 +00:00
{ latestResults . english_maths_standard_pass_pct !== null && (
< div className = { styles . metricCard }>
< div className = { styles . metricLabel }>
English & amp ; Maths Grade 4 +
< MetricTooltip metricKey = "english_maths_standard_pass_pct" />
</ div >
< div className = { styles . metricValue }>{ formatPercentage ( latestResults . english_maths_standard_pass_pct )}</ div >
{ secondaryAvg . english_maths_standard_pass_pct != null && (
< div className = { styles . metricHint }> National avg : { secondaryAvg . english_maths_standard_pass_pct . toFixed ( 0 )} % </ div >
)}
2026-03-25 10:06:36 +00:00
</ div >
)}
2026-03-28 14:59:40 +00:00
{ latestResults . english_maths_strong_pass_pct !== null && (
< div className = { styles . metricCard }>
< div className = { styles . metricLabel }>
English & amp ; Maths Grade 5 +
< MetricTooltip metricKey = "english_maths_strong_pass_pct" />
</ div >
< div className = { styles . metricValue }>{ formatPercentage ( latestResults . english_maths_strong_pass_pct )}</ div >
{ secondaryAvg . english_maths_strong_pass_pct != null && (
< div className = { styles . metricHint }> National avg : { secondaryAvg . english_maths_strong_pass_pct . toFixed ( 0 )} % </ div >
)}
2026-03-25 10:06:36 +00:00
</ div >
)}
</ div >
2026-03-28 14:59:40 +00:00
{ /* 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 +
< MetricTooltip metricKey = "ebacc_standard_pass_pct" />
</ 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 +
< MetricTooltip metricKey = "ebacc_strong_pass_pct" />
</ span >
< span className = { styles . metricValue }>{ formatPercentage ( latestResults . ebacc_strong_pass_pct )}</ span >
</ div >
)}
2026-03-25 10:06:36 +00:00
</ div >
2026-03-28 14:59:40 +00:00
</>
)}
</>
2026-03-25 10:34:19 +00:00
)}
2026-03-25 10:06:36 +00:00
</ section >
)}
2026-03-28 14:59:40 +00:00
{ /* Year 1 Phonics — primary only */ }
{ hasPhonics && isPrimary && phonics && (
2026-03-25 10:34:19 +00:00
< section id = "phonics" className = { styles . card }>
2026-03-27 18:30:37 +00:00
< h2 className = { styles . sectionTitle }> Year 1 Phonics ({ formatAcademicYear ( phonics . year )})</ h2 >
2026-03-25 10:06:36 +00:00
< p className = { styles . sectionSubtitle }>
Phonics is a key early reading skill . Children are tested at the end of Year 1 .
</ p >
< div className = { styles . metricsGrid }>
< div className = { styles . metricCard }>
< div className = { styles . metricLabel }> Passed the phonics check </ div >
< div className = { styles . metricValue }>{ formatPercentage ( phonics . year1_phonics_pct )}</ div >
2026-03-28 14:59:40 +00:00
< div className = { styles . metricHint }> Phonics is a key early reading skill tested at end of Year 1 </ div >
2026-03-25 10:06:36 +00:00
</ div >
{ phonics . year2_phonics_pct != null && (
< div className = { styles . metricCard }>
< div className = { styles . metricLabel }> Year 2 pupils who retook and passed </ div >
< div className = { styles . metricValue }>{ formatPercentage ( phonics . year2_phonics_pct )}</ div >
</ div >
)}
</ div >
</ section >
)}
{ /* School Life */ }
2026-03-25 10:34:19 +00:00
{ hasSchoolLife && (
< section id = "school-life" className = { styles . card }>
2026-03-25 10:06:36 +00:00
< h2 className = { styles . sectionTitle }> School Life </ h2 >
< div className = { styles . metricsGrid }>
{ census ? . class_size_avg != null && (
< div className = { styles . metricCard }>
< div className = { styles . metricLabel }> Average class size </ div >
< div className = { styles . metricValue }>{ census . class_size_avg . toFixed ( 1 )}</ div >
2026-03-28 14:59:40 +00:00
< div className = { styles . metricHint }> Average number of pupils per class </ div >
2026-03-25 10:06:36 +00:00
</ div >
)}
{ absenceData ? . overall_absence_rate != null && (
< div className = { styles . metricCard }>
2026-03-28 14:59:40 +00:00
< div className = { styles . metricLabel }>
Days missed ( overall absence )
< MetricTooltip metricKey = "overall_absence_pct" />
</ div >
2026-03-25 10:06:36 +00:00
< div className = { styles . metricValue }>{ formatPercentage ( absenceData . overall_absence_rate )}</ div >
2026-03-28 14:59:40 +00:00
{ primaryAvg . overall_absence_pct != null && (
< div className = { styles . metricHint }> National avg : ~ { primaryAvg . overall_absence_pct . toFixed ( 1 )} % </ div >
)}
2026-03-25 10:06:36 +00:00
</ div >
)}
{ absenceData ? . persistent_absence_rate != null && (
< div className = { styles . metricCard }>
2026-03-28 14:59:40 +00:00
< div className = { styles . metricLabel }>
Regularly missing school
< MetricTooltip metricKey = "persistent_absence_pct" />
</ div >
2026-03-25 10:06:36 +00:00
< div className = { styles . metricValue }>{ formatPercentage ( absenceData . persistent_absence_rate )}</ div >
2026-03-28 14:59:40 +00:00
{ primaryAvg . persistent_absence_pct != null && (
< div className = { styles . metricHint }> National avg : ~ { primaryAvg . persistent_absence_pct . toFixed ( 0 )} % </ div >
)}
2026-03-25 10:06:36 +00:00
</ div >
)}
</ div >
</ section >
)}
{ /* How Hard to Get In */ }
{ admissions && (
2026-03-25 10:34:19 +00:00
< section id = "admissions" className = { styles . card }>
2026-06-19 18:41:03 +01:00
< div className = { styles . admissionsHeader }>
< h2 className = { styles . sectionTitle }>
How Hard to Get Into This School { ! showAdmissionsTrend && ` ( ${ formatAcademicYear ( admissions . year ) } )` }
</ h2 >
{ showAdmissionsTrend && (
< div className = { styles . admissionsSeg } role = "group" aria-label = "Admissions view" >
< button type = "button" aria-pressed = { admissionsView === 'year' } onClick = {() => setAdmissionsView ( 'year' )}>
This year
</ button >
< button type = "button" aria-pressed = { admissionsView === 'trend' } onClick = {() => setAdmissionsView ( 'trend' )}>
{ admissionsHistory . length } - year trend
</ button >
</ div >
)}
</ div >
2026-04-14 10:01:19 +01:00
2026-06-19 18:41:03 +01:00
< div className = { styles . admissionsViewport }>
{ /* This-year Q&A */ }
< div className = { styles . admissionsViewYear } hidden = { showAdmissionsTrend && admissionsView !== 'year' }>
2026-06-30 16:07:54 +01:00
< dl className = { styles . admissionsTiles }>
2026-06-19 18:41:03 +01:00
{ admissions . places_offered != null && (
2026-06-30 16:07:54 +01:00
< div className = { styles . admissionsTile }>
< dd className = { styles . admissionsTileNum }>{ admissions . places_offered }</ dd >
< dt className = { styles . admissionsTileLabel }> Places offered </ dt >
2026-06-19 18:41:03 +01:00
</ div >
)}
{ admissions . first_preference_applications != null && (
2026-06-30 16:07:54 +01:00
< div className = { styles . admissionsTile }>
< dd className = { styles . admissionsTileNum }>{ admissions . first_preference_applications }</ dd >
< dt className = { styles . admissionsTileLabel }> Wanted it first </ dt >
2026-06-19 18:41:03 +01:00
</ div >
)}
{ admissions . first_preference_offer_pct != null && (
2026-06-30 16:07:54 +01:00
< div className = { ` ${ styles . admissionsTile } ${ styles . admissionsTileAccent } ` }>
< dd className = { styles . admissionsTileNum }>
2026-06-19 18:41:03 +01:00
{ admissions . first_preference_offers != null && admissions . first_preference_applications != null ? (
<>
{ admissions . first_preference_offers }
2026-06-30 16:07:54 +01:00
< span className = { styles . admissionsTileSub }>
of { admissions . first_preference_applications } · { formatPercentage ( admissions . first_preference_offer_pct )}
2026-06-19 18:41:03 +01:00
</ span >
</>
) : (
formatPercentage ( admissions . first_preference_offer_pct )
)}
</ dd >
2026-06-30 16:07:54 +01:00
< dt className = { styles . admissionsTileLabel }> Got their first choice </ dt >
2026-06-19 18:41:03 +01:00
</ div >
)}
{ admissions . total_applications != null && (
2026-06-30 16:07:54 +01:00
< div className = { styles . admissionsTile }>
< dd className = { styles . admissionsTileNum }>{ admissions . total_applications . toLocaleString ()}</ dd >
< dt className = { styles . admissionsTileLabel }> Applied in total </ dt >
2026-06-19 18:41:03 +01:00
</ div >
)}
</ dl >
2026-04-14 11:03:57 +01:00
</ div >
2026-06-19 18:41:03 +01:00
{ /* Multi-year trend */ }
{ showAdmissionsTrend && (
< div className = { styles . admissionsViewTrend } hidden = { admissionsView !== 'trend' }>
< div className = { styles . admissionsChartCap }> First - choice offer rate </ div >
2026-06-19 18:59:57 +01:00
< AdmissionsTrendChart history = { admissionsHistory } />
2026-06-19 18:41:03 +01:00
< p className = { styles . admissionsTrendSummary }>
This year ({ formatAcademicYear ( admissions . year )}),{ ' ' }
{ admissions . first_preference_applications != null && (
<>< strong >{ admissions . first_preference_applications }</ strong > families put it first for </>
2026-04-14 10:01:19 +01:00
)}
2026-06-19 18:41:03 +01:00
{ admissions . places_offered != null && <>< strong >{ admissions . places_offered }</ strong > places </>}
{ admissions . total_applications != null && ` — ${ admissions . total_applications . toLocaleString () } applications in total` }.
</ p >
2026-03-25 10:06:36 +00:00
</ div >
)}
2026-06-19 18:41:03 +01:00
</ div >
2026-03-25 10:06:36 +00:00
</ section >
)}
{ /* Pupils & Inclusion */ }
2026-03-25 10:34:19 +00:00
{ hasInclusionData && (
< section id = "inclusion" className = { styles . card }>
2026-03-25 10:06:36 +00:00
< h2 className = { styles . sectionTitle }> Pupils & amp ; Inclusion </ h2 >
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatGrid }>
2026-03-25 10:06:36 +00:00
{ latestResults ? . disadvantaged_pct != null && (
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }> Eligible for pupil premium </ div >
< div className = { styles . heroStatValue }>
2026-04-08 15:13:13 +01:00
{ formatPercentage ( latestResults . disadvantaged_pct )}
{ primaryAvg . disadvantaged_pct != null && (
< DeltaChip value = { latestResults . disadvantaged_pct } baseline = { primaryAvg . disadvantaged_pct } unit = "pts" size = "sm" />
)}
</ div >
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatHint }> Pupils from disadvantaged backgrounds { primaryAvg . disadvantaged_pct != null ? ` · national avg: ${ primaryAvg . disadvantaged_pct . toFixed ( 0 ) } %` : '' }</ div >
2026-03-25 10:06:36 +00:00
</ div >
)}
{ latestResults ? . eal_pct != null && (
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }>
2026-03-28 14:59:40 +00:00
English as an additional language
< MetricTooltip metricKey = "eal_pct" />
</ div >
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatValue }>
2026-04-13 21:22:24 +01:00
{ formatPercentage ( latestResults . eal_pct )}
{ primaryAvg . eal_pct != null && (
< DeltaChip value = { latestResults . eal_pct } baseline = { primaryAvg . eal_pct } unit = "pts" size = "sm" />
)}
</ div >
{ primaryAvg . eal_pct != null && (
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatHint }> National avg : { primaryAvg . eal_pct . toFixed ( 0 )} % </ div >
2026-04-13 21:22:24 +01:00
)}
2026-03-25 10:06:36 +00:00
</ div >
)}
{ latestResults ? . sen_support_pct != null && (
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }>
2026-03-28 14:59:40 +00:00
Pupils receiving SEN support
< MetricTooltip metricKey = "sen_support_pct" />
</ div >
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatValue }>
2026-04-08 15:13:13 +01:00
{ formatPercentage ( latestResults . sen_support_pct )}
{ primaryAvg . sen_support_pct != null && (
< DeltaChip value = { latestResults . sen_support_pct } baseline = { primaryAvg . sen_support_pct } unit = "pts" size = "sm" />
)}
</ div >
{ primaryAvg . sen_support_pct != null && (
2026-04-14 14:31:31 +01:00
< div className = { styles . heroStatHint }> National avg : { primaryAvg . sen_support_pct . toFixed ( 0 )} % </ div >
2026-04-08 15:13:13 +01:00
)}
2026-03-25 10:06:36 +00:00
</ div >
)}
2026-04-17 22:52:59 +01:00
{ hasGenderSplit && (() => {
const female = census ! . female_pupils ! ;
const male = census ! . male_pupils ! ;
const girlsPct = Math . round (( female / ( female + male )) * 100 );
const boysPct = 100 - girlsPct ;
return (
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }> Boys and girls </ div >
< div className = { styles . genderSplitValue }>
< span className = { styles . genderSplitGirls }>{ girlsPct } % </ span >
< span className = { styles . genderSplitLabel }> girls </ span >
< span className = { styles . genderSplitSep }> · </ span >
< span className = { styles . genderSplitBoys }>{ boysPct } % </ span >
< span className = { styles . genderSplitLabel }> boys </ span >
</ div >
< div
className = { styles . genderBar }
role = "img"
aria-label = { `Gender split: ${ girlsPct } % girls, ${ boysPct } % boys` }
>
< span className = { styles . genderBarGirls } style = {{ width : ` ${ girlsPct } %` }} />
< span className = { styles . genderBarBoys } style = {{ width : ` ${ boysPct } %` }} />
</ div >
< div className = { styles . heroStatHint }>
{ female . toLocaleString ()} girls , { male . toLocaleString ()} boys
</ div >
</ div >
);
})()}
2026-03-25 10:06:36 +00:00
</ div >
{ senDetail && (
<>
< h3 className = { styles . subSectionTitle }> Types of additional needs supported </ h3 >
< p className = { styles . sectionSubtitle }>
What proportion of pupils with additional needs have each type of support need .
</ p >
< div className = { styles . metricsGrid }>
{[
{ label : 'Speech & Language' , pct : senDetail.primary_need_speech_pct },
{ label : 'Autism (ASD)' , pct : senDetail.primary_need_autism_pct },
{ label : 'Learning Difficulties' , pct : senDetail.primary_need_mld_pct },
{ label : 'Specific Learning (e.g. Dyslexia)' , pct : senDetail.primary_need_spld_pct },
{ label : 'Social, Emotional & Mental Health' , pct : senDetail.primary_need_semh_pct },
{ label : 'Physical / Sensory' , pct : senDetail.primary_need_physical_pct },
]. filter ( n => n . pct != null ). map (({ label , pct }) => (
< div key = { label } className = { styles . metricCard }>
< div className = { styles . metricLabel }>{ label }</ div >
< div className = { styles . metricValue }>{ pct } % </ div >
</ div >
))}
</ div >
</>
)}
</ section >
)}
{ /* Location */ }
2026-03-25 10:34:19 +00:00
{ hasLocation && (
< section id = "location" className = { styles . card }>
2026-03-23 21:31:28 +00:00
< h2 className = { styles . sectionTitle }> Location </ h2 >
< div className = { styles . mapContainer }>
< SchoolMap
schools = {[ schoolInfo ]}
2026-03-25 10:34:19 +00:00
center = {[ schoolInfo . latitude ! , schoolInfo . longitude ! ]}
2026-03-23 21:31:28 +00:00
zoom = { 15 }
/>
</ div >
2026-02-02 20:34:35 +00:00
</ section >
)}
2026-03-25 10:06:36 +00:00
{ /* Local Area Context */ }
2026-03-25 10:34:19 +00:00
{ hasDeprivation && deprivation && (
< section id = "local-area" className = { styles . card }>
2026-03-28 14:59:40 +00:00
< h2 className = { styles . sectionTitle }>
Local Area Context
< MetricTooltip metricKey = "idaci_decile" />
</ h2 >
2026-03-25 10:06:36 +00:00
< div className = { styles . deprivationDots }>
{ Array . from ({ length : 10 }, ( _ , i ) => (
< div
key = { i }
className = { ` ${ styles . deprivationDot } ${ i < deprivation . idaci_decile ! ? styles . deprivationDotFilled : '' } ` }
title = { `Decile ${ i + 1 } ` }
/>
))}
</ div >
< div className = { styles . deprivationScaleLabel }>
< span > Most deprived </ span >
< span > Least deprived </ span >
</ div >
2026-03-25 10:34:19 +00:00
< p className = { styles . deprivationDesc }>{ deprivationDesc ( deprivation . idaci_decile ! )}</ p >
2026-03-25 10:06:36 +00:00
</ section >
)}
{ /* Finances */ }
2026-03-25 10:34:19 +00:00
{ hasFinance && finance && (
< section id = "finances" className = { styles . card }>
2026-03-27 18:30:37 +00:00
< h2 className = { styles . sectionTitle }> School Finances ({ formatAcademicYear ( finance . year )})</ h2 >
2026-03-25 10:06:36 +00:00
< p className = { styles . sectionSubtitle }>
Per - pupil spending shows how much the school has to spend on each child & apos ; s education .
</ p >
< div className = { styles . metricsGrid }>
< div className = { styles . metricCard }>
< div className = { styles . metricLabel }> Total spend per pupil per year </ div >
2026-03-25 10:34:19 +00:00
< div className = { styles . metricValue }> £ { Math . round ( finance . per_pupil_spend ! ). toLocaleString ()}</ div >
2026-03-28 14:59:40 +00:00
< div className = { styles . metricHint }> How much the school has to spend on each pupil annually </ div >
2026-03-25 10:06:36 +00:00
</ div >
{ finance . teacher_cost_pct != null && (
< div className = { styles . metricCard }>
< div className = { styles . metricLabel }> Share of budget spent on teachers </ div >
< div className = { styles . metricValue }>{ finance . teacher_cost_pct . toFixed ( 1 )} % </ div >
</ div >
)}
{ finance . staff_cost_pct != null && (
< div className = { styles . metricCard }>
< div className = { styles . metricLabel }> Share of budget spent on all staff </ div >
< div className = { styles . metricValue }>{ finance . staff_cost_pct . toFixed ( 1 )} % </ div >
</ div >
)}
</ div >
</ section >
)}
2026-03-25 10:34:19 +00:00
{ /* Results Over Time (merged: chart + historical table) */ }
2026-02-02 20:34:35 +00:00
{ yearlyData . length > 0 && (
2026-03-25 10:34:19 +00:00
< section id = "history" className = { styles . card }>
2026-03-25 10:06:36 +00:00
< h2 className = { styles . sectionTitle }> Results Over Time </ h2 >
2026-02-02 20:34:35 +00:00
< div className = { styles . chartContainer }>
< PerformanceChart
data = { yearlyData }
schoolName = { schoolInfo . school_name }
2026-03-28 14:59:40 +00:00
isSecondary = { isSecondary }
2026-04-08 15:13:13 +01:00
nationalRwmAvg = { isPrimary ? ( primaryAvg . rwm_expected_pct ?? null ) : null }
nationalAtt8Avg = { isSecondary ? ( secondaryAvg . attainment_8_score ?? null ) : null }
2026-04-09 13:55:14 +01:00
nationalByYear = { nationalAvg ? . by_year }
2026-02-02 20:34:35 +00:00
/>
</ div >
2026-03-25 10:34:19 +00:00
{ yearlyData . length > 1 && (
2026-04-13 21:22:24 +01:00
< details className = { styles . historyDisclosure }>
< summary className = { styles . historyToggle }> View raw year - by - year data </ summary >
2026-03-25 10:34:19 +00:00
< div className = { styles . tableWrapper }>
< table className = { styles . dataTable }>
< thead >
< tr >
< th > Year </ th >
2026-03-28 14:59:40 +00:00
{ isSecondary ? (
<>
< th > Attainment 8 </ th >
< th > Progress 8 </ th >
< th > English & amp ; Maths Grade 4 + </ th >
< th > English & amp ; Maths Grade 5 + </ th >
</>
) : (
<>
< th > Reading , Writing & amp ; Maths ( expected % )</ th >
< th > Exceeding expected ( % )</ th >
< th > Reading Progress </ th >
< th > Writing Progress </ th >
< th > Maths Progress </ th >
</>
)}
2026-03-25 10:34:19 +00:00
</ tr >
</ thead >
< tbody >
{ yearlyData . map (( result ) => (
< tr key = { result . year }>
2026-03-27 18:30:37 +00:00
< td className = { styles . yearCell }>{ formatAcademicYear ( result . year )}</ td >
2026-03-28 14:59:40 +00:00
{ 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 >
</>
)}
2026-03-25 10:34:19 +00:00
</ tr >
))}
</ tbody >
</ table >
</ div >
2026-04-13 21:22:24 +01:00
</ details >
2026-03-25 10:34:19 +00:00
)}
2026-02-02 20:34:35 +00:00
</ section >
)}
</ div >
);
}