diff --git a/nextjs-app/app/admissions/page.tsx b/nextjs-app/app/admissions/page.tsx new file mode 100644 index 0000000..c4cad97 --- /dev/null +++ b/nextjs-app/app/admissions/page.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from 'next'; +import { AdmissionsView } from '@/components/AdmissionsView'; + +export const dynamic = 'force-static'; + +export const metadata: Metadata = { + title: 'School Admissions Guide', + description: + 'Understand the Primary and Secondary school admissions process in England, with live countdowns to every key deadline and National Offer Day.', +}; + +export default function AdmissionsPage() { + return ; +} diff --git a/nextjs-app/components/AdmissionsView.module.css b/nextjs-app/components/AdmissionsView.module.css new file mode 100644 index 0000000..6460ee6 --- /dev/null +++ b/nextjs-app/components/AdmissionsView.module.css @@ -0,0 +1,426 @@ +.page { + max-width: 900px; + margin: 0 auto; + padding: 0 1.25rem 4rem; +} + +/* ─── Hero ───────────────────────────────────────────── */ + +.hero { + text-align: center; + padding: 3rem 0 2rem; +} + +.eyebrow { + 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; +} + +.eyebrowDot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent-teal, #2d7d7d); +} + +.heroTitle { + font-family: var(--font-playfair), 'Playfair Display', serif; + font-size: 2.75rem; + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.015em; + color: var(--text-primary, #1a1612); + margin-bottom: 0.85rem; +} + +.heroSub { + font-size: 1.05rem; + color: var(--text-secondary, #5c564d); + max-width: 640px; + margin: 0 auto; + line-height: 1.55; +} + +/* ─── Countdown strip ────────────────────────────────── */ + +.countdownSection { + padding: 0 0 2.5rem; + border-bottom: 1px solid var(--border-color, #e5dfd5); + margin-bottom: 2.5rem; +} + +.stripHeader { + margin-bottom: 0.85rem; +} + +.stripLabel { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted, #6d685f); +} + +.countdownRail { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.75rem; +} + +.chip { + background: var(--bg-card, #fff); + border: 1px solid var(--border-color, #e5dfd5); + border-radius: 12px; + padding: 1rem 1.1rem 0.9rem; + box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06); + display: flex; + flex-direction: column; + gap: 0.2rem; + position: relative; + overflow: hidden; +} + +.chip::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + border-radius: 12px 12px 0 0; +} + +.chipDeadline::before { background: var(--accent-coral, #e07256); } +.chipOffer::before { background: var(--accent-teal, #2d7d7d); } + +.chipUrgent { + border-color: rgba(224, 114, 86, 0.4); + background: rgba(224, 114, 86, 0.04); +} + +.chipTrack { + display: flex; + align-items: center; + gap: 0.3rem; + font-size: 0.6rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + margin-bottom: 0.15rem; +} + +.chipTrackDeadline { color: var(--accent-coral, #e07256); } +.chipTrackOffer { color: var(--accent-teal, #2d7d7d); } + +.chipTrackDot { + width: 5px; + height: 5px; + border-radius: 50%; + background: currentColor; + flex-shrink: 0; +} + +.chipDays { + font-family: var(--font-playfair), 'Playfair Display', serif; + font-size: 2.6rem; + font-weight: 700; + line-height: 1; + letter-spacing: -0.02em; +} + +.chipDeadline .chipDays, +.chipUrgent .chipDays { + color: var(--accent-coral-dark, #c45a3f); +} + +.chipOffer .chipDays { + color: var(--accent-teal, #2d7d7d); +} + +.chipDaysUnit { + font-size: 0.78rem; + font-weight: 500; + color: var(--text-muted, #6d685f); + margin-left: 0.2rem; + vertical-align: bottom; + line-height: 2; +} + +.chipMilestone { + font-size: 0.85rem; + font-weight: 600; + color: var(--text-primary, #1a1612); + line-height: 1.25; + margin-top: 0.1rem; +} + +.chipDate { + font-size: 0.75rem; + color: var(--text-muted, #6d685f); + margin-top: 0.05rem; +} + +/* ─── Track (Secondary / Primary) ────────────────────── */ + +.track { + margin-bottom: 3rem; + background: var(--bg-card, #fff); + border: 1px solid var(--border-color, #e5dfd5); + border-radius: 14px; + overflow: hidden; +} + +.trackHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1.5rem; + padding: 1.75rem 2rem 1.5rem; + border-bottom: 1px solid var(--border-color, #e5dfd5); + flex-wrap: wrap; +} + +.trackHeaderLeft { + flex: 1; + min-width: 0; +} + +.trackKicker { + display: block; + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--accent-coral, #e07256); + margin-bottom: 0.35rem; +} + +.trackTitle { + font-family: var(--font-playfair), 'Playfair Display', serif; + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary, #1a1612); + margin-bottom: 0.4rem; + line-height: 1.2; +} + +.trackSub { + font-size: 0.9rem; + color: var(--text-secondary, #5c564d); + line-height: 1.5; + margin: 0; +} + +.trackDates { + display: flex; + flex-direction: column; + gap: 0.5rem; + flex-shrink: 0; + text-align: right; +} + +.trackDateRow { + display: flex; + flex-direction: column; + gap: 0.05rem; +} + +.trackDateLabel { + font-size: 0.62rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted, #6d685f); +} + +.trackDateVal { + font-size: 0.88rem; + font-weight: 600; + color: var(--text-primary, #1a1612); +} + +/* ─── Timeline ───────────────────────────────────────── */ + +.timeline { + list-style: none; + padding: 1.5rem 2rem 1.75rem; + margin: 0; + display: flex; + flex-direction: column; + gap: 0; +} + +.step { + display: flex; + gap: 1rem; + align-items: flex-start; +} + +.stepDotCol { + display: flex; + flex-direction: column; + align-items: center; + flex-shrink: 0; + width: 20px; + padding-top: 0.15rem; +} + +.stepDot { + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--bg-card, #fff); + border: 2px solid var(--border-color, #e5dfd5); + flex-shrink: 0; + transition: border-color 0.2s ease, background 0.2s ease; +} + +.stepDeadline .stepDot { + background: rgba(224, 114, 86, 0.15); + border-color: var(--accent-coral, #e07256); +} + +.stepOffer .stepDot { + background: rgba(45, 125, 125, 0.15); + border-color: var(--accent-teal, #2d7d7d); +} + +.stepLine { + width: 2px; + flex: 1; + min-height: 24px; + background: var(--border-color, #e5dfd5); + margin: 4px 0; +} + +.stepContent { + padding-bottom: 1.5rem; + flex: 1; + min-width: 0; +} + +.step:last-child .stepContent { + padding-bottom: 0; +} + +.stepDate { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-muted, #6d685f); + margin-bottom: 0.2rem; +} + +.stepDeadline .stepDate { color: var(--accent-coral, #e07256); } +.stepOffer .stepDate { color: var(--accent-teal, #2d7d7d); } + +.stepTitle { + font-size: 0.95rem; + font-weight: 700; + color: var(--text-primary, #1a1612); + margin-bottom: 0.3rem; + line-height: 1.3; +} + +.stepDeadline .stepTitle { color: var(--accent-coral-dark, #c45a3f); } +.stepOffer .stepTitle { color: var(--accent-teal, #2d7d7d); } + +.stepBody { + font-size: 0.88rem; + color: var(--text-secondary, #5c564d); + line-height: 1.55; + margin: 0; +} + +/* ─── Tips ───────────────────────────────────────────── */ + +.tips { + margin-top: 1rem; +} + +.tipsHeading { + font-family: var(--font-playfair), 'Playfair Display', serif; + font-size: 1.35rem; + font-weight: 700; + color: var(--text-primary, #1a1612); + margin-bottom: 1.25rem; +} + +.tipsGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; +} + +.tipCard { + background: var(--bg-secondary, #f3ede4); + border-radius: 12px; + padding: 1.25rem; +} + +.tipNumber { + font-family: var(--font-playfair), 'Playfair Display', serif; + font-size: 1.75rem; + font-weight: 700; + color: var(--border-color, #e5dfd5); + line-height: 1; + margin-bottom: 0.6rem; +} + +.tipHeading { + font-size: 0.95rem; + font-weight: 700; + color: var(--text-primary, #1a1612); + margin-bottom: 0.4rem; + line-height: 1.3; +} + +.tipBody { + font-size: 0.875rem; + color: var(--text-secondary, #5c564d); + line-height: 1.55; + margin: 0; +} + +/* ─── Responsive ─────────────────────────────────────── */ + +@media (max-width: 768px) { + .heroTitle { + font-size: 2rem; + } + + .countdownRail { + grid-template-columns: repeat(2, 1fr); + } + + .trackHeader { + flex-direction: column; + padding: 1.25rem 1.25rem 1rem; + } + + .trackDates { + text-align: left; + flex-direction: row; + gap: 1.5rem; + } + + .timeline { + padding: 1.25rem; + } + + .tipsGrid { + grid-template-columns: 1fr; + } +} diff --git a/nextjs-app/components/AdmissionsView.tsx b/nextjs-app/components/AdmissionsView.tsx new file mode 100644 index 0000000..d8aaa03 --- /dev/null +++ b/nextjs-app/components/AdmissionsView.tsx @@ -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 ( +
+ + {/* Hero */} +
+ + +

