Compare commits

...
2 Commits
Author SHA1 Message Date
tudor cab7b4fe91 Merge pull request 'feat(detail): move religious character into header details + mobile reveal' (#83) from feat/detail-header-details-reveal into main
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 14s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 52s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 14s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 1s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 47s
Reviewed-on: #83
2026-07-24 12:15:14 +00:00
TudorandClaude Opus 4.8 96d5fcf5b0 feat(detail): move religious character into header details + mobile reveal
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m1s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 47s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 52s
Religious character now sits with the other labelled header details (near
County / Constituency) as "Religious character: <denomination>" ("None" for
Does not apply / None), instead of as an identity chip.

On mobile/tablet (≤768px) the header details block was hidden outright; it's
now collapsed behind a "Show all details" toggle that reveals the full block
(headteacher, website, pupils, trust, phone, religious character, county,
constituency). Applied to both the primary and secondary detail views for
parity. Extends the e2e journey with the mobile toggle behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 12:24:16 +01:00
5 changed files with 124 additions and 20 deletions
+18
View File
@@ -148,6 +148,24 @@ test('school detail page shows GIAS identity/contact details and drops the unwir
} }
}); });
test('header details collapse behind a "Show all details" toggle on mobile', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
const [urn] = await twoPrimaryUrns(page);
await page.goto(`/school/${urn}`);
await expect(page.locator('h1').first()).toBeVisible({ timeout: 15_000 });
// Collapsed by default on mobile/tablet …
const details = page.locator('#school-header-details');
await expect(details).toBeHidden();
const toggle = page.getByRole('button', { name: /show all details/i });
await expect(toggle).toBeVisible();
// … and the link reveals them (label flips to "Hide details").
await toggle.click();
await expect(details).toBeVisible();
await expect(page.getByRole('button', { name: /hide details/i })).toBeVisible();
});
test('a report-card school shows its report card, dated to the report-card inspection', async ({ page }) => { test('a report-card school shows its report card, dated to the report-card inspection', async ({ page }) => {
// Detail views detected report cards via `framework`, which the API never // Detail views detected report cards via `framework`, which the API never
// sets to "ReportCard" — so report-card schools rendered as legacy ratings // sets to "ReportCard" — so report-card schools rendered as legacy ratings
@@ -197,6 +197,22 @@
text-decoration: underline; text-decoration: underline;
} }
/* "Show all details" reveal — only rendered on mobile/tablet, where the
header details block is collapsed below the fold. Hidden on desktop. */
.detailsToggle {
display: none;
align-items: center;
gap: 0.25rem;
margin-top: 0.5rem;
padding: 0;
background: none;
border: none;
font-size: 0.8125rem;
font-weight: 600;
color: var(--accent-teal, #2d7d7d);
cursor: pointer;
}
/* Gender split card — sits in the Pupils & Inclusion heroStatGrid */ /* Gender split card — sits in the Pupils & Inclusion heroStatGrid */
.genderSplitValue { .genderSplitValue {
display: flex; display: flex;
@@ -1205,14 +1221,24 @@
gap: 0.375rem; gap: 0.375rem;
} }
/* Secondary header info (headteacher, website, pupil count, trust) /* Secondary header info (headteacher, website, pupil count, trust,
isn't needed above the fold on phones — pupil count lives in the contact, area) isn't needed above the fold on phones/tablets, so it's
Pupils & Inclusion section, website is one scroll away. Reclaim collapsed by default and revealed on demand via the "Show all details"
the ~3 vertical lines so the actual metrics surface sooner. */ link — reclaiming the vertical space so the metrics surface sooner. */
.detailsToggle {
display: inline-flex;
}
.headerDetails { .headerDetails {
display: none; display: none;
} }
.headerDetailsOpen {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.metricsGrid { .metricsGrid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
+25 -8
View File
@@ -99,6 +99,9 @@ export function SchoolDetailView({
const heroMapRef = useRef<SchoolHeroMapHandle>(null); const heroMapRef = useRef<SchoolHeroMapHandle>(null);
// "All ▾" jump menu listing every section. // "All ▾" jump menu listing every section.
const [sectionsOpen, setSectionsOpen] = useState(false); const [sectionsOpen, setSectionsOpen] = useState(false);
// Header details (headteacher, contact, trust, area) collapse behind a
// "Show all details" link on mobile/tablet, where they're below the fold.
const [detailsOpen, setDetailsOpen] = useState(false);
// Back returns to wherever the user came from; deep-links (no in-app history) // Back returns to wherever the user came from; deep-links (no in-app history)
// fall back to search so the button never dead-ends or leaves the site. // fall back to search so the button never dead-ends or leaves the site.
@@ -354,13 +357,6 @@ export function SchoolDetailView({
{schoolInfo.age_range && ( {schoolInfo.age_range && (
<span className={styles.metaItem}>{formatAgeRange(schoolInfo.age_range)}</span> <span className={styles.metaItem}>{formatAgeRange(schoolInfo.age_range)}</span>
)} )}
{schoolInfo.religious_denomination && (
<span className={styles.metaItem}>
{['Does not apply', 'None'].includes(schoolInfo.religious_denomination)
? 'None'
: schoolInfo.religious_denomination}
</span>
)}
{schoolInfo.nursery_provision && ( {schoolInfo.nursery_provision && (
<span className={styles.metaItem}>Nursery</span> <span className={styles.metaItem}>Nursery</span>
)} )}
@@ -391,7 +387,20 @@ export function SchoolDetailView({
)} )}
</p> </p>
)} )}
<div className={styles.headerDetails}> <button
type="button"
className={styles.detailsToggle}
aria-expanded={detailsOpen}
aria-controls="school-header-details"
onClick={() => setDetailsOpen((o) => !o)}
>
{detailsOpen ? 'Hide details' : 'Show all details'}
<span aria-hidden="true">{detailsOpen ? '▴' : '▾'}</span>
</button>
<div
id="school-header-details"
className={`${styles.headerDetails}${detailsOpen ? ` ${styles.headerDetailsOpen}` : ''}`}
>
{schoolInfo.headteacher_name && ( {schoolInfo.headteacher_name && (
<span className={styles.headerDetail}> <span className={styles.headerDetail}>
<strong>Headteacher:</strong> {schoolInfo.headteacher_name} <strong>Headteacher:</strong> {schoolInfo.headteacher_name}
@@ -433,6 +442,14 @@ export function SchoolDetailView({
</a> </a>
</span> </span>
)} )}
{schoolInfo.religious_denomination && (
<span className={styles.headerDetail}>
<strong>Religious character:</strong>{' '}
{['Does not apply', 'None'].includes(schoolInfo.religious_denomination)
? 'None'
: schoolInfo.religious_denomination}
</span>
)}
{schoolInfo.county && ( {schoolInfo.county && (
<span className={styles.headerDetail}> <span className={styles.headerDetail}>
<strong>County:</strong> {schoolInfo.county} <strong>County:</strong> {schoolInfo.county}
@@ -207,6 +207,22 @@
text-decoration: underline; text-decoration: underline;
} }
/* "Show all details" reveal — only rendered on mobile/tablet, where the
header details block is collapsed below the fold. Hidden on desktop. */
.detailsToggle {
display: none;
align-items: center;
gap: 0.25rem;
margin-top: 0.5rem;
padding: 0;
background: none;
border: none;
font-size: 0.8125rem;
font-weight: 600;
color: var(--accent-teal, #2d7d7d);
cursor: pointer;
}
.actions { .actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
@@ -1066,7 +1082,18 @@
padding: 0.1rem 0.375rem; padding: 0.1rem 0.375rem;
} }
/* Collapsed below the fold on phones/tablets; revealed via "Show all
details" so the metrics surface sooner. */
.detailsToggle {
display: inline-flex;
}
.headerDetails { .headerDetails {
display: none;
}
.headerDetailsOpen {
display: flex;
flex-direction: column; flex-direction: column;
gap: 0.375rem; gap: 0.375rem;
} }
@@ -83,6 +83,8 @@ export function SecondarySchoolDetailView({
const isInComparison = isSelected(schoolInfo.urn); const isInComparison = isSelected(schoolInfo.urn);
const [activeSection, setActiveSection] = useState<string>(''); const [activeSection, setActiveSection] = useState<string>('');
// Header details collapse behind a "Show all details" link on mobile/tablet.
const [detailsOpen, setDetailsOpen] = useState(false);
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null; const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
@@ -246,13 +248,6 @@ export function SecondarySchoolDetailView({
{schoolInfo.age_range && ( {schoolInfo.age_range && (
<span className={styles.badge}>{formatAgeRange(schoolInfo.age_range)}</span> <span className={styles.badge}>{formatAgeRange(schoolInfo.age_range)}</span>
)} )}
{schoolInfo.religious_denomination && (
<span className={styles.badge}>
{['Does not apply', 'None'].includes(schoolInfo.religious_denomination)
? 'None'
: schoolInfo.religious_denomination}
</span>
)}
{schoolInfo.nursery_provision && ( {schoolInfo.nursery_provision && (
<span className={styles.badge}>Nursery</span> <span className={styles.badge}>Nursery</span>
)} )}
@@ -288,7 +283,20 @@ export function SecondarySchoolDetailView({
)} )}
</p> </p>
)} )}
<div className={styles.headerDetails}> <button
type="button"
className={styles.detailsToggle}
aria-expanded={detailsOpen}
aria-controls="school-header-details"
onClick={() => setDetailsOpen((o) => !o)}
>
{detailsOpen ? 'Hide details' : 'Show all details'}
<span aria-hidden="true">{detailsOpen ? '▴' : '▾'}</span>
</button>
<div
id="school-header-details"
className={`${styles.headerDetails}${detailsOpen ? ` ${styles.headerDetailsOpen}` : ''}`}
>
{schoolInfo.headteacher_name && ( {schoolInfo.headteacher_name && (
<span className={styles.headerDetail}> <span className={styles.headerDetail}>
<strong>Headteacher:</strong> {schoolInfo.headteacher_name} <strong>Headteacher:</strong> {schoolInfo.headteacher_name}
@@ -326,6 +334,14 @@ export function SecondarySchoolDetailView({
</a> </a>
</span> </span>
)} )}
{schoolInfo.religious_denomination && (
<span className={styles.headerDetail}>
<strong>Religious character:</strong>{' '}
{['Does not apply', 'None'].includes(schoolInfo.religious_denomination)
? 'None'
: schoolInfo.religious_denomination}
</span>
)}
{schoolInfo.county && ( {schoolInfo.county && (
<span className={styles.headerDetail}> <span className={styles.headerDetail}>
<strong>County:</strong> {schoolInfo.county} <strong>County:</strong> {schoolInfo.county}