Files
kunstenkamp/apps/web/src/components/homepage/CountdownBanner.tsx
2026-03-11 10:45:40 +01:00

261 lines
7.2 KiB
TypeScript

import { useState } from "react";
import { REGISTRATION_OPENS_AT } from "@/lib/opening";
import { useRegistrationOpen } from "@/lib/useRegistrationOpen";
import { client } from "@/utils/orpc";
function pad(n: number): string {
return String(n).padStart(2, "0");
}
interface UnitBoxProps {
value: string;
label: string;
}
function UnitBox({ value, label }: UnitBoxProps) {
return (
<div className="flex flex-col items-center gap-1">
<div className="flex w-14 items-center justify-center rounded-sm bg-white/10 px-2 py-2 font-['Intro',sans-serif] text-3xl text-white tabular-nums sm:w-auto sm:px-4 sm:py-3 sm:text-5xl md:text-6xl lg:text-7xl">
{value}
</div>
<span className="font-['Intro',sans-serif] text-[9px] text-white/50 uppercase tracking-widest sm:text-xs">
{label}
</span>
</div>
);
}
// Reminder opt-in form — sits at the very bottom of the banner
function ReminderForm() {
const [email, setEmail] = useState("");
const [status, setStatus] = useState<
"idle" | "loading" | "subscribed" | "already_subscribed" | "error"
>("idle");
const [errorMessage, setErrorMessage] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!email || status === "loading") return;
setStatus("loading");
try {
const result = await client.subscribeReminder({ email });
if (!result.ok && result.reason === "already_open") {
setStatus("idle");
return;
}
setStatus("subscribed");
} catch (err) {
setErrorMessage(err instanceof Error ? err.message : "Er ging iets mis.");
setStatus("error");
}
}
const reminderTime = REGISTRATION_OPENS_AT.toLocaleTimeString("nl-BE", {
hour: "2-digit",
minute: "2-digit",
});
return (
<div
className="w-full"
style={{
animation: "reminderFadeIn 0.6s ease both",
animationDelay: "0.3s",
}}
>
<style>{`
@keyframes reminderFadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes reminderCheck {
from { opacity: 0; transform: scale(0.7); }
to { opacity: 1; transform: scale(1); }
}
`}</style>
{/* Hairline divider */}
<div
className="mx-auto mb-6 h-px w-24"
style={{
background:
"linear-gradient(to right, transparent, rgba(255,255,255,0.15), transparent)",
}}
/>
{status === "subscribed" || status === "already_subscribed" ? (
<div
className="flex flex-col items-center gap-2"
style={{
animation: "reminderCheck 0.4s cubic-bezier(0.34,1.56,0.64,1) both",
}}
>
{/* Checkmark glyph */}
<div
className="flex h-8 w-8 items-center justify-center rounded-full"
style={{
background: "rgba(255,255,255,0.12)",
border: "1px solid rgba(255,255,255,0.2)",
}}
>
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
aria-label="Vinkje"
role="img"
>
<path
d="M2.5 7L5.5 10L11.5 4"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<p
className="font-['Intro',sans-serif] text-sm tracking-wide"
style={{ color: "rgba(255,255,255,0.65)" }}
>
Herinnering ingepland voor {reminderTime}
</p>
</div>
) : (
<div className="flex flex-col items-center gap-3">
<p
className="font-['Intro',sans-serif] text-xs uppercase tracking-widest"
style={{ color: "rgba(255,255,255,0.35)", letterSpacing: "0.14em" }}
>
Herinnering ontvangen?
</p>
{/* Fused pill input + button */}
<form
onSubmit={handleSubmit}
className="flex w-full max-w-xs overflow-hidden"
style={{
borderRadius: "3px",
border: "1px solid rgba(255,255,255,0.18)",
background: "rgba(255,255,255,0.07)",
backdropFilter: "blur(6px)",
}}
>
<input
type="email"
required
placeholder="jouw@email.be"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={status === "loading"}
className="min-w-0 flex-1 bg-transparent px-4 py-2.5 text-sm text-white outline-none disabled:opacity-50"
style={{
fontFamily: "'Intro', sans-serif",
fontSize: "0.8rem",
letterSpacing: "0.02em",
}}
/>
{/* Vertical separator */}
<div
style={{
width: "1px",
background: "rgba(255,255,255,0.15)",
flexShrink: 0,
}}
/>
<button
type="submit"
disabled={status === "loading" || !email}
className="shrink-0 px-4 py-2.5 font-semibold text-xs transition-all disabled:opacity-40"
style={{
fontFamily: "'Intro', sans-serif",
letterSpacing: "0.06em",
color:
status === "loading"
? "rgba(255,255,255,0.5)"
: "rgba(255,255,255,0.9)",
background: "transparent",
cursor:
status === "loading" || !email ? "not-allowed" : "pointer",
whiteSpace: "nowrap",
}}
>
{status === "loading" ? "…" : "Stuur mij"}
</button>
</form>
{status === "error" && (
<p
className="font-['Intro',sans-serif] text-xs"
style={{ color: "rgba(255,140,140,0.8)" }}
>
{errorMessage}
</p>
)}
</div>
)}
</div>
);
}
/**
* Shown in place of the registration form while registration is not yet open.
*/
export function CountdownBanner() {
const { isOpen, days, hours, minutes, seconds } = useRegistrationOpen();
// Once open the parent component will unmount this — but render nothing just in case
if (isOpen) return null;
const openDate = REGISTRATION_OPENS_AT.toLocaleDateString("nl-BE", {
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
});
const openTime = REGISTRATION_OPENS_AT.toLocaleTimeString("nl-BE", {
hour: "2-digit",
minute: "2-digit",
});
return (
<div className="flex flex-col items-center gap-6 py-4 text-center sm:gap-8">
<div>
<p className="font-['Intro',sans-serif] text-base text-white/70 sm:text-lg md:text-xl">
Inschrijvingen openen op
</p>
<p className="mt-1 font-['Intro',sans-serif] text-lg text-white capitalize sm:text-xl md:text-2xl">
{openDate} om {openTime}
</p>
</div>
{/* Countdown — single row forced via grid, scales down on small screens */}
<div className="grid grid-cols-[auto_auto_auto_auto_auto_auto_auto] items-center gap-1.5 sm:flex sm:gap-4 md:gap-6">
<UnitBox value={String(days)} label="dagen" />
<span className="mb-6 font-['Intro',sans-serif] text-2xl text-white/40 sm:text-4xl">
:
</span>
<UnitBox value={pad(hours)} label="uren" />
<span className="mb-6 font-['Intro',sans-serif] text-2xl text-white/40 sm:text-4xl">
:
</span>
<UnitBox value={pad(minutes)} label="minuten" />
<span className="mb-6 font-['Intro',sans-serif] text-2xl text-white/40 sm:text-4xl">
:
</span>
<UnitBox value={pad(seconds)} label="seconden" />
</div>
<p className="max-w-md px-4 text-sm text-white/50">
Kom snel terug! Zodra de inschrijvingen openen kun je je hier
registreren als toeschouwer of als artiest.
</p>
{/* Email reminder opt-in — always last */}
<ReminderForm />
</div>
);
}