From ce470ca342f75824334123692a1015da7aa50de5 Mon Sep 17 00:00:00 2001 From: Tudor Date: Wed, 25 Mar 2026 10:34:19 +0000 Subject: [PATCH] fix(ui): remove duplicate data, merge sections, add sticky nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX audit round 2: - Remove Summary Strip (duplicated Ofsted grade + parent happy/safe/recommend) - Fold "% would recommend" into Ofsted section header - Merge SATs Results + Subject Breakdown into one section - Merge Results Over Time chart + Year-by-Year table into one section - Add sticky section nav with dynamic pills based on available data - Unify colour system: replace ad-hoc pill colours with semantic status classes - Guard Pupils & Inclusion so it only renders with actual data - Add year to Admissions section title - Fix progress score 0.0 colour (was neutral gap at ±0.1, now at 0) - Remove unused .metricTrend CSS class Page reduced from 16 to 13 sections. Co-Authored-By: Claude Opus 4.6 --- .../components/SchoolDetailView.module.css | 118 +++++---- nextjs-app/components/SchoolDetailView.tsx | 247 +++++++++--------- 2 files changed, 191 insertions(+), 174 deletions(-) diff --git a/nextjs-app/components/SchoolDetailView.module.css b/nextjs-app/components/SchoolDetailView.module.css index 14c434b..7191417 100644 --- a/nextjs-app/components/SchoolDetailView.module.css +++ b/nextjs-app/components/SchoolDetailView.module.css @@ -8,7 +8,7 @@ border: 1px solid var(--border-color, #e5dfd5); border-radius: 10px; padding: 1.25rem 1.5rem; - margin-bottom: 1rem; + margin-bottom: 0; box-shadow: var(--shadow-soft); } @@ -117,41 +117,51 @@ opacity: 0.9; } -/* Quick Summary Strip */ -.summaryStrip { - display: flex; - gap: 0.625rem; - flex-wrap: wrap; - margin: 0 0 1.25rem; +/* ── Sticky Section Navigation ──────────────────────── */ +.sectionNav { + position: sticky; + top: 0; + z-index: 10; + background: var(--bg-card, white); + border: 1px solid var(--border-color, #e5dfd5); + border-top: none; + border-radius: 0 0 10px 10px; + padding: 0.5rem 1rem; + margin-bottom: 1rem; + overflow-x: auto; + white-space: nowrap; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); } -.summaryPill { - padding: 0.35rem 0.875rem; - border-radius: 999px; - font-size: 0.8125rem; - font-weight: 600; +.sectionNav::-webkit-scrollbar { + display: none; +} + +.sectionNavInner { display: inline-flex; - align-items: center; - gap: 0.3rem; + gap: 0.375rem; } -.summaryPillGood { - background: #d1fae5; - color: #065f46; +.sectionNavLink { + display: inline-block; + padding: 0.3rem 0.625rem; + font-size: 0.75rem; + font-weight: 500; + color: var(--text-secondary, #5c564d); + text-decoration: none; + border-radius: 4px; + transition: all 0.15s ease; + white-space: nowrap; } -.summaryPillWarn { - background: #fef3c7; - color: #92400e; +.sectionNavLink:hover { + background: var(--bg-secondary, #f3ede4); + color: var(--text-primary, #1a1612); } -.summaryPillBad { - background: #fee2e2; - color: #991b1b; -} - -/* Unified card — replaces summary / chartsSection / detailedMetrics / - absenceSection / historySection / supplementarySection / mapSection */ +/* Unified card for all content sections */ .card { background: var(--bg-card, white); border: 1px solid var(--border-color, #e5dfd5); @@ -159,6 +169,7 @@ padding: 1.25rem 1.5rem; margin-bottom: 1rem; box-shadow: var(--shadow-soft); + scroll-margin-top: 3.5rem; } /* Section Title */ @@ -192,7 +203,7 @@ margin: -0.5rem 0 1rem; } -/* Response count badge (used in "What Parents Say") */ +/* Response count badge */ .responseBadge { font-size: 0.75rem; font-weight: 500; @@ -211,6 +222,18 @@ margin: 1.25rem 0 0.75rem; } +/* Parent recommendation line in Ofsted section */ +.parentRecommendLine { + font-size: 0.85rem; + color: var(--text-secondary, #5c564d); + margin: 0.5rem 0 0; +} + +.parentRecommendLine strong { + color: var(--accent-teal, #2d7d7d); + font-weight: 700; +} + /* Metrics Grid & Cards */ .metricsGrid { display: grid; @@ -252,11 +275,6 @@ font-style: italic; } -.metricTrend { - font-size: 1rem; - color: var(--accent-teal, #2d7d7d); -} - /* Progress score colour coding */ .progressPositive { color: var(--accent-teal, #2d7d7d); @@ -268,6 +286,22 @@ font-weight: 700; } +/* ── Semantic status colours (unified) ────────────── */ +.statusGood { + background: rgba(45, 125, 125, 0.10); + color: var(--accent-teal, #2d7d7d); +} + +.statusWarn { + background: rgba(201, 162, 39, 0.12); + color: #b8920e; +} + +.statusBad { + background: rgba(224, 114, 86, 0.12); + color: var(--accent-coral, #e07256); +} + /* Charts Section */ .chartContainer { width: 100%; @@ -339,6 +373,12 @@ margin-top: 0.5rem; } +.historicalSubtitle { + font-size: 0.8rem; + color: var(--text-muted, #8a847a); + margin: 1.25rem 0 0.25rem; +} + .dataTable { width: 100%; border-collapse: collapse; @@ -468,7 +508,7 @@ color: var(--text-primary, #1a1612); } -/* Admissions badges */ +/* Admissions badge — uses unified status colours */ .admissionsBadge { display: inline-flex; align-items: center; @@ -480,16 +520,6 @@ margin-top: 0.75rem; } -.admissionsBadgeWarn { - background: rgba(201, 162, 39, 0.15); - color: #b8920e; -} - -.admissionsBadgeGood { - background: rgba(60, 140, 60, 0.12); - color: #3c8c3c; -} - /* Deprivation dot scale */ .deprivationDots { display: flex; diff --git a/nextjs-app/components/SchoolDetailView.tsx b/nextjs-app/components/SchoolDetailView.tsx index d9afa6e..d357b33 100644 --- a/nextjs-app/components/SchoolDetailView.tsx +++ b/nextjs-app/components/SchoolDetailView.tsx @@ -36,17 +36,10 @@ const NATIONAL_AVG = { per_pupil_spend: 6000, }; -function pillClass(pct: number | null | undefined): string { - if (pct == null) return styles.summaryPillWarn; - if (pct >= 80) return styles.summaryPillGood; - if (pct >= 60) return styles.summaryPillWarn; - return styles.summaryPillBad; -} - function progressClass(val: number | null | undefined): string { if (val == null) return ''; - if (val > 0.1) return styles.progressPositive; - if (val < -0.1) return styles.progressNegative; + if (val > 0) return styles.progressPositive; + if (val < 0) return styles.progressNegative; return ''; } @@ -82,13 +75,39 @@ export function SchoolDetailView({ } }; - // Deprivation plain-English description 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).`; }; + // Guard for Pupils & Inclusion — only show if at least one metric is available + const hasInclusionData = (latestResults?.disadvantaged_pct != null) + || (latestResults?.eal_pct != null) + || (latestResults?.sen_support_pct != null) + || senDetail != null; + + const hasSchoolLife = absenceData != null || census?.class_size_avg != null; + const hasPhonics = phonics != null && phonics.year1_phonics_pct != null; + const hasDeprivation = deprivation != null && deprivation.idaci_decile != null; + const hasFinance = finance != null && finance.per_pupil_spend != null; + const hasLocation = schoolInfo.latitude != null && schoolInfo.longitude != null; + + // Build section nav items dynamically — only sections with data + const navItems: { id: string; label: string }[] = []; + if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' }); + if (parentView && parentView.total_responses != null && parentView.total_responses > 0) + navItems.push({ id: 'parents', label: 'Parents' }); + if (latestResults) navItems.push({ id: 'sats', label: 'SATs' }); + if (hasPhonics) navItems.push({ id: 'phonics', label: 'Phonics' }); + if (hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' }); + if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' }); + if (hasInclusionData) navItems.push({ id: 'inclusion', label: 'Pupils' }); + if (hasLocation) navItems.push({ id: 'location', label: 'Location' }); + if (hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' }); + if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' }); + if (yearlyData.length > 0) navItems.push({ id: 'history', label: 'History' }); + return (
{/* Back Navigation */} @@ -154,35 +173,20 @@ export function SchoolDetailView({
- {/* Quick Summary Strip */} - {(ofsted || parentView) && ( -
- {ofsted?.overall_effectiveness != null && ( - - Ofsted: {OFSTED_LABELS[ofsted.overall_effectiveness]} - - )} - {parentView?.q_happy_pct != null && ( - - {Math.round(parentView.q_happy_pct)}% say child is happy - - )} - {parentView?.q_safe_pct != null && ( - - {Math.round(parentView.q_safe_pct)}% say child is safe - - )} - {parentView?.q_recommend_pct != null && ( - - {Math.round(parentView.q_recommend_pct)}% would recommend - - )} -
+ {/* Sticky Section Navigation */} + {navItems.length > 0 && ( + )} {/* Ofsted Rating */} {ofsted && ( -
+

Ofsted Rating {ofsted.inspection_date && ( @@ -210,6 +214,11 @@ export function SchoolDetailView({ )} + {parentView?.q_recommend_pct != null && parentView.total_responses != null && parentView.total_responses > 0 && ( +

+ {Math.round(parentView.q_recommend_pct)}% of parents would recommend this school ({parentView.total_responses.toLocaleString()} responses) +

+ )}
{[ { label: 'Quality of Teaching', value: ofsted.quality_of_education }, @@ -233,7 +242,7 @@ export function SchoolDetailView({ {/* What Parents Say */} {parentView && parentView.total_responses != null && parentView.total_responses > 0 && ( -
+

What Parents Say @@ -267,13 +276,15 @@ export function SchoolDetailView({

)} - {/* SATs Results */} + {/* SATs Results (merged with Subject Breakdown) */} {latestResults && ( -
+

SATs Results ({latestResults.year})

End-of-primary-school tests taken by Year 6 pupils. National averages shown for comparison.

+ + {/* Headline numbers: RWM combined */}
{latestResults.rwm_expected_pct !== null && (
@@ -289,44 +300,10 @@ export function SchoolDetailView({
National avg: {NATIONAL_AVG.rwm_high}%
)} - {latestResults.reading_progress !== null && ( -
-
Reading Progress
-
- {formatProgress(latestResults.reading_progress)} -
-
- )} - {latestResults.writing_progress !== null && ( -
-
Writing Progress
-
- {formatProgress(latestResults.writing_progress)} -
-
- )} - {latestResults.maths_progress !== null && ( -
-
Maths Progress
-
- {formatProgress(latestResults.maths_progress)} -
-
- )}
- {(latestResults.reading_progress !== null || latestResults.writing_progress !== null || latestResults.maths_progress !== null) && ( -

- Progress scores measure how much pupils improved compared to similar schools nationally. Above 0 = better than average, below 0 = below average. -

- )} -
- )} - {/* Detailed Subject Breakdown */} - {latestResults && ( -
-

Subject Breakdown ({latestResults.year})

-
+ {/* Per-subject detail table */} +

Reading

@@ -345,7 +322,9 @@ export function SchoolDetailView({ {latestResults.reading_progress !== null && (
Progress score - {formatProgress(latestResults.reading_progress)} + + {formatProgress(latestResults.reading_progress)} +
)} {latestResults.reading_avg_score !== null && ( @@ -375,7 +354,9 @@ export function SchoolDetailView({ {latestResults.writing_progress !== null && (
Progress score - {formatProgress(latestResults.writing_progress)} + + {formatProgress(latestResults.writing_progress)} +
)}
@@ -399,7 +380,9 @@ export function SchoolDetailView({ {latestResults.maths_progress !== null && (
Progress score - {formatProgress(latestResults.maths_progress)} + + {formatProgress(latestResults.maths_progress)} +
)} {latestResults.maths_avg_score !== null && ( @@ -411,12 +394,18 @@ export function SchoolDetailView({
+ + {(latestResults.reading_progress !== null || latestResults.writing_progress !== null || latestResults.maths_progress !== null) && ( +

+ Progress scores measure how much pupils improved compared to similar schools nationally. Above 0 = better than average, below 0 = below average. +

+ )}
)} {/* Year 1 Phonics */} - {phonics && phonics.year1_phonics_pct != null && ( -
+ {hasPhonics && phonics && ( +

Year 1 Phonics ({phonics.year})

Phonics is a key early reading skill. Children are tested at the end of Year 1. @@ -438,8 +427,8 @@ export function SchoolDetailView({ )} {/* School Life */} - {(absenceData || census?.class_size_avg != null) && ( -

+ {hasSchoolLife && ( +

School Life

{census?.class_size_avg != null && ( @@ -469,8 +458,8 @@ export function SchoolDetailView({ {/* How Hard to Get In */} {admissions && ( -
-

How Hard to Get Into This School

+
+

How Hard to Get Into This School ({admissions.year})

{admissions.published_admission_number != null && (
@@ -492,7 +481,7 @@ export function SchoolDetailView({ )}
{admissions.oversubscribed != null && ( -
+
{admissions.oversubscribed ? '⚠ More applications than places last year' : '✓ Places were available last year'} @@ -502,8 +491,8 @@ export function SchoolDetailView({ )} {/* Pupils & Inclusion */} - {(latestResults || senDetail) && ( -
+ {hasInclusionData && ( +

Pupils & Inclusion

{latestResults?.disadvantaged_pct != null && ( @@ -553,13 +542,13 @@ export function SchoolDetailView({ )} {/* Location */} - {schoolInfo.latitude && schoolInfo.longitude && ( -
+ {hasLocation && ( +

Location

@@ -567,8 +556,8 @@ export function SchoolDetailView({ )} {/* Local Area Context */} - {deprivation && deprivation.idaci_decile != null && ( -
+ {hasDeprivation && deprivation && ( +

Local Area Context

{Array.from({ length: 10 }, (_, i) => ( @@ -583,13 +572,13 @@ export function SchoolDetailView({ Most deprived Least deprived
-

{deprivationDesc(deprivation.idaci_decile)}

+

{deprivationDesc(deprivation.idaci_decile!)}

)} {/* Finances */} - {finance && finance.per_pupil_spend != null && ( -
+ {hasFinance && finance && ( +

School Finances ({finance.year})

Per-pupil spending shows how much the school has to spend on each child's education. @@ -597,7 +586,7 @@ export function SchoolDetailView({

Total spend per pupil per year
-
£{Math.round(finance.per_pupil_spend).toLocaleString()}
+
£{Math.round(finance.per_pupil_spend!).toLocaleString()}
National avg: ~£{NATIONAL_AVG.per_pupil_spend.toLocaleString()}
{finance.teacher_cost_pct != null && ( @@ -616,9 +605,9 @@ export function SchoolDetailView({
)} - {/* Results Over Time */} + {/* Results Over Time (merged: chart + historical table) */} {yearlyData.length > 0 && ( -
+

Results Over Time

-
- )} - - {/* Historical Data Table */} - {yearlyData.length > 1 && ( -
-

Year-by-Year Summary

-
- - - - - - - - - - - - - {yearlyData.map((result) => ( - - - - - - - - - ))} - -
YearReading, Writing & Maths (expected %)Exceeding expected (%)Reading ProgressWriting ProgressMaths Progress
{result.year}{result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'}{result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'}{result.reading_progress !== null ? formatProgress(result.reading_progress) : '-'}{result.writing_progress !== null ? formatProgress(result.writing_progress) : '-'}{result.maths_progress !== null ? formatProgress(result.maths_progress) : '-'}
-
+ {yearlyData.length > 1 && ( + <> +

Detailed year-by-year figures

+
+ + + + + + + + + + + + + {yearlyData.map((result) => ( + + + + + + + + + ))} + +
YearReading, Writing & Maths (expected %)Exceeding expected (%)Reading ProgressWriting ProgressMaths Progress
{result.year}{result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'}{result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'}{result.reading_progress !== null ? formatProgress(result.reading_progress) : '-'}{result.writing_progress !== null ? formatProgress(result.writing_progress) : '-'}{result.maths_progress !== null ? formatProgress(result.maths_progress) : '-'}
+
+ + )}
)}