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

- 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:
Tudor Sitaru
2026-04-15 17:00:21 +01:00
parent 3327728df0
commit f6b9d650f8
7 changed files with 971 additions and 0 deletions
+80
View File
@@ -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<string | null>(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
</div>
)}
{/* Admissions countdown strip — only on landing page */}
{!isSearchActive && (
<section className={styles.admissionsStrip}>
<div className={styles.stripHeader}>
<span className={styles.stripLabel}>Key admissions deadlines</span>
<a href="/admissions" className={styles.stripCta}>Full admissions guide </a>
</div>
<div className={styles.countdownRail}>
{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 (
<div key={chip.milestone} className={chipClass}>
<span className={trackClass}>
<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 ? formatCountdownDate(chip.month, chip.day) : ''}
</div>
</div>
);
})}
</div>
</section>
)}
{/* How it works — only on landing page */}
{!isSearchActive && (
<section className={styles.howItWorks}>