|
|
@@ -20,34 +20,23 @@ import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
import { useComparison } from '@/hooks/useComparison';
|
|
|
|
import { useComparison } from '@/hooks/useComparison';
|
|
|
|
import { SchoolHeroMap, type SchoolHeroMapHandle } from '../SchoolHeroMap';
|
|
|
|
import { SchoolHeroMap, type SchoolHeroMapHandle } from '../SchoolHeroMap';
|
|
|
|
import type {
|
|
|
|
import type { School, SchoolResult, SchoolCensus } from '@/lib/types';
|
|
|
|
School, SchoolResult, AbsenceData,
|
|
|
|
import { formatAgeRange, isProposedToClose } from '@/lib/utils';
|
|
|
|
OfstedInspection, SchoolCensus,
|
|
|
|
import type { NavItem } from '@/lib/schoolSections';
|
|
|
|
SchoolAdmissions,
|
|
|
|
|
|
|
|
SchoolDeprivation, SchoolFinance, NationalAverages,
|
|
|
|
|
|
|
|
} from '@/lib/types';
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
|
|
formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose, ofstedLegacyAreas,
|
|
|
|
|
|
|
|
} from '@/lib/utils';
|
|
|
|
|
|
|
|
import { computeSchoolFlags, type NavItem } from '@/lib/schoolSections';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { track, getNavigationSource } from '@/lib/analytics';
|
|
|
|
import { track, getNavigationSource } from '@/lib/analytics';
|
|
|
|
import styles from './SchoolDetailShell.module.css';
|
|
|
|
import styles from './SchoolDetailShell.module.css';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Only what the chrome itself renders. Everything the sections need — Ofsted,
|
|
|
|
|
|
|
|
* admissions, deprivation, finance, national averages — goes straight to the
|
|
|
|
|
|
|
|
* section composers in page.tsx and never reaches the client.
|
|
|
|
|
|
|
|
*/
|
|
|
|
export interface SchoolDetailShellProps {
|
|
|
|
export interface SchoolDetailShellProps {
|
|
|
|
schoolInfo: School;
|
|
|
|
schoolInfo: School;
|
|
|
|
|
|
|
|
/** Only for the header's pupil-count fallback. */
|
|
|
|
yearlyData: SchoolResult[];
|
|
|
|
yearlyData: SchoolResult[];
|
|
|
|
absenceData: AbsenceData | null;
|
|
|
|
|
|
|
|
ofsted: OfstedInspection | null;
|
|
|
|
|
|
|
|
census: SchoolCensus | null;
|
|
|
|
census: SchoolCensus | null;
|
|
|
|
admissions: SchoolAdmissions | null;
|
|
|
|
|
|
|
|
admissionsHistory: SchoolAdmissions[];
|
|
|
|
|
|
|
|
deprivation: SchoolDeprivation | null;
|
|
|
|
|
|
|
|
finance: SchoolFinance | null;
|
|
|
|
|
|
|
|
/** Fetched on the server so the England-comparison deltas are in the
|
|
|
|
|
|
|
|
* initial HTML; null when the endpoint is unavailable. */
|
|
|
|
|
|
|
|
nationalAvg: NationalAverages | null;
|
|
|
|
|
|
|
|
/** Section list for the sticky nav, computed on the server. */
|
|
|
|
/** Section list for the sticky nav, computed on the server. */
|
|
|
|
navItems: NavItem[];
|
|
|
|
navItems: NavItem[];
|
|
|
|
/** The server-rendered sections. */
|
|
|
|
/** The server-rendered sections. */
|
|
|
@@ -55,9 +44,7 @@ export interface SchoolDetailShellProps {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function SchoolDetailShell({
|
|
|
|
export function SchoolDetailShell({
|
|
|
|
schoolInfo, yearlyData, absenceData,
|
|
|
|
schoolInfo, yearlyData, census, navItems, children,
|
|
|
|
ofsted, census, admissions, admissionsHistory, deprivation, finance,
|
|
|
|
|
|
|
|
nationalAvg, navItems, children,
|
|
|
|
|
|
|
|
}: SchoolDetailShellProps) {
|
|
|
|
}: SchoolDetailShellProps) {
|
|
|
|
const router = useRouter();
|
|
|
|
const router = useRouter();
|
|
|
|
const { addSchool, removeSchool, isSelected } = useComparison();
|
|
|
|
const { addSchool, removeSchool, isSelected } = useComparison();
|
|
|
@@ -135,21 +122,14 @@ export function SchoolDetailShell({
|
|
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
|
|
}, [sectionsOpen]);
|
|
|
|
}, [sectionsOpen]);
|
|
|
|
|
|
|
|
|
|
|
|
// Derived data-shape logic lives in lib/schoolSections so the server route
|
|
|
|
// The chrome needs only these four. The section-shape flags are computed
|
|
|
|
// can compute the section list without importing this client component.
|
|
|
|
// once on the server (lib/schoolSections) and consumed by the section
|
|
|
|
const flags = computeSchoolFlags({
|
|
|
|
// composers; recomputing them here would duplicate that work for values
|
|
|
|
schoolInfo, yearlyData, absenceData, census, deprivation, finance,
|
|
|
|
// this component never renders.
|
|
|
|
});
|
|
|
|
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
|
|
|
const {
|
|
|
|
|
|
|
|
latestResults, isAllThrough, isSecondary, isPrimary,
|
|
|
|
|
|
|
|
hasGenderSplit, hasInclusionData, hasSchoolLife, hasDeprivation,
|
|
|
|
|
|
|
|
hasFinance, hasLocation, hasKS2Results, hasKS4Results, hasAnyResults,
|
|
|
|
|
|
|
|
isSpecial, ks2Placeholder, suppressKs2Comparison, suppressKs4Comparison,
|
|
|
|
|
|
|
|
} = flags;
|
|
|
|
|
|
|
|
const phase = schoolInfo.phase ?? '';
|
|
|
|
const phase = schoolInfo.phase ?? '';
|
|
|
|
|
|
|
|
const isAllThrough = phase.toLowerCase() === 'all-through';
|
|
|
|
const primaryAvg = nationalAvg?.primary ?? {};
|
|
|
|
const hasLocation = schoolInfo.latitude != null && schoolInfo.longitude != null;
|
|
|
|
const secondaryAvg = nationalAvg?.secondary ?? {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleComparisonToggle = () => {
|
|
|
|
const handleComparisonToggle = () => {
|
|
|
|
if (isInComparison) {
|
|
|
|
if (isInComparison) {
|
|
|
@@ -172,13 +152,6 @@ export function SchoolDetailShell({
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [schoolInfo.urn]);
|
|
|
|
}, [schoolInfo.urn]);
|
|
|
|
|
|
|
|
|
|
|
|
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).`;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Track active section as user scrolls
|
|
|
|
// Track active section as user scrolls
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
const ids = navItems.map(n => n.id);
|
|
|
|
const ids = navItems.map(n => n.id);
|
|
|
@@ -211,29 +184,6 @@ export function SchoolDetailShell({
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [navItems.map(n => n.id).join(',')]);
|
|
|
|
}, [navItems.map(n => n.id).join(',')]);
|
|
|
|
|
|
|
|
|
|
|
|
// A report card is identified by the presence of report-card area
|
|
|
|
|
|
|
|
// judgements, NOT by `framework` — the API sets `framework` to the raw
|
|
|
|
|
|
|
|
// event grouping (e.g. "Schools - S5") even for report-card schools, so
|
|
|
|
|
|
|
|
// the old `framework === 'ReportCard'` test never matched and report cards
|
|
|
|
|
|
|
|
// were rendered as legacy ratings dated to a pre-Nov-2025 inspection.
|
|
|
|
|
|
|
|
const isReportCard = !!(
|
|
|
|
|
|
|
|
ofsted?.report_card && Object.keys(ofsted.report_card).length > 0
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
// A report card is dated by its own inspection (rc_inspection_date); the
|
|
|
|
|
|
|
|
// legacy inspection_date belongs to an older inspection and must never
|
|
|
|
|
|
|
|
// date a report card (report cards exist only from Nov 2025).
|
|
|
|
|
|
|
|
const ofstedInspectedDate = isReportCard
|
|
|
|
|
|
|
|
? ofsted?.rc_inspection_date ?? null
|
|
|
|
|
|
|
|
: ofsted?.inspection_date ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Ofsted: detect if all OEIF sub-grades match the overall ───────────
|
|
|
|
|
|
|
|
const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : [];
|
|
|
|
|
|
|
|
const oeifAllSameGrade =
|
|
|
|
|
|
|
|
!!ofsted &&
|
|
|
|
|
|
|
|
!isReportCard &&
|
|
|
|
|
|
|
|
oeifAreas.length >= 3 &&
|
|
|
|
|
|
|
|
oeifAreas.every((a) => a.value === ofsted.overall_effectiveness);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Label shown in the mobile "section" menu button — the section in view.
|
|
|
|
// Label shown in the mobile "section" menu button — the section in view.
|
|
|
|
const activeNavLabel = (navItems.find((n) => n.id === activeSection) ?? navItems[0])?.label ?? '';
|
|
|
|
const activeNavLabel = (navItems.find((n) => n.id === activeSection) ?? navItems[0])?.label ?? '';
|
|
|
|
|
|
|
|
|
|
|
|