From 4db36b9099e71f5246cb2af9e0269858bdf6c6b5 Mon Sep 17 00:00:00 2001 From: Tudor Sitaru Date: Wed, 1 Apr 2026 15:47:51 +0100 Subject: [PATCH] feat(ui): add phase indicators to school list rows Add coloured left-border and phase label pill to visually differentiate school phases (Primary, Secondary, All-through, Post-16, Nursery) in search result lists. Colours are accessible (WCAG AA) and don't clash with existing Ofsted/trend semantic colours. Co-Authored-By: Claude Opus 4.6 --- nextjs-app/app/globals.css | 17 +++++++++++++++ nextjs-app/components/SchoolRow.module.css | 21 ++++++++++++++++++- nextjs-app/components/SchoolRow.tsx | 10 +++++++-- .../components/SecondarySchoolRow.module.css | 21 ++++++++++++++++++- nextjs-app/components/SecondarySchoolRow.tsx | 10 +++++++-- nextjs-app/lib/utils.ts | 19 +++++++++++++++++ 6 files changed, 92 insertions(+), 6 deletions(-) diff --git a/nextjs-app/app/globals.css b/nextjs-app/app/globals.css index 19cca8a..ae80291 100644 --- a/nextjs-app/app/globals.css +++ b/nextjs-app/app/globals.css @@ -58,6 +58,23 @@ --transition: 0.2s ease; --transition-slow: 0.4s ease; + + /* Phase indicators */ + --phase-primary: #5b8cbf; + --phase-primary-bg: rgba(91, 140, 191, 0.10); + --phase-primary-text: #3d6a99; + --phase-secondary: #9b6bb0; + --phase-secondary-bg: rgba(155, 107, 176, 0.10); + --phase-secondary-text: #7a4f93; + --phase-all-through: #7a9a6d; + --phase-all-through-bg: rgba(122, 154, 109, 0.10); + --phase-all-through-text: #5a7a4d; + --phase-post16: #c4915e; + --phase-post16-bg: rgba(196, 145, 94, 0.10); + --phase-post16-text: #9a6d3a; + --phase-nursery: #e0a0b0; + --phase-nursery-bg: rgba(224, 160, 176, 0.10); + --phase-nursery-text: #b06070; } * { diff --git a/nextjs-app/components/SchoolRow.module.css b/nextjs-app/components/SchoolRow.module.css index 0b0e7ba..939b2a5 100644 --- a/nextjs-app/components/SchoolRow.module.css +++ b/nextjs-app/components/SchoolRow.module.css @@ -12,10 +12,14 @@ } .row:hover { - border-left-color: var(--accent-coral, #e07256); box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06); } +/* Phase border colours */ +.phasePrimary { border-left-color: var(--phase-primary, #5b8cbf); } +.phaseAllThrough { border-left-color: var(--phase-all-through, #7a9a6d); } +.phaseNursery { border-left-color: var(--phase-nursery, #e0a0b0); } + .rowInCompare { border-left-color: var(--accent-teal, #2d7d7d); background: var(--bg-secondary, #f3ede4); @@ -59,6 +63,21 @@ color: var(--accent-coral, #e07256); } +/* Phase label pill */ +.phaseLabel { + display: inline-block; + padding: 0.0625rem 0.375rem; + font-size: 0.6875rem; + font-weight: 600; + border-radius: 3px; + white-space: nowrap; + margin-right: 0.25rem; +} + +.phaseLabelPrimary { background: var(--phase-primary-bg); color: var(--phase-primary-text); } +.phaseLabelAllThrough { background: var(--phase-all-through-bg); color: var(--phase-all-through-text); } +.phaseLabelNursery { background: var(--phase-nursery-bg); color: var(--phase-nursery-text); } + /* Line 2: context tags */ .line2 { display: flex; diff --git a/nextjs-app/components/SchoolRow.tsx b/nextjs-app/components/SchoolRow.tsx index 2e555e0..6ff255b 100644 --- a/nextjs-app/components/SchoolRow.tsx +++ b/nextjs-app/components/SchoolRow.tsx @@ -9,7 +9,7 @@ */ import type { School } from '@/lib/types'; -import { formatPercentage, formatProgress, calculateTrend, schoolUrl } from '@/lib/utils'; +import { formatPercentage, formatProgress, calculateTrend, getPhaseStyle, schoolUrl } from '@/lib/utils'; import { progressBand } from '@/lib/metrics'; import styles from './SchoolRow.module.css'; @@ -36,6 +36,7 @@ export function SchoolRow({ onRemoveFromCompare, }: SchoolRowProps) { const trend = calculateTrend(school.rwm_expected_pct, school.prev_rwm_expected_pct); + const phase = getPhaseStyle(school.phase); // Use reading progress as representative; fall back to writing, then maths const progressScore = @@ -55,7 +56,7 @@ export function SchoolRow({ school.religious_denomination !== 'Does not apply'; return ( -
+
{/* Left: four content lines */}
@@ -78,6 +79,11 @@ export function SchoolRow({ {/* Line 2: Context tags */}
+ {phase.label && ( + + {phase.label} + + )} {school.school_type && {school.school_type}} {school.age_range && {school.age_range}} {showDenomination && {school.religious_denomination}} diff --git a/nextjs-app/components/SecondarySchoolRow.module.css b/nextjs-app/components/SecondarySchoolRow.module.css index f31ff4a..64a234a 100644 --- a/nextjs-app/components/SecondarySchoolRow.module.css +++ b/nextjs-app/components/SecondarySchoolRow.module.css @@ -12,10 +12,14 @@ } .row:hover { - border-left-color: var(--accent-coral, #e07256); box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06); } +/* Phase border colours */ +.phaseSecondary { border-left-color: var(--phase-secondary, #9b6bb0); } +.phaseAllThrough { border-left-color: var(--phase-all-through, #7a9a6d); } +.phasePost16 { border-left-color: var(--phase-post16, #c4915e); } + .rowInCompare { border-left-color: var(--accent-teal, #2d7d7d); background: var(--bg-secondary, #f3ede4); @@ -144,6 +148,21 @@ border-radius: 3px; } +/* Phase label pill */ +.phaseLabel { + display: inline-block; + padding: 0.0625rem 0.375rem; + font-size: 0.6875rem; + font-weight: 600; + border-radius: 3px; + white-space: nowrap; + margin-right: 0.25rem; +} + +.phaseLabelSecondary { background: var(--phase-secondary-bg); color: var(--phase-secondary-text); } +.phaseLabelAllThrough { background: var(--phase-all-through-bg); color: var(--phase-all-through-text); } +.phaseLabelPost16 { background: var(--phase-post16-bg); color: var(--phase-post16-text); } + .provisionTag { display: inline-block; padding: 0.0625rem 0.375rem; diff --git a/nextjs-app/components/SecondarySchoolRow.tsx b/nextjs-app/components/SecondarySchoolRow.tsx index ccabda3..9debdc6 100644 --- a/nextjs-app/components/SecondarySchoolRow.tsx +++ b/nextjs-app/components/SecondarySchoolRow.tsx @@ -11,7 +11,7 @@ 'use client'; import type { School } from '@/lib/types'; -import { schoolUrl } from '@/lib/utils'; +import { getPhaseStyle, schoolUrl } from '@/lib/utils'; import styles from './SecondarySchoolRow.module.css'; const OFSTED_LABELS: Record = { @@ -58,6 +58,7 @@ export function SecondarySchoolRow({ } }; + const phase = getPhaseStyle(school.phase); const att8 = school.attainment_8_score ?? null; const laDelta = att8 != null && laAvgAttainment8 != null ? att8 - laAvgAttainment8 : null; @@ -67,7 +68,7 @@ export function SecondarySchoolRow({ const showGender = school.gender && school.gender.toLowerCase() !== 'mixed'; return ( -
+
{/* Left: four content lines */}
@@ -90,6 +91,11 @@ export function SecondarySchoolRow({ {/* Line 2: Context tags */}
+ {phase.label && ( + + {phase.label} + + )} {school.school_type && {school.school_type}} {school.age_range && {school.age_range}} {showGender && ( diff --git a/nextjs-app/lib/utils.ts b/nextjs-app/lib/utils.ts index 25653b6..a0d1823 100644 --- a/nextjs-app/lib/utils.ts +++ b/nextjs-app/lib/utils.ts @@ -364,6 +364,25 @@ export function parseQueryString(search: string): Record { * Handles both 4-digit start years (2023 → "2023/24") and * 6-digit EES codes (202526 → "2025/26"). */ +export function getPhaseStyle(phase?: string | null): { key: string; label: string } { + switch (phase?.toLowerCase()) { + case 'primary': + case 'middle deemed primary': + return { key: 'Primary', label: 'Primary' }; + case 'secondary': + case 'middle deemed secondary': + return { key: 'Secondary', label: 'Secondary' }; + case 'all-through': + return { key: 'AllThrough', label: 'All-through' }; + case '16 plus': + return { key: 'Post16', label: 'Post-16' }; + case 'nursery': + return { key: 'Nursery', label: 'Nursery' }; + default: + return { key: '', label: '' }; + } +} + export function formatAcademicYear(year: number): string { const s = year.toString(); if (s.length === 6) {