"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(null); const [successToken, setSuccessToken] = useState(null); // Performer form state const [performerData, setPerformerData] = useState({ firstName: "", lastName: "", email: "", phone: "", artForm: "", experience: "", isOver16: false, extraQuestions: "", }); const [performerErrors, setPerformerErrors] = useState({}); const [performerTouched, setPerformerTouched] = useState< Record >({}); // Watcher form state const [watcherData, setWatcherData] = useState({ firstName: "", lastName: "", email: "", phone: "", extraQuestions: "", }); const [watcherErrors, setWatcherErrors] = useState({}); const [watcherTouched, setWatcherTouched] = useState>( {}, ); // Guests state (bezoeker only) const [guests, setGuests] = useState([]); const [guestErrors, setGuestErrors] = useState([]); 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) => { 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 = { 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) => { const { name, value } = e.target; setPerformerTouched((prev) => ({ ...prev, [name]: true })); const errMap: Record = { 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) => { const { name, value } = e.target; setWatcherData((prev) => ({ ...prev, [name]: value })); if (watcherTouched[name]) { const errMap: Record = { 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) => { const { name, value } = e.target; setWatcherTouched((prev) => ({ ...prev, [name]: true })); const errMap: Record = { 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 (

Gelukt!

Je inschrijving is bevestigd. We sturen je zo dadelijk een bevestigingsmail.

Geen mail ontvangen? Gebruik deze link:

{manageUrl}
Bekijk mijn inschrijving
); } return (

Schrijf je nu in!

Doe je mee of kom je kijken? Kies je rol en vul het formulier in.

{/* Two-column choice cards */} {!selectedType && (
{/* Performer card */} {/* Watcher card */}
)} {/* Performer form */} {selectedType === "performer" && (
Ik wil optreden
{performerTouched.firstName && performerErrors.firstName && ( {performerErrors.firstName} )}
{performerTouched.lastName && performerErrors.lastName && ( {performerErrors.lastName} )}
{performerTouched.email && performerErrors.email && ( {performerErrors.email} )}
{performerTouched.phone && performerErrors.phone && ( {performerErrors.phone} )}
{/* Performer-specific fields */}

Optreden details

{performerTouched.artForm && performerErrors.artForm && ( {performerErrors.artForm} )}
{/* Age confirmation */}