diff --git a/nextjs-app/components/HomeView.tsx b/nextjs-app/components/HomeView.tsx index 11a8d5d..14b983f 100644 --- a/nextjs-app/components/HomeView.tsx +++ b/nextjs-app/components/HomeView.tsx @@ -5,7 +5,7 @@ 'use client'; -import { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useSearchParams, useRouter, usePathname } from 'next/navigation'; import { FilterBar } from './FilterBar'; import { SchoolRow } from './SchoolRow'; @@ -13,9 +13,9 @@ import { SecondarySchoolRow } from './SecondarySchoolRow'; import { SchoolMap } from './SchoolMap'; import { EmptyState } from './EmptyState'; import { useComparisonContext } from '@/context/ComparisonContext'; -import { fetchSchools, fetchLAaverages } from '@/lib/api'; +import { fetchSchools, fetchLAaverages, fetchNationalAverages } from '@/lib/api'; import type { SchoolsResponse, Filters, School } from '@/lib/types'; -import { schoolUrl } from '@/lib/utils'; +import { schoolUrl, buildOfstedListBadge } from '@/lib/utils'; import styles from './HomeView.module.css'; interface HomeViewProps { @@ -37,6 +37,7 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp const [hasMore, setHasMore] = useState(initialSchools.total_pages > 1); const [isLoadingMore, setIsLoadingMore] = useState(false); const [laAverages, setLaAverages] = useState>({}); + const [nationalAvgRwm, setNationalAvgRwm] = useState(null); const [mapSchools, setMapSchools] = useState([]); const [isLoadingMap, setIsLoadingMap] = useState(false); const prevSearchParamsRef = useRef(searchParams.toString()); @@ -90,6 +91,13 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp .catch(() => {}); }, [isSecondaryView, isMixedView]); + // Fetch national averages (supplementary — never blocks render) + useEffect(() => { + fetchNationalAverages() + .then(data => setNationalAvgRwm(data.primary?.rwm_expected_pct ?? null)) + .catch(() => {}); + }, []); + const handleLoadMore = async () => { if (isLoadingMore || !hasMore) return; setIsLoadingMore(true); @@ -252,6 +260,7 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp center={initialSchools.location_info?.coordinates} referencePoint={initialSchools.location_info?.coordinates} onMarkerClick={setSelectedMapSchool} + nationalAvgRwm={nationalAvgRwm} />
@@ -264,6 +273,7 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp school={school} onAddToCompare={addSchool} isInCompare={selectedSchools.some(s => s.urn === school.urn)} + nationalAvgRwm={nationalAvgRwm} />
))} @@ -278,6 +288,7 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp school={selectedMapSchool} onAddToCompare={addSchool} isInCompare={selectedSchools.some(s => s.urn === selectedMapSchool.urn)} + nationalAvgRwm={nationalAvgRwm} /> @@ -306,6 +317,7 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp onAddToCompare={addSchool} onRemoveFromCompare={removeSchool} isInCompare={selectedSchools.some(s => s.urn === school.urn)} + nationalAvgRwm={nationalAvgRwm} /> ) ))} @@ -339,9 +351,28 @@ interface CompactSchoolItemProps { school: School; onAddToCompare: (school: School) => void; isInCompare: boolean; + nationalAvgRwm?: number | null; } -function CompactSchoolItem({ school, onAddToCompare, isInCompare }: CompactSchoolItemProps) { +function CompactSchoolItem({ school, onAddToCompare, isInCompare, nationalAvgRwm }: CompactSchoolItemProps) { + const ofstedBadge = buildOfstedListBadge(school); + const isSecondary = school.attainment_8_score != null; + + // vs-national delta for primary schools + const rwmDelta = + !isSecondary && school.rwm_expected_pct != null && nationalAvgRwm != null + ? Math.round(school.rwm_expected_pct - nationalAvgRwm) + : null; + + const deltaStyle: React.CSSProperties = + rwmDelta == null + ? {} + : rwmDelta >= 2 + ? { fontSize: '0.7rem', color: 'var(--accent-teal, #2d7d7d)', fontWeight: 600 } + : rwmDelta <= -2 + ? { fontSize: '0.7rem', color: 'var(--accent-coral, #e07256)', fontWeight: 600 } + : { fontSize: '0.7rem', color: 'var(--text-muted, #8a847a)' }; + return (
@@ -355,24 +386,47 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare }: CompactSchoo )}
-
- {school.school_type && {school.school_type}} - {school.local_authority && {school.local_authority}} + {/* Ofsted badge */} +
+ + {ofstedBadge.label} +
+ {/* Headline metric + delta */}
- {school.attainment_8_score != null - ? school.attainment_8_score.toFixed(1) - : school.rwm_expected_pct !== null - ? `${school.rwm_expected_pct}%` - : '-'} - {' '} - {school.attainment_8_score != null ? 'Att 8' : 'RWM'} - - - {school.total_pupils || '-'} pupils + {isSecondary + ? (school.attainment_8_score != null ? school.attainment_8_score.toFixed(1) : '-') + : (school.rwm_expected_pct != null ? `${school.rwm_expected_pct}%` : '-')} + + {' '} + {isSecondary ? 'Att 8' : 'RWM'} + {rwmDelta != null && ( + + {rwmDelta >= 2 + ? `+${rwmDelta} pts vs national` + : rwmDelta <= -2 + ? `${rwmDelta} pts vs national` + : '≈ national avg'} + + )}
diff --git a/nextjs-app/components/SchoolMap.tsx b/nextjs-app/components/SchoolMap.tsx index e81993c..72774e2 100644 --- a/nextjs-app/components/SchoolMap.tsx +++ b/nextjs-app/components/SchoolMap.tsx @@ -27,9 +27,10 @@ interface SchoolMapProps { zoom?: number; referencePoint?: [number, number]; onMarkerClick?: (school: School) => void; + nationalAvgRwm?: number | null; } -export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarkerClick }: SchoolMapProps) { +export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarkerClick, nationalAvgRwm }: SchoolMapProps) { const wrapperRef = useRef(null); const [isFullscreen, setIsFullscreen] = useState(false); diff --git a/nextjs-app/components/SchoolRow.module.css b/nextjs-app/components/SchoolRow.module.css index 0aea318..b087c9e 100644 --- a/nextjs-app/components/SchoolRow.module.css +++ b/nextjs-app/components/SchoolRow.module.css @@ -207,7 +207,7 @@ /* ── vs-national delta line (under RWM metric) ──────────────────────────── */ .vsNational { font-size: 0.7rem; color: var(--accent-teal, #2d7d7d); font-weight: 600; } .vsNationalNeg { font-size: 0.7rem; color: var(--accent-coral, #e07256); font-weight: 600; } -.vsNationalFlat{ font-size: 0.7rem; color: var(--text-muted, #8a847a); } +.vsNationalFlat { font-size: 0.7rem; color: var(--text-muted, #8a847a); } /* ── Mobile ──────────────────────────────────────────── */ @media (max-width: 640px) {