feat(ui): add phase indicators to school list rows
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 12s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 49s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 11s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 12s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 49s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 11s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,23 @@
|
|||||||
|
|
||||||
--transition: 0.2s ease;
|
--transition: 0.2s ease;
|
||||||
--transition-slow: 0.4s 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -12,10 +12,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.row:hover {
|
.row:hover {
|
||||||
border-left-color: var(--accent-coral, #e07256);
|
|
||||||
box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06);
|
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 {
|
.rowInCompare {
|
||||||
border-left-color: var(--accent-teal, #2d7d7d);
|
border-left-color: var(--accent-teal, #2d7d7d);
|
||||||
background: var(--bg-secondary, #f3ede4);
|
background: var(--bg-secondary, #f3ede4);
|
||||||
@@ -59,6 +63,21 @@
|
|||||||
color: var(--accent-coral, #e07256);
|
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 */
|
/* Line 2: context tags */
|
||||||
.line2 {
|
.line2 {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { School } from '@/lib/types';
|
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 { progressBand } from '@/lib/metrics';
|
||||||
import styles from './SchoolRow.module.css';
|
import styles from './SchoolRow.module.css';
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ export function SchoolRow({
|
|||||||
onRemoveFromCompare,
|
onRemoveFromCompare,
|
||||||
}: SchoolRowProps) {
|
}: SchoolRowProps) {
|
||||||
const trend = calculateTrend(school.rwm_expected_pct, school.prev_rwm_expected_pct);
|
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
|
// Use reading progress as representative; fall back to writing, then maths
|
||||||
const progressScore =
|
const progressScore =
|
||||||
@@ -55,7 +56,7 @@ export function SchoolRow({
|
|||||||
school.religious_denomination !== 'Does not apply';
|
school.religious_denomination !== 'Does not apply';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.row} ${isInCompare ? styles.rowInCompare : ''}`}>
|
<div className={`${styles.row} ${phase.key ? styles[`phase${phase.key}`] : ''} ${isInCompare ? styles.rowInCompare : ''}`}>
|
||||||
{/* Left: four content lines */}
|
{/* Left: four content lines */}
|
||||||
<div className={styles.rowContent}>
|
<div className={styles.rowContent}>
|
||||||
|
|
||||||
@@ -78,6 +79,11 @@ export function SchoolRow({
|
|||||||
|
|
||||||
{/* Line 2: Context tags */}
|
{/* Line 2: Context tags */}
|
||||||
<div className={styles.line2}>
|
<div className={styles.line2}>
|
||||||
|
{phase.label && (
|
||||||
|
<span className={`${styles.phaseLabel} ${styles[`phaseLabel${phase.key}`]}`}>
|
||||||
|
{phase.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{school.school_type && <span>{school.school_type}</span>}
|
{school.school_type && <span>{school.school_type}</span>}
|
||||||
{school.age_range && <span>{school.age_range}</span>}
|
{school.age_range && <span>{school.age_range}</span>}
|
||||||
{showDenomination && <span>{school.religious_denomination}</span>}
|
{showDenomination && <span>{school.religious_denomination}</span>}
|
||||||
|
|||||||
@@ -12,10 +12,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.row:hover {
|
.row:hover {
|
||||||
border-left-color: var(--accent-coral, #e07256);
|
|
||||||
box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06);
|
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 {
|
.rowInCompare {
|
||||||
border-left-color: var(--accent-teal, #2d7d7d);
|
border-left-color: var(--accent-teal, #2d7d7d);
|
||||||
background: var(--bg-secondary, #f3ede4);
|
background: var(--bg-secondary, #f3ede4);
|
||||||
@@ -144,6 +148,21 @@
|
|||||||
border-radius: 3px;
|
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 {
|
.provisionTag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.0625rem 0.375rem;
|
padding: 0.0625rem 0.375rem;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { School } from '@/lib/types';
|
import type { School } from '@/lib/types';
|
||||||
import { schoolUrl } from '@/lib/utils';
|
import { getPhaseStyle, schoolUrl } from '@/lib/utils';
|
||||||
import styles from './SecondarySchoolRow.module.css';
|
import styles from './SecondarySchoolRow.module.css';
|
||||||
|
|
||||||
const OFSTED_LABELS: Record<number, string> = {
|
const OFSTED_LABELS: Record<number, string> = {
|
||||||
@@ -58,6 +58,7 @@ export function SecondarySchoolRow({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const phase = getPhaseStyle(school.phase);
|
||||||
const att8 = school.attainment_8_score ?? null;
|
const att8 = school.attainment_8_score ?? null;
|
||||||
const laDelta =
|
const laDelta =
|
||||||
att8 != null && laAvgAttainment8 != null ? att8 - laAvgAttainment8 : null;
|
att8 != null && laAvgAttainment8 != null ? att8 - laAvgAttainment8 : null;
|
||||||
@@ -67,7 +68,7 @@ export function SecondarySchoolRow({
|
|||||||
const showGender = school.gender && school.gender.toLowerCase() !== 'mixed';
|
const showGender = school.gender && school.gender.toLowerCase() !== 'mixed';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.row} ${isInCompare ? styles.rowInCompare : ''}`}>
|
<div className={`${styles.row} ${phase.key ? styles[`phase${phase.key}`] : ''} ${isInCompare ? styles.rowInCompare : ''}`}>
|
||||||
{/* Left: four content lines */}
|
{/* Left: four content lines */}
|
||||||
<div className={styles.rowContent}>
|
<div className={styles.rowContent}>
|
||||||
|
|
||||||
@@ -90,6 +91,11 @@ export function SecondarySchoolRow({
|
|||||||
|
|
||||||
{/* Line 2: Context tags */}
|
{/* Line 2: Context tags */}
|
||||||
<div className={styles.line2}>
|
<div className={styles.line2}>
|
||||||
|
{phase.label && (
|
||||||
|
<span className={`${styles.phaseLabel} ${styles[`phaseLabel${phase.key}`]}`}>
|
||||||
|
{phase.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{school.school_type && <span>{school.school_type}</span>}
|
{school.school_type && <span>{school.school_type}</span>}
|
||||||
{school.age_range && <span>{school.age_range}</span>}
|
{school.age_range && <span>{school.age_range}</span>}
|
||||||
{showGender && (
|
{showGender && (
|
||||||
|
|||||||
@@ -364,6 +364,25 @@ export function parseQueryString(search: string): Record<string, string> {
|
|||||||
* Handles both 4-digit start years (2023 → "2023/24") and
|
* Handles both 4-digit start years (2023 → "2023/24") and
|
||||||
* 6-digit EES codes (202526 → "2025/26").
|
* 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 {
|
export function formatAcademicYear(year: number): string {
|
||||||
const s = year.toString();
|
const s = year.toString();
|
||||||
if (s.length === 6) {
|
if (s.length === 6) {
|
||||||
|
|||||||
Reference in New Issue
Block a user