feat: gate registration behind 16 March 2026 19:00 opening date

- Add REGISTRATION_OPENS_AT const in lib/opening.ts as single source of truth
- Add useRegistrationOpen hook with live 1s countdown ticker
- Add CountdownBanner component (DD/HH/MM/SS) with canvas-confetti on open
- EventRegistrationForm shows countdown instead of form while closed
- Login page hides/disables signup while closed; login always available
- WatcherForm AccountModal guards signUp call as defence-in-depth

Closes #2
This commit is contained in:
2026-03-10 13:18:30 +01:00
parent 2f0dc46469
commit cf47f25a4d
8 changed files with 305 additions and 58 deletions

View File

@@ -0,0 +1,45 @@
import { useEffect, useState } from "react";
import { REGISTRATION_OPENS_AT } from "./opening";
interface RegistrationOpenState {
isOpen: boolean;
days: number;
hours: number;
minutes: number;
seconds: number;
}
function compute(): RegistrationOpenState {
const diff = REGISTRATION_OPENS_AT.getTime() - Date.now();
if (diff <= 0) {
return { isOpen: true, days: 0, hours: 0, minutes: 0, seconds: 0 };
}
const totalSeconds = Math.floor(diff / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return { isOpen: false, days, hours, minutes, seconds };
}
/**
* Returns live countdown state to {@link REGISTRATION_OPENS_AT}.
* Ticks every second; clears the interval once registration is open.
*/
export function useRegistrationOpen(): RegistrationOpenState {
const [state, setState] = useState<RegistrationOpenState>(compute);
useEffect(() => {
if (state.isOpen) return;
const id = setInterval(() => {
const next = compute();
setState(next);
if (next.isOpen) clearInterval(id);
}, 1000);
return () => clearInterval(id);
}, [state.isOpen]);
return state;
}