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' ;
2026-07-01 18:12:12 +01:00
import { useEffect , useRef , useState } from 'react' ;
2026-03-28 22:36:00 +00:00
import { useRouter } from 'next/navigation' ;
2026-06-02 13:46:45 +01:00
import dynamic from 'next/dynamic' ;
2026-03-28 22:36:00 +00:00
import { useComparison } from '@/hooks/useComparison' ;
import { MetricTooltip } from './MetricTooltip' ;
2026-07-01 18:12:12 +01:00
import { SchoolHeroMap , type SchoolHeroMapHandle } from './SchoolHeroMap' ;
2026-06-02 13:46:45 +01:00
const PerformanceChart = dynamic (
() => import ( './PerformanceChart' ). then (( m ) => m . PerformanceChart ),
{ ssr : false },
);
2026-03-28 22:36:00 +00:00
import type {
School , SchoolResult , AbsenceData ,
2026-07-06 09:01:26 +01:00
OfstedInspection , SchoolCensus ,
2026-03-28 22:36:00 +00:00
SchoolAdmissions , SenDetail , Phonics ,
SchoolDeprivation , SchoolFinance , NationalAverages ,
} from '@/lib/types' ;
2026-07-08 22:05:55 +01:00
import { formatPercentage , formatProgress , formatAcademicYear , formatAgeRange , isProposedToClose } from '@/lib/utils' ;
2026-04-08 21:05:33 +01:00
import { DeltaChip } from './DeltaChip' ;
2026-05-19 22:04:22 +01:00
import { track , getNavigationSource } from '@/lib/analytics' ;
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 ;
census : SchoolCensus | null ;
admissions : SchoolAdmissions | null ;
senDetail : SenDetail | null ;
phonics : Phonics | null ;
deprivation : SchoolDeprivation | null ;
finance : SchoolFinance | null ;
}
export function SecondarySchoolDetailView ({
schoolInfo , yearlyData ,
2026-07-06 09:01:26 +01:00
ofsted , census , admissions , senDetail , deprivation , finance , absenceData ,
2026-03-28 22:36:00 +00:00
} : SecondarySchoolDetailViewProps ) {
const router = useRouter ();
2026-07-01 18:12:12 +01:00
// Hero map — the "View on map" link opens its fullscreen view.
const heroMapRef = useRef < SchoolHeroMapHandle >( null );
2026-03-28 22:36:00 +00:00
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 ?? {};
2026-07-07 10:40:42 +01:00
// GIAS OfficialSixthForm flag; missing (pipeline not yet re-run) => false.
const hasSixthForm = schoolInfo . has_sixth_form ?? false ;
2026-03-28 22:36:00 +00:00
const hasFinance = finance != null && finance . per_pupil_spend != null ;
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 );
2026-05-19 22:04:22 +01:00
track ( 'compare_school_removed' , { urn : schoolInfo.urn , from : 'detail' });
2026-03-28 22:36:00 +00:00
} else {
addSchool ( schoolInfo );
2026-05-19 22:04:22 +01:00
track ( 'compare_school_added' , { urn : schoolInfo.urn , from : 'detail' });
2026-03-28 22:36:00 +00:00
}
};
2026-07-01 14:48:17 +01:00
// Back returns wherever the user came from; deep-links 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' );
}
};
const scrollToTop = () => {
if ( typeof window !== 'undefined' ) window . scrollTo ({ top : 0 , behavior : 'smooth' });
};
2026-05-19 22:04:22 +01:00
useEffect (() => {
track ( 'school_viewed' , {
urn : schoolInfo.urn ,
phase : schoolInfo.phase || 'secondary' ,
local_authority : schoolInfo.local_authority || 'unknown' ,
from : getNavigationSource (),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ schoolInfo . urn ]);
2026-07-01 09:51:46 +01:00
// Build nav items dynamically based on available data.
// Engagement-led order (matches the primary page): recognised Ofsted badge,
// then the most-sought sections — results, admissions, history — with the
// experience and context sections following.
2026-03-29 14:48:06 +01:00
const navItems : { id : string ; label : string }[] = [];
if ( ofsted ) navItems . push ({ id : 'ofsted' , label : 'Ofsted' });
if ( hasResults ) navItems . push ({ id : 'gcse' , label : 'GCSEs' });
if ( admissions ) navItems . push ({ id : 'admissions' , label : 'Admissions' });
2026-07-01 09:51:46 +01:00
if ( yearlyData . length > 1 ) navItems . push ({ id : 'history' , label : 'History' });
2026-03-29 14:48:06 +01:00
if ( hasWellbeing ) navItems . push ({ id : 'wellbeing' , label : 'Wellbeing' });
if ( hasFinance ) navItems . push ({ id : 'finances' , label : 'Finances' });
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 );
})();
2026-07-01 18:12:12 +01:00
// National Attainment 8 baseline for the "Results Over Time" chart.
2026-04-08 21:05:33 +01:00
const heroAtt8Nat = secondaryAvg . attainment_8_score ?? null ;
2026-07-01 12:45:49 +01:00
2026-03-28 22:36:00 +00:00
return (
< div className = { styles . container }>
2026-07-01 14:48:17 +01:00
{ /* Standalone back link, above the header — returns wherever the user
came from. Scrolls away; the sticky bar keeps a "back to top" control. */ }
< button type = "button" onClick = { handleBack } className = { styles . topBack }>
< span aria-hidden = "true" > ← </ span > Back
</ button >
2026-07-01 18:12:12 +01:00
{ /* ── Header — the location map band blends into the school title ── */ }
< header className = { ` ${ styles . header }${ hasLocation ? ` ${ styles . headerHasMap } ` : '' } ` }>
{ hasLocation && (
< SchoolHeroMap ref = { heroMapRef } lat = { schoolInfo . latitude ! } lng = { schoolInfo . longitude ! } />
)}
2026-03-28 22:36:00 +00:00
< 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 && (
2026-07-01 23:05:05 +01:00
< span className = { styles . badge }>{ formatAgeRange ( schoolInfo . age_range )}</ span >
2026-03-28 22:36:00 +00:00
)}
{ hasSixthForm && (
< span className = { styles . badge }> Sixth form </ span >
)}
{ admissionsTag && (
< span className = { ` ${ styles . badge } ${ admissionsTag === 'Selective' ? styles.badgeSelective : styles.badgeFaith } ` }>
{ admissionsTag }
</ span >
)}
</ div >
2026-07-08 22:05:55 +01:00
{ isProposedToClose ( schoolInfo ) && (
< div className = { styles . closingStrip } role = "note" >
< strong > ⚠ Proposed to close </ strong > — this school is proposed for closure ,
2026-07-08 22:51:32 +01:00
check with the local authority before applying .
2026-07-08 22:05:55 +01:00
</ div >
)}
2026-03-28 22:36:00 +00:00
{ schoolInfo . address && (
< p className = { styles . address }>
{ schoolInfo . address }{ schoolInfo . postcode && `, ${ schoolInfo . postcode } ` }
2026-07-01 18:12:12 +01:00
{ hasLocation && (
<>
{ ' · ' }
< button
type = "button"
className = { styles . mapLink }
onClick = {() => { heroMapRef . current ? . open (); track ( 'section_nav_used' , { section : 'location' , via : 'hero_link' }); }}
>
View on map ↗
</ button >
</>
)}
2026-03-28 22:36:00 +00:00
</ 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 }>
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-28 22:36:00 +00:00
School website ↗
</ a >
</ span >
)}
2026-04-09 16:04:41 +01:00
{ ( schoolInfo.total_pupils != null || latestResults ? .total_pupils != null ) && (
2026-03-28 22:36:00 +00:00
< span className = { styles . headerDetail }>
2026-04-09 16:04:41 +01:00
< strong > Pupils : </ strong > { ( schoolInfo.total_pupils ?? latestResults ! .total_pupils !) .toLocaleString () }
2026-03-28 22:36:00 +00:00
{ 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 }
2026-07-02 16:17:12 +01:00
aria-label = { isInComparison ? 'In comparison' : 'Add to compare' }
2026-03-28 22:36:00 +00:00
>
2026-07-02 16:17:12 +01:00
{ /* On phones the map hero shows only the glyph (nav-bar style). */ }
< span className = { styles . btnCompareLabel }>
{ isInComparison ? '✓ In Comparison' : '+ Add to Compare' }
</ span >
< span className = { styles . btnCompareGlyph } aria-hidden = "true" >
{ isInComparison ? '✓' : '+' }
</ span >
2026-03-28 22:36:00 +00:00
</ button >
</ div >
</ div >
</ 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 }>
2026-07-01 14:48:17 +01:00
< button onClick = { scrollToTop } className = { styles . backBtn } aria-label = "Back to top" > ↑ Top </ 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 } ` : '' } ` }
2026-05-19 22:04:22 +01:00
onClick = {() => track ( 'section_nav_used' , { section : id })}
2026-04-08 21:05:33 +01:00
>
{ 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
2026-07-01 22:26:48 +01:00
href = { `https://reports.ofsted.gov.uk/inspection-reports/find-inspection-report/provider/ELS/ ${ schoolInfo . urn } ` }
2026-03-29 14:48:06 +01:00
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-29 14:48:06 +01:00
>
2026-07-01 22:26:48 +01:00
Ofsted reports ↗
2026-03-29 14:48:06 +01:00
</ 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 >
< div className = { styles . metricsGrid }>
{ 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-28 22:36:00 +00:00
)}
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 ]}
2026-03-29 10:59:30 +01:00
</ div >
2026-03-28 22:36:00 +00:00
</ div >
2026-03-29 14:48:06 +01:00
) : null ;
})}
</ div >
</>
) : 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-28 22:36:00 +00: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-28 22:36:00 +00: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 }>
{[
{ 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 ! ]}
</ div >
</ div >
))}
</ div >
</>
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
{ /* ── 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 >
)}
2026-04-15 09:21:21 +01:00
{ /* Hero stat cards — top GCSE metrics */ }
< div className = { styles . heroStatGrid }>
2026-03-29 14:48:06 +01:00
{ latestResults . attainment_8_score != null && (
2026-04-15 09:21:21 +01:00
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }>
Attainment 8 score
2026-03-29 14:48:06 +01:00
< MetricTooltip metricKey = "attainment_8_score" />
</ div >
2026-04-15 09:21:21 +01:00
< div className = { styles . heroStatValue }>
{ latestResults . attainment_8_score . toFixed ( 1 )}
{ secondaryAvg . attainment_8_score != null && (
< DeltaChip
value = { latestResults . attainment_8_score }
baseline = { secondaryAvg . attainment_8_score }
unit = "pts"
size = "sm"
/>
)}
</ div >
2026-03-29 14:48:06 +01:00
{ secondaryAvg . attainment_8_score != null && (
2026-04-15 09:21:21 +01:00
< div className = { styles . heroStatHint }> National avg : { secondaryAvg . attainment_8_score . toFixed ( 1 )}</ div >
2026-03-29 14:48:06 +01:00
)}
2026-03-28 22:36:00 +00:00
</ div >
)}
2026-03-29 14:48:06 +01:00
{ latestResults . progress_8_score != null && (
2026-04-15 09:21:21 +01:00
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }>
Progress 8 score
2026-03-29 14:48:06 +01:00
< MetricTooltip metricKey = "progress_8_score" />
2026-03-28 22:36:00 +00:00
</ div >
2026-04-15 09:21:21 +01:00
< div className = { ` ${ styles . heroStatValue } ${ progressClass ( latestResults . progress_8_score , styles ) } ` }>
2026-03-29 14:48:06 +01:00
{ formatProgress ( latestResults . progress_8_score )}
2026-03-28 22:36:00 +00:00
</ div >
2026-04-15 09:21:21 +01:00
{( latestResults . progress_8_lower_ci != null && latestResults . progress_8_upper_ci != null ) ? (
< div className = { styles . heroStatHint }>
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-04-15 09:21:21 +01:00
) : (
< div className = { styles . heroStatHint }> National baseline : 0.0 </ div >
2026-03-29 14:48:06 +01:00
)}
</ div >
)}
{ latestResults . english_maths_strong_pass_pct != null && (
2026-04-15 09:21:21 +01:00
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }>
2026-03-29 14:48:06 +01:00
English & amp ; Maths Grade 5 +
< MetricTooltip metricKey = "english_maths_strong_pass_pct" />
</ div >
2026-04-15 09:21:21 +01:00
< div className = { styles . heroStatValue }>
{ formatPercentage ( latestResults . english_maths_strong_pass_pct )}
{ secondaryAvg . english_maths_strong_pass_pct != null && (
< DeltaChip
value = { latestResults . english_maths_strong_pass_pct }
baseline = { secondaryAvg . english_maths_strong_pass_pct }
unit = "pts"
size = "sm"
/>
)}
</ div >
2026-03-29 14:48:06 +01:00
{ secondaryAvg . english_maths_strong_pass_pct != null && (
2026-04-15 09:21:21 +01:00
< div className = { styles . heroStatHint }> National avg : { secondaryAvg . english_maths_strong_pass_pct . toFixed ( 0 )} % </ div >
)}
</ div >
)}
{ latestResults . english_maths_standard_pass_pct != null && (
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }>
English & amp ; Maths Grade 4 +
< MetricTooltip metricKey = "english_maths_standard_pass_pct" />
</ div >
< div className = { styles . heroStatValue }>
{ formatPercentage ( latestResults . english_maths_standard_pass_pct )}
{ secondaryAvg . english_maths_standard_pass_pct != null && (
< DeltaChip
value = { latestResults . english_maths_standard_pass_pct }
baseline = { secondaryAvg . english_maths_standard_pass_pct }
unit = "pts"
size = "sm"
/>
)}
</ div >
{ secondaryAvg . english_maths_standard_pass_pct != null && (
< div className = { styles . heroStatHint }> National avg : { secondaryAvg . english_maths_standard_pass_pct . toFixed ( 0 )} % </ div >
2026-03-29 14:48:06 +01:00
)}
</ div >
2026-03-28 22:36:00 +00:00
)}
</ div >
2026-04-15 09:21:21 +01:00
{ /* Attainment 8 visual bar (0– 80 scale) */ }
{ latestResults . attainment_8_score != null && (
< div className = { styles . att8Viz }>
< div className = { styles . att8VizLabel }> Attainment 8 — school vs national </ div >
< div className = { styles . att8VizTrack }>
< div
className = { styles . att8VizFill }
style = {{ width : ` ${ Math . min (( latestResults . attainment_8_score / 80 ) * 100 , 100 ) } %` }}
/>
{ secondaryAvg . attainment_8_score != null && (
< div
className = { styles . att8VizNatLine }
style = {{ left : ` ${ ( secondaryAvg . attainment_8_score / 80 ) * 100 } %` }}
>
< div className = { styles . att8VizNatPill }>
Nat avg { secondaryAvg . attainment_8_score . toFixed ( 1 )}
</ div >
</ div >
)}
</ div >
< div className = { styles . att8VizTicks }>
< span > 0 </ span >< span > 20 </ span >< span > 40 </ span >< span > 60 </ span >< span > 80 </ span >
</ div >
</ div >
)}
{ /* Progress 8 number line with CI */ }
{ latestResults . progress_8_score != null && ! p8Suspended && (
< div className = { styles . p8Viz }>
< div className = { styles . p8VizLabel }> Progress 8 — relative to national baseline ( 0 )</ div >
{(() => {
const p8 = latestResults . progress_8_score ! ;
const lo = latestResults . progress_8_lower_ci ?? p8 ;
const hi = latestResults . progress_8_upper_ci ?? p8 ;
const range = 6 ; // − 3 to +3
const toX = ( v : number ) => ` ${ Math . min ( Math . max ((( v + 3 ) / range ) * 100 , 0 ), 100 ) } %` ;
return (
< div className = { styles . p8VizTrack }>
{ /* CI band */ }
< div
className = { styles . p8VizCi }
style = {{ left : toX ( lo ), width : `calc( ${ toX ( hi ) } - ${ toX ( lo ) } )` }}
/>
{ /* Zero line */ }
< div className = { styles . p8VizZero } style = {{ left : toX ( 0 ) }} />
{ /* Score dot */ }
< div
className = { ` ${ styles . p8VizDot } ${ p8 < 0 ? styles . p8VizDotNeg : '' } ` }
style = {{ left : toX ( p8 ) }}
/>
</ div >
);
})()}
< div className = { styles . p8VizTicks }>
< span > − 3 </ span >< span > − 2 </ span >< span > − 1 </ span >< span > 0 </ span >< span > + 1 </ span >< span > + 2 </ span >< span > + 3 </ span >
</ div >
</ 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 >
</>
)}
</ 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 >
2026-03-28 22:36:00 +00:00
2026-03-29 14:48:06 +01:00
{ 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 }>
2026-04-14 09:45:43 +01:00
{ admissions . places_offered != null && (
2026-03-29 14:48:06 +01:00
< div className = { styles . metricCard }>
2026-04-14 09:45:43 +01:00
< div className = { styles . metricLabel }> Year 7 places offered </ div >
< div className = { styles . metricValue }>{ admissions . places_offered }</ 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-07-01 09:51:46 +01:00
{ /* ── History table ──────────────────────────────── */ }
{ yearlyData . length > 1 && (
< section id = "history" className = { styles . card }>
< h2 className = { styles . sectionTitle }> Historical Results </ h2 >
{ yearlyData . length > 0 && (
<>
< h3 className = { styles . subSectionTitle } style = {{ marginTop : '1.25rem' }}> Results Over Time </ h3 >
< div className = { styles . chartContainer }>
< PerformanceChart
data = { yearlyData }
schoolName = { schoolInfo . school_name }
isSecondary = { true }
nationalAtt8Avg = { heroAtt8Nat }
nationalByYear = { nationalAvg ? . by_year }
/>
</ div >
</>
)}
< details className = { styles . historyDisclosure }>
< summary className = { styles . historyToggle }> View raw year - by - year data </ summary >
< div className = { styles . tableWrapper }>
< table className = { styles . dataTable }>
< thead >
< tr >
< th > Year </ th >
< th > Attainment 8 </ th >
< th > Progress 8 </ th >
< th > Eng & 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 >
</ details >
</ section >
)}
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-04-15 09:21:21 +01:00
< div className = { styles . heroStatGrid }>
2026-03-28 22:36:00 +00:00
{ latestResults ? . sen_support_pct != null && (
2026-04-15 09:21:21 +01:00
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }>
SEN support
2026-03-28 22:36:00 +00:00
< MetricTooltip metricKey = "sen_support_pct" />
</ div >
2026-04-15 09:21:21 +01:00
< div className = { styles . heroStatValue }>{ formatPercentage ( latestResults . sen_support_pct )}</ div >
< div className = { styles . heroStatHint }> Without an EHCP </ div >
2026-03-28 22:36:00 +00:00
</ div >
)}
{ latestResults ? . sen_ehcp_pct != null && (
2026-04-15 09:21:21 +01:00
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }>
Pupils with EHCP
2026-03-28 22:36:00 +00:00
< MetricTooltip metricKey = "sen_ehcp_pct" />
</ div >
2026-04-15 09:21:21 +01:00
< div className = { styles . heroStatValue }>{ formatPercentage ( latestResults . sen_ehcp_pct )}</ div >
< div className = { styles . heroStatHint }> Education , Health and Care Plan </ div >
2026-03-28 22:36:00 +00:00
</ div >
)}
2026-04-17 22:36:33 +01:00
{(() => {
const total = census ? . total_pupils ?? schoolInfo . total_pupils ?? latestResults ? . total_pupils ?? null ;
if ( total == null ) return null ;
const female = census ? . female_pupils ?? null ;
const male = census ? . male_pupils ?? null ;
const isMixed = schoolInfo . gender === 'Mixed' || schoolInfo . gender == null ;
const hasSplit = isMixed && female != null && male != null && female + male > 0 ;
const sum = hasSplit ? female ! + male ! : 0 ;
const girlsPct = hasSplit ? Math . round (( female ! / sum) * 100) : 0;
const boysPct = hasSplit ? 100 - girlsPct : 0 ;
return (
< div className = { styles . heroStatCard }>
< div className = { styles . heroStatLabel }> Total pupils </ div >
< div className = { styles . heroStatValue }>{ total . toLocaleString ()}</ div >
{ hasSplit && (
<>
< 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 . genderSplitHint }>
< span className = { styles . genderSplitGirls }>{ girlsPct } % girls </ span >
< span className = { styles . genderSplitSep }> · </ span >
< span className = { styles . genderSplitBoys }>{ boysPct } % boys </ span >
</ div >
</>
)}
{ schoolInfo . capacity != null && ! hasSplit && (
< div className = { styles . heroStatHint }> Capacity : { schoolInfo . capacity }</ div >
)}
</ div >
);
})()}
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
)}
{ /* 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 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 && (
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 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 && (
< 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 >
)}
{ 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 >
)}
2026-03-28 22:36:00 +00:00
</ div >
);
}