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:
@@ -12,8 +12,8 @@
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 3rem;
|
||||
grid-template-columns: 1.5fr 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,23 +15,22 @@ export function Footer() {
|
||||
<div className={styles.section}>
|
||||
<h3 className={styles.title}>SchoolCompare</h3>
|
||||
<p className={styles.description}>
|
||||
Compare primary and secondary schools across England.
|
||||
Compare primary and secondary schools across England. Free, independent, built on public data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h4 className={styles.sectionTitle}>Product</h4>
|
||||
<ul className={styles.links}>
|
||||
<li><a href="/" className={styles.link}>Search schools</a></li>
|
||||
<li><a href="/rankings" className={styles.link}>Rankings</a></li>
|
||||
<li><a href="/compare" className={styles.link}>Compare shortlist</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h4 className={styles.sectionTitle}>Resources</h4>
|
||||
<ul className={styles.links}>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.gov.uk/government/organisations/department-for-education"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
Department for Education
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.gov.uk/school-performance-tables"
|
||||
@@ -42,6 +41,16 @@ export function Footer() {
|
||||
School Performance Tables
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://reports.ofsted.gov.uk/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
Ofsted reports
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,32 +4,73 @@
|
||||
|
||||
.heroSection {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
padding-top: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-top: 2.5rem;
|
||||
}
|
||||
|
||||
.heroEyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-teal, #2d7d7d);
|
||||
background: rgba(45, 125, 125, 0.1);
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.heroEyebrowDot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-teal, #2d7d7d);
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
font-size: 2.5rem;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1a1612);
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 0.85rem;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.015em;
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
max-width: 840px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.heroEmph {
|
||||
color: var(--accent-coral-dark, #c45a3f);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.heroDescription {
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.05rem;
|
||||
color: var(--text-secondary, #5c564d);
|
||||
margin: 0 auto;
|
||||
max-width: 600px;
|
||||
margin: 0 auto 0.5rem;
|
||||
max-width: 680px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.heroDescription strong {
|
||||
color: var(--text-primary, #1a1612);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.heroSection {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
.heroTitle {
|
||||
font-size: 1.75rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.heroDescription {
|
||||
font-size: 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,26 +464,67 @@
|
||||
}
|
||||
|
||||
.discoverySection {
|
||||
padding: 2rem var(--page-padding, 2rem);
|
||||
padding: 0.5rem 0 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.discoveryCount {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.discoveryCount strong {
|
||||
color: var(--text-primary);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.discoveryHints {
|
||||
color: var(--text-muted);
|
||||
.nearMeRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.nearMeBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1.375rem;
|
||||
background: var(--accent-teal, #2d7d7d);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, transform 0.15s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.nearMeBtn:hover:not(:disabled) {
|
||||
background: #235f5f;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.nearMeBtn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.nearMeBtnSpinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.35);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: nearMeSpin 0.7s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes nearMeSpin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.geoError {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--accent-coral, #e07256);
|
||||
margin: 0;
|
||||
max-width: 340px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quickSearches {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -473,6 +555,550 @@
|
||||
border-color: var(--accent-coral);
|
||||
}
|
||||
|
||||
.exploringRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.exploringLabel {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted, #6d685f);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.exploringChips {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.exploringChip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 0.95rem;
|
||||
background: var(--bg-card, white);
|
||||
border: 1px solid var(--border-color, #e5dfd5);
|
||||
border-radius: 999px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #5c564d);
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.exploringChip:hover {
|
||||
border-color: var(--accent-coral, #e07256);
|
||||
color: var(--accent-coral-dark, #c45a3f);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.chipDot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.55;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── How it works section ─────────────────────────────── */
|
||||
|
||||
.howItWorks {
|
||||
padding: 3rem 0 1rem;
|
||||
}
|
||||
|
||||
.hiwHeader {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hiwHeading {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1a1612);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hiwSub {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted, #6d685f);
|
||||
}
|
||||
|
||||
.hiwGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.hiwCard {
|
||||
background: var(--bg-card, white);
|
||||
border: 1px solid var(--border-color, #e5dfd5);
|
||||
border-radius: 14px;
|
||||
padding: 1.25rem;
|
||||
box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.hiwVisual {
|
||||
background: var(--bg-secondary, #f3ede4);
|
||||
border-radius: 10px;
|
||||
padding: 0.9rem;
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.hiwPhaseBlock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.hiwPhaseLabel {
|
||||
font-size: 0.58rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted, #6d685f);
|
||||
}
|
||||
|
||||
.hiwPhaseLabel strong {
|
||||
color: var(--accent-teal, #2d7d7d);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hiwCardBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.hiwStep {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-coral, #e07256);
|
||||
}
|
||||
|
||||
.hiwTitle {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1a1612);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.hiwDesc {
|
||||
font-size: 0.86rem;
|
||||
color: var(--text-secondary, #5c564d);
|
||||
line-height: 1.45;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Mini cascade (performance card) */
|
||||
.miniCascade {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.miniCascadeCol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.miniSubj {
|
||||
font-size: 0.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted, #6d685f);
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.miniRowHead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.48rem;
|
||||
color: var(--text-muted, #6d685f);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.miniRowHead strong {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-size: 0.62rem;
|
||||
color: var(--text-primary, #1a1612);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.miniTrack {
|
||||
height: 5px;
|
||||
border-radius: 2px;
|
||||
background: rgba(45, 125, 125, 0.08);
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.miniBarExp {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: var(--accent-teal-light, #3a9e9e);
|
||||
}
|
||||
|
||||
.miniBarExc {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: var(--accent-teal, #2d7d7d);
|
||||
}
|
||||
|
||||
.miniNatPill {
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
transform: translateX(-50%);
|
||||
background: var(--accent-coral, #e07256);
|
||||
color: #fff;
|
||||
font-size: 0.4rem;
|
||||
font-weight: 700;
|
||||
padding: 0.05rem 0.2rem;
|
||||
border-radius: 3px;
|
||||
z-index: 2;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Attainment 8 mini bar */
|
||||
.att8Row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.att8BarWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.att8BarHead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.5rem;
|
||||
color: var(--text-muted, #6d685f);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.att8Track {
|
||||
height: 7px;
|
||||
background: rgba(45, 125, 125, 0.08);
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.att8Fill {
|
||||
height: 100%;
|
||||
background: var(--accent-teal, #2d7d7d);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.att8NatLine {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
width: 1.5px;
|
||||
background: rgba(224, 114, 86, 0.6);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.att8Score {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.att8Value {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-teal, #2d7d7d);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.att8Delta {
|
||||
font-size: 0.55rem;
|
||||
color: var(--accent-teal, #2d7d7d);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
/* Ofsted preview */
|
||||
.ofstedPreview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.4rem 0.2rem;
|
||||
}
|
||||
|
||||
.ofstedHead {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 1.5px solid var(--border-color, #e5dfd5);
|
||||
}
|
||||
|
||||
.ofstedBullet {
|
||||
display: block;
|
||||
width: 3px;
|
||||
height: 1em;
|
||||
background: var(--accent-coral, #e07256);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ofstedTitle {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1a1612);
|
||||
}
|
||||
|
||||
.ofstedBadge {
|
||||
align-self: flex-start;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(45, 125, 125, 0.12);
|
||||
color: var(--accent-teal, #2d7d7d);
|
||||
font-size: 0.55rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.ofstedVerdict {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: var(--text-primary, #1a1612);
|
||||
}
|
||||
|
||||
.ofstedVerdict em {
|
||||
color: var(--accent-teal, #2d7d7d);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.ofstedMeta {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-muted, #6d685f);
|
||||
}
|
||||
|
||||
/* Compare preview */
|
||||
.comparePreview {
|
||||
width: 100%;
|
||||
background: var(--bg-card, white);
|
||||
border: 1px solid var(--border-color, #e5dfd5);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.compareHead {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr repeat(2, 1fr);
|
||||
background: rgba(45, 125, 125, 0.1);
|
||||
padding: 0.35rem 0.5rem;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.compareHeadCell {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-teal, #2d7d7d);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.compareHeadLabel {
|
||||
font-family: inherit;
|
||||
font-size: 0.48rem;
|
||||
color: var(--text-muted, #6d685f);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.compareRow {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr repeat(2, 1fr);
|
||||
padding: 0.3rem 0.5rem;
|
||||
gap: 0.35rem;
|
||||
border-top: 1px solid var(--border-color, #e5dfd5);
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.compareRowLabel {
|
||||
font-size: 0.55rem;
|
||||
color: var(--text-muted, #6d685f);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.compareRowVal {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1a1612);
|
||||
}
|
||||
|
||||
.compareRowValHi {
|
||||
color: var(--accent-teal, #2d7d7d);
|
||||
}
|
||||
|
||||
.compareFoot {
|
||||
font-size: 0.52rem;
|
||||
color: var(--text-muted, #6d685f);
|
||||
padding: 0.35rem 0.5rem;
|
||||
background: var(--bg-secondary, #f3ede4);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--border-color, #e5dfd5);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hiwGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.hiwHeader {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.hiwHeading {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Editorial section ───────────────────────────────── */
|
||||
|
||||
.editorial {
|
||||
padding: 2rem 0 3rem;
|
||||
}
|
||||
|
||||
.editorialGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr;
|
||||
gap: 2rem;
|
||||
padding: 1.75rem;
|
||||
background: var(--bg-card, white);
|
||||
border: 1px solid var(--border-color, #e5dfd5);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.editorialText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.editorialKicker {
|
||||
font-size: 0.68rem;
|
||||
color: var(--accent-coral, #e07256);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.editorialHeading {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1a1612);
|
||||
margin: 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.editorialText p {
|
||||
font-size: 0.92rem;
|
||||
color: var(--text-secondary, #5c564d);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.factbox {
|
||||
background: var(--bg-secondary, #f3ede4);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.factboxHeading {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1a1612);
|
||||
margin: 0 0 0.85rem;
|
||||
}
|
||||
|
||||
.factRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--border-color, #e5dfd5);
|
||||
font-size: 0.85rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.factRow:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.factKey {
|
||||
color: var(--text-muted, #6d685f);
|
||||
}
|
||||
|
||||
.factVal {
|
||||
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1a1612);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.editorialGrid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 1.25rem;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.resultsHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -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,15 +197,212 @@ 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 */}
|
||||
|
||||
Reference in New Issue
Block a user