diff --git a/nextjs-app/components/SecondarySchoolDetailView.module.css b/nextjs-app/components/SecondarySchoolDetailView.module.css index f6f6ae4..4ae1c12 100644 --- a/nextjs-app/components/SecondarySchoolDetailView.module.css +++ b/nextjs-app/components/SecondarySchoolDetailView.module.css @@ -26,11 +26,11 @@ } .schoolName { - font-size: 1.5rem; + font-size: clamp(2rem, 5vw, 3.25rem); font-weight: 700; color: var(--text-primary, #1a1612); margin-bottom: 0.5rem; - line-height: 1.2; + line-height: 1.15; font-family: var(--font-playfair), 'Playfair Display', serif; overflow-wrap: break-word; } @@ -207,6 +207,17 @@ color: var(--text-primary, #1a1612); } +.tabBtnActive { + background: var(--accent-coral, #e07256); + color: white; + font-weight: 600; +} + +.tabBtnActive:hover { + background: var(--accent-coral-dark, #c45a3f); + color: white; +} + /* ── Card ────────────────────────────────────────────── */ .card { background: var(--bg-card, white); @@ -652,6 +663,145 @@ margin-top: 0.25rem; } +/* ── Ofsted all-same collapse ────────────────────────── */ +.ofstedAllSame { + font-size: 0.9375rem; + color: var(--text-secondary, #5c564d); + margin: 0.5rem 0 0; + line-height: 1.5; +} + +.ofstedAllSame strong { + color: var(--text-primary, #1a1612); +} + +/* ── Hero chips strip ────────────────────────────────── */ +.heroChips { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.25rem; +} + +.heroChip { + flex: 0 0 240px; + padding: 0.75rem 1rem; + border-radius: 8px; + border-left: 3px solid var(--border-color, #e5dfd5); + background: var(--bg-secondary, #f3ede4); + color: var(--text-primary, #1a1612); + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.heroChipTitle { + font-size: 0.9375rem; + font-weight: 700; + line-height: 1.3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.heroChipSub { + font-size: 0.75rem; + color: var(--text-secondary, #5c564d); + line-height: 1.4; +} + +.heroChipDetail { + font-size: 0.75rem; + font-weight: 600; + line-height: 1.4; + margin-top: 0.1rem; +} + +/* Hero tone system */ +.tone-teal { --hero-tone: var(--accent-teal, #2d7d7d); } +.tone-green { --hero-tone: #3c8c3c; } +.tone-gold { --hero-tone: var(--accent-gold, #c9a227); } +.tone-coral { --hero-tone: var(--accent-coral, #e07256); } +.tone-neutral { --hero-tone: var(--text-muted, #8a847a); } + +.heroChip.tone-teal, +.heroChip.tone-green, +.heroChip.tone-gold, +.heroChip.tone-coral, +.heroChip.tone-neutral { + border-left-color: var(--hero-tone); + background: color-mix(in srgb, var(--hero-tone) 10%, var(--bg-card, white)); +} + +.heroChip.tone-neutral { + background: var(--bg-secondary, #f3ede4); +} + +/* ── Hero at-a-glance stats ──────────────────────────── */ +.heroStats { + display: flex; + flex-wrap: wrap; + gap: 1.25rem 3rem; + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border-color, #e5dfd5); +} + +.heroStat { + display: flex; + flex-direction: column; + gap: 0.35rem; + min-width: 0; + flex: 0 0 auto; +} + +.heroStatNumber, +.heroStatNumberSerif { + font-family: var(--font-playfair), 'Playfair Display', serif; + font-weight: 700; + line-height: 1; + color: var(--text-primary, #1a1612); + min-height: clamp(2rem, 4vw, 2.75rem); + display: flex; + align-items: flex-end; +} + +.heroStatNumber { + font-size: clamp(2rem, 4vw, 2.75rem); + font-variant-numeric: tabular-nums; +} + +.heroStatNumberSerif { + font-size: clamp(1.75rem, 3.5vw, 2.25rem); +} + +.heroStatNumberSerif.tone-teal, +.heroStatNumberSerif.tone-green, +.heroStatNumberSerif.tone-gold, +.heroStatNumberSerif.tone-coral, +.heroStatNumberSerif.tone-neutral { + color: var(--hero-tone); +} + +.heroStatLabel { + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-secondary, #5c564d); +} + +.heroStatFoot { + font-size: 0.75rem; + color: var(--text-muted, #8a847a); +} + +.heroDataNote { + margin: 0.5rem 0 0; + font-size: 0.75rem; + color: var(--text-muted, #8a847a); +} + /* ── Responsive ──────────────────────────────────────── */ @media (max-width: 768px) { .header { @@ -673,7 +823,6 @@ } .schoolName { - font-size: 1.25rem; word-break: break-word; } @@ -733,6 +882,19 @@ .admissionsTypeBadge { font-size: 0.75rem; } + + .heroChips { + gap: 0.5rem; + margin-top: 1rem; + } + + .heroChip { + min-width: 100%; + } + + .heroStats { + gap: 1rem 1.5rem; + } } @media (max-width: 480px) { diff --git a/nextjs-app/components/SecondarySchoolDetailView.tsx b/nextjs-app/components/SecondarySchoolDetailView.tsx index ca4d091..7219f82 100644 --- a/nextjs-app/components/SecondarySchoolDetailView.tsx +++ b/nextjs-app/components/SecondarySchoolDetailView.tsx @@ -18,7 +18,8 @@ import type { SchoolAdmissions, SenDetail, Phonics, SchoolDeprivation, SchoolFinance, NationalAverages, } from '@/lib/types'; -import { formatPercentage, formatProgress, formatAcademicYear } from '@/lib/utils'; +import { formatPercentage, formatProgress, formatAcademicYear, buildOfstedHeroChip } from '@/lib/utils'; +import { DeltaChip } from './DeltaChip'; import styles from './SecondarySchoolDetailView.module.css'; const OFSTED_LABELS: Record = { @@ -75,6 +76,8 @@ export function SecondarySchoolDetailView({ const { addSchool, removeSchool, isSelected } = useComparison(); const isInComparison = isSelected(schoolInfo.urn); + const [activeSection, setActiveSection] = useState(''); + const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null; const [nationalAvg, setNationalAvg] = useState(null); @@ -124,6 +127,50 @@ export function SecondarySchoolDetailView({ if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' }); if (yearlyData.length > 1) navItems.push({ id: 'history', label: 'History' }); + // Track active section as user scrolls + useEffect(() => { + const ids = navItems.map(n => n.id); + if (!ids.length) return; + const observers: IntersectionObserver[] = []; + const ratioMap: Record = {}; + 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) : ''; + return (
{/* ── Header ─────────────────────────────────────── */} @@ -190,6 +237,75 @@ export function SecondarySchoolDetailView({
+ + {/* Hero signal chips */} +
+
+
{ofstedHeroChip.title}
+
{ofstedHeroChip.subtitle}
+ {ofstedHeroChip.detail && ( +
{ofstedHeroChip.detail}
+ )} +
+ + {admissions?.oversubscribed && ( +
+
Oversubscribed
+
+ {admissions.first_preference_offer_pct != null + ? `${Math.round(admissions.first_preference_offer_pct)}% of first-choice applicants offered a place` + : 'More applicants than places'} +
+
+ )} +
+ + {/* At-a-glance stats row */} + {latestResults && ( +
+ {heroAtt8 != null && ( +
+
{heroAtt8.toFixed(1)}
+
Attainment 8 score
+ {heroAtt8Nat != null && ( + + )} +
+ )} + + {ofsted && ( +
+
+ {ofstedHeroChip.state === 'oeif' + ? ofstedHeroChip.title.replace(/^Ofsted\s+/, '') + : ofstedHeroChip.state === 'reportCard' + ? 'Report Card' + : '—'} +
+
{ofstedHeroChip.subtitle}
+ {ofstedHeroChip.detail && ( +
{ofstedHeroChip.detail}
+ )} +
+ )} + + {admissions?.first_preference_offer_pct != null && ( +
+
+ {Math.round(admissions.first_preference_offer_pct)}% +
+
First-choice offer rate
+ {admissions.oversubscribed && ( +
Oversubscribed
+ )} +
+ )} +
+ )} + + {heroAcademicYear && ( +

Latest data: {heroAcademicYear}

+ )} {/* ── Sticky section navigation ─────────────────────── */} @@ -198,7 +314,13 @@ export function SecondarySchoolDetailView({ {navItems.length > 0 &&
} {navItems.map(({ id, label }) => ( - {label} + + {label} + ))}
@@ -265,24 +387,30 @@ export function SecondarySchoolDetailView({

From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections.

-
- {[ - { 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 && ( -
-
{label}
-
- {OFSTED_LABELS[value]} + {oeifAllSameGrade ? ( +

+ Rated {OFSTED_LABELS[ofsted.overall_effectiveness]} across all inspected areas — Quality of Teaching, Behaviour, Pupils' Development and Leadership. +

+ ) : ( +
+ {[ + { 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 && ( +
+
{label}
+
+ {OFSTED_LABELS[value]} +
-
- ))} -
+ ))} +
+ )} ) : ( <> @@ -489,6 +617,7 @@ export function SecondarySchoolDetailView({ data={yearlyData} schoolName={schoolInfo.school_name} isSecondary={true} + nationalAtt8Avg={heroAtt8Nat} />