feat(home): implement redesigned homepage
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 13s
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 13s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 13s
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 13s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
- Hero: Playfair heading with coral italic accent, teal eyebrow pill, richer sub-copy describing both primary and secondary coverage - Discovery: geolocation "Schools near me" button (reverse-geocodes via postcodes.io → /?postcode=…&radius=1), plus Start exploring chips linking to /rankings and /compare - How it works: 3-card grid showing miniature real-UI previews for Performance (primary SATs cascade + secondary Att8 bar), Ofsted inspection card, and side-by-side Compare table - Editorial: text column + factbox (totalSchools, LA count, coverage dates) rendered inside a white card below the how-it-works section - Footer: expanded to 3 columns (brand blurb, Product, Resources); links updated to / /rankings /compare and real gov.uk/ofsted URLs - All new sections visible only on landing (no search active) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
||||
import { FilterBar } from './FilterBar';
|
||||
import { SchoolRow } from './SchoolRow';
|
||||
@@ -41,6 +41,8 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
|
||||
const [mapSchools, setMapSchools] = useState<School[]>([]);
|
||||
const [isLoadingMap, setIsLoadingMap] = useState(false);
|
||||
const prevSearchParamsRef = useRef(searchParams.toString());
|
||||
const [geoState, setGeoState] = useState<'idle' | 'requesting' | 'error'>('idle');
|
||||
const [geoError, setGeoError] = useState<string | null>(null);
|
||||
|
||||
const hasSearch = searchParams.get('search') || searchParams.get('postcode');
|
||||
const isLocationSearch = !!searchParams.get('postcode');
|
||||
@@ -117,6 +119,47 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
|
||||
}
|
||||
};
|
||||
|
||||
const handleNearMe = useCallback(() => {
|
||||
if (!navigator.geolocation) {
|
||||
setGeoState('error');
|
||||
setGeoError('Geolocation is not supported by your browser. Enter a postcode instead.');
|
||||
return;
|
||||
}
|
||||
setGeoState('requesting');
|
||||
setGeoError(null);
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
async (position) => {
|
||||
const { latitude, longitude } = position.coords;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.postcodes.io/postcodes?lon=${longitude}&lat=${latitude}&limit=1`
|
||||
);
|
||||
const data = await res.json();
|
||||
if (data.result && data.result.length > 0) {
|
||||
const postcode = data.result[0].postcode as string;
|
||||
setGeoState('idle');
|
||||
router.push(`/?postcode=${encodeURIComponent(postcode)}&radius=1`);
|
||||
} else {
|
||||
setGeoState('error');
|
||||
setGeoError('No postcode found near your location. Try entering one above.');
|
||||
}
|
||||
} catch {
|
||||
setGeoState('error');
|
||||
setGeoError('Could not look up your location. Please try again.');
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
setGeoState('error');
|
||||
if (err.code === err.PERMISSION_DENIED) {
|
||||
setGeoError('Location access was denied. Enter a postcode above to find nearby schools.');
|
||||
} else {
|
||||
setGeoError('Could not get your location. Please try again or enter a postcode.');
|
||||
}
|
||||
},
|
||||
{ timeout: 10000, maximumAge: 60000 }
|
||||
);
|
||||
}, [router]);
|
||||
|
||||
const sortedSchools = [...allSchools].sort((a, b) => {
|
||||
if (sortOrder === 'rwm_desc') return (b.rwm_expected_pct ?? -Infinity) - (a.rwm_expected_pct ?? -Infinity);
|
||||
if (sortOrder === 'rwm_asc') return (a.rwm_expected_pct ?? Infinity) - (b.rwm_expected_pct ?? Infinity);
|
||||
@@ -132,8 +175,16 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
|
||||
{/* Combined Hero + Search and Filters */}
|
||||
{!isSearchActive && (
|
||||
<div className={styles.heroSection}>
|
||||
<h1 className={styles.heroTitle}>Find Local Schools</h1>
|
||||
<p className={styles.heroDescription}>Compare school results (SATs and GCSE), for thousands of schools across England</p>
|
||||
<span className={styles.heroEyebrow}>
|
||||
<span className={styles.heroEyebrowDot} aria-hidden="true" />
|
||||
2024/25 results · updated April 2026
|
||||
</span>
|
||||
<h1 className={styles.heroTitle}>
|
||||
Every English school, <em className={styles.heroEmph}>compared.</em>
|
||||
</h1>
|
||||
<p className={styles.heroDescription}>
|
||||
<strong>24,000+ primary and secondary schools</strong> with Key Stage 2 SATs, GCSE results, Ofsted grades, progress scores and admissions data — side by side, in one place.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -146,17 +197,214 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
|
||||
{/* Discovery section shown on landing page before any search */}
|
||||
{!isSearchActive && initialSchools.schools.length === 0 && (
|
||||
<div className={styles.discoverySection}>
|
||||
{totalSchools && <p className={styles.discoveryCount}><strong>{totalSchools.toLocaleString()}+</strong> primary and secondary schools across England</p>}
|
||||
<p className={styles.discoveryHints}>Try searching for a school name, or enter a postcode to find schools near you.</p>
|
||||
<div className={styles.quickSearches}>
|
||||
<span className={styles.quickSearchLabel}>Quick searches:</span>
|
||||
{['Manchester', 'Bristol', 'Leeds', 'Birmingham'].map(city => (
|
||||
<a key={city} href={`/?search=${city}`} className={styles.quickSearchChip}>{city}</a>
|
||||
))}
|
||||
<div className={styles.nearMeRow}>
|
||||
<button
|
||||
className={styles.nearMeBtn}
|
||||
onClick={handleNearMe}
|
||||
disabled={geoState === 'requesting'}
|
||||
>
|
||||
{geoState === 'requesting' ? (
|
||||
<>
|
||||
<span className={styles.nearMeBtnSpinner} aria-hidden="true" />
|
||||
Locating you…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
|
||||
<path d="M12 2a7 7 0 0 1 7 7c0 5.25-7 13-7 13S5 14.25 5 9a7 7 0 0 1 7-7z"/>
|
||||
<circle cx="12" cy="9" r="2.5"/>
|
||||
</svg>
|
||||
Schools near me
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{geoError && <p className={styles.geoError} role="alert">{geoError}</p>}
|
||||
</div>
|
||||
<div className={styles.exploringRow}>
|
||||
<span className={styles.exploringLabel}>Start exploring</span>
|
||||
<div className={styles.exploringChips}>
|
||||
<a href="/rankings" className={styles.exploringChip}>
|
||||
<span className={styles.chipDot} aria-hidden="true" />
|
||||
Top-rated primary schools
|
||||
</a>
|
||||
<a href="/rankings" className={styles.exploringChip}>
|
||||
<span className={styles.chipDot} aria-hidden="true" />
|
||||
Top-rated secondary schools
|
||||
</a>
|
||||
<a href="/compare" className={styles.exploringChip}>
|
||||
<span className={styles.chipDot} aria-hidden="true" />
|
||||
Start a comparison
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* How it works — only on landing page */}
|
||||
{!isSearchActive && (
|
||||
<section className={styles.howItWorks}>
|
||||
<div className={styles.hiwHeader}>
|
||||
<h2 className={styles.hiwHeading}>What you'll see on every school</h2>
|
||||
<span className={styles.hiwSub}>Primary or secondary — the page adapts to the phase</span>
|
||||
</div>
|
||||
<div className={styles.hiwGrid}>
|
||||
|
||||
{/* Card 1 — Performance */}
|
||||
<div className={styles.hiwCard}>
|
||||
<div className={styles.hiwVisual}>
|
||||
{/* Primary: mini cascade */}
|
||||
<div className={styles.hiwPhaseBlock}>
|
||||
<div className={styles.hiwPhaseLabel}>Primary · Year 6 · <strong>Key Stage 2 SATs</strong></div>
|
||||
<div className={styles.miniCascade}>
|
||||
{[
|
||||
{ subj: 'Reading', exp: 96, exc: 73, nat: 75 },
|
||||
{ subj: 'Writing', exp: 81, exc: 15, nat: 72 },
|
||||
{ subj: 'Maths', exp: 85, exc: 47, nat: 74 },
|
||||
].map(({ subj, exp, exc, nat }) => (
|
||||
<div key={subj} className={styles.miniCascadeCol}>
|
||||
<div className={styles.miniSubj}>{subj}</div>
|
||||
<div className={styles.miniRowHead}><span>Expected</span><strong>{exp}%</strong></div>
|
||||
<div className={styles.miniTrack}>
|
||||
<div className={styles.miniNatPill} style={{ left: `${nat}%` }}>{nat}%</div>
|
||||
<div className={styles.miniBarExp} style={{ width: `${exp}%` }} />
|
||||
</div>
|
||||
<div className={styles.miniRowHead}><span>Exceeding</span><strong>{exc}%</strong></div>
|
||||
<div className={styles.miniTrack}>
|
||||
<div className={styles.miniBarExc} style={{ width: `${exc}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Secondary: Attainment 8 */}
|
||||
<div className={styles.hiwPhaseBlock}>
|
||||
<div className={styles.hiwPhaseLabel}>Secondary · Year 11 · <strong>GCSE Attainment 8</strong></div>
|
||||
<div className={styles.att8Row}>
|
||||
<div className={styles.att8BarWrap}>
|
||||
<div className={styles.att8BarHead}><span>This school</span><span>National avg 50.2</span></div>
|
||||
<div className={styles.att8Track}>
|
||||
<div className={styles.att8Fill} style={{ width: '62%' }} />
|
||||
<div className={styles.att8NatLine} style={{ left: '50%' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.att8Score}>
|
||||
<div className={styles.att8Value}>62.4</div>
|
||||
<div className={styles.att8Delta}>+12.2 vs national</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hiwCardBody}>
|
||||
<div className={styles.hiwStep}>Performance</div>
|
||||
<div className={styles.hiwTitle}>Results against the national average</div>
|
||||
<p className={styles.hiwDesc}>For primary schools, each subject's Expected and Exceeding percentages side by side. For secondary schools, GCSE Attainment 8 with the national benchmark overlaid.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 2 — Ofsted */}
|
||||
<div className={styles.hiwCard}>
|
||||
<div className={styles.hiwVisual}>
|
||||
<div className={styles.ofstedPreview}>
|
||||
<div className={styles.ofstedHead}>
|
||||
<span className={styles.ofstedBullet} />
|
||||
<span className={styles.ofstedTitle}>Latest Ofsted inspection</span>
|
||||
</div>
|
||||
<span className={styles.ofstedBadge}>OUTSTANDING</span>
|
||||
<div className={styles.ofstedVerdict}>Rated <em>Outstanding</em> at last inspection.</div>
|
||||
<div className={styles.ofstedMeta}>Full inspection · March 2024</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hiwCardBody}>
|
||||
<div className={styles.hiwStep}>Judgement</div>
|
||||
<div className={styles.hiwTitle}>Ofsted at a glance</div>
|
||||
<p className={styles.hiwDesc}>Current grade, inspection date, and a plain-English headline — without opening a 40-page report.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 3 — Compare */}
|
||||
<div className={styles.hiwCard}>
|
||||
<div className={styles.hiwVisual}>
|
||||
<div className={styles.comparePreview}>
|
||||
<div className={styles.compareHead}>
|
||||
<div className={`${styles.compareHeadCell} ${styles.compareHeadLabel}`}>Metric</div>
|
||||
<div className={styles.compareHeadCell}>Our Lady<br />Queen of Heaven</div>
|
||||
<div className={styles.compareHeadCell}>St Mary's<br />Catholic Primary</div>
|
||||
</div>
|
||||
{[
|
||||
{ label: 'Reading, Writing & Maths', a: '70%', b: '64%', aHi: true },
|
||||
{ label: 'Ofsted', a: 'Outstanding', b: 'Good', aHi: true },
|
||||
{ label: 'Reading progress', a: '+2.1', b: '+0.4', aHi: true },
|
||||
].map(({ label, a, b, aHi }) => (
|
||||
<div key={label} className={styles.compareRow}>
|
||||
<span className={styles.compareRowLabel}>{label}</span>
|
||||
<span className={`${styles.compareRowVal} ${aHi ? styles.compareRowValHi : ''}`}>{a}</span>
|
||||
<span className={styles.compareRowVal}>{b}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.compareFoot}>+ pin up to 5 schools</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hiwCardBody}>
|
||||
<div className={styles.hiwStep}>Compare</div>
|
||||
<div className={styles.hiwTitle}>Side-by-side shortlists</div>
|
||||
<p className={styles.hiwDesc}>Pin up to five schools and every metric aligns in the same columns — works for primary and secondary alike.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Editorial — only on landing page */}
|
||||
{!isSearchActive && (
|
||||
<section className={styles.editorial}>
|
||||
<div className={styles.editorialGrid}>
|
||||
<div className={styles.editorialText}>
|
||||
<div className={styles.editorialKicker}>About school data</div>
|
||||
<h2 className={styles.editorialHeading}>Making UK school performance data actually readable</h2>
|
||||
<p>
|
||||
School performance data in England is rich but fragmented. The Department for Education publishes
|
||||
Key Stage 2 SATs, GCSE attainment, Ofsted outcomes, progress scores, admissions figures and
|
||||
demographics — each in its own table, each with its own jargon.
|
||||
</p>
|
||||
<p>
|
||||
SchoolCompare brings it all into one place. Every school page shows performance against the national
|
||||
average, explains what the numbers mean, and lets you shortlist schools side by side. Built for
|
||||
parents, governors, journalists, and anyone who wants to understand a school without reading a
|
||||
40-page inspection report.
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.factbox}>
|
||||
<h3 className={styles.factboxHeading}>Coverage at a glance</h3>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Schools covered</span>
|
||||
<span className={styles.factVal}>{totalSchools ? `${totalSchools.toLocaleString()}` : '24,000+'}</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Local authorities</span>
|
||||
<span className={styles.factVal}>{filters.local_authorities.length > 0 ? filters.local_authorities.length : 152}</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Phases</span>
|
||||
<span className={styles.factVal}>Primary & Secondary</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Latest results year</span>
|
||||
<span className={styles.factVal}>2024/25</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Historical data</span>
|
||||
<span className={styles.factVal}>2016–2025</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Metrics per school</span>
|
||||
<span className={styles.factVal}>40+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Results Section */}
|
||||
<section className={`${styles.results} ${resultsView === 'map' && isLocationSearch ? styles.mapViewResults : ''}`}>
|
||||
{!hasSearch && initialSchools.schools.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user