1366 lines
44 KiB
TypeScript
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>
|
|
);
|
|
}
|