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:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user