feat(search): print the school page's type and flags in search rows
Both rows now take their type label and tags from lib/schoolFacts, so a parent reads the same words in the list and on the page: "State school" instead of "Academy converter", "Girls' school" instead of "Girls", the religious character instead of "Faith priority", and a Nursery class flag on primaries. Who-can-apply tags are outlined with an inset ring, which keeps them the size of their neighbours; the Selective tag no longer borrows the status colour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca47d08186
commit
ad9d3b67f7
6 files changed
+88
-42
No files matched your search
@@ -34,3 +34,21 @@ describe('SchoolRow religious character', () => {
|
||||
expect(screen.getByText('Church of England')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SchoolRow shares the school page flags', () => {
|
||||
it("prints the type in the search filter's terms", () => {
|
||||
render(<SchoolRow school={{ ...base, type_group: 'state' }} />);
|
||||
expect(screen.getByText('State school')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Free schools')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('flags a nursery class', () => {
|
||||
render(<SchoolRow school={{ ...base, nursery_provision: 'Has Nursery Classes' }} />);
|
||||
expect(screen.getByText('Nursery class')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("flags a boys' school", () => {
|
||||
render(<SchoolRow school={{ ...base, gender: 'Boys' }} />);
|
||||
expect(screen.getByText("Boys' school")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -79,7 +79,7 @@ describe('SecondarySchoolRow admissions tag', () => {
|
||||
expect(screen.queryByText('Selective')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(['None', 'Does not apply', '', null])(
|
||||
it.each(['None', 'Does not apply'])(
|
||||
'gives no faith tag when the religious character is %p',
|
||||
(religious_denomination) => {
|
||||
render(
|
||||
@@ -87,16 +87,32 @@ describe('SecondarySchoolRow admissions tag', () => {
|
||||
school={{ ...base, admissions_policy: 'Not applicable', religious_denomination }}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText(religious_denomination)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Faith priority')).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it('tags a school with a religious character', () => {
|
||||
it('tags the religious character the register records, not "Faith priority"', () => {
|
||||
render(
|
||||
<SecondarySchoolRow
|
||||
school={{ ...base, admissions_policy: 'Not applicable', religious_denomination: 'Church of England' }}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('Faith priority')).toBeInTheDocument();
|
||||
expect(screen.getByText('Church of England')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Faith priority')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SecondarySchoolRow shares the school page flags', () => {
|
||||
it("prints the type in the search filter's terms", () => {
|
||||
render(<SecondarySchoolRow school={{ ...base, type_group: 'state' }} />);
|
||||
expect(screen.getByText('State school')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Academy')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("flags a girls' school and fees", () => {
|
||||
render(<SecondarySchoolRow school={{ ...base, type_group: 'independent', gender: 'Girls' }} />);
|
||||
expect(screen.getByText("Girls' school")).toBeInTheDocument();
|
||||
expect(screen.getByText('Fee-paying')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -100,6 +100,19 @@
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Changes who can apply or what it costs. Outlined, not tinted: a fact, not
|
||||
a verdict. An inset ring keeps the box the size of its neighbours. */
|
||||
.conditionTag {
|
||||
display: inline-block;
|
||||
padding: 0.0625rem 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--ink-rgb), 0.4);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Line 3: stats */
|
||||
.line3 {
|
||||
display: flex;
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
* Four-line row for primary school search results
|
||||
*
|
||||
* Line 1: School name · Ofsted badge (framework-aware)
|
||||
* Line 2: School type · Age range · Denomination · Gender
|
||||
* Line 2: Phase · Type · Age range · the school page's flags (lib/schoolFacts)
|
||||
* Line 3: Reading, Writing & Maths % · trend arrow · vs-national delta · Pupils
|
||||
* Line 4: Local authority · Distance
|
||||
*/
|
||||
|
||||
import type { School } from '@/lib/types';
|
||||
import { formatPercentage, calculateTrend, getPhaseStyle, schoolUrl, buildOfstedListBadge, formatAgeRange, hasReligiousCharacter, isProposedToClose, isSpecialSchool, listRwmValue } from '@/lib/utils';
|
||||
import { formatPercentage, calculateTrend, getPhaseStyle, schoolUrl, buildOfstedListBadge, formatAgeRange, isProposedToClose, isSpecialSchool, listRwmValue } from '@/lib/utils';
|
||||
import { schoolFlags, schoolTypeLabel } from '@/lib/schoolFacts';
|
||||
import styles from './SchoolRow.module.css';
|
||||
|
||||
interface SchoolRowProps {
|
||||
@@ -33,8 +34,8 @@ export function SchoolRow({
|
||||
const phase = getPhaseStyle(school.phase);
|
||||
const ofstedBadge = buildOfstedListBadge(school);
|
||||
|
||||
const showGender = school.gender && school.gender.toLowerCase() !== 'mixed';
|
||||
const showDenomination = hasReligiousCharacter(school.religious_denomination);
|
||||
const typeLabel = schoolTypeLabel(school);
|
||||
const flags = schoolFlags(school);
|
||||
|
||||
// The school's OWN figure and its year-over-year trend are same-school
|
||||
// measures — shown whenever there's a real value (not the all-zero
|
||||
@@ -77,10 +78,16 @@ export function SchoolRow({
|
||||
{phase.label}
|
||||
</span>
|
||||
)}
|
||||
{school.school_type && <span className={styles.attr}>{school.school_type}</span>}
|
||||
{typeLabel && <span className={styles.attr}>{typeLabel}</span>}
|
||||
{school.age_range && <span className={styles.attr}>{formatAgeRange(school.age_range)}</span>}
|
||||
{showDenomination && <span className={styles.attr}>{school.religious_denomination}</span>}
|
||||
{showGender && <span className={styles.attr}>{school.gender}</span>}
|
||||
{flags.map((flag) => (
|
||||
<span
|
||||
key={flag.label}
|
||||
className={flag.kind === 'condition' ? styles.conditionTag : styles.attr}
|
||||
>
|
||||
{flag.label}
|
||||
</span>
|
||||
))}
|
||||
{isProposedToClose(school) && (
|
||||
<span className={`${styles.attr} ${styles.attrClosing}`}>⚠ Proposed to close</span>
|
||||
)}
|
||||
|
||||
@@ -183,9 +183,17 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.selectiveTag {
|
||||
background: rgba(var(--status-below-rgb), 0.1);
|
||||
color: var(--status-below);
|
||||
/* Changes who can apply or what it costs. Outlined, not tinted: a fact, not
|
||||
a verdict. An inset ring keeps the box the size of its neighbours. */
|
||||
.conditionTag {
|
||||
display: inline-block;
|
||||
padding: 0.0625rem 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--ink-rgb), 0.4);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ── Ofsted badge ────────────────────────────────────── */
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Four-line row for secondary school search results
|
||||
*
|
||||
* Line 1: School name · Ofsted badge
|
||||
* Line 2: School type · Age range · Gender · Sixth form · Admissions tag
|
||||
* Line 2: Phase · Type · Age range · the school page's flags (lib/schoolFacts)
|
||||
* Line 3: Attainment 8 (large) · ±LA avg delta · Pupils
|
||||
* Line 4: LA name · distance
|
||||
*/
|
||||
@@ -11,22 +11,10 @@
|
||||
'use client';
|
||||
|
||||
import type { School } from '@/lib/types';
|
||||
import { buildOfstedListBadge, getPhaseStyle, schoolUrl, formatAgeRange, hasReligiousCharacter, isProposedToClose, isSpecialSchool } from '@/lib/utils';
|
||||
import { buildOfstedListBadge, getPhaseStyle, schoolUrl, formatAgeRange, isProposedToClose, isSpecialSchool } from '@/lib/utils';
|
||||
import { schoolFlags, schoolTypeLabel } from '@/lib/schoolFacts';
|
||||
import styles from './SecondarySchoolRow.module.css';
|
||||
|
||||
function detectAdmissionsTag(school: School): string | null {
|
||||
// Exact match: "Non-selective" contains "selective", so a substring test
|
||||
// tagged every comprehensive as Selective.
|
||||
if (school.admissions_policy?.trim().toLowerCase() === 'selective') return 'Selective';
|
||||
if (hasReligiousCharacter(school.religious_denomination)) return 'Faith priority';
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasSixthForm(school: School): boolean {
|
||||
// GIAS OfficialSixthForm flag; missing (pipeline not yet re-run) => false.
|
||||
return school.has_sixth_form ?? false;
|
||||
}
|
||||
|
||||
interface SecondarySchoolRowProps {
|
||||
school: School;
|
||||
isLocationSearch?: boolean;
|
||||
@@ -64,9 +52,8 @@ export function SecondarySchoolRow({
|
||||
? att8 - laAvgAttainment8
|
||||
: null;
|
||||
|
||||
const admissionsTag = detectAdmissionsTag(school);
|
||||
const sixthForm = hasSixthForm(school);
|
||||
const showGender = school.gender && school.gender.toLowerCase() !== 'mixed';
|
||||
const typeLabel = schoolTypeLabel(school);
|
||||
const flags = schoolFlags(school);
|
||||
|
||||
return (
|
||||
<div className={`${styles.row} ${phase.key ? styles[`phase${phase.key}`] : ''} ${isInCompare ? styles.rowInCompare : ''}`}>
|
||||
@@ -90,19 +77,16 @@ export function SecondarySchoolRow({
|
||||
{phase.label}
|
||||
</span>
|
||||
)}
|
||||
{school.school_type && <span className={styles.attr}>{school.school_type}</span>}
|
||||
{typeLabel && <span className={styles.attr}>{typeLabel}</span>}
|
||||
{school.age_range && <span className={styles.attr}>{formatAgeRange(school.age_range)}</span>}
|
||||
{showGender && (
|
||||
<span className={styles.provisionTag}>{school.gender}</span>
|
||||
)}
|
||||
{sixthForm && (
|
||||
<span className={styles.provisionTag}>Sixth form</span>
|
||||
)}
|
||||
{admissionsTag && (
|
||||
<span className={`${styles.provisionTag} ${admissionsTag === 'Selective' ? styles.selectiveTag : ''}`}>
|
||||
{admissionsTag}
|
||||
{flags.map((flag) => (
|
||||
<span
|
||||
key={flag.label}
|
||||
className={flag.kind === 'condition' ? styles.conditionTag : styles.provisionTag}
|
||||
>
|
||||
{flag.label}
|
||||
</span>
|
||||
)}
|
||||
))}
|
||||
{isProposedToClose(school) && (
|
||||
<span className={`${styles.provisionTag} ${styles.closingTag}`}>⚠ Proposed to close</span>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user