fix(compare): mobile measure-first cards to match the mockup
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 41s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m57s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 41s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m57s
The grid sections (At a glance, Ofsted, Getting a place, Who goes there) collapsed generically on mobile — grey label pills, full names wrapping to 3 lines, no dots — making the page ~2x the mockup's height and 'significantly different' from the mobile design. Each measure is now wrapped in a <Measure> that is display:contents on desktop (so the label + cells still flow into the shared aligned grid, unchanged) and a white card on mobile with compact [dot][short name] [value] rows — matching the mobile mockup. The sticky school bar becomes scrollable short-name pills on mobile too. Adds a shortName() util. Desktop layout is unchanged (display:contents dissolves the wrapper). Validated the card mechanism and real content shapes (report-card cell, badges, %+chip rows) via static previews at both widths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
@@ -95,4 +95,12 @@ describe('CompareOfsted', () => {
|
|||||||
expect(links).toHaveLength(3);
|
expect(links).toHaveLength(3);
|
||||||
expect(links[0]).toHaveAttribute('href', 'https://reports.ofsted.gov.uk/provider/21/1');
|
expect(links[0]).toHaveAttribute('href', 'https://reports.ofsted.gov.uk/provider/21/1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders a per-measure mobile tag with the short school name', () => {
|
||||||
|
render(<CompareOfsted schools={schools} data={data} />);
|
||||||
|
// Each measure repeats the schools, so the short name ("Graded" from
|
||||||
|
// "Graded School") appears once per measure (4) via the cell tag.
|
||||||
|
expect(screen.getAllByText('Graded').length).toBe(4);
|
||||||
|
expect(screen.getAllByText('Card').length).toBe(4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
debounce,
|
debounce,
|
||||||
buildOfstedListBadge,
|
buildOfstedListBadge,
|
||||||
metricKind,
|
metricKind,
|
||||||
|
shortName,
|
||||||
computeYBounds,
|
computeYBounds,
|
||||||
} from '@/lib/utils';
|
} from '@/lib/utils';
|
||||||
|
|
||||||
@@ -223,3 +224,17 @@ describe('isProposedToClose', () => {
|
|||||||
expect(isProposedToClose({})).toBe(false);
|
expect(isProposedToClose({})).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('shortName', () => {
|
||||||
|
it('drops the trailing establishment-type words', () => {
|
||||||
|
expect(shortName('Barclay Primary School')).toBe('Barclay');
|
||||||
|
expect(shortName('Elmhurst Primary School')).toBe('Elmhurst');
|
||||||
|
expect(shortName("St Mary's Catholic Primary School")).toBe("St Mary's");
|
||||||
|
expect(shortName('Riverside Community Junior School')).toBe('Riverside');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a name that carries no type suffix, capping very long ones', () => {
|
||||||
|
expect(shortName('Beaver Road')).toBe('Beaver Road');
|
||||||
|
expect(shortName('A'.repeat(30), 10)).toBe('AAAAAAAAA…');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -132,6 +132,11 @@
|
|||||||
color: var(--accent-coral-dark, #b04a2e);
|
color: var(--accent-coral-dark, #b04a2e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Full name on desktop, short name on the compact mobile pills. */
|
||||||
|
.chipNameShort {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.chipMeta {
|
.chipMeta {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
@@ -163,3 +168,40 @@
|
|||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
max-width: 75ch;
|
max-width: 75ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile: the sticky school bar becomes compact, horizontally-scrollable
|
||||||
|
pills with short names (matching the mobile mockup) instead of full-width
|
||||||
|
cards whose names wrap to several lines. */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.schoolChip {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 0;
|
||||||
|
border-top-width: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chipName {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chipNameFull {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chipNameShort {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chipMeta {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chipRemove {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import type {
|
|||||||
NationalAverages,
|
NationalAverages,
|
||||||
School,
|
School,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
import { CHART_COLORS, schoolUrl } from '@/lib/utils';
|
import { CHART_COLORS, schoolUrl, shortName } from '@/lib/utils';
|
||||||
import { fetchComparison } from '@/lib/api';
|
import { fetchComparison } from '@/lib/api';
|
||||||
import { track } from '@/lib/analytics';
|
import { track } from '@/lib/analytics';
|
||||||
import styles from './ComparisonView.module.css';
|
import styles from './ComparisonView.module.css';
|
||||||
@@ -349,7 +349,8 @@ export function ComparisonView({
|
|||||||
/>
|
/>
|
||||||
<span className={styles.chipText}>
|
<span className={styles.chipText}>
|
||||||
<a className={styles.chipName} href={schoolUrl(school.urn, school.school_name)}>
|
<a className={styles.chipName} href={schoolUrl(school.urn, school.school_name)}>
|
||||||
{school.school_name}
|
<span className={styles.chipNameFull}>{school.school_name}</span>
|
||||||
|
<span className={styles.chipNameShort}>{shortName(school.school_name)}</span>
|
||||||
</a>
|
</a>
|
||||||
<span className={styles.chipMeta}>
|
<span className={styles.chipMeta}>
|
||||||
{[school.local_authority, school.school_type].filter(Boolean).join(' · ')}
|
{[school.local_authority, school.school_type].filter(Boolean).join(' · ')}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import { summariseAdmissions } from '@/lib/compareLogic';
|
import { summariseAdmissions } from '@/lib/compareLogic';
|
||||||
import type { ComparisonData, School } from '@/lib/types';
|
import type { ComparisonData, School } from '@/lib/types';
|
||||||
import { CHART_COLORS } from '@/lib/utils';
|
import { CHART_COLORS } from '@/lib/utils';
|
||||||
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||||
|
|
||||||
export function CompareAdmissions({
|
export function CompareAdmissions({
|
||||||
schools,
|
schools,
|
||||||
@@ -49,9 +49,10 @@ export function CompareAdmissions({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SectionGrid schools={schools}>
|
<SectionGrid schools={schools}>
|
||||||
<RowLabel tip="How many application forms named the school at any preference rank — not the number of families competing head-to-head for a place.">
|
<Measure
|
||||||
Interest in the school
|
tip="How many application forms named the school at any preference rank — not the number of families competing head-to-head for a place."
|
||||||
</RowLabel>
|
label="Interest in the school"
|
||||||
|
>
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const a = rows[i];
|
const a = rows[i];
|
||||||
return (
|
return (
|
||||||
@@ -68,7 +69,9 @@ export function CompareAdmissions({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<RowLabel>First-choice families offered a place</RowLabel>
|
</Measure>
|
||||||
|
|
||||||
|
<Measure label="First-choice families offered a place">
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const summary = summariseAdmissions(rows[i]);
|
const summary = summariseAdmissions(rows[i]);
|
||||||
return (
|
return (
|
||||||
@@ -95,7 +98,9 @@ export function CompareAdmissions({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<RowLabel>What this means</RowLabel>
|
</Measure>
|
||||||
|
|
||||||
|
<Measure label="What this means">
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const a = rows[i];
|
const a = rows[i];
|
||||||
const summary = summariseAdmissions(a);
|
const summary = summariseAdmissions(a);
|
||||||
@@ -118,6 +123,7 @@ export function CompareAdmissions({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
</SectionGrid>
|
</SectionGrid>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
type ReportCardSummary,
|
type ReportCardSummary,
|
||||||
} from '@/lib/compareLogic';
|
} from '@/lib/compareLogic';
|
||||||
import type { Benchmarks, ComparisonData, NationalAverages, School } from '@/lib/types';
|
import type { Benchmarks, ComparisonData, NationalAverages, School } from '@/lib/types';
|
||||||
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||||
|
|
||||||
function ReportCardChips({ summary }: { summary: ReportCardSummary }) {
|
function ReportCardChips({ summary }: { summary: ReportCardSummary }) {
|
||||||
return (
|
return (
|
||||||
@@ -69,7 +69,7 @@ export function CompareAtAGlance({
|
|||||||
return (
|
return (
|
||||||
<Section title="At a glance" how="The short version — each row below is explained in its own section further down.">
|
<Section title="At a glance" how="The short version — each row below is explained in its own section further down.">
|
||||||
<SectionGrid schools={schools}>
|
<SectionGrid schools={schools}>
|
||||||
<RowLabel>Latest Ofsted inspection</RowLabel>
|
<Measure label="Latest Ofsted inspection">
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const display = ofstedDisplay(data[String(school.urn)]?.ofsted);
|
const display = ofstedDisplay(data[String(school.urn)]?.ofsted);
|
||||||
return (
|
return (
|
||||||
@@ -87,16 +87,16 @@ export function CompareAtAGlance({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
|
|
||||||
<RowLabel
|
<Measure
|
||||||
tip={
|
tip={
|
||||||
isSecondary
|
isSecondary
|
||||||
? 'Average Attainment 8 score across GCSE subjects (latest year).'
|
? 'Average Attainment 8 score across GCSE subjects (latest year).'
|
||||||
: '% of Year 6 pupils reaching the expected standard in reading, writing and maths (latest year).'
|
: '% of Year 6 pupils reaching the expected standard in reading, writing and maths (latest year).'
|
||||||
}
|
}
|
||||||
|
label={isSecondary ? 'Attainment 8 score' : 'Children reaching the expected standard'}
|
||||||
>
|
>
|
||||||
{isSecondary ? 'Attainment 8 score' : 'Children reaching the expected standard'}
|
|
||||||
</RowLabel>
|
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const value = headlineValues[i];
|
const value = headlineValues[i];
|
||||||
return (
|
return (
|
||||||
@@ -131,8 +131,9 @@ export function CompareAtAGlance({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
|
|
||||||
<RowLabel>Getting a place</RowLabel>
|
<Measure label="Getting a place">
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const summary = summariseAdmissions(data[String(school.urn)]?.admissions);
|
const summary = summariseAdmissions(data[String(school.urn)]?.admissions);
|
||||||
return (
|
return (
|
||||||
@@ -148,8 +149,9 @@ export function CompareAtAGlance({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
|
|
||||||
<RowLabel>Size</RowLabel>
|
<Measure label="Size">
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const census = data[String(school.urn)]?.census;
|
const census = data[String(school.urn)]?.census;
|
||||||
const pupils = census?.total_pupils ?? school.total_pupils ?? null;
|
const pupils = census?.total_pupils ?? school.total_pupils ?? null;
|
||||||
@@ -174,6 +176,7 @@ export function CompareAtAGlance({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
</SectionGrid>
|
</SectionGrid>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { verdict } from '@/lib/compareLogic';
|
import { verdict } from '@/lib/compareLogic';
|
||||||
import type { Benchmarks, ComparisonData, School } from '@/lib/types';
|
import type { Benchmarks, ComparisonData, School } from '@/lib/types';
|
||||||
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||||
|
|
||||||
function pctSplit(part: number | null | undefined, total: number | null | undefined): string | null {
|
function pctSplit(part: number | null | undefined, total: number | null | undefined): string | null {
|
||||||
if (part == null || total == null || total === 0) return null;
|
if (part == null || total == null || total === 0) return null;
|
||||||
@@ -48,7 +48,7 @@ export function CompareCommunity({
|
|||||||
how="The school's community, from the latest school census. State-school averages are computed from our dataset and shown for context — there's no “right” number here."
|
how="The school's community, from the latest school census. State-school averages are computed from our dataset and shown for context — there's no “right” number here."
|
||||||
>
|
>
|
||||||
<SectionGrid schools={schools}>
|
<SectionGrid schools={schools}>
|
||||||
<RowLabel>Pupils on roll</RowLabel>
|
<Measure label="Pupils on roll">
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const info = data[String(school.urn)]?.school_info as (School & { gias_total_pupils?: number | null; capacity?: number | null }) | undefined;
|
const info = data[String(school.urn)]?.school_info as (School & { gias_total_pupils?: number | null; capacity?: number | null }) | undefined;
|
||||||
const census = data[String(school.urn)]?.census;
|
const census = data[String(school.urn)]?.census;
|
||||||
@@ -74,8 +74,9 @@ export function CompareCommunity({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
|
|
||||||
<RowLabel>Girls / boys</RowLabel>
|
<Measure label="Girls / boys">
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const census = data[String(school.urn)]?.census;
|
const census = data[String(school.urn)]?.census;
|
||||||
const girls = pctSplit(census?.female_pupils, census?.total_pupils);
|
const girls = pctSplit(census?.female_pupils, census?.total_pupils);
|
||||||
@@ -86,10 +87,12 @@ export function CompareCommunity({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
|
|
||||||
<RowLabel tip="% of pupils eligible for free school meals — a common measure of how many pupils come from lower-income families. Benchmark computed across state schools in our dataset.">
|
<Measure
|
||||||
Free school meals
|
tip="% of pupils eligible for free school meals — a common measure of how many pupils come from lower-income families. Benchmark computed across state schools in our dataset."
|
||||||
</RowLabel>
|
label="Free school meals"
|
||||||
|
>
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const fsm = data[String(school.urn)]?.census?.fsm_pct ?? null;
|
const fsm = data[String(school.urn)]?.census?.fsm_pct ?? null;
|
||||||
return (
|
return (
|
||||||
@@ -104,10 +107,12 @@ export function CompareCommunity({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
|
|
||||||
<RowLabel tip="% of pupils whose first language is known or believed to be other than English. State-school average computed from our dataset.">
|
<Measure
|
||||||
English as an additional language
|
tip="% of pupils whose first language is known or believed to be other than English. State-school average computed from our dataset."
|
||||||
</RowLabel>
|
label="English as an additional language"
|
||||||
|
>
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const eal = data[String(school.urn)]?.census?.eal_pct ?? null;
|
const eal = data[String(school.urn)]?.census?.eal_pct ?? null;
|
||||||
return (
|
return (
|
||||||
@@ -116,10 +121,12 @@ export function CompareCommunity({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
|
|
||||||
<RowLabel tip="% of pupils receiving SEN support (not including EHC plans). A high figure can mean the school hosts specialist provision — often a strength, not a warning sign. State-school average computed from our dataset.">
|
<Measure
|
||||||
Extra learning support (SEN)
|
tip="% of pupils receiving SEN support (not including EHC plans). A high figure can mean the school hosts specialist provision — often a strength, not a warning sign. State-school average computed from our dataset."
|
||||||
</RowLabel>
|
label="Extra learning support (SEN)"
|
||||||
|
>
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const rows = data[String(school.urn)]?.yearly_data ?? [];
|
const rows = data[String(school.urn)]?.yearly_data ?? [];
|
||||||
let sen: number | null = null;
|
let sen: number | null = null;
|
||||||
@@ -143,8 +150,9 @@ export function CompareCommunity({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
|
|
||||||
<RowLabel>Faith character</RowLabel>
|
<Measure label="Faith character">
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const info = data[String(school.urn)]?.school_info;
|
const info = data[String(school.urn)]?.school_info;
|
||||||
const faith = info?.religious_denomination;
|
const faith = info?.religious_denomination;
|
||||||
@@ -155,8 +163,9 @@ export function CompareCommunity({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
|
|
||||||
<RowLabel>Ages</RowLabel>
|
<Measure label="Ages">
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const info = data[String(school.urn)]?.school_info;
|
const info = data[String(school.urn)]?.school_info;
|
||||||
return (
|
return (
|
||||||
@@ -165,8 +174,9 @@ export function CompareCommunity({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
|
|
||||||
<RowLabel>Run by</RowLabel>
|
<Measure label="Run by">
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const info = data[String(school.urn)]?.school_info;
|
const info = data[String(school.urn)]?.school_info;
|
||||||
const trust = info?.trust_name;
|
const trust = info?.trust_name;
|
||||||
@@ -177,6 +187,7 @@ export function CompareCommunity({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
</SectionGrid>
|
</SectionGrid>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
type OfstedDisplay,
|
type OfstedDisplay,
|
||||||
} from '@/lib/compareLogic';
|
} from '@/lib/compareLogic';
|
||||||
import type { ComparisonData, OfstedInspection, School } from '@/lib/types';
|
import type { ComparisonData, OfstedInspection, School } from '@/lib/types';
|
||||||
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||||
|
|
||||||
const GRADE_TONE: Record<number, 'good' | 'warn' | 'bad'> = {
|
const GRADE_TONE: Record<number, 'good' | 'warn' | 'bad'> = {
|
||||||
1: 'good',
|
1: 'good',
|
||||||
@@ -157,14 +157,15 @@ export function CompareOfsted({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SectionGrid schools={schools}>
|
<SectionGrid schools={schools}>
|
||||||
<RowLabel>Result</RowLabel>
|
<Measure label="Result">
|
||||||
{schools.map((school, i) => (
|
{schools.map((school, i) => (
|
||||||
<Cell key={school.urn} school={school} index={i}>
|
<Cell key={school.urn} school={school} index={i}>
|
||||||
<ResultCell display={displays[i]} />
|
<ResultCell display={displays[i]} />
|
||||||
</Cell>
|
</Cell>
|
||||||
))}
|
))}
|
||||||
|
</Measure>
|
||||||
|
|
||||||
<RowLabel>Inspected</RowLabel>
|
<Measure label="Inspected">
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const ofsted = data[String(school.urn)]?.ofsted;
|
const ofsted = data[String(school.urn)]?.ofsted;
|
||||||
const age = yearsSince(ofsted?.inspection_date ?? null);
|
const age = yearsSince(ofsted?.inspection_date ?? null);
|
||||||
@@ -176,9 +177,12 @@ export function CompareOfsted({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<RowLabel tip="Older-style inspections: one rating per judgement area, where published. New-style inspections: the full report card, one rating per area of school life.">
|
</Measure>
|
||||||
Judgement detail
|
|
||||||
</RowLabel>
|
<Measure
|
||||||
|
tip="Older-style inspections: one rating per judgement area, where published. New-style inspections: the full report card, one rating per area of school life."
|
||||||
|
label="Judgement detail"
|
||||||
|
>
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const ofsted = data[String(school.urn)]?.ofsted;
|
const ofsted = data[String(school.urn)]?.ofsted;
|
||||||
return (
|
return (
|
||||||
@@ -196,9 +200,12 @@ export function CompareOfsted({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<RowLabel tip="Links to the school's page on ofsted.gov.uk, where all its inspection reports are listed.">
|
</Measure>
|
||||||
Ofsted page
|
|
||||||
</RowLabel>
|
<Measure
|
||||||
|
tip="Links to the school's page on ofsted.gov.uk, where all its inspection reports are listed."
|
||||||
|
label="Ofsted page"
|
||||||
|
>
|
||||||
{schools.map((school, i) => {
|
{schools.map((school, i) => {
|
||||||
const url =
|
const url =
|
||||||
data[String(school.urn)]?.ofsted?.ofsted_page_url ??
|
data[String(school.urn)]?.ofsted?.ofsted_page_url ??
|
||||||
@@ -211,6 +218,7 @@ export function CompareOfsted({
|
|||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Measure>
|
||||||
</SectionGrid>
|
</SectionGrid>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,43 +31,71 @@
|
|||||||
margin-top: 1.25rem;
|
margin-top: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile base: each measure is a card; each cell is a school row led by a
|
||||||
|
colour dot + short name. `display: contents` at ≥761px dissolves the card
|
||||||
|
back into the shared grid. */
|
||||||
|
.measure {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
padding: 0.75rem 0.85rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.rowLabel {
|
.rowLabel {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-secondary);
|
color: var(--text-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
background: var(--bg-secondary);
|
padding: 0 0 0.1rem;
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.4rem 0.6rem;
|
|
||||||
margin-top: 0.8rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
padding: 0.4rem 0.6rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.45rem 0;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
margin-top: 0.45rem;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell::before {
|
.cellTag {
|
||||||
content: attr(data-school);
|
display: inline-flex;
|
||||||
display: block;
|
align-items: center;
|
||||||
font-size: 0.72rem;
|
gap: 0.4rem;
|
||||||
|
width: 5rem;
|
||||||
|
flex: none;
|
||||||
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--sc, var(--text-muted));
|
color: var(--sc, var(--text-secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cellDot {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--dot, var(--text-muted));
|
||||||
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.big {
|
.big {
|
||||||
font-size: 1.35rem;
|
font-size: 1.05rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
.small {
|
||||||
display: block;
|
display: block;
|
||||||
|
flex-basis: 100%;
|
||||||
|
padding-left: 5.5rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-top: 0.1rem;
|
margin-top: -0.05rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
@@ -197,20 +225,37 @@
|
|||||||
gap: 0 0.75rem;
|
gap: 0 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dissolve the per-measure card so its label + cells become grid items of
|
||||||
|
.grid, keeping columns aligned across every measure. */
|
||||||
|
.measure {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cellTag {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.rowLabel {
|
.rowLabel {
|
||||||
background: none;
|
color: var(--text-secondary);
|
||||||
border-radius: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.85rem 0.5rem 0.85rem 0;
|
padding: 0.85rem 0.5rem 0.85rem 0;
|
||||||
border-bottom: 1px solid var(--border-light);
|
border-bottom: 1px solid var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
|
display: block;
|
||||||
padding: 0.85rem 0.25rem;
|
padding: 0.85rem 0.25rem;
|
||||||
|
border-top: none;
|
||||||
border-bottom: 1px solid var(--border-light);
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell::before {
|
.big {
|
||||||
content: none;
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
flex-basis: auto;
|
||||||
|
padding-left: 0;
|
||||||
|
margin-top: 0.1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import type { CSSProperties, ReactNode } from 'react';
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
|
|
||||||
import type { School } from '@/lib/types';
|
import type { School } from '@/lib/types';
|
||||||
import { CHART_TEXT_COLORS } from '@/lib/utils';
|
import { CHART_COLORS, CHART_TEXT_COLORS, shortName } from '@/lib/utils';
|
||||||
import styles from './compareSections.module.css';
|
import styles from './compareSections.module.css';
|
||||||
|
|
||||||
export function Section({
|
export function Section({
|
||||||
@@ -61,6 +61,29 @@ export function RowLabel({ children, tip }: { children: ReactNode; tip?: string
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One measure = its row label plus a cell per school. `display: contents` on
|
||||||
|
* desktop (see CSS) makes these flow into the section grid as if this wrapper
|
||||||
|
* weren't here, keeping columns aligned across measures; on mobile the wrapper
|
||||||
|
* becomes a card so each measure reads as its own block.
|
||||||
|
*/
|
||||||
|
export function Measure({
|
||||||
|
label,
|
||||||
|
tip,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: ReactNode;
|
||||||
|
tip?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={styles.measure}>
|
||||||
|
<RowLabel tip={tip}>{label}</RowLabel>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Cell({
|
export function Cell({
|
||||||
school,
|
school,
|
||||||
index,
|
index,
|
||||||
@@ -73,9 +96,19 @@ export function Cell({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.cell}
|
className={styles.cell}
|
||||||
data-school={school.school_name}
|
style={
|
||||||
style={{ '--sc': CHART_TEXT_COLORS[index % CHART_TEXT_COLORS.length] } as CSSProperties}
|
{
|
||||||
|
'--sc': CHART_TEXT_COLORS[index % CHART_TEXT_COLORS.length],
|
||||||
|
'--dot': CHART_COLORS[index % CHART_COLORS.length],
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
{/* Mobile-only per-school tag (dot + short name); hidden on desktop,
|
||||||
|
where the column header identifies the school. */}
|
||||||
|
<span className={styles.cellTag}>
|
||||||
|
<span className={styles.cellDot} aria-hidden="true" />
|
||||||
|
{shortName(school.school_name)}
|
||||||
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,6 +59,24 @@ export function truncate(text: string, maxLength: number): string {
|
|||||||
return text.slice(0, maxLength).trim() + '...';
|
return text.slice(0, maxLength).trim() + '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A compact school label for tight spaces (mobile compare rows, chip bars):
|
||||||
|
* drop the trailing establishment-type words so "Barclay Primary School" →
|
||||||
|
* "Barclay", "St Mary's Catholic Primary School" → "St Mary's". Falls back to
|
||||||
|
* a length-capped truncation for names that don't carry a type suffix.
|
||||||
|
*/
|
||||||
|
export function shortName(name: string, maxLength = 20): string {
|
||||||
|
let s = name
|
||||||
|
.replace(
|
||||||
|
/\s+(primary|junior|infant|nursery|community|foundation|catholic|academy|school|college)\b.*$/i,
|
||||||
|
'',
|
||||||
|
)
|
||||||
|
.trim();
|
||||||
|
if (!s) s = name;
|
||||||
|
if (s.length > maxLength) s = s.slice(0, maxLength - 1).trim() + '…';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a school's age range for display, e.g. "3-11" → "Ages 3–11".
|
* Format a school's age range for display, e.g. "3-11" → "Ages 3–11".
|
||||||
* Display-only — leaves the raw `age_range` field (used for sixth-form
|
* Display-only — leaves the raw `age_range` field (used for sixth-form
|
||||||
|
|||||||
Reference in New Issue
Block a user