School Admissions Guide

+

+ 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. +

+
+ + {/* Countdown strip */} +
+
+ Days until next milestone +
+
+ {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 ( +
+ + +
+ {days ?? '—'} + {days !== null && days} +
+
{chip.milestone}
+
{days !== null ? fmtDate(chip.month, chip.day) : ''}
+
+ ); + })} +
+
+ + {/* Secondary track */} +
+
+
+ Year 7 entry +

Secondary school admissions

+

For children starting secondary school (Year 7) in September. Applications are submitted in the autumn of Year 6.

+
+
+
+ Deadline + {fmtDate(10, 31)} +
+
+ Offer Day + {fmtDate(3, 1)} +
+
+
+ +
    + {SECONDARY_STEPS.map((step, i) => ( +
  1. +
    +
    + {i < SECONDARY_STEPS.length - 1 &&
    } +
    +
    + {step.date &&
    {step.date}
    } +
    {step.title}
    +

    {step.body}

    +
    +
  2. + ))} +
+
+ + {/* Primary track */} +
+
+
+ Reception entry +

Primary school admissions

+

For children starting Reception (Year R) in September. Applications are submitted in the autumn of the year before entry.

+
+
+
+ Deadline + {fmtDate(1, 15)} +
+
+ Offer Day + {fmtDate(4, 16)} +
+
+
+ +
    + {PRIMARY_STEPS.map((step, i) => ( +
  1. +
    +
    + {i < PRIMARY_STEPS.length - 1 &&
    } +
    +
    + {step.date &&
    {step.date}
    } +
    {step.title}
    +

    {step.body}

    +
    +
  2. + ))} +
