feat(school-detail): map-blended hero, remove at-a-glance stats
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 14s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 52s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 14s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 52s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
Replace the header's at-a-glance stats row with a location map that sits atop the hero and blends into the school title. The map is a static, non-interactive preview (never traps page scroll) with a coral pin; the whole band — or the inline "View on map ↗" link by the address — opens a fullscreen, interactive map. Compare floats glassy over the band. The separate "Location" section (and its nav item) is removed; the map now lives only in the hero. Schools without lat/long render the header with no map band, as before. New: SchoolHeroMap (fullscreen wrapper, forwardRef open handle) + LeafletHeroMapInner (minimal single-school map with interaction toggle). Applied to both primary and secondary detail views; dead heroStats/tone/ mapContainer CSS removed (shared .heroStat* card classes kept). Also drops the orphaned "Latest data" note and does not reintroduce an Ofsted strip in the hero (it would duplicate the Ofsted section directly below). Mockup kept at mockups/header-map-hero.html. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useComparison } from '@/hooks/useComparison';
|
||||
import { SchoolMap } from './SchoolMap';
|
||||
import { SchoolHeroMap, type SchoolHeroMapHandle } from './SchoolHeroMap';
|
||||
import { MetricTooltip } from './MetricTooltip';
|
||||
import type {
|
||||
School, SchoolResult, AbsenceData,
|
||||
@@ -19,7 +19,6 @@ import type {
|
||||
} from '@/lib/types';
|
||||
import {
|
||||
formatPercentage, formatProgress, formatAcademicYear,
|
||||
buildOfstedHeroChip,
|
||||
} from '@/lib/utils';
|
||||
import { DeltaChip } from './DeltaChip';
|
||||
|
||||
@@ -93,6 +92,8 @@ export function SchoolDetailView({
|
||||
// Carry the "Add to Compare" CTA into the sticky bar once the hero's button leaves.
|
||||
const heroActionsRef = useRef<HTMLDivElement | null>(null);
|
||||
const [heroCtaVisible, setHeroCtaVisible] = useState(true);
|
||||
// Hero map — the "View on map" link opens its fullscreen view.
|
||||
const heroMapRef = useRef<SchoolHeroMapHandle>(null);
|
||||
// "All ▾" jump menu listing every section.
|
||||
const [sectionsOpen, setSectionsOpen] = useState(false);
|
||||
|
||||
@@ -236,7 +237,6 @@ export function SchoolDetailView({
|
||||
if (parentView && parentView.total_responses != null && parentView.total_responses > 0)
|
||||
navItems.push({ id: 'parents', label: 'Parents' });
|
||||
if (hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' });
|
||||
if (hasLocation) navItems.push({ id: 'location', label: 'Location' });
|
||||
if (hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' });
|
||||
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
|
||||
|
||||
@@ -285,26 +285,6 @@ export function SchoolDetailView({
|
||||
return subs.length >= 3 && subs.every(v => v === ofsted.overall_effectiveness);
|
||||
})();
|
||||
|
||||
// ── Hero: framework-aware signal chip + narrative summary ─────────────
|
||||
const ofstedHeroChip = buildOfstedHeroChip(ofsted);
|
||||
|
||||
// KS2 headline numbers for the at-a-glance row
|
||||
const heroRwm = isPrimary ? latestResults?.rwm_expected_pct ?? null : null;
|
||||
const heroRwmNat = primaryAvg.rwm_expected_pct ?? null;
|
||||
|
||||
// KS4 headline number for secondary/all-through schools
|
||||
const heroAtt8 = isSecondary ? latestResults?.attainment_8_score ?? null : null;
|
||||
const heroAtt8Nat = secondaryAvg.attainment_8_score ?? null;
|
||||
|
||||
const heroAcademicYear = latestResults ? formatAcademicYear(latestResults.year) : '';
|
||||
|
||||
// Scorecard renders if any tile has content, so a results-less school still
|
||||
// shows its Ofsted signal (previously carried by the now-removed chip strip).
|
||||
const hasHeroStats = heroRwm != null
|
||||
|| heroAtt8 != null
|
||||
|| ofsted != null
|
||||
|| admissions?.first_preference_offer_pct != null;
|
||||
|
||||
// Label shown in the mobile "section" menu button — the section in view.
|
||||
const activeNavLabel = (navItems.find((n) => n.id === activeSection) ?? navItems[0])?.label ?? '';
|
||||
|
||||
@@ -317,8 +297,11 @@ export function SchoolDetailView({
|
||||
<span aria-hidden="true">←</span> Back
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<header className={styles.header}>
|
||||
{/* Header — the location map band blends down into the school title. */}
|
||||
<header className={`${styles.header}${hasLocation ? ` ${styles.headerHasMap}` : ''}`}>
|
||||
{hasLocation && (
|
||||
<SchoolHeroMap ref={heroMapRef} lat={schoolInfo.latitude!} lng={schoolInfo.longitude!} />
|
||||
)}
|
||||
<div className={styles.headerContent}>
|
||||
<div className={styles.titleSection}>
|
||||
<h1 className={styles.schoolName}>{schoolInfo.school_name}</h1>
|
||||
@@ -336,6 +319,18 @@ export function SchoolDetailView({
|
||||
{schoolInfo.address && (
|
||||
<p className={styles.address}>
|
||||
{schoolInfo.address}{schoolInfo.postcode && `, ${schoolInfo.postcode}`}
|
||||
{hasLocation && (
|
||||
<>
|
||||
{' · '}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.mapLink}
|
||||
onClick={() => { heroMapRef.current?.open(); track('section_nav_used', { section: 'location', via: 'hero_link' }); }}
|
||||
>
|
||||
View on map ↗
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<div className={styles.headerDetails}>
|
||||
@@ -383,69 +378,6 @@ export function SchoolDetailView({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* At-a-glance stats row — the single home for the headline numbers.
|
||||
Shows whenever any tile has content (results, Ofsted, or admissions),
|
||||
so schools without KS2/KS4 results still carry their Ofsted signal. */}
|
||||
{hasHeroStats && (
|
||||
<div className={styles.heroStats}>
|
||||
{isPrimary && heroRwm != null && (
|
||||
<div className={styles.heroStat}>
|
||||
<div className={styles.heroStatNumber}>{Math.round(heroRwm)}%</div>
|
||||
<div className={styles.heroStatLabel}>Reading, Writing & Maths</div>
|
||||
{heroRwmNat != null && (
|
||||
<DeltaChip value={heroRwm} baseline={heroRwmNat} unit="pts" suffix="vs national" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSecondary && heroAtt8 != null && (
|
||||
<div className={styles.heroStat}>
|
||||
<div className={styles.heroStatNumber}>{heroAtt8.toFixed(1)}</div>
|
||||
<div className={styles.heroStatLabel}>Attainment 8 score</div>
|
||||
{heroAtt8Nat != null && (
|
||||
<DeltaChip value={heroAtt8} baseline={heroAtt8Nat} unit="pts" suffix="vs national" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ofsted && (
|
||||
<div className={styles.heroStat}>
|
||||
<div className={`${styles.heroStatNumberSerif} ${styles[`tone-${ofstedHeroChip.tone}`]}`}>
|
||||
{ofstedHeroChip.state === 'oeif'
|
||||
? ofstedHeroChip.title.replace(/^Ofsted\s+/, '')
|
||||
: ofstedHeroChip.state === 'reportCard'
|
||||
? 'Report Card'
|
||||
: '—'}
|
||||
</div>
|
||||
<div className={styles.heroStatLabel}>
|
||||
{ofstedHeroChip.subtitle}
|
||||
</div>
|
||||
{ofstedHeroChip.detail && (
|
||||
<div className={styles.heroStatFoot}>{ofstedHeroChip.detail}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{admissions?.first_preference_offer_pct != null && (
|
||||
<div className={styles.heroStat}>
|
||||
<div className={styles.heroStatNumber}>
|
||||
{Math.round(admissions.first_preference_offer_pct)}%
|
||||
</div>
|
||||
<div className={styles.heroStatLabel}>First-choice offer rate</div>
|
||||
{admissions.oversubscribed && (
|
||||
<div className={styles.heroStatFoot}>Oversubscribed</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{heroAcademicYear && (
|
||||
<p className={styles.heroDataNote}>
|
||||
Latest data: {heroAcademicYear}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Sticky Section Navigation — docks under the global header */}
|
||||
@@ -1266,20 +1198,6 @@ export function SchoolDetailView({
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Location */}
|
||||
{hasLocation && (
|
||||
<section id="location" className={styles.card}>
|
||||
<h2 className={styles.sectionTitle}>Location</h2>
|
||||
<div className={styles.mapContainer}>
|
||||
<SchoolMap
|
||||
schools={[schoolInfo]}
|
||||
center={[schoolInfo.latitude!, schoolInfo.longitude!]}
|
||||
zoom={15}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Local Area Context */}
|
||||
{hasDeprivation && deprivation && (
|
||||
<section id="local-area" className={styles.card}>
|
||||
|
||||
Reference in New Issue
Block a user