Files
kunstenkamp/apps/web/src/components/homepage/EventRegistrationForm.tsx
2026-03-03 10:45:05 +01:00

1366 lines
44 KiB
TypeScript

"use client";
import { useMutation } from "@tanstack/react-query";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import { orpc } from "@/utils/orpc";
type RegistrationType = "performer" | "watcher" | null;
interface GuestEntry {
firstName: string;
lastName: string;
email: string;
phone: string;
}
interface GuestErrors {
firstName?: string;
lastName?: string;
email?: string;
phone?: string;
}
interface PerformerErrors {
firstName?: string;
lastName?: string;
email?: string;
phone?: string;
artForm?: string;
isOver16?: string;
}
interface WatcherErrors {
firstName?: string;
lastName?: string;
email?: string;
phone?: string;
}
export default function EventRegistrationForm() {
const [selectedType, setSelectedType] = useState<RegistrationType>(null);
const [successToken, setSuccessToken] = useState<string | null>(null);
// Performer form state
const [performerData, setPerformerData] = useState({
firstName: "",
lastName: "",
email: "",
phone: "",
artForm: "",
experience: "",
isOver16: false,
extraQuestions: "",
});
const [performerErrors, setPerformerErrors] = useState<PerformerErrors>({});
const [performerTouched, setPerformerTouched] = useState<
Record<string, boolean>
>({});
// Watcher form state
const [watcherData, setWatcherData] = useState({
firstName: "",
lastName: "",
email: "",
phone: "",
extraQuestions: "",
});
const [watcherErrors, setWatcherErrors] = useState<WatcherErrors>({});
const [watcherTouched, setWatcherTouched] = useState<Record<string, boolean>>(
{},
);
// Guests state (bezoeker only)
const [guests, setGuests] = useState<GuestEntry[]>([]);
const [guestErrors, setGuestErrors] = useState<GuestErrors[]>([]);
const submitMutation = useMutation({
...orpc.submitRegistration.mutationOptions(),
onSuccess: (data) => {
if (data.managementToken) {
setSuccessToken(data.managementToken);
}
setPerformerData({
firstName: "",
lastName: "",
email: "",
phone: "",
artForm: "",
experience: "",
isOver16: false,
extraQuestions: "",
});
setWatcherData({
firstName: "",
lastName: "",
email: "",
phone: "",
extraQuestions: "",
});
setGuests([]);
setGuestErrors([]);
setPerformerErrors({});
setWatcherErrors({});
setPerformerTouched({});
setWatcherTouched({});
},
onError: (error) => {
toast.error(`Er is iets misgegaan: ${error.message}`);
},
});
const handleReset = useCallback(() => {
setSuccessToken(null);
setSelectedType(null);
}, []);
const validateTextField = (
value: string,
required: boolean,
label: string,
minLen = 2,
): string | undefined => {
if (required && !value.trim()) return `${label} is verplicht`;
if (value.trim() && value.length < minLen)
return `${label} moet minimaal ${minLen} tekens bevatten`;
return undefined;
};
const validateEmail = (value: string): string | undefined => {
if (!value.trim()) return "E-mail is verplicht";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
return "Voer een geldig e-mailadres in";
return undefined;
};
const validatePhone = (value: string): string | undefined => {
if (value && !/^[\d\s\-+()]{10,}$/.test(value.replace(/\s/g, ""))) {
return "Voer een geldig telefoonnummer in";
}
return undefined;
};
const validatePerformerForm = useCallback((): boolean => {
const errs: PerformerErrors = {
firstName: validateTextField(performerData.firstName, true, "Voornaam"),
lastName: validateTextField(performerData.lastName, true, "Achternaam"),
email: validateEmail(performerData.email),
phone: validatePhone(performerData.phone),
artForm: !performerData.artForm.trim()
? "Kunstvorm is verplicht"
: undefined,
isOver16: !performerData.isOver16
? "Je moet 16 jaar of ouder zijn om op te treden"
: undefined,
};
setPerformerErrors(errs);
setPerformerTouched({
firstName: true,
lastName: true,
email: true,
phone: true,
artForm: true,
isOver16: true,
});
return !Object.values(errs).some(Boolean);
}, [performerData]);
const validateWatcherForm = useCallback((): boolean => {
const errs: WatcherErrors = {
firstName: validateTextField(watcherData.firstName, true, "Voornaam"),
lastName: validateTextField(watcherData.lastName, true, "Achternaam"),
email: validateEmail(watcherData.email),
phone: validatePhone(watcherData.phone),
};
setWatcherErrors(errs);
setWatcherTouched({
firstName: true,
lastName: true,
email: true,
phone: true,
});
return !Object.values(errs).some(Boolean);
}, [watcherData]);
const handlePerformerChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value, type } = e.target;
const checked = (e.target as HTMLInputElement).checked;
const newValue = type === "checkbox" ? checked : value;
setPerformerData((prev) => ({ ...prev, [name]: newValue }));
if (type !== "checkbox" && performerTouched[name]) {
const fieldErrs: Record<string, string | undefined> = {
firstName: validateTextField(
name === "firstName" ? value : performerData.firstName,
true,
"Voornaam",
),
lastName: validateTextField(
name === "lastName" ? value : performerData.lastName,
true,
"Achternaam",
),
email:
name === "email"
? validateEmail(value)
: validateEmail(performerData.email),
phone: name === "phone" ? validatePhone(value) : undefined,
artForm:
name === "artForm" && !value.trim()
? "Kunstvorm is verplicht"
: undefined,
};
setPerformerErrors((prev) => ({ ...prev, [name]: fieldErrs[name] }));
}
},
[performerTouched, performerData],
);
const handlePerformerBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setPerformerTouched((prev) => ({ ...prev, [name]: true }));
const errMap: Record<string, string | undefined> = {
firstName: validateTextField(value, true, "Voornaam"),
lastName: validateTextField(value, true, "Achternaam"),
email: validateEmail(value),
phone: validatePhone(value),
artForm: !value.trim() ? "Kunstvorm is verplicht" : undefined,
};
setPerformerErrors((prev) => ({ ...prev, [name]: errMap[name] }));
},
[],
);
const handleWatcherChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setWatcherData((prev) => ({ ...prev, [name]: value }));
if (watcherTouched[name]) {
const errMap: Record<string, string | undefined> = {
firstName: validateTextField(value, true, "Voornaam"),
lastName: validateTextField(value, true, "Achternaam"),
email: validateEmail(value),
phone: validatePhone(value),
};
setWatcherErrors((prev) => ({ ...prev, [name]: errMap[name] }));
}
},
[watcherTouched],
);
const handleWatcherBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setWatcherTouched((prev) => ({ ...prev, [name]: true }));
const errMap: Record<string, string | undefined> = {
firstName: validateTextField(value, true, "Voornaam"),
lastName: validateTextField(value, true, "Achternaam"),
email: validateEmail(value),
phone: validatePhone(value),
};
setWatcherErrors((prev) => ({ ...prev, [name]: errMap[name] }));
},
[],
);
const handlePerformerSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
if (!validatePerformerForm()) {
toast.error("Controleer je invoer");
return;
}
submitMutation.mutate({
firstName: performerData.firstName.trim(),
lastName: performerData.lastName.trim(),
email: performerData.email.trim(),
phone: performerData.phone.trim() || undefined,
registrationType: "performer",
artForm: performerData.artForm.trim() || undefined,
experience: performerData.experience.trim() || undefined,
isOver16: performerData.isOver16,
extraQuestions: performerData.extraQuestions.trim() || undefined,
});
},
[performerData, submitMutation, validatePerformerForm],
);
const validateGuests = useCallback((): boolean => {
const errs: GuestErrors[] = guests.map((g) => ({
firstName: !g.firstName.trim() ? "Voornaam is verplicht" : undefined,
lastName: !g.lastName.trim() ? "Achternaam is verplicht" : undefined,
email:
g.email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(g.email.trim())
? "Voer een geldig e-mailadres in"
: undefined,
phone:
g.phone.trim() && !/^[\d\s\-+()]{10,}$/.test(g.phone.replace(/\s/g, ""))
? "Voer een geldig telefoonnummer in"
: undefined,
}));
setGuestErrors(errs);
return !errs.some((e) => Object.values(e).some(Boolean));
}, [guests]);
const handleAddGuest = useCallback(() => {
if (guests.length >= 9) return;
setGuests((prev) => [
...prev,
{ firstName: "", lastName: "", email: "", phone: "" },
]);
setGuestErrors((prev) => [...prev, {}]);
}, [guests.length]);
const handleRemoveGuest = useCallback((index: number) => {
setGuests((prev) => prev.filter((_, i) => i !== index));
setGuestErrors((prev) => prev.filter((_, i) => i !== index));
}, []);
const handleGuestChange = useCallback(
(index: number, field: keyof GuestEntry, value: string) => {
setGuests((prev) => {
const next = [...prev];
next[index] = { ...next[index], [field]: value } as GuestEntry;
return next;
});
},
[],
);
const handleWatcherSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
const watcherValid = validateWatcherForm();
const guestsValid = validateGuests();
if (!watcherValid || !guestsValid) {
toast.error("Controleer je invoer");
return;
}
submitMutation.mutate({
firstName: watcherData.firstName.trim(),
lastName: watcherData.lastName.trim(),
email: watcherData.email.trim(),
phone: watcherData.phone.trim() || undefined,
registrationType: "watcher",
guests: guests.map((g) => ({
firstName: g.firstName.trim(),
lastName: g.lastName.trim(),
email: g.email.trim() || undefined,
phone: g.phone.trim() || undefined,
})),
extraQuestions: watcherData.extraQuestions.trim() || undefined,
});
},
[watcherData, guests, submitMutation, validateWatcherForm, validateGuests],
);
const inputClasses = (hasError: boolean) =>
`w-full border-b bg-transparent pb-2 text-lg text-white placeholder:text-white/40 focus:outline-none focus:ring-0 transition-colors ${hasError ? "border-red-400" : "border-white/30 focus:border-white"}`;
const manageUrl = successToken
? `${typeof window !== "undefined" ? window.location.origin : ""}/manage/${successToken}`
: "";
if (successToken) {
return (
<section
id="registration"
className="relative z-30 flex w-full items-center justify-center bg-[#214e51]/96 px-6 py-16 md:px-12"
>
<div className="mx-auto w-full max-w-3xl">
<div className="rounded-lg border border-white/20 bg-white/5 p-8 md:p-12">
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-green-500/20 text-green-400">
<svg
className="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-label="Succes"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h2 className="mb-4 font-['Intro',sans-serif] text-3xl text-white md:text-4xl">
Gelukt!
</h2>
<p className="mb-6 text-lg text-white/80">
Je inschrijving is bevestigd. We sturen je zo dadelijk een
bevestigingsmail.
</p>
<div className="mb-8 rounded-lg border border-white/10 bg-white/5 p-6">
<p className="mb-2 text-sm text-white/60">
Geen mail ontvangen? Gebruik deze link:
</p>
<a
href={manageUrl}
className="break-all text-sm text-white/80 underline underline-offset-2 hover:text-white"
>
{manageUrl}
</a>
</div>
<div className="flex flex-wrap items-center gap-4">
<a
href={manageUrl}
className="inline-flex items-center bg-white px-6 py-3 font-['Intro',sans-serif] text-[#214e51] text-lg transition-all hover:scale-105 hover:bg-gray-100"
>
Bekijk mijn inschrijving
</a>
<button
type="button"
onClick={handleReset}
className="text-white/60 underline underline-offset-2 transition-colors hover:text-white"
>
Nog een inschrijving
</button>
</div>
</div>
</div>
</section>
);
}
return (
<section
id="registration"
className="relative z-30 w-full bg-[#214e51]/96 px-6 py-16 md:px-12"
>
<div className="mx-auto w-full max-w-6xl">
<h2 className="mb-2 font-['Intro',sans-serif] text-3xl text-white md:text-4xl">
Schrijf je nu in!
</h2>
<p className="mb-12 max-w-3xl text-lg text-white/80 md:text-xl">
Doe je mee of kom je kijken? Kies je rol en vul het formulier in.
</p>
{/* Two-column choice cards */}
{!selectedType && (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{/* Performer card */}
<button
type="button"
onClick={() => setSelectedType("performer")}
className="group relative flex flex-col overflow-hidden border border-white/20 bg-white/5 p-8 text-left transition-all duration-300 hover:border-white/50 hover:bg-white/10 md:p-10"
>
{/* Decorative top stripe */}
<div className="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-amber-400 to-orange-500 opacity-80" />
<div className="mb-6 flex h-14 w-14 items-center justify-center rounded-full bg-amber-400/15 text-amber-300">
{/* Microphone icon */}
<svg
className="h-7 w-7"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z"
/>
</svg>
</div>
<h3 className="mb-3 font-['Intro',sans-serif] text-2xl text-white md:text-3xl">
Ik wil optreden
</h3>
<p className="mb-6 text-white/70">
Deel jouw talent met het publiek. Muziek, theater, dans, comedy
alles is welkom. Je moet 16 jaar of ouder zijn.
</p>
<div className="mt-auto flex items-center gap-2 font-medium text-amber-300 text-sm">
<span>Inschrijven als artiest</span>
<svg
className="h-4 w-4 transition-transform group-hover:translate-x-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
/>
</svg>
</div>
</button>
{/* Watcher card */}
<button
type="button"
onClick={() => setSelectedType("watcher")}
className="group relative flex flex-col overflow-hidden border border-white/20 bg-white/5 p-8 text-left transition-all duration-300 hover:border-white/50 hover:bg-white/10 md:p-10"
>
{/* Decorative top stripe */}
<div className="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-teal-400 to-cyan-400 opacity-80" />
<div className="mb-6 flex h-14 w-14 items-center justify-center rounded-full bg-teal-400/15 text-teal-300">
{/* Ticket / audience icon */}
<svg
className="h-7 w-7"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z"
/>
</svg>
</div>
<h3 className="mb-3 font-['Intro',sans-serif] text-2xl text-white md:text-3xl">
Ik wil komen kijken
</h3>
<p className="mb-4 text-white/70">
Geniet van een avond vol talent en kunst. Je betaald 5EUR inkom
dat je mag gebruiken op je drinkkaart.
</p>
{/* Drink card badge */}
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-teal-400/40 bg-teal-400/10 px-4 py-1.5">
<span className="font-semibold text-sm text-teal-300">
Drinkkaart 5 EUR inbegrepen bij inkom.
</span>
</div>
<div className="mt-auto flex items-center gap-2 font-medium text-sm text-teal-300">
<span>Inschrijven als bezoeker</span>
<svg
className="h-4 w-4 transition-transform group-hover:translate-x-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
/>
</svg>
</div>
</button>
</div>
)}
{/* Performer form */}
{selectedType === "performer" && (
<div>
<div className="mb-8 flex items-center gap-4">
<button
type="button"
onClick={() => setSelectedType(null)}
className="flex items-center gap-2 text-white/60 transition-colors hover:text-white"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
/>
</svg>
Terug
</button>
<div className="h-px flex-1 bg-white/10" />
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-amber-400/20 text-amber-300">
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z"
/>
</svg>
</div>
<span className="font-['Intro',sans-serif] text-amber-300">
Ik wil optreden
</span>
</div>
</div>
<form
onSubmit={handlePerformerSubmit}
className="flex flex-col gap-6"
noValidate
>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="flex flex-col gap-2">
<label htmlFor="p-firstName" className="text-white text-xl">
Voornaam <span className="text-red-300">*</span>
</label>
<input
type="text"
id="p-firstName"
name="firstName"
value={performerData.firstName}
onChange={handlePerformerChange}
onBlur={handlePerformerBlur}
placeholder="Jouw voornaam"
autoComplete="given-name"
aria-required="true"
aria-invalid={
performerTouched.firstName && !!performerErrors.firstName
}
className={inputClasses(
!!performerTouched.firstName &&
!!performerErrors.firstName,
)}
/>
{performerTouched.firstName && performerErrors.firstName && (
<span className="text-red-300 text-sm" role="alert">
{performerErrors.firstName}
</span>
)}
</div>
<div className="flex flex-col gap-2">
<label htmlFor="p-lastName" className="text-white text-xl">
Achternaam <span className="text-red-300">*</span>
</label>
<input
type="text"
id="p-lastName"
name="lastName"
value={performerData.lastName}
onChange={handlePerformerChange}
onBlur={handlePerformerBlur}
placeholder="Jouw achternaam"
autoComplete="family-name"
aria-required="true"
aria-invalid={
performerTouched.lastName && !!performerErrors.lastName
}
className={inputClasses(
!!performerTouched.lastName && !!performerErrors.lastName,
)}
/>
{performerTouched.lastName && performerErrors.lastName && (
<span className="text-red-300 text-sm" role="alert">
{performerErrors.lastName}
</span>
)}
</div>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="flex flex-col gap-2">
<label htmlFor="p-email" className="text-white text-xl">
E-mail <span className="text-red-300">*</span>
</label>
<input
type="email"
id="p-email"
name="email"
value={performerData.email}
onChange={handlePerformerChange}
onBlur={handlePerformerBlur}
placeholder="jouw@email.be"
autoComplete="email"
inputMode="email"
aria-required="true"
aria-invalid={
performerTouched.email && !!performerErrors.email
}
className={inputClasses(
!!performerTouched.email && !!performerErrors.email,
)}
/>
{performerTouched.email && performerErrors.email && (
<span className="text-red-300 text-sm" role="alert">
{performerErrors.email}
</span>
)}
</div>
<div className="flex flex-col gap-2">
<label htmlFor="p-phone" className="text-white text-xl">
Telefoon
</label>
<input
type="tel"
id="p-phone"
name="phone"
value={performerData.phone}
onChange={handlePerformerChange}
onBlur={handlePerformerBlur}
placeholder="06-12345678"
autoComplete="tel"
inputMode="tel"
aria-invalid={
performerTouched.phone && !!performerErrors.phone
}
className={inputClasses(
!!performerTouched.phone && !!performerErrors.phone,
)}
/>
{performerTouched.phone && performerErrors.phone && (
<span className="text-red-300 text-sm" role="alert">
{performerErrors.phone}
</span>
)}
</div>
</div>
{/* Performer-specific fields */}
<div className="border border-amber-400/20 bg-amber-400/5 p-6">
<p className="mb-5 text-amber-300/80 text-sm uppercase tracking-wider">
Optreden details
</p>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<label htmlFor="p-artForm" className="text-white text-xl">
Kunstvorm <span className="text-red-300">*</span>
</label>
<input
type="text"
id="p-artForm"
name="artForm"
value={performerData.artForm}
onChange={handlePerformerChange}
onBlur={handlePerformerBlur}
placeholder="Muziek, Theater, Dans, etc."
autoComplete="off"
list="artFormSuggestions"
aria-required="true"
aria-invalid={
performerTouched.artForm && !!performerErrors.artForm
}
className={inputClasses(
!!performerTouched.artForm && !!performerErrors.artForm,
)}
/>
<datalist id="artFormSuggestions">
<option value="Muziek" />
<option value="Theater" />
<option value="Dans" />
<option value="Beeldende Kunst" />
<option value="Woordkunst" />
<option value="Comedy" />
</datalist>
{performerTouched.artForm && performerErrors.artForm && (
<span className="text-red-300 text-sm" role="alert">
{performerErrors.artForm}
</span>
)}
</div>
<div className="flex flex-col gap-2">
<label
htmlFor="p-experience"
className="text-white text-xl"
>
Ervaring
</label>
<input
type="text"
id="p-experience"
name="experience"
value={performerData.experience}
onChange={handlePerformerChange}
onBlur={handlePerformerBlur}
placeholder="Beginner / Gevorderd / Professional"
autoComplete="off"
list="experienceSuggestions"
className={inputClasses(false)}
/>
<datalist id="experienceSuggestions">
<option value="Beginner" />
<option value="Gevorderd" />
<option value="Professional" />
</datalist>
</div>
{/* Age confirmation */}
<div>
<label
htmlFor="p-isOver16"
className="flex cursor-pointer items-start gap-4"
>
<div className="relative mt-0.5 flex shrink-0">
<input
type="checkbox"
id="p-isOver16"
name="isOver16"
checked={performerData.isOver16}
onChange={handlePerformerChange}
className="peer sr-only"
/>
<div
className={`h-6 w-6 border-2 bg-transparent transition-colors peer-checked:bg-amber-400 peer-focus:ring-2 peer-focus:ring-amber-400/50 peer-focus:ring-offset-2 peer-focus:ring-offset-[#214e51] ${performerTouched.isOver16 && performerErrors.isOver16 ? "border-red-400" : "border-white/50 peer-checked:border-amber-400"}`}
/>
<svg
className="pointer-events-none absolute top-0 left-0 h-6 w-6 scale-0 text-[#214e51] transition-transform peer-checked:scale-100"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3"
aria-hidden="true"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
<div className="flex flex-col gap-1">
<span className="text-lg text-white">
Ik ben 16 jaar of ouder{" "}
<span className="text-red-300">*</span>
</span>
<span className="text-sm text-white/60">
Je moet minimaal 16 jaar oud zijn om op te treden.
</span>
</div>
</label>
{performerTouched.isOver16 && performerErrors.isOver16 && (
<span
className="mt-2 block text-red-300 text-sm"
role="alert"
>
{performerErrors.isOver16}
</span>
)}
</div>
</div>
</div>
<div className="flex flex-col gap-2 border border-white/10 p-6">
<label
htmlFor="p-extraQuestions"
className="text-white text-xl"
>
Vragen of opmerkingen
</label>
<textarea
id="p-extraQuestions"
name="extraQuestions"
value={performerData.extraQuestions}
onChange={handlePerformerChange}
onBlur={handlePerformerBlur}
placeholder="Heb je nog vragen of iets dat we moeten weten?"
rows={3}
autoComplete="off"
className="resize-none bg-transparent text-lg text-white transition-all placeholder:text-white/40 focus:outline-none"
/>
</div>
<div className="flex flex-col items-center gap-4 pt-4">
<button
type="submit"
disabled={submitMutation.isPending}
aria-busy={submitMutation.isPending}
className="bg-amber-400 px-12 py-4 font-['Intro',sans-serif] text-[#1a3d40] text-xl transition-all hover:scale-105 hover:bg-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-400/50 focus:ring-offset-2 focus:ring-offset-[#214e51] disabled:cursor-not-allowed disabled:opacity-50 md:text-2xl"
>
{submitMutation.isPending ? (
<span className="flex items-center gap-2">
<svg
className="h-5 w-5 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-label="Laden"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Bezig...
</span>
) : (
"Bevestigen"
)}
</button>
<a
href="/contact"
className="text-sm text-white/60 transition-colors hover:text-white"
>
Nog vragen? Neem contact op
</a>
</div>
</form>
</div>
)}
{/* Watcher form */}
{selectedType === "watcher" && (
<div>
<div className="mb-8 flex items-center gap-4">
<button
type="button"
onClick={() => setSelectedType(null)}
className="flex items-center gap-2 text-white/60 transition-colors hover:text-white"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
/>
</svg>
Terug
</button>
<div className="h-px flex-1 bg-white/10" />
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-teal-400/20 text-teal-300">
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z"
/>
</svg>
</div>
<span className="font-['Intro',sans-serif] text-teal-300">
Ik wil komen kijken
</span>
</div>
</div>
{/* Drink card callout — price updates live with guest count */}
<div className="mb-8 flex items-center gap-4 rounded-lg border border-teal-400/30 bg-teal-400/10 p-5">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-teal-400/20 font-bold text-teal-300 text-xl">
{5 + guests.length * 2}
</div>
<div>
<p className="font-semibold text-white">
Drinkkaart inbegrepen
</p>
<p className="text-sm text-white/70">
Je betaald bij inkom{" "}
<strong className="text-teal-300">5</strong> (+ 2 per
medebezoeker) dat gaat naar je drinkkaart.
{guests.length > 0 && (
<span className="ml-1 font-semibold text-teal-300">
Totaal: {5 + guests.length * 2} voor {1 + guests.length}{" "}
personen.
</span>
)}
</p>
</div>
</div>
<form
onSubmit={handleWatcherSubmit}
className="flex flex-col gap-6"
noValidate
>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="flex flex-col gap-2">
<label htmlFor="w-firstName" className="text-white text-xl">
Voornaam <span className="text-red-300">*</span>
</label>
<input
type="text"
id="w-firstName"
name="firstName"
value={watcherData.firstName}
onChange={handleWatcherChange}
onBlur={handleWatcherBlur}
placeholder="Jouw voornaam"
autoComplete="given-name"
aria-required="true"
aria-invalid={
watcherTouched.firstName && !!watcherErrors.firstName
}
className={inputClasses(
!!watcherTouched.firstName && !!watcherErrors.firstName,
)}
/>
{watcherTouched.firstName && watcherErrors.firstName && (
<span className="text-red-300 text-sm" role="alert">
{watcherErrors.firstName}
</span>
)}
</div>
<div className="flex flex-col gap-2">
<label htmlFor="w-lastName" className="text-white text-xl">
Achternaam <span className="text-red-300">*</span>
</label>
<input
type="text"
id="w-lastName"
name="lastName"
value={watcherData.lastName}
onChange={handleWatcherChange}
onBlur={handleWatcherBlur}
placeholder="Jouw achternaam"
autoComplete="family-name"
aria-required="true"
aria-invalid={
watcherTouched.lastName && !!watcherErrors.lastName
}
className={inputClasses(
!!watcherTouched.lastName && !!watcherErrors.lastName,
)}
/>
{watcherTouched.lastName && watcherErrors.lastName && (
<span className="text-red-300 text-sm" role="alert">
{watcherErrors.lastName}
</span>
)}
</div>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="flex flex-col gap-2">
<label htmlFor="w-email" className="text-white text-xl">
E-mail <span className="text-red-300">*</span>
</label>
<input
type="email"
id="w-email"
name="email"
value={watcherData.email}
onChange={handleWatcherChange}
onBlur={handleWatcherBlur}
placeholder="jouw@email.be"
autoComplete="email"
inputMode="email"
aria-required="true"
aria-invalid={watcherTouched.email && !!watcherErrors.email}
className={inputClasses(
!!watcherTouched.email && !!watcherErrors.email,
)}
/>
{watcherTouched.email && watcherErrors.email && (
<span className="text-red-300 text-sm" role="alert">
{watcherErrors.email}
</span>
)}
</div>
<div className="flex flex-col gap-2">
<label htmlFor="w-phone" className="text-white text-xl">
Telefoon
</label>
<input
type="tel"
id="w-phone"
name="phone"
value={watcherData.phone}
onChange={handleWatcherChange}
onBlur={handleWatcherBlur}
placeholder="06-12345678"
autoComplete="tel"
inputMode="tel"
aria-invalid={watcherTouched.phone && !!watcherErrors.phone}
className={inputClasses(
!!watcherTouched.phone && !!watcherErrors.phone,
)}
/>
{watcherTouched.phone && watcherErrors.phone && (
<span className="text-red-300 text-sm" role="alert">
{watcherErrors.phone}
</span>
)}
</div>
</div>
{/* Guests section */}
<div className="border border-teal-400/20 bg-teal-400/5 p-6">
<div className="mb-4 flex items-center justify-between">
<p className="text-sm text-teal-300/80 uppercase tracking-wider">
Medebezoekers{" "}
<span className="text-white/50 normal-case">
({guests.length}/9)
</span>
</p>
{guests.length < 9 && (
<button
type="button"
onClick={handleAddGuest}
className="flex items-center gap-1.5 rounded border border-teal-400/40 bg-teal-400/10 px-3 py-1.5 text-sm text-teal-300 transition-colors hover:bg-teal-400/20"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 4.5v15m7.5-7.5h-15"
/>
</svg>
Medebezoeker toevoegen
</button>
)}
</div>
{guests.length === 0 && (
<p className="text-sm text-white/40">
Kom je met iemand mee? Voeg je medebezoekers toe. Elke extra
persoon kost 2 op de drinkkaart.
</p>
)}
<div className="flex flex-col gap-6">
{guests.map((guest, idx) => (
<div
key={`guest-${
// biome-ignore lint/suspicious/noArrayIndexKey: index is stable here
idx
}`}
className="relative border border-teal-400/20 p-4"
>
<div className="mb-3 flex items-center justify-between">
<span className="font-medium text-sm text-teal-300">
Medebezoeker {idx + 1}
</span>
<button
type="button"
onClick={() => handleRemoveGuest(idx)}
className="flex items-center gap-1 text-red-400/70 text-sm transition-colors hover:text-red-300"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
Verwijderen
</button>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="flex flex-col gap-2">
<label
htmlFor={`guest-${idx}-firstName`}
className="text-white/80"
>
Voornaam <span className="text-red-300">*</span>
</label>
<input
type="text"
id={`guest-${idx}-firstName`}
value={guest.firstName}
onChange={(e) =>
handleGuestChange(
idx,
"firstName",
e.target.value,
)
}
placeholder="Voornaam"
autoComplete="off"
className={inputClasses(
!!guestErrors[idx]?.firstName,
)}
/>
{guestErrors[idx]?.firstName && (
<span className="text-red-300 text-sm" role="alert">
{guestErrors[idx].firstName}
</span>
)}
</div>
<div className="flex flex-col gap-2">
<label
htmlFor={`guest-${idx}-lastName`}
className="text-white/80"
>
Achternaam <span className="text-red-300">*</span>
</label>
<input
type="text"
id={`guest-${idx}-lastName`}
value={guest.lastName}
onChange={(e) =>
handleGuestChange(idx, "lastName", e.target.value)
}
placeholder="Achternaam"
autoComplete="off"
className={inputClasses(
!!guestErrors[idx]?.lastName,
)}
/>
{guestErrors[idx]?.lastName && (
<span className="text-red-300 text-sm" role="alert">
{guestErrors[idx].lastName}
</span>
)}
</div>
<div className="flex flex-col gap-2">
<label
htmlFor={`guest-${idx}-email`}
className="text-white/80"
>
E-mail
</label>
<input
type="email"
id={`guest-${idx}-email`}
value={guest.email}
onChange={(e) =>
handleGuestChange(idx, "email", e.target.value)
}
placeholder="optioneel@email.be"
autoComplete="off"
inputMode="email"
className={inputClasses(!!guestErrors[idx]?.email)}
/>
{guestErrors[idx]?.email && (
<span className="text-red-300 text-sm" role="alert">
{guestErrors[idx].email}
</span>
)}
</div>
<div className="flex flex-col gap-2">
<label
htmlFor={`guest-${idx}-phone`}
className="text-white/80"
>
Telefoon
</label>
<input
type="tel"
id={`guest-${idx}-phone`}
value={guest.phone}
onChange={(e) =>
handleGuestChange(idx, "phone", e.target.value)
}
placeholder="06-12345678"
autoComplete="off"
inputMode="tel"
className={inputClasses(!!guestErrors[idx]?.phone)}
/>
{guestErrors[idx]?.phone && (
<span className="text-red-300 text-sm" role="alert">
{guestErrors[idx].phone}
</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
<div className="flex flex-col gap-2 border border-white/10 p-6">
<label
htmlFor="w-extraQuestions"
className="text-white text-xl"
>
Vragen of opmerkingen
</label>
<textarea
id="w-extraQuestions"
name="extraQuestions"
value={watcherData.extraQuestions}
onChange={handleWatcherChange}
onBlur={handleWatcherBlur}
placeholder="Heb je nog vragen of iets dat we moeten weten?"
rows={3}
autoComplete="off"
className="resize-none bg-transparent text-lg text-white transition-all placeholder:text-white/40 focus:outline-none"
/>
</div>
<div className="flex flex-col items-center gap-4 pt-4">
<button
type="submit"
disabled={submitMutation.isPending}
aria-busy={submitMutation.isPending}
className="bg-white px-12 py-4 font-['Intro',sans-serif] text-[#214e51] text-xl transition-all hover:scale-105 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-[#214e51] disabled:cursor-not-allowed disabled:opacity-50 md:text-2xl"
>
{submitMutation.isPending ? (
<span className="flex items-center gap-2">
<svg
className="h-5 w-5 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-label="Laden"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Bezig...
</span>
) : (
"Bevestigen"
)}
</button>
<a
href="/contact"
className="text-sm text-white/60 transition-colors hover:text-white"
>
Nog vragen? Neem contact op
</a>
</div>
</form>
</div>
)}
</div>
</section>
);
}