+
+ + {/* Tips */} +
+

Three things most parents get wrong

+
+ {TIPS.map((tip, i) => ( +
+
{String(i + 1).padStart(2, '0')}
+

{tip.heading}

+

{tip.body}

+
+ ))} +
+
+ +
+ ); +} diff --git a/nextjs-app/components/Footer.tsx b/nextjs-app/components/Footer.tsx index c327bf2..be11755 100644 --- a/nextjs-app/components/Footer.tsx +++ b/nextjs-app/components/Footer.tsx @@ -25,6 +25,7 @@ export function Footer() {
  • Search schools
  • Rankings
  • Compare shortlist
  • +
  • Admissions guide
  • diff --git a/nextjs-app/components/HomeView.module.css b/nextjs-app/components/HomeView.module.css index 05503a5..6627b8a 100644 --- a/nextjs-app/components/HomeView.module.css +++ b/nextjs-app/components/HomeView.module.css @@ -1173,3 +1173,156 @@ .loadMoreButton { min-width: 160px; } + +/* ========================================================= + Admissions Countdown Strip + ========================================================= */ + +.admissionsStrip { + padding: 1.5rem 0 2rem; + border-top: 1px solid var(--border-color, #e5dfd5); +} + +.stripHeader { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 1rem; + flex-wrap: wrap; + gap: 0.5rem; +} + +.stripLabel { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted, #6d685f); +} + +.stripCta { + font-size: 0.82rem; + color: var(--accent-teal, #2d7d7d); + font-weight: 600; + text-decoration: none; +} + +.stripCta:hover { + text-decoration: underline; +} + +.countdownRail { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.75rem; +} + +.countdownChip { + background: var(--bg-card, #fff); + border: 1px solid var(--border-color, #e5dfd5); + border-radius: 12px; + padding: 1rem 1.1rem 0.9rem; + box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06); + display: flex; + flex-direction: column; + gap: 0.2rem; + position: relative; + overflow: hidden; +} + +.countdownChip::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + border-radius: 12px 12px 0 0; +} + +.countdownChipDeadline::before { + background: var(--accent-coral, #e07256); +} + +.countdownChipOffer::before { + background: var(--accent-teal, #2d7d7d); +} + +.countdownChipUrgent { + border-color: rgba(224, 114, 86, 0.4); + background: rgba(224, 114, 86, 0.04); +} + +.chipTrack { + display: flex; + align-items: center; + gap: 0.3rem; + font-size: 0.6rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + margin-bottom: 0.15rem; +} + +.chipTrackDeadline { + color: var(--accent-coral, #e07256); +} + +.chipTrackOffer { + color: var(--accent-teal, #2d7d7d); +} + +.chipTrackDot { + width: 5px; + height: 5px; + border-radius: 50%; + background: currentColor; + flex-shrink: 0; +} + +.chipDays { + font-family: var(--font-playfair), 'Playfair Display', serif; + font-size: 2.6rem; + font-weight: 700; + line-height: 1; + letter-spacing: -0.02em; +} + +.countdownChipDeadline .chipDays, +.countdownChipUrgent .chipDays { + color: var(--accent-coral-dark, #c45a3f); +} + +.countdownChipOffer .chipDays { + color: var(--accent-teal, #2d7d7d); +} + +.chipDaysUnit { + font-family: 'DM Sans', sans-serif; + font-size: 0.78rem; + font-weight: 500; + color: var(--text-muted, #6d685f); + margin-left: 0.2rem; + vertical-align: bottom; + line-height: 2; +} + +.chipMilestone { + font-size: 0.85rem; + font-weight: 600; + color: var(--text-primary, #1a1612); + line-height: 1.25; + margin-top: 0.1rem; +} + +.chipDate { + font-size: 0.75rem; + color: var(--text-muted, #6d685f); + margin-top: 0.05rem; +} + +@media (max-width: 768px) { + .countdownRail { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/nextjs-app/components/HomeView.tsx b/nextjs-app/components/HomeView.tsx index aedb624..3a017f7 100644 --- a/nextjs-app/components/HomeView.tsx +++ b/nextjs-app/components/HomeView.tsx @@ -24,6 +24,39 @@ interface HomeViewProps { totalSchools?: number | null; } +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 formatCountdownDate(month: number, day: number): string { + 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 target.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'long', year: 'numeric' }); +} + +interface CountdownChipData { + type: 'deadline' | 'offer'; + track: string; + milestone: string; + month: number; + day: number; +} + +const ADMISSIONS_CHIPS: CountdownChipData[] = [ + { 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 }, +]; + export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProps) { const searchParams = useSearchParams(); const router = useRouter(); @@ -43,6 +76,7 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp const prevSearchParamsRef = useRef(searchParams.toString()); const [geoState, setGeoState] = useState<'idle' | 'requesting' | 'error'>('idle'); const [geoError, setGeoError] = useState(null); + const [chipDays, setChipDays] = useState<(number | null)[]>(ADMISSIONS_CHIPS.map(() => null)); const hasSearch = searchParams.get('search') || searchParams.get('postcode'); const isLocationSearch = !!searchParams.get('postcode'); @@ -100,6 +134,11 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp .catch(() => {}); }, []); + // Compute admissions countdown days client-side to avoid SSR mismatch + useEffect(() => { + setChipDays(ADMISSIONS_CHIPS.map(c => daysUntil(c.month, c.day))); + }, []); + const handleLoadMore = async () => { if (isLoadingMore || !hasMore) return; setIsLoadingMore(true); @@ -240,6 +279,47 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp )} + {/* Admissions countdown strip — only on landing page */} + {!isSearchActive && ( +
    +
    + Key admissions deadlines + Full admissions guide → +
    +
    + {ADMISSIONS_CHIPS.map((chip, i) => { + const days = chipDays[i]; + const isUrgent = days !== null && days <= 14; + const chipClass = [ + styles.countdownChip, + chip.type === 'deadline' ? styles.countdownChipDeadline : styles.countdownChipOffer, + isUrgent ? styles.countdownChipUrgent : '', + ].filter(Boolean).join(' '); + const trackClass = [ + styles.chipTrack, + chip.type === 'deadline' ? styles.chipTrackDeadline : styles.chipTrackOffer, + ].join(' '); + return ( +
    + + +
    + {days ?? '—'} + {days !== null && days} +
    +
    {chip.milestone}
    +
    + {days !== null ? formatCountdownDate(chip.month, chip.day) : ''} +
    +
    + ); + })} +
    +
    + )} + {/* How it works — only on landing page */} {!isSearchActive && (
    diff --git a/nextjs-app/components/Navigation.tsx b/nextjs-app/components/Navigation.tsx index 4c33a8c..0c4a18d 100644 --- a/nextjs-app/components/Navigation.tsx +++ b/nextjs-app/components/Navigation.tsx @@ -59,6 +59,12 @@ export function Navigation() { > Rankings + + Admissions +