feat(admissions): add admissions guide page and homepage countdown strip
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 14s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 51s
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 14s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 51s
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
- New /admissions route with AdmissionsView client component - Live countdowns (days until) to Primary/Secondary deadlines and Offer Days - Step-by-step timelines for both tracks with highlighted milestone rows - Tips section covering equal preference rule, late applications, waiting lists - Homepage countdown strip (4 cards) between discovery chips and how-it-works - Admissions nav link and footer link added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import styles from './AdmissionsView.module.css';
|
||||
|
||||
/* ─── Date helpers ─────────────────────────────────────── */
|
||||
|
||||
function daysUntil(month: number, day: number): number {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const y = today.getFullYear();
|
||||
let target = new Date(y, month - 1, day);
|
||||
if (target <= today) target = new Date(y + 1, month - 1, day);
|
||||
return Math.round((target.getTime() - today.getTime()) / 86_400_000);
|
||||
}
|
||||
|
||||
function nextDate(month: number, day: number): Date {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const y = today.getFullYear();
|
||||
const target = new Date(y, month - 1, day);
|
||||
if (target <= today) return new Date(y + 1, month - 1, day);
|
||||
return target;
|
||||
}
|
||||
|
||||
function fmtDate(month: number, day: number): string {
|
||||
return nextDate(month, day).toLocaleDateString('en-GB', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
/* ─── Data ─────────────────────────────────────────────── */
|
||||
|
||||
interface Chip {
|
||||
type: 'deadline' | 'offer';
|
||||
track: string;
|
||||
milestone: string;
|
||||
month: number;
|
||||
day: number;
|
||||
}
|
||||
|
||||
const CHIPS: Chip[] = [
|
||||
{ type: 'offer', track: 'Primary · Offer Day', milestone: 'Primary National Offer Day', month: 4, day: 16 },
|
||||
{ type: 'deadline', track: 'Secondary · Deadline', milestone: 'Secondary applications close', month: 10, day: 31 },
|
||||
{ type: 'deadline', track: 'Primary · Deadline', milestone: 'Primary applications close', month: 1, day: 15 },
|
||||
{ type: 'offer', track: 'Secondary · Offer Day', milestone: 'Secondary National Offer Day', month: 3, day: 1 },
|
||||
];
|
||||
|
||||
interface Step {
|
||||
date?: string;
|
||||
title: string;
|
||||
body: string;
|
||||
highlight?: 'deadline' | 'offer';
|
||||
}
|
||||
|
||||
const SECONDARY_STEPS: Step[] = [
|
||||
{
|
||||
title: 'Check entry criteria',
|
||||
body: 'Look at each school\'s admissions policy — catchment areas, faith criteria, sibling priority, and aptitude tests vary widely. Use school detail pages on SchoolCompare for admissions history.',
|
||||
},
|
||||
{
|
||||
date: 'September',
|
||||
title: 'Portal opens',
|
||||
body: 'Your local council opens its online admissions portal. Register early to avoid last-minute technical issues. You apply through your home council even if you prefer schools in neighbouring boroughs.',
|
||||
},
|
||||
{
|
||||
date: '31 October',
|
||||
title: 'Application deadline',
|
||||
body: 'Submit your ranked list of up to six schools. Councils treat all preferences equally — list schools in the genuine order you want them, not strategically.',
|
||||
highlight: 'deadline',
|
||||
},
|
||||
{
|
||||
date: '1 March',
|
||||
title: 'National Offer Day',
|
||||
body: 'Results are published online, usually from 12:01 am. You\'ll receive an email or letter with your allocated school.',
|
||||
highlight: 'offer',
|
||||
},
|
||||
{
|
||||
date: '~15 March',
|
||||
title: 'Accept or decline',
|
||||
body: 'Respond by the deadline your council gives — typically around 15 March. Accepting does not prevent you from keeping a place on a waiting list for a preferred school.',
|
||||
},
|
||||
{
|
||||
title: 'Appeals',
|
||||
body: 'If unsuccessful, you can appeal within 20 school days of the refusal letter. Secondary appeals consider whether prejudice to the school outweighs your case — success rates vary.',
|
||||
},
|
||||
];
|
||||
|
||||
const PRIMARY_STEPS: Step[] = [
|
||||
{
|
||||
title: 'Research entry criteria',
|
||||
body: 'Faith schools, language units, and distance-based catchments differ by school. Start by reading each school\'s admissions policy on their website or the council\'s website.',
|
||||
},
|
||||
{
|
||||
date: 'September',
|
||||
title: 'Portal opens',
|
||||
body: 'Apply through your home council\'s portal, even if your preferred school is in another borough. Most councils accept applications from September.',
|
||||
},
|
||||
{
|
||||
date: '15 January',
|
||||
title: 'Application deadline',
|
||||
body: 'List up to 3–6 schools (the number varies by council) in genuine preference order. The equal preference rule means all preferences are considered before any offers are made.',
|
||||
highlight: 'deadline',
|
||||
},
|
||||
{
|
||||
date: '16 April',
|
||||
title: 'National Offer Day',
|
||||
body: 'Results are published online. Reception offers are sent on 16 April (or the next working day if that falls on a weekend or bank holiday).',
|
||||
highlight: 'offer',
|
||||
},
|
||||
{
|
||||
date: '~1 May',
|
||||
title: 'Accept or decline',
|
||||
body: 'Respond by your council\'s deadline, typically around 1 May. Accepting secures the place while you wait to see if a preferred school\'s waiting list moves.',
|
||||
},
|
||||
{
|
||||
title: 'Appeals',
|
||||
body: 'Infant class-size appeals (Reception to Year 2) have a very narrow legal test and a low success rate. For Year 3+, appeals follow the same process as secondary.',
|
||||
},
|
||||
];
|
||||
|
||||
interface Tip {
|
||||
heading: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
const TIPS: Tip[] = [
|
||||
{
|
||||
heading: 'Equal preference rule',
|
||||
body: 'Councils rank offers by your eligibility for each school, not by the order you listed them. You cannot game the system — put schools in the order you actually want them.',
|
||||
},
|
||||
{
|
||||
heading: 'Late applications go to the back',
|
||||
body: 'Submit before the deadline even if your child does not turn the required age until later in the year. Late applicants are only considered after all on-time applications.',
|
||||
},
|
||||
{
|
||||
heading: 'Waiting lists',
|
||||
body: 'You can go on waiting lists for multiple schools simultaneously. Lists are ordered by admissions criteria, not when you joined. They can move significantly over the summer.',
|
||||
},
|
||||
];
|
||||
|
||||
/* ─── Component ────────────────────────────────────────── */
|
||||
|
||||
export function AdmissionsView() {
|
||||
const [chipDays, setChipDays] = useState<(number | null)[]>(CHIPS.map(() => null));
|
||||
|
||||
useEffect(() => {
|
||||
setChipDays(CHIPS.map(c => daysUntil(c.month, c.day)));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
|
||||
{/* Hero */}
|
||||
<section className={styles.hero}>
|
||||
<span className={styles.eyebrow}>
|
||||
<span className={styles.eyebrowDot} aria-hidden="true" />
|
||||
England · Primary & Secondary
|
||||
</span>
|
||||
<h1 className={styles.heroTitle}>School Admissions Guide</h1>
|
||||
<p className={styles.heroSub}>
|
||||
Everything parents need to know about applying for a school place in England — from opening dates to National Offer Day, with live countdowns to every key milestone.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Countdown strip */}
|
||||
<section className={styles.countdownSection}>
|
||||
<div className={styles.stripHeader}>
|
||||
<span className={styles.stripLabel}>Days until next milestone</span>
|
||||
</div>
|
||||
<div className={styles.countdownRail}>
|
||||
{CHIPS.map((chip, i) => {
|
||||
const days = chipDays[i];
|
||||
const isUrgent = days !== null && days <= 14;
|
||||
const chipClass = [
|
||||
styles.chip,
|
||||
chip.type === 'deadline' ? styles.chipDeadline : styles.chipOffer,
|
||||
isUrgent ? styles.chipUrgent : '',
|
||||
].filter(Boolean).join(' ');
|
||||
return (
|
||||
<div key={chip.milestone} className={chipClass}>
|
||||
<span className={[styles.chipTrack, chip.type === 'deadline' ? styles.chipTrackDeadline : styles.chipTrackOffer].join(' ')}>
|
||||
<span className={styles.chipTrackDot} aria-hidden="true" />
|
||||
{chip.track}
|
||||
</span>
|
||||
<div>
|
||||
<span className={styles.chipDays}>{days ?? '—'}</span>
|
||||
{days !== null && <span className={styles.chipDaysUnit}>days</span>}
|
||||
</div>
|
||||
<div className={styles.chipMilestone}>{chip.milestone}</div>
|
||||
<div className={styles.chipDate}>{days !== null ? fmtDate(chip.month, chip.day) : ''}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Secondary track */}
|
||||
<section className={styles.track}>
|
||||
<div className={styles.trackHeader}>
|
||||
<div className={styles.trackHeaderLeft}>
|
||||
<span className={styles.trackKicker}>Year 7 entry</span>
|
||||
<h2 className={styles.trackTitle}>Secondary school admissions</h2>
|
||||
<p className={styles.trackSub}>For children starting secondary school (Year 7) in September. Applications are submitted in the autumn of Year 6.</p>
|
||||
</div>
|
||||
<div className={styles.trackDates}>
|
||||
<div className={styles.trackDateRow}>
|
||||
<span className={styles.trackDateLabel}>Deadline</span>
|
||||
<span className={styles.trackDateVal}>{fmtDate(10, 31)}</span>
|
||||
</div>
|
||||
<div className={styles.trackDateRow}>
|
||||
<span className={styles.trackDateLabel}>Offer Day</span>
|
||||
<span className={styles.trackDateVal}>{fmtDate(3, 1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ol className={styles.timeline}>
|
||||
{SECONDARY_STEPS.map((step, i) => (
|
||||
<li key={i} className={[styles.step, step.highlight === 'deadline' ? styles.stepDeadline : step.highlight === 'offer' ? styles.stepOffer : ''].filter(Boolean).join(' ')}>
|
||||
<div className={styles.stepDotCol}>
|
||||
<div className={styles.stepDot} />
|
||||
{i < SECONDARY_STEPS.length - 1 && <div className={styles.stepLine} />}
|
||||
</div>
|
||||
<div className={styles.stepContent}>
|
||||
{step.date && <div className={styles.stepDate}>{step.date}</div>}
|
||||
<div className={styles.stepTitle}>{step.title}</div>
|
||||
<p className={styles.stepBody}>{step.body}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* Primary track */}
|
||||
<section className={styles.track}>
|
||||
<div className={styles.trackHeader}>
|
||||
<div className={styles.trackHeaderLeft}>
|
||||
<span className={styles.trackKicker}>Reception entry</span>
|
||||
<h2 className={styles.trackTitle}>Primary school admissions</h2>
|
||||
<p className={styles.trackSub}>For children starting Reception (Year R) in September. Applications are submitted in the autumn of the year before entry.</p>
|
||||
</div>
|
||||
<div className={styles.trackDates}>
|
||||
<div className={styles.trackDateRow}>
|
||||
<span className={styles.trackDateLabel}>Deadline</span>
|
||||
<span className={styles.trackDateVal}>{fmtDate(1, 15)}</span>
|
||||
</div>
|
||||
<div className={styles.trackDateRow}>
|
||||
<span className={styles.trackDateLabel}>Offer Day</span>
|
||||
<span className={styles.trackDateVal}>{fmtDate(4, 16)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ol className={styles.timeline}>
|
||||
{PRIMARY_STEPS.map((step, i) => (
|
||||
<li key={i} className={[styles.step, step.highlight === 'deadline' ? styles.stepDeadline : step.highlight === 'offer' ? styles.stepOffer : ''].filter(Boolean).join(' ')}>
|
||||
<div className={styles.stepDotCol}>
|
||||
<div className={styles.stepDot} />
|
||||
{i < PRIMARY_STEPS.length - 1 && <div className={styles.stepLine} />}
|
||||
</div>
|
||||
<div className={styles.stepContent}>
|
||||
{step.date && <div className={styles.stepDate}>{step.date}</div>}
|
||||
<div className={styles.stepTitle}>{step.title}</div>
|
||||
<p className={styles.stepBody}>{step.body}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* Tips */}
|
||||
<section className={styles.tips}>
|
||||
<h2 className={styles.tipsHeading}>Three things most parents get wrong</h2>
|
||||
<div className={styles.tipsGrid}>
|
||||
{TIPS.map((tip, i) => (
|
||||
<div key={i} className={styles.tipCard}>
|
||||
<div className={styles.tipNumber}>{String(i + 1).padStart(2, '0')}</div>
|
||||
<h3 className={styles.tipHeading}>{tip.heading}</h3>
|
||||
<p className={styles.tipBody}>{tip.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user