diff --git a/apps/web/src/components/homepage/EventRegistrationForm.tsx b/apps/web/src/components/homepage/EventRegistrationForm.tsx index 1b45dd3..3e7cd8b 100644 --- a/apps/web/src/components/homepage/EventRegistrationForm.tsx +++ b/apps/web/src/components/homepage/EventRegistrationForm.tsx @@ -36,6 +36,7 @@ export default function EventRegistrationForm() { token={successState.token} email={successState.email} name={successState.name} + isLoggedIn={isLoggedIn} onReset={() => { setSuccessState(null); setSelectedType(null); diff --git a/apps/web/src/components/homepage/Hero.tsx b/apps/web/src/components/homepage/Hero.tsx index edb38e8..cc9fc17 100644 --- a/apps/web/src/components/homepage/Hero.tsx +++ b/apps/web/src/components/homepage/Hero.tsx @@ -83,7 +83,7 @@ export default function Hero() { {/* Bottom Right - Dark Teal with date - above mic */}

- VRIJDAG 18 + VRIJDAG 24
april

@@ -153,7 +153,7 @@ export default function Hero() {

VRIJDAG
- 18 april + 24 april

diff --git a/apps/web/src/components/registration/GuestList.tsx b/apps/web/src/components/registration/GuestList.tsx index 1dd0a32..c0ada89 100644 --- a/apps/web/src/components/registration/GuestList.tsx +++ b/apps/web/src/components/registration/GuestList.tsx @@ -204,6 +204,51 @@ export function GuestList({ )} +
+ + onChange(idx, "birthdate", e.target.value)} + autoComplete="off" + className={inputCls(!!errors[idx]?.birthdate)} + /> + {errors[idx]?.birthdate && ( + + {errors[idx].birthdate} + + )} +
+ +
+ + onChange(idx, "postcode", e.target.value)} + placeholder="1234 AB" + autoComplete="off" + className={inputCls(!!errors[idx]?.postcode)} + /> + {errors[idx]?.postcode && ( + + {errors[idx].postcode} + + )} +
+
- {/* Account creation prompt */} - {!drinkkaartPromptDismissed && ( + {/* Account creation prompt — hidden when already logged in */} + {!isLoggedIn && !drinkkaartPromptDismissed && (

Maak een gratis account aan diff --git a/apps/web/src/components/registration/WatcherForm.tsx b/apps/web/src/components/registration/WatcherForm.tsx index b8387ee..34fce49 100644 --- a/apps/web/src/components/registration/WatcherForm.tsx +++ b/apps/web/src/components/registration/WatcherForm.tsx @@ -389,7 +389,14 @@ export function WatcherForm({ if (guests.length >= 9) return; setGuests((prev) => [ ...prev, - { firstName: "", lastName: "", email: "", phone: "" }, + { + firstName: "", + lastName: "", + email: "", + phone: "", + birthdate: "", + postcode: "", + }, ]); setGuestErrors((prev) => [...prev, {}]); } @@ -418,6 +425,8 @@ export function WatcherForm({ lastName: g.lastName.trim(), email: g.email.trim() || undefined, phone: g.phone.trim() || undefined, + birthdate: g.birthdate.trim(), + postcode: g.postcode.trim(), })), extraQuestions: data.extraQuestions.trim() || undefined, giftAmount, diff --git a/apps/web/src/lib/registration.ts b/apps/web/src/lib/registration.ts index f90fefb..5ad1c7e 100644 --- a/apps/web/src/lib/registration.ts +++ b/apps/web/src/lib/registration.ts @@ -28,6 +28,8 @@ export interface GuestEntry { lastName: string; email: string; phone: string; + birthdate: string; + postcode: string; } export interface GuestErrors { @@ -35,6 +37,8 @@ export interface GuestErrors { lastName?: string; email?: string; phone?: string; + birthdate?: string; + postcode?: string; } /** @@ -51,6 +55,8 @@ export function parseGuests(raw: string | null | undefined): GuestEntry[] { lastName: g.lastName ?? "", email: g.email ?? "", phone: g.phone ?? "", + birthdate: g.birthdate ?? "", + postcode: g.postcode ?? "", })); } catch { return []; @@ -102,6 +108,8 @@ export function validateGuests(guests: GuestEntry[]): { g.phone.trim() && !/^[\d\s\-+()]{10,}$/.test(g.phone.replace(/\s/g, "")) ? "Voer een geldig telefoonnummer in" : undefined, + birthdate: !g.birthdate.trim() ? "Geboortedatum is verplicht" : undefined, + postcode: !g.postcode.trim() ? "Postcode is verplicht" : undefined, })); const valid = !errors.some((e) => Object.values(e).some(Boolean)); return { errors, valid }; diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 24e6ad4..3911b75 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -17,7 +17,7 @@ import appCss from "../index.css?url"; const siteUrl = "https://kunstenkamp.be"; const siteTitle = "Kunstenkamp Open Mic Night - Ongedesemd Woord"; const siteDescription = - "Doe mee met de Open Mic Night op 18 april! Een avond vol muziek, theater, dans, woordkunst en meer. Iedereen is welkom - van beginner tot professional."; + "Doe mee met de Open Mic Night op 24 april! Een avond vol muziek, theater, dans, woordkunst en meer. Iedereen is welkom - van beginner tot professional."; const eventImage = `${siteUrl}/assets/og-image.jpg`; export interface RouterAppContext { diff --git a/apps/web/src/routes/account.tsx b/apps/web/src/routes/account.tsx index d6b45b7..4630efd 100644 --- a/apps/web/src/routes/account.tsx +++ b/apps/web/src/routes/account.tsx @@ -90,7 +90,7 @@ function AccountPage() { orpc.drinkkaart.getMyDrinkkaart.queryOptions(), ); - const registrationQuery = useQuery(orpc.getMyRegistration.queryOptions()); + const registrationQuery = useQuery(orpc.getMyRegistrations.queryOptions()); // Handle topup=success redirect (from Lemon Squeezy returning to /account) useEffect(() => { @@ -128,7 +128,7 @@ function AccountPage() { | { name?: string; email?: string } | undefined; - const registration = registrationQuery.data; + const registrations = registrationQuery.data ?? []; const drinkkaart = drinkkaartQuery.data; const isLoading = @@ -170,112 +170,122 @@ function AccountPage() { Mijn Inschrijving

- {registration ? ( -
- {/* Type badge */} -
-
- {registration.registrationType === "performer" ? ( - - - Artiest - - ) : ( - - - Bezoeker - + {registrations.length > 0 ? ( +
+ {registrations.map((registration) => ( +
+ {/* Type badge */} +
+
+ {registration.registrationType === "performer" ? ( + + + Artiest + + ) : ( + + + Bezoeker + + )} +
+ {(registration.registrationType !== "performer" || + (registration.giftAmount ?? 0) > 0) && ( + + )} +
+ + {/* Name */} +

+ {registration.firstName} {registration.lastName} +

+ + {/* Art form (performer only) */} + {registration.registrationType === "performer" && + registration.artForm && ( +

+ Kunstvorm:{" "} + + {registration.artForm} + +

+ )} + + {/* Guests (watcher only) */} + {registration.registrationType === "watcher" && + registration.guests.length > 0 && ( +

+ {registration.guests.length + 1} personen (jij +{" "} + {registration.guests.length} gast + {registration.guests.length > 1 ? "en" : ""}) +

+ )} + + {/* Drink card value */} + {registration.registrationType === "watcher" && + (registration.drinkCardValue ?? 0) > 0 && ( +

+ Drinkkaart:{" "} + + €{registration.drinkCardValue} + +

+ )} + + {/* Gift */} + {(registration.giftAmount ?? 0) > 0 && ( +

+ Gift:{" "} + + €{(registration.giftAmount ?? 0) / 100} + +

+ )} + + {/* Date */} +

+ Ingeschreven op{" "} + {new Date(registration.createdAt).toLocaleDateString( + "nl-BE", + { + day: "numeric", + month: "long", + year: "numeric", + }, + )} +

+ + {/* Action */} + {registration.managementToken && ( +
+ + Beheer inschrijving + + +
)}
- -
- - {/* Name */} -

- {registration.firstName} {registration.lastName} -

- - {/* Art form (performer only) */} - {registration.registrationType === "performer" && - registration.artForm && ( -

- Kunstvorm:{" "} - - {registration.artForm} - -

- )} - - {/* Guests (watcher only) */} - {registration.registrationType === "watcher" && - registration.guests.length > 0 && ( -

- {registration.guests.length + 1} personen (jij +{" "} - {registration.guests.length} gast - {registration.guests.length > 1 ? "en" : ""}) -

- )} - - {/* Drink card value */} - {registration.registrationType === "watcher" && - (registration.drinkCardValue ?? 0) > 0 && ( -

- Drinkkaart:{" "} - - €{registration.drinkCardValue} - -

- )} - - {/* Gift */} - {(registration.giftAmount ?? 0) > 0 && ( -

- Gift:{" "} - - €{(registration.giftAmount ?? 0) / 100} - -

- )} - - {/* Date */} -

- Ingeschreven op{" "} - {new Date(registration.createdAt).toLocaleDateString( - "nl-BE", - { - day: "numeric", - month: "long", - year: "numeric", - }, - )} -

- - {/* Action */} - {registration.managementToken && ( -
- - Beheer inschrijving - - -
- )} + ))}
) : (
diff --git a/apps/web/src/routes/admin/index.tsx b/apps/web/src/routes/admin/index.tsx index bccca42..15756d6 100644 --- a/apps/web/src/routes/admin/index.tsx +++ b/apps/web/src/routes/admin/index.tsx @@ -13,7 +13,7 @@ import { Users, X, } from "lucide-react"; -import { useMemo, useState } from "react"; +import { Fragment, useMemo, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { @@ -44,6 +44,19 @@ function AdminPage() { const pageSize = 20; const [copiedId, setCopiedId] = useState(null); + const [expandedGuests, setExpandedGuests] = useState>(new Set()); + + const toggleGuests = (id: string) => { + setExpandedGuests((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; // Get current session to check user role const sessionQuery = useQuery({ @@ -278,6 +291,25 @@ function AdminPage() { return `€${euros.toFixed(euros % 1 === 0 ? 0 : 2)}`; }; + const parseGuestsJson = ( + raw: string | null | undefined, + ): Array<{ + firstName: string; + lastName: string; + email?: string; + phone?: string; + birthdate?: string; + postcode?: string; + }> => { + if (!raw) return []; + try { + const parsed = JSON.parse(raw as string); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + }; + // Check if user is admin const user = sessionQuery.data?.data?.user as | { role?: string; name?: string } @@ -486,7 +518,6 @@ function AdminPage() { )} - {/* Stats Cards */}
@@ -592,7 +623,6 @@ function AdminPage() {
- {/* Filters */} @@ -700,7 +730,6 @@ function AdminPage() {
- {/* Export Button */}

@@ -715,7 +744,6 @@ function AdminPage() { {exportMutation.isPending ? "Exporteren..." : "Exporteer CSV"}

- {/* Registrations Table / Cards */} @@ -803,15 +831,9 @@ function AdminPage() { sortedRegistrations.map((reg) => { const isPerformer = reg.registrationType === "performer"; - const guestCount = (() => { - if (!reg.guests) return 0; - try { - const g = JSON.parse(reg.guests as string); - return Array.isArray(g) ? g.length : 0; - } catch { - return 0; - } - })(); + const guests = parseGuestsJson(reg.guests); + const guestCount = guests.length; + const isGuestsExpanded = expandedGuests.has(reg.id); const detailLabel = isPerformer ? reg.artForm || "-" @@ -833,121 +855,190 @@ function AdminPage() { })(); return ( - - - {reg.firstName} {reg.lastName} - - - {reg.email} - - - {reg.phone || "-"} - - - - {isPerformer ? "Artiest" : "Bezoeker"} - - - - {detailLabel} - - - {guestCount > 0 - ? `${guestCount} gast${guestCount === 1 ? "" : "en"}` - : "-"} - - - {formatCents(reg.giftAmount)} - - - {isPerformer ? ( - - - ) : reg.paymentStatus === "paid" ? ( - - - Betaald - - ) : reg.paymentStatus === - "extra_payment_pending" ? ( - - - Extra (€ - {((reg.paymentAmount ?? 0) / 100).toFixed(0)}) - - ) : ( - - Open - - )} - - - {dateLabel} - - - {reg.postcode || "-"} - - - {reg.birthdate || "-"} - - - {isPerformer ? reg.experience || "-" : "-"} - - - {isPerformer ? ( - reg.isOver16 ? ( - Ja - ) : ( - Nee - ) - ) : ( - "-" - )} - - - - {reg.extraQuestions || "-"} - - - - {reg.managementToken ? ( - + ) : ( + "-" + )} + + + {formatCents(reg.giftAmount)} + + + {isPerformer ? ( + - + ) : reg.paymentStatus === "paid" ? ( + + + Betaald + + ) : reg.paymentStatus === + "extra_payment_pending" ? ( + + + Extra (€ + {((reg.paymentAmount ?? 0) / 100).toFixed(0)}) + + ) : ( + + Open + + )} + + + {dateLabel} + + + {reg.postcode || "-"} + + + {reg.birthdate || "-"} + + + {isPerformer ? reg.experience || "-" : "-"} + + + {isPerformer ? ( + reg.isOver16 ? ( + Ja ) : ( - - )} - - ) : ( - - )} - - + Nee + ) + ) : ( + "-" + )} + + + + {reg.extraQuestions || "-"} + + + + {reg.managementToken ? ( + + ) : ( + + )} + + + {isGuestsExpanded && + guests.map((guest, gi) => ( + + + ↳ {guest.firstName} {guest.lastName} + + + {guest.email || "-"} + + + {guest.phone || "-"} + + + + Gast + + + + - + + + - + + + - + + + - + + + - + + + {guest.postcode || "-"} + + + {guest.birthdate || "-"} + + + - + + + - + + + - + + + - + + + ))} + ); }) )}
- - {/* Mobile Cards */}
{registrationsQuery.isLoading ? (
@@ -962,15 +1053,9 @@ function AdminPage() { {sortedRegistrations.map((reg) => { const isPerformer = reg.registrationType === "performer"; - const guestCount = (() => { - if (!reg.guests) return 0; - try { - const g = JSON.parse(reg.guests as string); - return Array.isArray(g) ? g.length : 0; - } catch { - return 0; - } - })(); + const guests = parseGuestsJson(reg.guests); + const guestCount = guests.length; + const isGuestsExpanded = expandedGuests.has(reg.id); const detailLabel = isPerformer ? reg.artForm || "-" @@ -992,126 +1077,171 @@ function AdminPage() { })(); return ( -
-
-
-
- - {reg.firstName} {reg.lastName} - +
+
+
+
+
+ + {reg.firstName} {reg.lastName} + +
+
+ {reg.email} + {reg.phone && ( + + • {reg.phone} + + )} +
-
- {reg.email} - {reg.phone && ( - • {reg.phone} +
+ + {isPerformer ? "Artiest" : "Bezoeker"} + + {reg.managementToken && ( + )}
-
- - {isPerformer ? "Artiest" : "Bezoeker"} - - {reg.managementToken && ( + +
+
+ Details:{" "} + {detailLabel} +
+ {guestCount > 0 && ( )} + {(reg.giftAmount ?? 0) > 0 && ( +
+ Gift:{" "} + {formatCents(reg.giftAmount)} +
+ )} + {!isPerformer && ( +
+ Betaling:{" "} + {reg.paymentStatus === "paid" ? ( + + + Betaald + + ) : reg.paymentStatus === + "extra_payment_pending" ? ( + + + Extra (€ + {((reg.paymentAmount ?? 0) / 100).toFixed( + 0, + )} + ) + + ) : ( + Open + )} +
+ )} + {reg.postcode && ( +
+ Postcode:{" "} + {reg.postcode} +
+ )} + {reg.birthdate && ( +
+ + Geboortedatum: + {" "} + {reg.birthdate} +
+ )} + {isPerformer && reg.experience && ( +
+ Ervaring:{" "} + {reg.experience} +
+ )} + {isPerformer && ( +
+ 16+:{" "} + {reg.isOver16 ? ( + Ja + ) : ( + Nee + )} +
+ )} + {reg.extraQuestions && ( +
+ + Opmerkingen: + {" "} + {reg.extraQuestions} +
+ )} +
{dateLabel}
- -
-
- Details:{" "} - {detailLabel} + {isGuestsExpanded && guestCount > 0 && ( +
+
+ Gasten +
+
+ {guests.map((guest, gi) => ( +
+
+ {guest.firstName} {guest.lastName} +
+
+ {guest.email && {guest.email}} + {guest.phone && {guest.phone}} + {guest.birthdate && ( + {guest.birthdate} + )} + {guest.postcode && ( + {guest.postcode} + )} +
+
+ ))} +
- {guestCount > 0 && ( -
- Gasten:{" "} - {guestCount} -
- )} - {(reg.giftAmount ?? 0) > 0 && ( -
- Gift:{" "} - {formatCents(reg.giftAmount)} -
- )} - {!isPerformer && ( -
- Betaling:{" "} - {reg.paymentStatus === "paid" ? ( - - - Betaald - - ) : reg.paymentStatus === - "extra_payment_pending" ? ( - - - Extra (€ - {((reg.paymentAmount ?? 0) / 100).toFixed(0)}) - - ) : ( - Open - )} -
- )} - {reg.postcode && ( -
- Postcode:{" "} - {reg.postcode} -
- )} - {reg.birthdate && ( -
- - Geboortedatum: - {" "} - {reg.birthdate} -
- )} - {isPerformer && reg.experience && ( -
- Ervaring:{" "} - {reg.experience} -
- )} - {isPerformer && ( -
- 16+:{" "} - {reg.isOver16 ? ( - Ja - ) : ( - Nee - )} -
- )} - {reg.extraQuestions && ( -
- - Opmerkingen: - {" "} - {reg.extraQuestions} -
- )} -
{dateLabel}
-
+ )}
); })} @@ -1120,8 +1250,6 @@ function AdminPage() {
- - {/* Pagination */} {pagination && pagination.totalPages > 1 && (
- {/* Payment status - shown for everyone with pending/extra payment or gift */} - {(data.paymentStatus !== "paid" || (data.giftAmount ?? 0) > 0) && ( -
- {data.paymentStatus === "paid" ? ( - - ) : data.paymentStatus === "extra_payment_pending" ? ( - - ) : ( - - )} -
- )} + {/* Payment status - not shown for performers without a gift */} + {(!isPerformer || (data.giftAmount ?? 0) > 0) && + (data.paymentStatus !== "paid" || (data.giftAmount ?? 0) > 0) && ( +
+ {data.paymentStatus === "paid" ? ( + + ) : data.paymentStatus === "extra_payment_pending" ? ( + + ) : ( + + )} +
+ )} {/* Gift display */} {(data.giftAmount ?? 0) > 0 && ( @@ -710,6 +720,16 @@ function ManageRegistrationPage() {

{g.firstName} {g.lastName}

+ {g.birthdate && ( +

+ Geboortedatum: {g.birthdate} +

+ )} + {g.postcode && ( +

+ Postcode: {g.postcode} +

+ )} {g.email && (

{g.email}

)} diff --git a/packages/api/src/email.ts b/packages/api/src/email.ts index 997ec0c..55362d2 100644 --- a/packages/api/src/email.ts +++ b/packages/api/src/email.ts @@ -86,7 +86,7 @@ function registrationConfirmationHtml(params: { Hoi ${params.firstName},

- We hebben je inschrijving voor Open Mic Night — vrijdag 18 april 2026 in goede orde ontvangen. + We hebben je inschrijving voor Open Mic Night — vrijdag 24 april 2026 in goede orde ontvangen.

diff --git a/packages/api/src/routers/index.ts b/packages/api/src/routers/index.ts index 9b14ca9..de56b9b 100644 --- a/packages/api/src/routers/index.ts +++ b/packages/api/src/routers/index.ts @@ -36,6 +36,8 @@ function parseGuestsJson(raw: string | null): Array<{ lastName: string; email?: string; phone?: string; + birthdate?: string; + postcode?: string; }> { if (!raw) return []; try { @@ -218,6 +220,8 @@ const guestSchema = z.object({ lastName: z.string().min(1), email: z.string().email().optional().or(z.literal("")), phone: z.string().optional(), + birthdate: z.string().min(1), + postcode: z.string().min(1), }); const coreRegistrationFields = { @@ -542,7 +546,7 @@ export const appRouter = { const guestSummary = guests .map( (g) => - `${g.firstName} ${g.lastName}${g.email ? ` <${g.email}>` : ""}${g.phone ? ` (${g.phone})` : ""}`, + `${g.firstName} ${g.lastName}${g.birthdate ? ` (${g.birthdate})` : ""}${g.postcode ? ` [${g.postcode}]` : ""}${g.email ? ` <${g.email}>` : ""}${g.phone ? ` (${g.phone})` : ""}`, ) .join(" | "); return [ @@ -593,7 +597,7 @@ export const appRouter = { return result; }), - getMyRegistration: protectedProcedure.handler(async ({ context }) => { + getMyRegistrations: protectedProcedure.handler(async ({ context }) => { const email = context.session.user.email; const rows = await db @@ -602,16 +606,12 @@ export const appRouter = { .where( and(eq(registration.email, email), isNull(registration.cancelledAt)), ) - .orderBy(desc(registration.createdAt)) - .limit(1); + .orderBy(desc(registration.createdAt)); - const row = rows[0]; - if (!row) return null; - - return { + return rows.map((row) => ({ ...row, guests: parseGuestsJson(row.guests), - }; + })); }), // ---------------------------------------------------------------------------