From 0d99f7c5f5d46609dfcde3f7a789095d5d574831 Mon Sep 17 00:00:00 2001 From: zias Date: Sat, 14 Mar 2026 19:36:16 +0100 Subject: [PATCH] feat(registration): add watcher capacity limits and update pricing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 70-person capacity limit for watchers with real-time availability checks. Update drink card pricing to €5 per person (was €5 base + €2 per guest). Add feature flag to bypass registration countdown. Show under-review notice for performer registrations. Update FAQ performance duration from 5-7 to 5 minutes. --- .../homepage/EventRegistrationForm.tsx | 12 ++- .../components/homepage/HoeInschrijven.tsx | 5 +- apps/web/src/components/homepage/Info.tsx | 2 +- .../components/registration/PerformerForm.tsx | 27 ++++++ .../components/registration/TypeSelector.tsx | 85 +++++++++++++------ .../components/registration/WatcherForm.tsx | 2 +- apps/web/src/lib/opening.ts | 7 ++ apps/web/src/lib/registration.ts | 3 +- apps/web/src/lib/useRegistrationOpen.ts | 5 +- apps/web/src/routes/manage.$token.tsx | 2 +- packages/api/src/routers/index.ts | 51 ++++++++++- 11 files changed, 162 insertions(+), 39 deletions(-) diff --git a/apps/web/src/components/homepage/EventRegistrationForm.tsx b/apps/web/src/components/homepage/EventRegistrationForm.tsx index c590b70..fbcb12d 100644 --- a/apps/web/src/components/homepage/EventRegistrationForm.tsx +++ b/apps/web/src/components/homepage/EventRegistrationForm.tsx @@ -1,3 +1,4 @@ +import { useQuery } from "@tanstack/react-query"; import confetti from "canvas-confetti"; import { useEffect, useRef, useState } from "react"; import { CountdownBanner } from "@/components/homepage/CountdownBanner"; @@ -5,6 +6,7 @@ import { PerformerForm } from "@/components/registration/PerformerForm"; import { TypeSelector } from "@/components/registration/TypeSelector"; import { WatcherForm } from "@/components/registration/WatcherForm"; import { useRegistrationOpen } from "@/lib/useRegistrationOpen"; +import { orpc } from "@/utils/orpc"; function fireConfetti() { const colors = ["#d82560", "#52979b", "#d09035", "#214e51", "#ffffff"]; @@ -48,6 +50,8 @@ export default function EventRegistrationForm() { const { isOpen } = useRegistrationOpen(); const confettiFired = useRef(false); + const { data: capacity } = useQuery(orpc.getWatcherCapacity.queryOptions()); + useEffect(() => { if (isOpen && !confettiFired.current) { confettiFired.current = true; @@ -88,7 +92,13 @@ export default function EventRegistrationForm() { Doe je mee of kom je kijken? Kies je rol en vul het formulier in.

- {!selectedType && } + {!selectedType && ( + + )} {selectedType === "performer" && ( setSelectedType(null)} diff --git a/apps/web/src/components/homepage/HoeInschrijven.tsx b/apps/web/src/components/homepage/HoeInschrijven.tsx index bc7b094..b3d83cf 100644 --- a/apps/web/src/components/homepage/HoeInschrijven.tsx +++ b/apps/web/src/components/homepage/HoeInschrijven.tsx @@ -39,9 +39,8 @@ export default function HoeInschrijven() {

We vragen een bijdrage van{" "} - €5 per inschrijving en{" "} - €2 per extra persoon{" "} - die je meebrengt. + €5 per persoon — voor + jou én iedereen die je meebrengt.

diff --git a/apps/web/src/components/homepage/Info.tsx b/apps/web/src/components/homepage/Info.tsx index 0ac0f0a..d983624 100644 --- a/apps/web/src/components/homepage/Info.tsx +++ b/apps/web/src/components/homepage/Info.tsx @@ -12,7 +12,7 @@ const faqQuestions = [ { question: "Hoelang mag mijn optreden duren?", answer: - "Elke deelnemer krijgt 5-7 minuten podiumtijd. Zo houden we de avond dynamisch en krijgt iedereen een kans om te shinen.", + "Elke deelnemer krijgt 5 minuten podiumtijd. Zo houden we de avond dynamisch en krijgt iedereen een kans om te shinen.", }, { question: "Waar vindt het plaats?", diff --git a/apps/web/src/components/registration/PerformerForm.tsx b/apps/web/src/components/registration/PerformerForm.tsx index 61ca36f..4f09e39 100644 --- a/apps/web/src/components/registration/PerformerForm.tsx +++ b/apps/web/src/components/registration/PerformerForm.tsx @@ -441,6 +441,33 @@ export function PerformerForm({ onBack, onSuccess }: Props) { + {/* Under review notice */} +
+ +
+

+ Jouw inschrijving staat onder voorbehoud +

+

+ Na het indienen wordt je aanvraag beoordeeld. Je ontvangt een + bevestiging of je effectief uitgenodigd wordt om op te treden. +

+
+
+
); diff --git a/apps/web/src/components/registration/WatcherForm.tsx b/apps/web/src/components/registration/WatcherForm.tsx index b9b475e..67d757e 100644 --- a/apps/web/src/components/registration/WatcherForm.tsx +++ b/apps/web/src/components/registration/WatcherForm.tsx @@ -227,7 +227,7 @@ export function WatcherForm({ onBack, onSuccess }: Props) {

Drinkkaart inbegrepen

Je betaald bij registratie - €5 (+ €2 per + €5 (+ €5 per medebezoeker) dat gaat naar je drinkkaart. {guests.length > 0 && ( diff --git a/apps/web/src/lib/opening.ts b/apps/web/src/lib/opening.ts index 55efe73..eb82f21 100644 --- a/apps/web/src/lib/opening.ts +++ b/apps/web/src/lib/opening.ts @@ -3,3 +3,10 @@ * Change this date to reschedule — all gating logic imports from here. */ export const REGISTRATION_OPENS_AT = new Date("2026-03-16T19:00:00+01:00"); + +/** + * Feature flag for the registration countdown gate. + * Set to `false` to bypass the countdown and always show the registration form. + * Set to `true` to enforce the countdown until REGISTRATION_OPENS_AT. + */ +export const COUNTDOWN_ENABLED = true; diff --git a/apps/web/src/lib/registration.ts b/apps/web/src/lib/registration.ts index f90fefb..80e728f 100644 --- a/apps/web/src/lib/registration.ts +++ b/apps/web/src/lib/registration.ts @@ -6,8 +6,9 @@ // --------------------------------------------------------------------------- export const DRINK_CARD_BASE = 5; // €5 for primary registrant -export const DRINK_CARD_PER_GUEST = 2; // €2 per additional guest +export const DRINK_CARD_PER_GUEST = 5; // €5 per additional guest (same as primary) export const MAX_GUESTS = 9; +export const MAX_WATCHERS = 70; // max total watcher spots (people, not registrations) /** Returns drink card value in euros for a given number of extra guests. */ export function calculateDrinkCard(guestCount: number): number { diff --git a/apps/web/src/lib/useRegistrationOpen.ts b/apps/web/src/lib/useRegistrationOpen.ts index 6f7791e..ab94624 100644 --- a/apps/web/src/lib/useRegistrationOpen.ts +++ b/apps/web/src/lib/useRegistrationOpen.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { REGISTRATION_OPENS_AT } from "./opening"; +import { COUNTDOWN_ENABLED, REGISTRATION_OPENS_AT } from "./opening"; interface RegistrationOpenState { isOpen: boolean; @@ -10,6 +10,9 @@ interface RegistrationOpenState { } function compute(): RegistrationOpenState { + if (!COUNTDOWN_ENABLED) { + return { isOpen: true, days: 0, hours: 0, minutes: 0, seconds: 0 }; + } const diff = REGISTRATION_OPENS_AT.getTime() - Date.now(); if (diff <= 0) { return { isOpen: true, days: 0, hours: 0, minutes: 0, seconds: 0 }; diff --git a/apps/web/src/routes/manage.$token.tsx b/apps/web/src/routes/manage.$token.tsx index 286f515..3016fbc 100644 --- a/apps/web/src/routes/manage.$token.tsx +++ b/apps/web/src/routes/manage.$token.tsx @@ -569,7 +569,7 @@ function ManageRegistrationPage() { Jouw inschrijving

- Open Mic Night — vrijdag 18 april 2026 + Open Mic Night — vrijdag 24 april 2026

{/* Type badge */} diff --git a/packages/api/src/routers/index.ts b/packages/api/src/routers/index.ts index 073b95c..c0366ab 100644 --- a/packages/api/src/routers/index.ts +++ b/packages/api/src/routers/index.ts @@ -62,9 +62,11 @@ const REMINDER_24H_WINDOW_END = new Date( // Shared helpers // --------------------------------------------------------------------------- -/** Drink card price in euros: €5 base + €2 per extra guest. */ +const MAX_WATCHERS = 70; // max total watcher spots (people, not registrations) + +/** Drink card price in euros: €5 per person (primary + guests). */ function drinkCardEuros(guestCount: number): number { - return 5 + guestCount * 2; + return 5 + guestCount * 5; } /** Drink card price in cents for payment processing. */ @@ -301,6 +303,28 @@ export const appRouter = { healthCheck: publicProcedure.handler(() => "OK"), drinkkaart: drinkkaartRouter, + getWatcherCapacity: publicProcedure.handler(async () => { + const rows = await db + .select({ guests: registration.guests }) + .from(registration) + .where( + and( + eq(registration.registrationType, "watcher"), + isNull(registration.cancelledAt), + ), + ); + const takenSpots = rows.reduce((sum, r) => { + const g = r.guests ? (JSON.parse(r.guests) as unknown[]) : []; + return sum + 1 + g.length; + }, 0); + return { + total: MAX_WATCHERS, + taken: takenSpots, + available: Math.max(0, MAX_WATCHERS - takenSpots), + isFull: takenSpots >= MAX_WATCHERS, + }; + }), + privateData: protectedProcedure.handler(({ context }) => ({ message: "This is private", user: context.session?.user, @@ -313,6 +337,29 @@ export const appRouter = { const isPerformer = input.registrationType === "performer"; const guests = isPerformer ? [] : (input.guests ?? []); + // Enforce max watcher capacity (70 people total, counting guests) + if (!isPerformer) { + const rows = await db + .select({ guests: registration.guests }) + .from(registration) + .where( + and( + eq(registration.registrationType, "watcher"), + isNull(registration.cancelledAt), + ), + ); + const takenSpots = rows.reduce((sum, r) => { + const g = r.guests ? (JSON.parse(r.guests) as unknown[]) : []; + return sum + 1 + g.length; + }, 0); + const newSpots = 1 + guests.length; + if (takenSpots + newSpots > MAX_WATCHERS) { + throw new Error( + `Er zijn helaas niet genoeg plaatsen meer beschikbaar. Nog ${MAX_WATCHERS - takenSpots} ${MAX_WATCHERS - takenSpots === 1 ? "plaats" : "plaatsen"} vrij.`, + ); + } + } + await db.insert(registration).values({ id: randomUUID(), firstName: input.firstName,