From 130cb9186d22c3beb55d9452935a8f5ccb0ce91c Mon Sep 17 00:00:00 2001 From: zias Date: Mon, 20 Apr 2026 21:30:00 +0200 Subject: [PATCH] feat: postponed --- .../homepage/EventRegistrationForm.tsx | 137 +++++++++++++++++- apps/web/src/components/homepage/Hero.tsx | 37 +++-- apps/web/src/lib/opening.ts | 6 + apps/web/src/routes/admin/index.tsx | 41 ++++++ packages/api/src/constants.ts | 7 + packages/api/src/routers/index.ts | 14 +- 6 files changed, 225 insertions(+), 17 deletions(-) diff --git a/apps/web/src/components/homepage/EventRegistrationForm.tsx b/apps/web/src/components/homepage/EventRegistrationForm.tsx index fbcb12d..613b82d 100644 --- a/apps/web/src/components/homepage/EventRegistrationForm.tsx +++ b/apps/web/src/components/homepage/EventRegistrationForm.tsx @@ -5,8 +5,128 @@ import { CountdownBanner } from "@/components/homepage/CountdownBanner"; import { PerformerForm } from "@/components/registration/PerformerForm"; import { TypeSelector } from "@/components/registration/TypeSelector"; import { WatcherForm } from "@/components/registration/WatcherForm"; +import { POSTPONED } from "@/lib/opening"; import { useRegistrationOpen } from "@/lib/useRegistrationOpen"; -import { orpc } from "@/utils/orpc"; +import { client, orpc } from "@/utils/orpc"; + +function PostponedBanner() { + const [email, setEmail] = useState(""); + const [status, setStatus] = useState< + "idle" | "loading" | "subscribed" | "error" + >("idle"); + const [errorMessage, setErrorMessage] = useState(""); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!email || status === "loading") return; + setStatus("loading"); + try { + await client.subscribeReminder({ email }); + setStatus("subscribed"); + } catch (err) { + setErrorMessage(err instanceof Error ? err.message : "Er ging iets mis."); + setStatus("error"); + } + } + + return ( +
+ {/* Postponed notice */} +
+
+ Aankondiging +
+

+ Dit evenement is uitgesteld +

+

+ De Open Mic Night van 24 april gaat helaas niet door op de geplande datum. We werken aan een nieuwe datum en laten je zo snel mogelijk weten wanneer het evenement plaatsvindt. +

+
+ + {/* Hairline divider */} +
+ + {/* Waitlist signup */} +
+

+ Schrijf je in op de wachtlijst +

+

+ Ontvang een e-mail zodra de nieuwe datum bekend is. +

+ + {status === "subscribed" ? ( +
+ +
+ + + +
+

+ Je staat op de wachtlijst! We houden je op de hoogte. +

+
+ ) : ( +
+ setEmail(e.target.value)} + disabled={status === "loading"} + className="min-w-0 flex-1 bg-transparent px-4 py-3 text-sm text-white outline-none placeholder:text-white/30 disabled:opacity-50" + style={{ fontFamily: "'Intro', sans-serif", fontSize: "0.85rem" }} + /> +
+ + + )} + + {status === "error" && ( +

+ {errorMessage} +

+ )} +
+
+ ); +} function fireConfetti() { const colors = ["#d82560", "#52979b", "#d09035", "#214e51", "#ffffff"]; @@ -53,7 +173,7 @@ export default function EventRegistrationForm() { const { data: capacity } = useQuery(orpc.getWatcherCapacity.queryOptions()); useEffect(() => { - if (isOpen && !confettiFired.current) { + if (isOpen && !confettiFired.current && !POSTPONED) { confettiFired.current = true; fireConfetti(); } @@ -63,6 +183,19 @@ export default function EventRegistrationForm() { null, ); + if (POSTPONED) { + return ( +
+
+ +
+
+ ); + } + if (!isOpen) { return (
-

- VRIJDAG 24 -
- april -

+ {POSTPONED ? ( +

+ UITGESTELD +

+ ) : ( +

+ VRIJDAG 24 +
+ april +

+ )}
@@ -97,7 +104,7 @@ export default function Hero() { type="button" className="bg-black px-[40px] py-[10px] font-['Intro',sans-serif] font-normal text-[30px] text-white transition-all hover:scale-105 hover:bg-gray-900" > - Registreer je nu! + {POSTPONED ? "Schrijf je in op de wachtlijst" : "Registreer je nu!"} @@ -150,11 +157,17 @@ export default function Hero() {

-

- VRIJDAG -
- 24 april -

+ {POSTPONED ? ( +

+ UITGESTELD +

+ ) : ( +

+ VRIJDAG +
+ 24 april +

+ )}
@@ -166,7 +179,7 @@ export default function Hero() { type="button" className="w-full bg-black py-3 font-['Intro',sans-serif] font-normal text-lg text-white transition-all hover:scale-105 hover:bg-gray-900" > - Registreer je nu! + {POSTPONED ? "Schrijf je in op de wachtlijst" : "Registreer je nu!"} diff --git a/apps/web/src/lib/opening.ts b/apps/web/src/lib/opening.ts index eb82f21..859c6f0 100644 --- a/apps/web/src/lib/opening.ts +++ b/apps/web/src/lib/opening.ts @@ -10,3 +10,9 @@ export const REGISTRATION_OPENS_AT = new Date("2026-03-16T19:00:00+01:00"); * Set to `true` to enforce the countdown until REGISTRATION_OPENS_AT. */ export const COUNTDOWN_ENABLED = true; + +/** + * Set to `true` when the event has been postponed. + * Hides the registration form and shows a waitlist signup instead. + */ +export const POSTPONED = true; diff --git a/apps/web/src/routes/admin/index.tsx b/apps/web/src/routes/admin/index.tsx index bdaf7d3..8721893 100644 --- a/apps/web/src/routes/admin/index.tsx +++ b/apps/web/src/routes/admin/index.tsx @@ -181,6 +181,7 @@ function AdminPage() { }), ); const adminRequestsQuery = useQuery(orpc.getAdminRequests.queryOptions()); + const waitlistQuery = useQuery(orpc.getWaitlistSubscribers.queryOptions()); const exportMutation = useMutation({ ...orpc.exportRegistrations.mutationOptions(), @@ -553,6 +554,46 @@ function AdminPage() { /> + {/* ── Wachtlijst ── */} + {waitlistQuery.data && waitlistQuery.data.length > 0 && ( +
+
+

+ Wachtlijst +

+ + {waitlistQuery.data.length} + +
+
+ + + + + + + + + {waitlistQuery.data.map((row) => ( + + + + + ))} + +
E-mailIngeschreven op
{row.email} + {new Date(row.createdAt).toLocaleString("nl-BE", { + day: "numeric", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + })} +
+
+
+ )} + {/* ── Filters + export row ── */}
diff --git a/packages/api/src/constants.ts b/packages/api/src/constants.ts index 7b3fdc2..e3d2ec6 100644 --- a/packages/api/src/constants.ts +++ b/packages/api/src/constants.ts @@ -8,3 +8,10 @@ export const OPENS = "maandag 16 maart 2026 om 19:00"; // Registration opens — used for reminder scheduling windows export const REGISTRATION_OPENS_AT = new Date("2026-03-16T19:00:00+01:00"); + +/** + * Set to `true` when the event has been postponed. + * Allows waitlist email subscriptions even after the original registration + * open date has passed. + */ +export const POSTPONED = true; diff --git a/packages/api/src/routers/index.ts b/packages/api/src/routers/index.ts index d16c203..e9bcdea 100644 --- a/packages/api/src/routers/index.ts +++ b/packages/api/src/routers/index.ts @@ -18,7 +18,7 @@ import { sum, } from "drizzle-orm"; import { z } from "zod"; -import { REGISTRATION_OPENS_AT } from "../constants"; +import { POSTPONED, REGISTRATION_OPENS_AT } from "../constants"; import { emailLog, sendCancellationEmail, @@ -902,6 +902,13 @@ export const appRouter = { return { success: true, message: "Admin toegang goedgekeurd" }; }), + getWaitlistSubscribers: adminProcedure.handler(async () => { + return db + .select() + .from(reminder) + .orderBy(desc(reminder.createdAt)); + }), + rejectAdminRequest: adminProcedure .input(z.object({ requestId: z.string() })) .handler(async ({ input, context }) => { @@ -943,8 +950,9 @@ export const appRouter = { .handler(async ({ input, context }) => { const now = Date.now(); - // Registration is already open — no point subscribing - if (now >= REGISTRATION_OPENS_AT.getTime()) { + // Registration is already open — no point subscribing, unless the event + // is postponed (in which case we collect waitlist signups instead). + if (now >= REGISTRATION_OPENS_AT.getTime() && !POSTPONED) { return { ok: false, reason: "already_open" as const }; }