+ )}
+
+ {/* Guests section for watchers */}
+ {isEditingWatcher && (
+
)}
@@ -358,6 +618,19 @@ function ManageRegistrationPage() {
Open Mic Night — vrijdag 18 april 2026
+ {/* Type badge */}
+
@@ -378,17 +651,51 @@ function ManageRegistrationPage() {
-
-
Rol
-
- {data.wantsToPerform
- ? `Optreden${data.artForm ? ` — ${data.artForm}` : ""}`
- : "Toeschouwer"}
-
- {data.wantsToPerform && data.experience && (
-
{data.experience}
- )}
-
+ {isPerformer && (
+
+
Optreden
+
+ {data.artForm || "—"}
+ {data.experience && (
+
+ ({data.experience})
+
+ )}
+
+
+ {data.isOver16 ? "16+ bevestigd" : "Leeftijd niet bevestigd"}
+
+
+ )}
+
+ {!isPerformer && viewGuests.length > 0 && (
+
+
+ Medebezoekers ({viewGuests.length})
+
+
+ {viewGuests.map((g, idx) => (
+
+
+ {g.firstName} {g.lastName}
+
+ {g.email && (
+
{g.email}
+ )}
+ {g.phone && (
+
{g.phone}
+ )}
+
+ ))}
+
+
+ )}
{data.extraQuestions && (
diff --git a/packages/api/src/routers/index.ts b/packages/api/src/routers/index.ts
index 08c5e70..517a9a5 100644
--- a/packages/api/src/routers/index.ts
+++ b/packages/api/src/routers/index.ts
@@ -12,19 +12,34 @@ import {
} from "../email";
import { adminProcedure, protectedProcedure, publicProcedure } from "../index";
+const registrationTypeSchema = z.enum(["performer", "watcher"]);
+
+const guestSchema = z.object({
+ firstName: z.string().min(1),
+ lastName: z.string().min(1),
+ email: z.string().email().optional().or(z.literal("")),
+ phone: z.string().optional(),
+});
+
const submitRegistrationSchema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(),
phone: z.string().optional(),
- wantsToPerform: z.boolean().default(false),
+ registrationType: registrationTypeSchema.default("watcher"),
+ // Performer-specific
artForm: z.string().optional(),
experience: z.string().optional(),
+ isOver16: z.boolean().optional(),
+ // Watcher-specific: drinkCardValue is computed server-side, guests are named
+ guests: z.array(guestSchema).max(9).optional(),
+ // Shared
extraQuestions: z.string().optional(),
});
const getRegistrationsSchema = z.object({
search: z.string().optional(),
+ registrationType: registrationTypeSchema.optional(),
artForm: z.string().optional(),
fromDate: z.string().datetime().optional(),
toDate: z.string().datetime().optional(),
@@ -48,15 +63,22 @@ export const appRouter = {
.input(submitRegistrationSchema)
.handler(async ({ input }) => {
const managementToken = randomUUID();
+ const isPerformer = input.registrationType === "performer";
+ const guests = isPerformer ? [] : (input.guests ?? []);
+ // €5 for primary registrant + €2 per extra guest
+ const drinkCardValue = isPerformer ? 0 : 5 + guests.length * 2;
await db.insert(registration).values({
id: randomUUID(),
firstName: input.firstName,
lastName: input.lastName,
email: input.email,
phone: input.phone || null,
- wantsToPerform: input.wantsToPerform,
- artForm: input.artForm || null,
- experience: input.experience || null,
+ registrationType: input.registrationType,
+ artForm: isPerformer ? input.artForm || null : null,
+ experience: isPerformer ? input.experience || null : null,
+ isOver16: isPerformer ? (input.isOver16 ?? false) : false,
+ drinkCardValue,
+ guests: guests.length > 0 ? JSON.stringify(guests) : null,
extraQuestions: input.extraQuestions || null,
managementToken,
});
@@ -65,7 +87,7 @@ export const appRouter = {
to: input.email,
firstName: input.firstName,
managementToken,
- wantsToPerform: input.wantsToPerform,
+ wantsToPerform: isPerformer,
artForm: input.artForm,
}).catch((err) =>
console.error("Failed to send confirmation email:", err),
@@ -98,9 +120,11 @@ export const appRouter = {
lastName: z.string().min(1),
email: z.string().email(),
phone: z.string().optional(),
- wantsToPerform: z.boolean().default(false),
+ registrationType: registrationTypeSchema.default("watcher"),
artForm: z.string().optional(),
experience: z.string().optional(),
+ isOver16: z.boolean().optional(),
+ guests: z.array(guestSchema).max(9).optional(),
extraQuestions: z.string().optional(),
}),
)
@@ -119,6 +143,9 @@ export const appRouter = {
const row = rows[0];
if (!row) throw new Error("Inschrijving niet gevonden of al geannuleerd");
+ const isPerformer = input.registrationType === "performer";
+ const guests = isPerformer ? [] : (input.guests ?? []);
+ const drinkCardValue = isPerformer ? 0 : 5 + guests.length * 2;
await db
.update(registration)
.set({
@@ -126,9 +153,12 @@ export const appRouter = {
lastName: input.lastName,
email: input.email,
phone: input.phone || null,
- wantsToPerform: input.wantsToPerform,
- artForm: input.artForm || null,
- experience: input.experience || null,
+ registrationType: input.registrationType,
+ artForm: isPerformer ? input.artForm || null : null,
+ experience: isPerformer ? input.experience || null : null,
+ isOver16: isPerformer ? (input.isOver16 ?? false) : false,
+ drinkCardValue,
+ guests: guests.length > 0 ? JSON.stringify(guests) : null,
extraQuestions: input.extraQuestions || null,
})
.where(eq(registration.managementToken, input.token));
@@ -137,7 +167,7 @@ export const appRouter = {
to: input.email,
firstName: input.firstName,
managementToken: input.token,
- wantsToPerform: input.wantsToPerform,
+ wantsToPerform: isPerformer,
artForm: input.artForm,
}).catch((err) => console.error("Failed to send update email:", err));
@@ -192,6 +222,12 @@ export const appRouter = {
);
}
+ if (input.registrationType) {
+ conditions.push(
+ eq(registration.registrationType, input.registrationType),
+ );
+ }
+
if (input.artForm) {
conditions.push(eq(registration.artForm, input.artForm));
}
@@ -235,20 +271,29 @@ export const appRouter = {
const today = new Date();
today.setHours(0, 0, 0, 0);
- const [totalResult, todayResult, artFormResult] = await Promise.all([
- db.select({ count: count() }).from(registration),
- db
- .select({ count: count() })
- .from(registration)
- .where(gte(registration.createdAt, today)),
- db
- .select({
- artForm: registration.artForm,
- count: count(),
- })
- .from(registration)
- .groupBy(registration.artForm),
- ]);
+ const [totalResult, todayResult, artFormResult, typeResult] =
+ await Promise.all([
+ db.select({ count: count() }).from(registration),
+ db
+ .select({ count: count() })
+ .from(registration)
+ .where(gte(registration.createdAt, today)),
+ db
+ .select({
+ artForm: registration.artForm,
+ count: count(),
+ })
+ .from(registration)
+ .where(eq(registration.registrationType, "performer"))
+ .groupBy(registration.artForm),
+ db
+ .select({
+ registrationType: registration.registrationType,
+ count: count(),
+ })
+ .from(registration)
+ .groupBy(registration.registrationType),
+ ]);
return {
total: totalResult[0]?.count ?? 0,
@@ -257,6 +302,10 @@ export const appRouter = {
artForm: r.artForm,
count: r.count,
})),
+ byType: typeResult.map((r) => ({
+ registrationType: r.registrationType,
+ count: r.count,
+ })),
};
}),
@@ -272,24 +321,46 @@ export const appRouter = {
"Last Name",
"Email",
"Phone",
- "Wants To Perform",
+ "Type",
"Art Form",
"Experience",
+ "Is Over 16",
+ "Drink Card Value",
+ "Guest Count",
+ "Guests",
"Extra Questions",
"Created At",
];
- const rows = data.map((r) => [
- r.id,
- r.firstName,
- r.lastName,
- r.email,
- r.phone || "",
- r.wantsToPerform ? "Yes" : "No",
- r.artForm || "",
- r.experience || "",
- r.extraQuestions || "",
- r.createdAt.toISOString(),
- ]);
+ const rows = data.map((r) => {
+ const guests: Array<{
+ firstName: string;
+ lastName: string;
+ email?: string;
+ phone?: string;
+ }> = r.guests ? JSON.parse(r.guests) : [];
+ const guestSummary = guests
+ .map(
+ (g) =>
+ `${g.firstName} ${g.lastName}${g.email ? ` <${g.email}>` : ""}${g.phone ? ` (${g.phone})` : ""}`,
+ )
+ .join(" | ");
+ return [
+ r.id,
+ r.firstName,
+ r.lastName,
+ r.email,
+ r.phone || "",
+ r.registrationType,
+ r.artForm || "",
+ r.experience || "",
+ r.isOver16 ? "Yes" : "No",
+ String(r.drinkCardValue ?? 0),
+ String(guests.length),
+ guestSummary,
+ r.extraQuestions || "",
+ r.createdAt.toISOString(),
+ ];
+ });
const csvContent = [
headers.join(","),
@@ -321,16 +392,17 @@ export const appRouter = {
.limit(1);
if (existingRequest.length > 0) {
- if (existingRequest[0].status === "pending") {
+ const existing = existingRequest[0]!;
+ if (existing.status === "pending") {
return { success: false, message: "Je hebt al een aanvraag openstaan" };
}
- if (existingRequest[0].status === "approved") {
+ if (existing.status === "approved") {
return {
success: false,
message: "Je aanvraag is al goedgekeurd, log opnieuw in",
};
}
- if (existingRequest[0].status === "rejected") {
+ if (existing.status === "rejected") {
// Allow re-requesting if previously rejected
await db
.update(adminRequest)
@@ -387,7 +459,8 @@ export const appRouter = {
throw new Error("Aanvraag niet gevonden");
}
- if (request[0].status !== "pending") {
+ const req = request[0]!;
+ if (req.status !== "pending") {
throw new Error("Deze aanvraag is al behandeld");
}
@@ -405,7 +478,7 @@ export const appRouter = {
await db
.update(user)
.set({ role: "admin" })
- .where(eq(user.id, request[0].userId));
+ .where(eq(user.id, req.userId));
return { success: true, message: "Admin toegang goedgekeurd" };
}),
@@ -423,7 +496,8 @@ export const appRouter = {
throw new Error("Aanvraag niet gevonden");
}
- if (request[0].status !== "pending") {
+ const req = request[0]!;
+ if (req.status !== "pending") {
throw new Error("Deze aanvraag is al behandeld");
}
diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts
index 96fc8bb..ed48af6 100644
--- a/packages/db/drizzle.config.ts
+++ b/packages/db/drizzle.config.ts
@@ -2,7 +2,7 @@ import dotenv from "dotenv";
import { defineConfig } from "drizzle-kit";
dotenv.config({
- path: "../../apps/web/.env",
+ path: "../env/.env",
});
export default defineConfig({
diff --git a/packages/db/src/migrations/0002_registration_type_redesign.sql b/packages/db/src/migrations/0002_registration_type_redesign.sql
new file mode 100644
index 0000000..8bd872e
--- /dev/null
+++ b/packages/db/src/migrations/0002_registration_type_redesign.sql
@@ -0,0 +1,20 @@
+-- Migration: Replace wantsToPerform with registrationType + add isOver16 + drinkCardValue
+-- Migrate existing 'wants_to_perform' data to 'registration_type' before dropping
+
+ALTER TABLE `registration` ADD `registration_type` text NOT NULL DEFAULT 'watcher';--> statement-breakpoint
+ALTER TABLE `registration` ADD `is_over_16` integer NOT NULL DEFAULT false;--> statement-breakpoint
+ALTER TABLE `registration` ADD `drink_card_value` integer DEFAULT 0;--> statement-breakpoint
+
+-- Backfill registration_type from wants_to_perform
+UPDATE `registration` SET `registration_type` = 'performer' WHERE `wants_to_perform` = 1;--> statement-breakpoint
+UPDATE `registration` SET `registration_type` = 'watcher' WHERE `wants_to_perform` = 0;--> statement-breakpoint
+
+-- Backfill drink_card_value for watchers
+UPDATE `registration` SET `drink_card_value` = 5 WHERE `wants_to_perform` = 0;--> statement-breakpoint
+
+-- Create index on registration_type
+CREATE INDEX `registration_registrationType_idx` ON `registration` (`registration_type`);--> statement-breakpoint
+
+-- Drop old wants_to_perform column (SQLite requires recreating the table)
+-- SQLite doesn't support DROP COLUMN directly in older versions, but Turso/libSQL does
+ALTER TABLE `registration` DROP COLUMN `wants_to_perform`;
diff --git a/packages/db/src/migrations/0003_add_guests.sql b/packages/db/src/migrations/0003_add_guests.sql
new file mode 100644
index 0000000..681b879
--- /dev/null
+++ b/packages/db/src/migrations/0003_add_guests.sql
@@ -0,0 +1,2 @@
+-- Migration: Add guests column (JSON text) to registration table
+ALTER TABLE `registration` ADD `guests` text;
diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json
index 042e610..a60f1cd 100644
--- a/packages/db/src/migrations/meta/_journal.json
+++ b/packages/db/src/migrations/meta/_journal.json
@@ -15,6 +15,13 @@
"when": 1772480778632,
"tag": "0001_third_stark_industries",
"breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "6",
+ "when": 1772520000000,
+ "tag": "0002_registration_type_redesign",
+ "breakpoints": true
}
]
}
diff --git a/packages/db/src/schema/registrations.ts b/packages/db/src/schema/registrations.ts
index e007fdf..32829a0 100644
--- a/packages/db/src/schema/registrations.ts
+++ b/packages/db/src/schema/registrations.ts
@@ -9,11 +9,19 @@ export const registration = sqliteTable(
lastName: text("last_name").notNull(),
email: text("email").notNull(),
phone: text("phone"),
- wantsToPerform: integer("wants_to_perform", { mode: "boolean" })
- .notNull()
- .default(false),
+ // registrationType: 'performer' | 'watcher'
+ registrationType: text("registration_type").notNull().default("watcher"),
+ // Performer-specific fields
artForm: text("art_form"),
experience: text("experience"),
+ isOver16: integer("is_over_16", { mode: "boolean" })
+ .notNull()
+ .default(false),
+ // Watcher-specific fields
+ drinkCardValue: integer("drink_card_value").default(0),
+ // Guests: JSON array of {firstName, lastName, email?, phone?} objects
+ guests: text("guests"),
+ // Shared
extraQuestions: text("extra_questions"),
managementToken: text("management_token").unique(),
cancelledAt: integer("cancelled_at", { mode: "timestamp_ms" }),
@@ -23,6 +31,7 @@ export const registration = sqliteTable(
},
(table) => [
index("registration_email_idx").on(table.email),
+ index("registration_registrationType_idx").on(table.registrationType),
index("registration_artForm_idx").on(table.artForm),
index("registration_createdAt_idx").on(table.createdAt),
index("registration_managementToken_idx").on(table.managementToken),
diff --git a/packages/infra/alchemy.run.ts b/packages/infra/alchemy.run.ts
index 495ac25..111a2c8 100644
--- a/packages/infra/alchemy.run.ts
+++ b/packages/infra/alchemy.run.ts
@@ -29,7 +29,7 @@ export const web = await TanStackStart("web", {
SMTP_PASS: getEnvVar("SMTP_PASS"),
SMTP_FROM: getEnvVar("SMTP_FROM"),
},
- domains: ["kunstenkamp.be"],
+ domains: ["kunstenkamp.be", "www.kunstenkamp.be"],
});
console.log(`Web -> ${web.url}`);