2026-03-28 22:36:00 +00:00
/**
* SecondarySchoolDetailView Component
2026-03-29 14:48:06 +01:00
* Dedicated detail view for secondary schools with scroll-to-section navigation.
* All sections render at once; the sticky nav scrolls to each.
2026-03-28 22:36:00 +00:00
*/
'use client' ;
import { useEffect , useState } from 'react' ;
import { useRouter } from 'next/navigation' ;
import { useComparison } from '@/hooks/useComparison' ;
import { PerformanceChart } from './PerformanceChart' ;
import { MetricTooltip } from './MetricTooltip' ;
2026-03-29 15:06:27 +01:00
import { SchoolMap } from './SchoolMap' ;
2026-03-28 22:36:00 +00:00
import type {
School , SchoolResult , AbsenceData ,
OfstedInspection , OfstedParentView , SchoolCensus ,
SchoolAdmissions , SenDetail , Phonics ,
SchoolDeprivation , SchoolFinance , NationalAverages ,
} from '@/lib/types' ;
2026-04-08 21:05:33 +01:00
import { formatPercentage , formatProgress , formatAcademicYear , buildOfstedHeroChip } from '@/lib/utils' ;
import { DeltaChip } from './DeltaChip' ;
2026-03-28 22:36:00 +00:00
import styles from './SecondarySchoolDetailView.module.css' ;
const OFSTED_LABELS : Record < number , string > = {
1 : 'Outstanding' , 2 : 'Good' , 3 : 'Requires Improvement' , 4 : 'Inadequate' ,
} ;
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-29 14:48:06 +01:00
function progressClass ( val : number | null | undefined , modStyles : Record < string , string > ) : string {
if ( val == null ) return '' ;
if ( val > 0 ) return modStyles . progressPositive ;
if ( val < 0 ) return modStyles . progressNegative ;
return '' ;
}
function deprivationDesc ( decile : number ) : string {
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-03-28 22:36:00 +00:00
interface SecondarySchoolDetailViewProps {
schoolInfo : School ;
yearlyData : SchoolResult [ ] ;
absenceData : AbsenceData | null ;
ofsted : OfstedInspection | null ;
parentView : OfstedParentView | null ;
census : SchoolCensus | null ;
admissions : SchoolAdmissions | null ;
senDetail : SenDetail | null ;
phonics : Phonics | null ;
deprivation : SchoolDeprivation | null ;
finance : SchoolFinance | null ;
}
export function SecondarySchoolDetailView ( {
schoolInfo , yearlyData ,
ofsted , parentView , admissions , senDetail , deprivation , finance , absenceData ,
} : SecondarySchoolDetailViewProps ) {
const router = useRouter ( ) ;
const { addSchool , removeSchool , isSelected } = useComparison ( ) ;
const isInComparison = isSelected ( schoolInfo . urn ) ;
2026-04-08 21:05:33 +01:00
const [ activeSection , setActiveSection ] = useState < string > ( '' ) ;
2026-03-28 22:36:00 +00:00
const latestResults = yearlyData . length > 0 ? yearlyData [ yearlyData . length - 1 ] : null ;
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 secondaryAvg = nationalAvg ? . secondary ? ? { } ;
const hasSixthForm = schoolInfo . age_range ? . includes ( '18' ) ? ? false ;
const hasFinance = finance != null && finance . per_pupil_spend != null ;
2026-03-29 14:48:06 +01:00
const hasParents = parentView != null && parentView . total_responses != null && parentView . total_responses > 0 ;
2026-03-28 22:36:00 +00:00
const hasDeprivation = deprivation != null && deprivation . idaci_decile != null ;
2026-03-29 15:06:27 +01:00
const hasLocation = schoolInfo . latitude != null && schoolInfo . longitude != null ;
2026-03-29 14:48:06 +01:00
const hasWellbeing = ( latestResults ? . sen_support_pct != null || latestResults ? . sen_ehcp_pct != null ) || hasDeprivation ;
2026-03-28 22:36:00 +00:00
const p8Suspended = latestResults != null && latestResults . year >= 202425 ;
2026-03-29 14:48:06 +01:00
const hasResults = latestResults ? . attainment_8_score != null ;
2026-03-28 22:36:00 +00:00
const admissionsTag = ( ( ) = > {
const policy = schoolInfo . admissions_policy ? . toLowerCase ( ) ? ? '' ;
if ( policy . includes ( 'selective' ) ) return 'Selective' ;
const denom = schoolInfo . religious_denomination ? ? '' ;
if ( denom && denom !== 'Does not apply' ) return 'Faith priority' ;
return null ;
} ) ( ) ;
const handleComparisonToggle = ( ) = > {
if ( isInComparison ) {
removeSchool ( schoolInfo . urn ) ;
} else {
addSchool ( schoolInfo ) ;
}
} ;
2026-03-29 14:48:06 +01:00
// Build nav items dynamically based on available data
const navItems : { id : string ; label : string } [ ] = [ ] ;
if ( ofsted ) navItems . push ( { id : 'ofsted' , label : 'Ofsted' } ) ;
if ( hasParents ) navItems . push ( { id : 'parents' , label : 'Parents' } ) ;
if ( hasResults ) navItems . push ( { id : 'gcse' , label : 'GCSEs' } ) ;
if ( admissions ) navItems . push ( { id : 'admissions' , label : 'Admissions' } ) ;
if ( hasWellbeing ) navItems . push ( { id : 'wellbeing' , label : 'Wellbeing' } ) ;
2026-03-29 15:06:27 +01:00
if ( hasLocation ) navItems . push ( { id : 'location' , label : 'Location' } ) ;
2026-03-29 14:48:06 +01:00
if ( hasFinance ) navItems . push ( { id : 'finances' , label : 'Finances' } ) ;
if ( yearlyData . length > 1 ) navItems . push ( { id : 'history' , label : 'History' } ) ;
2026-04-08 21:05:33 +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 ( ',' ) ] ) ;
// ── 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 ) ;
} ) ( ) ;
// ── Hero signal chip & stats ─────────────────────────────────────────
const ofstedHeroChip = buildOfstedHeroChip ( ofsted ) ;
const heroAtt8 = latestResults ? . attainment_8_score ? ? null ;
const heroAtt8Nat = secondaryAvg . attainment_8_score ? ? null ;
const heroAcademicYear = latestResults ? formatAcademicYear ( latestResults . year ) : '' ;
2026-03-28 22:36:00 +00:00
return (
< div className = { styles . container } >
{ /* ── Header ─────────────────────────────────────── */ }
< header className = { styles . header } >
< div className = { styles . headerContent } >
< div className = { styles . titleSection } >
< h1 className = { styles . schoolName } > { schoolInfo . school_name } < / h1 >
< div className = { styles . badges } >
{ schoolInfo . school_type && (
< span className = { styles . badge } > { schoolInfo . school_type } < / span >
) }
{ schoolInfo . gender && schoolInfo . gender !== 'Mixed' && (
< span className = { styles . badge } > { schoolInfo . gender } & apos ; s school < / span >
) }
{ schoolInfo . age_range && (
< span className = { styles . badge } > { schoolInfo . age_range } < / span >
) }
{ hasSixthForm && (
< span className = { styles . badge } > Sixth form < / span >
) }
{ admissionsTag && (
< span className = { ` ${ styles . badge } ${ admissionsTag === 'Selective' ? styles.badgeSelective : styles.badgeFaith } ` } >
{ admissionsTag }
< / span >
) }
< / div >
{ schoolInfo . address && (
< p className = { styles . address } >
{ schoolInfo . address } { schoolInfo . postcode && ` , ${ schoolInfo . postcode } ` }
< / p >
) }
< div className = { styles . headerDetails } >
{ schoolInfo . headteacher_name && (
< span className = { styles . headerDetail } >
< strong > Headteacher : < / strong > { schoolInfo . headteacher_name }
< / span >
) }
{ schoolInfo . website && (
< span className = { styles . headerDetail } >
< a href = { / ^ https ? : \ / \ //i.test(schoolInfo.website) ? schoolInfo.website : `https://${schoolInfo.website}`} target="_blank" rel="noopener noreferrer">
School website ↗
< / a >
< / span >
) }
{ latestResults ? .total_pupils ! = null & & (
< span className = { styles . headerDetail } >
< strong > Pupils : < / strong > { latestResults.total_pupils.toLocaleString ( ) }
{ schoolInfo.capacity ! = null & & ` (capacity: ${ schoolInfo . capacity } ) ` }
< / span >
) }
{ schoolInfo.trust_name & & (
< span className = { styles . headerDetail } >
Part of < strong > { schoolInfo.trust_name } < / strong >
< / span >
) }
< / div >
< / div >
< div className = { styles . actions } >
< button
onClick = { handleComparisonToggle }
className = { isInComparison ? styles.btnRemove : styles.btnAdd }
>
{ isInComparison ? '✓ In Comparison' : '+ Add to Compare' }
< / button >
< / div >
< / div >
2026-04-08 21:05:33 +01:00
{ /* Hero signal chips */ }
< div className = { styles . heroChips } >
< div className = { ` ${ styles . heroChip } ${ styles [ ` tone- ${ ofstedHeroChip . tone } ` ] } ` } >
< 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 & & (
< div className = { ` ${ styles . heroChip } ${ styles [ 'tone-coral' ] } ` } >
< div className = { styles . heroChipTitle } > Oversubscribed < / div >
< div className = { styles . heroChipSub } >
{ admissions.first_preference_offer_pct ! = null
? ` ${ Math . round ( admissions . first_preference_offer_pct ) } % of first-choice applicants offered a place `
: 'More applicants than places' }
< / div >
< / div >
) }
< / div >
{ /* At-a-glance stats row */ }
{ latestResults & & (
< div className = { styles . heroStats } >
{ 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 } >
< div className = { ` ${ styles . heroStatNumberSerif } ${ styles [ ` tone- ${ ofstedHeroChip . tone } ` ] } ` } >
{ 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-03-28 22:36:00 +00:00
< / header >
2026-03-29 14:48:06 +01:00
{ /* ── Sticky section navigation ─────────────────────── */ }
< nav className = { styles . tabNav } aria-label = "Page sections" >
2026-03-28 22:36:00 +00:00
< div className = { styles . tabNavInner } >
< button onClick = { ( ) = > router . back ( ) } className = { styles . backBtn } > ← Back < / button >
2026-03-29 14:48:06 +01:00
{ navItems.length > 0 && < div className = { styles . tabNavDivider } / > }
{ navItems . map ( ( { id , label } ) = > (
2026-04-08 21:05:33 +01:00
< a
key = { id }
href = { ` # ${ id } ` }
className = { ` ${ styles . tabBtn } ${ activeSection === id ? ` ${ styles . tabBtnActive } ` : '' } ` }
>
{ label }
< / a >
2026-03-28 22:36:00 +00:00
) ) }
< / div >
< / nav >
2026-03-29 14:48:06 +01:00
{ /* ── Ofsted ─────────────────────────────────────── */ }
{ ofsted && (
< section id = "ofsted" className = { styles . card } >
< h2 className = { styles . sectionTitle } >
{ ofsted . framework === 'ReportCard' ? 'Ofsted Report Card' : 'Ofsted Rating' }
{ 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 }
>
Full report ↗
< / a >
< / h2 >
{ ofsted . framework === 'ReportCard' ? (
< >
< p className = { styles . ofstedDisclaimer } >
From November 2025 , Ofsted replaced single overall grades with Report Cards rating schools across several areas .
< / p >
2026-03-28 22:36:00 +00:00
< div className = { styles . metricsGrid } >
2026-03-29 14:48:06 +01:00
{ ofsted . rc_safeguarding_met != null && (
2026-03-28 22:36:00 +00:00
< div className = { styles . metricCard } >
2026-03-29 14:48:06 +01:00
< 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' }
2026-03-28 22:36:00 +00:00
< / div >
< / div >
) }
2026-03-29 14:48:06 +01:00
{ RC_CATEGORIES . filter ( ( { key } ) = > key !== 'rc_early_years' || ofsted [ key ] != null ) . 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-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
< / >
) : ofsted . overall_effectiveness ? (
< >
< div className = { styles . ofstedHeader } >
< span className = { ` ${ styles . ofstedGrade } ${ styles [ ` ofstedGrade ${ ofsted . overall_effectiveness } ` ] } ` } >
{ OFSTED_LABELS [ ofsted . overall_effectiveness ] }
< / span >
{ ofsted . previous_overall != null &&
ofsted . previous_overall !== ofsted . overall_effectiveness && (
< span className = { styles . ofstedPrevious } >
Previously : { OFSTED_LABELS [ ofsted . previous_overall ] }
< / span >
2026-03-29 10:59:30 +01:00
) }
< / div >
2026-03-29 14:48:06 +01:00
< p className = { styles . ofstedDisclaimer } >
From September 2024 , Ofsted no longer makes an overall effectiveness judgement in inspections .
< / p >
2026-04-08 21:05:33 +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-29 14:48:06 +01:00
< / div >
2026-04-08 21:05:33 +01:00
) ) }
< / div >
) }
2026-03-29 14:48:06 +01:00
< / >
) : (
< >
< p className = { styles . sectionSubtitle } >
From September 2024 , Ofsted no longer gives a single overall grade .
< / p >
< div className = { styles . metricsGrid } >
2026-03-28 22:36:00 +00:00
{ [
2026-03-29 14:48:06 +01:00
{ label : 'Quality of Education' , value : ofsted.quality_of_education } ,
{ label : 'Behaviour & Attitudes' , value : ofsted.behaviour_attitudes } ,
{ label : 'Personal Development' , value : ofsted.personal_development } ,
{ label : 'Leadership & Management' , value : ofsted.leadership_management } ,
] . filter ( ( { value } ) = > value != null ) . map ( ( { label , value } ) = > (
< div key = { label } className = { styles . metricCard } >
< div className = { styles . metricLabel } > { label } < / div >
< div className = { ` ${ styles . metricValue } ${ styles [ ` ofstedGrade ${ value } ` ] } ` } >
{ OFSTED_LABELS [ value ! ] }
2026-03-28 22:36:00 +00:00
< / div >
< / div >
) ) }
< / div >
2026-03-29 14:48:06 +01:00
< / >
2026-03-28 22:36:00 +00:00
) }
2026-03-29 14:48:06 +01:00
{ hasParents && (
< p className = { styles . parentRecommendLine } >
< strong > { Math . round ( parentView ! . q_recommend_pct ! ) } % < / strong > of parents would recommend this school ( { parentView ! . total_responses ! . toLocaleString ( ) } responses )
2026-03-28 22:36:00 +00:00
< / p >
2026-03-29 14:48:06 +01:00
) }
< / section >
) }
2026-03-28 22:36:00 +00:00
2026-03-29 14:48:06 +01:00
{ /* ── Parent View ────────────────────────────────── */ }
{ hasParents && parentView && (
< section id = "parents" className = { styles . card } >
< 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 >
2026-03-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
) ) }
< / div >
< / section >
) }
2026-03-28 22:36:00 +00:00
2026-03-29 14:48:06 +01:00
{ /* ── GCSE Results ───────────────────────────────── */ }
{ hasResults && latestResults && (
< section id = "gcse" className = { styles . card } >
< h2 className = { styles . sectionTitle } >
GCSE Results ( { formatAcademicYear ( latestResults . year ) } )
< / h2 >
< p className = { styles . sectionSubtitle } >
GCSE results for Year 11 pupils . National averages shown for comparison .
< / p >
{ p8Suspended && (
< div className = { styles . p8Banner } >
Progress 8 scores for 2024 / 25 are not used for accountability purposes following the KS2 assessment disruption . Treat with caution .
< / div >
) }
< div className = { styles . metricsGrid } >
{ latestResults . attainment_8_score != null && (
< div className = { styles . metricCard } >
< div className = { styles . metricLabel } >
Attainment 8
< MetricTooltip metricKey = "attainment_8_score" / >
2026-03-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
< 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 >
) }
< / div >
) }
{ latestResults . progress_8_score != null && (
< div className = { styles . metricCard } >
< div className = { styles . metricLabel } >
Progress 8
< MetricTooltip metricKey = "progress_8_score" / >
2026-03-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
< div className = { ` ${ styles . metricValue } ${ progressClass ( latestResults . progress_8_score , styles ) } ` } >
{ formatProgress ( latestResults . progress_8_score ) }
2026-03-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
{ ( latestResults . progress_8_lower_ci != null || latestResults . progress_8_upper_ci != null ) && (
< div className = { styles . metricHint } >
CI : { latestResults . progress_8_lower_ci ? . toFixed ( 2 ) ? ? '?' } to { latestResults . progress_8_upper_ci ? . toFixed ( 2 ) ? ? '?' }
2026-03-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
) }
< / div >
) }
{ 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" / >
2026-03-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
< 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 >
) }
< / div >
2026-03-28 22:36:00 +00:00
) }
2026-03-29 14:48:06 +01: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" / >
2026-03-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
< 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 >
) }
< / div >
2026-03-28 22:36:00 +00:00
) }
< / div >
2026-03-29 14:48:06 +01:00
{ /* Progress 8 component breakdown */ }
{ ( latestResults . progress_8_english != null || latestResults . progress_8_maths != null ||
latestResults . progress_8_ebacc != null || latestResults . progress_8_open != null ) && (
< >
< h3 className = { styles . subSectionTitle } > Attainment 8 Components ( Progress 8 contribution ) < / h3 >
< div className = { styles . metricTable } >
{ [
{ label : 'English' , val : latestResults.progress_8_english } ,
{ label : 'Maths' , val : latestResults.progress_8_maths } ,
{ label : 'EBacc subjects' , val : latestResults.progress_8_ebacc } ,
{ label : 'Open (other GCSEs)' , val : latestResults.progress_8_open } ,
] . filter ( r = > r . val != null ) . map ( ( { label , val } ) = > (
< div key = { label } className = { styles . metricRow } >
< span className = { styles . metricName } > { label } < / span >
< span className = { ` ${ styles . metricValue } ${ progressClass ( val , styles ) } ` } >
{ formatProgress ( val ! ) }
< / span >
< / div >
) ) }
< / div >
< / >
) }
{ /* 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 + < / 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 + < / span >
< span className = { styles . metricValue } > { formatPercentage ( latestResults . ebacc_strong_pass_pct ) } < / span >
< / div >
) }
{ latestResults . ebacc_avg_score != null && (
< div className = { styles . metricRow } >
< span className = { styles . metricName } > EBacc average point score < / span >
< span className = { styles . metricValue } > { latestResults . ebacc_avg_score . toFixed ( 2 ) } < / span >
< / div >
) }
< / div >
< / >
) }
{ /* Performance chart */ }
2026-03-28 22:36:00 +00:00
{ yearlyData . length > 0 && (
2026-03-29 14:48:06 +01:00
< >
< h3 className = { styles . subSectionTitle } style = { { marginTop : '1.25rem' } } > Results Over Time < / h3 >
2026-03-28 22:36:00 +00:00
< div className = { styles . chartContainer } >
< PerformanceChart
data = { yearlyData }
schoolName = { schoolInfo . school_name }
isSecondary = { true }
2026-04-08 21:05:33 +01:00
nationalAtt8Avg = { heroAtt8Nat }
2026-04-09 13:55:14 +01:00
nationalByYear = { nationalAvg ? . by_year }
2026-03-28 22:36:00 +00:00
/ >
< / div >
2026-03-29 14:48:06 +01:00
< / >
2026-03-28 22:36:00 +00:00
) }
2026-03-29 14:48:06 +01:00
< / section >
2026-03-28 22:36:00 +00:00
) }
2026-03-29 14:48:06 +01:00
{ /* ── Admissions ─────────────────────────────────── */ }
{ admissions && (
< section id = "admissions" className = { styles . card } >
< h2 className = { styles . sectionTitle } > Admissions < / h2 >
{ admissionsTag && (
< div className = { ` ${ styles . admissionsTypeBadge } ${ admissionsTag === 'Selective' ? styles.admissionsSelective : styles.admissionsFaith } ` } >
< strong > { admissionsTag } < / strong > { ' ' }
{ admissionsTag === 'Selective'
? '— Entry to this school is by selective examination (e.g. 11+).'
: ` — This school has a faith-based admissions priority ( ${ schoolInfo . religious_denomination } ). ` }
< / div >
) }
< div className = { styles . metricsGrid } >
{ admissions . published_admission_number != null && (
< div className = { styles . metricCard } >
< div className = { styles . metricLabel } > Year 7 places per year ( PAN ) < / div >
< div className = { styles . metricValue } > { admissions . published_admission_number } < / div >
2026-03-28 22:36:00 +00:00
< / div >
) }
2026-03-29 14:48:06 +01:00
{ admissions . total_applications != null && (
< div className = { styles . metricCard } >
< div className = { styles . metricLabel } > Total applications < / div >
< div className = { styles . metricValue } > { admissions . total_applications . toLocaleString ( ) } < / div >
< / div >
2026-03-28 22:36:00 +00:00
) }
2026-03-29 14:48:06 +01:00
{ admissions . first_preference_applications != null && (
< div className = { styles . metricCard } >
< div className = { styles . metricLabel } > 1 st preference applications < / div >
< div className = { styles . metricValue } > { admissions . first_preference_applications . toLocaleString ( ) } < / div >
< / div >
) }
{ admissions . first_preference_offer_pct != null && (
< div className = { styles . metricCard } >
< div className = { styles . metricLabel } > Families who got their first choice < / div >
< div className = { styles . metricValue } > { formatPercentage ( admissions . first_preference_offer_pct ) } < / div >
2026-03-28 22:36:00 +00:00
< / div >
) }
< / div >
2026-03-29 14:48:06 +01:00
{ admissions . oversubscribed != null && (
< div className = { ` ${ styles . admissionsBadge } ${ admissions . oversubscribed ? styles.statusWarn : styles.statusGood } ` } >
{ admissions . oversubscribed
? '⚠ Applications exceeded places last year'
: '✓ Places were available last year' }
< / div >
) }
2026-03-29 15:07:52 +01:00
< p className = { styles . sectionSubtitle } style = { { marginTop : '1rem' } } >
Historical distance cut - off data is not available for this school . Contact the admissions authority for oversubscription criteria details .
< / p >
2026-03-29 14:48:06 +01:00
{ hasSixthForm && (
< div className = { styles . sixthFormNote } >
This school has a sixth form ( Post - 16 provision ) . Post - 16 destination data coming soon .
< / div >
) }
< / section >
2026-03-28 22:36:00 +00:00
) }
2026-03-29 14:48:06 +01:00
{ /* ── Wellbeing ──────────────────────────────────── */ }
{ hasWellbeing && (
< section id = "wellbeing" className = { styles . card } >
< h2 className = { styles . sectionTitle } > Wellbeing & amp ; Context < / h2 >
2026-03-28 22:36:00 +00:00
{ /* SEN */ }
{ ( latestResults ? . sen_support_pct != null || latestResults ? . sen_ehcp_pct != null ) && (
2026-03-29 14:48:06 +01:00
< >
< h3 className = { styles . subSectionTitle } > Special Educational Needs ( SEN ) < / h3 >
2026-03-28 22:36:00 +00:00
< div className = { styles . metricsGrid } >
{ latestResults ? . sen_support_pct != null && (
< div className = { styles . metricCard } >
< div className = { styles . metricLabel } >
Pupils receiving SEN support
< MetricTooltip metricKey = "sen_support_pct" / >
< / div >
< div className = { styles . metricValue } > { formatPercentage ( latestResults . sen_support_pct ) } < / div >
< div className = { styles . metricHint } > SEN support without an EHCP < / div >
< / div >
) }
{ latestResults ? . sen_ehcp_pct != null && (
< div className = { styles . metricCard } >
< div className = { styles . metricLabel } >
Pupils with an EHCP
< MetricTooltip metricKey = "sen_ehcp_pct" / >
< / div >
< div className = { styles . metricValue } > { formatPercentage ( latestResults . sen_ehcp_pct ) } < / div >
< div className = { styles . metricHint } > Education , Health and Care Plan < / div >
< / div >
) }
{ latestResults ? . total_pupils != null && (
< div className = { styles . metricCard } >
< div className = { styles . metricLabel } > Total pupils < / div >
< div className = { styles . metricValue } > { latestResults . total_pupils . toLocaleString ( ) } < / div >
2026-03-29 14:48:06 +01:00
{ schoolInfo . capacity != null && (
< div className = { styles . metricHint } > Capacity : { schoolInfo . capacity } < / div >
) }
2026-03-28 22:36:00 +00:00
< / div >
) }
< / div >
2026-03-29 14:48:06 +01:00
< / >
2026-03-28 22:36:00 +00:00
) }
{ /* Deprivation */ }
{ hasDeprivation && deprivation && (
2026-03-29 14:48:06 +01:00
< >
< h3 className = { styles . subSectionTitle } style = { { marginTop : '1.25rem' } } >
2026-03-28 22:36:00 +00:00
Local Area Context
< MetricTooltip metricKey = "idaci_decile" / >
2026-03-29 14:48:06 +01:00
< / h3 >
2026-03-28 22:36:00 +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 >
< p className = { styles . deprivationDesc } > { deprivationDesc ( deprivation . idaci_decile ! ) } < / p >
2026-03-29 14:48:06 +01:00
< / >
2026-03-28 22:36:00 +00:00
) }
2026-03-29 14:48:06 +01:00
< / section >
2026-03-28 22:36:00 +00:00
) }
2026-03-29 15:06:27 +01:00
{ /* ── Location ───────────────────────────────────── */ }
{ hasLocation && (
< section id = "location" className = { styles . card } >
< h2 className = { styles . sectionTitle } > Location < / h2 >
< div className = { styles . mapContainer } >
< SchoolMap
schools = { [ schoolInfo ] }
center = { [ schoolInfo . latitude ! , schoolInfo . longitude ! ] }
zoom = { 15 }
/ >
< / div >
< / section >
) }
2026-03-29 14:48:06 +01:00
{ /* ── Finances ───────────────────────────────────── */ }
{ hasFinance && finance && (
< section id = "finances" className = { styles . card } >
< h2 className = { styles . sectionTitle } > School Finances ( { formatAcademicYear ( finance . year ) } ) < / h2 >
< 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 >
< div className = { styles . metricValue } > £ { Math . round ( finance . per_pupil_spend ! ) . toLocaleString ( ) } < / div >
< div className = { styles . metricHint } > How much the school has to spend on each pupil annually < / div >
2026-03-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
{ 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 >
2026-03-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
) }
{ finance . staff_cost_pct != null && (
2026-03-28 22:36:00 +00:00
< div className = { styles . metricCard } >
2026-03-29 14:48:06 +01:00
< div className = { styles . metricLabel } > Share of budget spent on all staff < / div >
< div className = { styles . metricValue } > { finance . staff_cost_pct . toFixed ( 1 ) } % < / div >
2026-03-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
) }
{ finance . premises_cost_pct != null && (
< div className = { styles . metricCard } >
< div className = { styles . metricLabel } > Share of budget spent on premises < / div >
< div className = { styles . metricValue } > { finance . premises_cost_pct . toFixed ( 1 ) } % < / div >
< / div >
) }
2026-03-28 22:36:00 +00:00
< / div >
2026-03-29 14:48:06 +01:00
< / section >
) }
{ /* ── History table ──────────────────────────────── */ }
{ yearlyData . length > 1 && (
< section id = "history" className = { styles . card } >
< h2 className = { styles . sectionTitle } > Historical Results < / h2 >
< div className = { styles . tableWrapper } >
< table className = { styles . dataTable } >
< thead >
< tr >
< th > Year < / th >
< th > Attainment 8 < / th >
< th > Progress 8 < / th >
< th > Eng & amp ; 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 >
< / section >
2026-03-28 22:36:00 +00:00
) }
< / div >
) ;
}