feat:multiple bezoekers
This commit is contained in:
@@ -8,6 +8,31 @@ export const Route = createFileRoute("/manage/$token")({
|
||||
component: ManageRegistrationPage,
|
||||
});
|
||||
|
||||
interface GuestEntry {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
function parseGuests(raw: string | null | undefined): GuestEntry[] {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.map((g) => ({
|
||||
firstName: g.firstName ?? "",
|
||||
lastName: g.lastName ?? "",
|
||||
email: g.email ?? "",
|
||||
phone: g.phone ?? "",
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function ManageRegistrationPage() {
|
||||
const { token } = useParams({ from: "/manage/$token" });
|
||||
const queryClient = useQueryClient();
|
||||
@@ -18,11 +43,13 @@ function ManageRegistrationPage() {
|
||||
lastName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
wantsToPerform: false,
|
||||
registrationType: "watcher" as "performer" | "watcher",
|
||||
artForm: "",
|
||||
experience: "",
|
||||
isOver16: false,
|
||||
extraQuestions: "",
|
||||
});
|
||||
const [formGuests, setFormGuests] = useState<GuestEntry[]>([]);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
...orpc.getRegistrationByToken.queryOptions({ input: { token } }),
|
||||
@@ -58,30 +85,41 @@ function ManageRegistrationPage() {
|
||||
lastName: data.lastName,
|
||||
email: data.email,
|
||||
phone: data.phone || "",
|
||||
wantsToPerform: data.wantsToPerform ?? false,
|
||||
registrationType:
|
||||
(data.registrationType as "performer" | "watcher") ?? "watcher",
|
||||
artForm: data.artForm || "",
|
||||
experience: data.experience || "",
|
||||
isOver16: data.isOver16 ?? false,
|
||||
extraQuestions: data.extraQuestions || "",
|
||||
});
|
||||
setFormGuests(parseGuests(data.guests));
|
||||
setIsEditing(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const isPerformer = formData.registrationType === "performer";
|
||||
updateMutation.mutate({
|
||||
token,
|
||||
firstName: formData.firstName.trim(),
|
||||
lastName: formData.lastName.trim(),
|
||||
email: formData.email.trim(),
|
||||
phone: formData.phone.trim() || undefined,
|
||||
wantsToPerform: formData.wantsToPerform,
|
||||
artForm: formData.wantsToPerform
|
||||
? formData.artForm.trim() || undefined
|
||||
: undefined,
|
||||
experience: formData.wantsToPerform
|
||||
registrationType: formData.registrationType,
|
||||
artForm: isPerformer ? formData.artForm.trim() || undefined : undefined,
|
||||
experience: isPerformer
|
||||
? formData.experience.trim() || undefined
|
||||
: undefined,
|
||||
isOver16: isPerformer ? formData.isOver16 : false,
|
||||
guests: isPerformer
|
||||
? []
|
||||
: formGuests.map((g) => ({
|
||||
firstName: g.firstName.trim(),
|
||||
lastName: g.lastName.trim(),
|
||||
email: g.email.trim() || undefined,
|
||||
phone: g.phone.trim() || undefined,
|
||||
})),
|
||||
extraQuestions: formData.extraQuestions.trim() || undefined,
|
||||
});
|
||||
};
|
||||
@@ -96,6 +134,33 @@ function ManageRegistrationPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddGuest = () => {
|
||||
if (formGuests.length >= 9) return;
|
||||
setFormGuests((prev) => [
|
||||
...prev,
|
||||
{ firstName: "", lastName: "", email: "", phone: "" },
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRemoveGuest = (index: number) => {
|
||||
setFormGuests((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleGuestChange = (
|
||||
index: number,
|
||||
field: keyof GuestEntry,
|
||||
value: string,
|
||||
) => {
|
||||
setFormGuests((prev) => {
|
||||
const next = [...prev];
|
||||
next[index] = { ...next[index], [field]: value };
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const inputClasses =
|
||||
"w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none";
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#214e51]">
|
||||
@@ -133,7 +198,13 @@ function ManageRegistrationPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const isPerformer = data.registrationType === "performer";
|
||||
const viewGuests = parseGuests(data.guests);
|
||||
|
||||
if (isEditing) {
|
||||
const isEditingWatcher = formData.registrationType === "watcher";
|
||||
const totalDrinkCard = isEditingWatcher ? 5 + formGuests.length * 2 : 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#214e51]">
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
@@ -161,7 +232,7 @@ function ManageRegistrationPage() {
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({ ...p, firstName: e.target.value }))
|
||||
}
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -176,7 +247,7 @@ function ManageRegistrationPage() {
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({ ...p, lastName: e.target.value }))
|
||||
}
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,7 +264,7 @@ function ManageRegistrationPage() {
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({ ...p, email: e.target.value }))
|
||||
}
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -208,46 +279,57 @@ function ManageRegistrationPage() {
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({ ...p, phone: e.target.value }))
|
||||
}
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label
|
||||
htmlFor="wantsToPerform"
|
||||
className="flex cursor-pointer items-center gap-4"
|
||||
>
|
||||
<div className="relative flex shrink-0">
|
||||
<input
|
||||
id="wantsToPerform"
|
||||
type="checkbox"
|
||||
checked={formData.wantsToPerform}
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({
|
||||
...p,
|
||||
wantsToPerform: e.target.checked,
|
||||
artForm: e.target.checked ? p.artForm : "",
|
||||
experience: e.target.checked ? p.experience : "",
|
||||
}))
|
||||
}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-6 w-6 border-2 border-white/50 bg-transparent transition-colors peer-checked:border-white peer-checked:bg-white" />
|
||||
<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-label="Geselecteerd"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
{/* Registration type */}
|
||||
<div>
|
||||
<p className="mb-3 text-white">Type inschrijving</p>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex cursor-pointer items-center gap-3">
|
||||
<div className="relative flex shrink-0">
|
||||
<input
|
||||
type="radio"
|
||||
name="registrationType"
|
||||
value="performer"
|
||||
checked={formData.registrationType === "performer"}
|
||||
onChange={() =>
|
||||
setFormData((p) => ({
|
||||
...p,
|
||||
registrationType: "performer",
|
||||
}))
|
||||
}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white/50 bg-transparent transition-colors peer-checked:border-amber-400 peer-checked:bg-amber-400" />
|
||||
</div>
|
||||
<span className="text-white">Optreden</span>
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-center gap-3">
|
||||
<div className="relative flex shrink-0">
|
||||
<input
|
||||
type="radio"
|
||||
name="registrationType"
|
||||
value="watcher"
|
||||
checked={formData.registrationType === "watcher"}
|
||||
onChange={() =>
|
||||
setFormData((p) => ({
|
||||
...p,
|
||||
registrationType: "watcher",
|
||||
}))
|
||||
}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white/50 bg-transparent transition-colors peer-checked:border-teal-400 peer-checked:bg-teal-400" />
|
||||
</div>
|
||||
<span className="text-white">Kijken</span>
|
||||
</label>
|
||||
</div>
|
||||
<span className="text-white text-xl">Ik wil optreden</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{formData.wantsToPerform && (
|
||||
<div className="space-y-6 border border-white/20 p-6">
|
||||
{formData.registrationType === "performer" && (
|
||||
<div className="space-y-6 border border-amber-400/20 bg-amber-400/5 p-6">
|
||||
<div>
|
||||
<label htmlFor="artForm" className="mb-2 block text-white">
|
||||
Kunstvorm *
|
||||
@@ -255,16 +337,13 @@ function ManageRegistrationPage() {
|
||||
<input
|
||||
id="artForm"
|
||||
type="text"
|
||||
required={formData.wantsToPerform}
|
||||
required={formData.registrationType === "performer"}
|
||||
value={formData.artForm}
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({
|
||||
...p,
|
||||
artForm: e.target.value,
|
||||
}))
|
||||
setFormData((p) => ({ ...p, artForm: e.target.value }))
|
||||
}
|
||||
list="artFormSuggestions"
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
<datalist id="artFormSuggestions">
|
||||
<option value="Muziek" />
|
||||
@@ -290,7 +369,7 @@ function ManageRegistrationPage() {
|
||||
}))
|
||||
}
|
||||
list="experienceSuggestions"
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
<datalist id="experienceSuggestions">
|
||||
<option value="Beginner" />
|
||||
@@ -298,6 +377,187 @@ function ManageRegistrationPage() {
|
||||
<option value="Professional" />
|
||||
</datalist>
|
||||
</div>
|
||||
<label className="flex cursor-pointer items-center gap-3">
|
||||
<div className="relative flex shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isOver16}
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({
|
||||
...p,
|
||||
isOver16: e.target.checked,
|
||||
}))
|
||||
}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-6 w-6 border-2 border-white/50 bg-transparent transition-colors peer-checked:border-amber-400 peer-checked:bg-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-label="Geselecteerd"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-white">Ik ben 16 jaar of ouder</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Guests section for watchers */}
|
||||
{isEditingWatcher && (
|
||||
<div className="border border-teal-400/20 bg-teal-400/5 p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-teal-300/80 uppercase tracking-wider">
|
||||
Medebezoekers{" "}
|
||||
<span className="text-white/50 normal-case">
|
||||
({formGuests.length}/9)
|
||||
</span>
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-white/60">
|
||||
Drinkkaart totaal: €{totalDrinkCard} voor{" "}
|
||||
{1 + formGuests.length} personen
|
||||
</p>
|
||||
</div>
|
||||
{formGuests.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>
|
||||
Toevoegen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{formGuests.length === 0 && (
|
||||
<p className="text-sm text-white/40">
|
||||
Geen medebezoekers toegevoegd.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{formGuests.map((guest, idx) => (
|
||||
<div
|
||||
key={`edit-guest-${
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: stable index
|
||||
idx
|
||||
}`}
|
||||
className="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="text-red-400/70 text-sm transition-colors hover:text-red-300"
|
||||
>
|
||||
Verwijderen
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
htmlFor={`edit-guest-${idx}-firstName`}
|
||||
className="mb-1 block text-sm text-white/70"
|
||||
>
|
||||
Voornaam *
|
||||
</label>
|
||||
<input
|
||||
id={`edit-guest-${idx}-firstName`}
|
||||
type="text"
|
||||
required
|
||||
value={guest.firstName}
|
||||
onChange={(e) =>
|
||||
handleGuestChange(
|
||||
idx,
|
||||
"firstName",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder="Voornaam"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={`edit-guest-${idx}-lastName`}
|
||||
className="mb-1 block text-sm text-white/70"
|
||||
>
|
||||
Achternaam *
|
||||
</label>
|
||||
<input
|
||||
id={`edit-guest-${idx}-lastName`}
|
||||
type="text"
|
||||
required
|
||||
value={guest.lastName}
|
||||
onChange={(e) =>
|
||||
handleGuestChange(idx, "lastName", e.target.value)
|
||||
}
|
||||
placeholder="Achternaam"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={`edit-guest-${idx}-email`}
|
||||
className="mb-1 block text-sm text-white/70"
|
||||
>
|
||||
E-mail
|
||||
</label>
|
||||
<input
|
||||
id={`edit-guest-${idx}-email`}
|
||||
type="email"
|
||||
value={guest.email}
|
||||
onChange={(e) =>
|
||||
handleGuestChange(idx, "email", e.target.value)
|
||||
}
|
||||
placeholder="optioneel@email.be"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={`edit-guest-${idx}-phone`}
|
||||
className="mb-1 block text-sm text-white/70"
|
||||
>
|
||||
Telefoon
|
||||
</label>
|
||||
<input
|
||||
id={`edit-guest-${idx}-phone`}
|
||||
type="tel"
|
||||
value={guest.phone}
|
||||
onChange={(e) =>
|
||||
handleGuestChange(idx, "phone", e.target.value)
|
||||
}
|
||||
placeholder="06-12345678"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -358,6 +618,19 @@ function ManageRegistrationPage() {
|
||||
Open Mic Night — vrijdag 18 april 2026
|
||||
</p>
|
||||
|
||||
{/* Type badge */}
|
||||
<div
|
||||
className={`mb-6 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 font-semibold text-sm ${isPerformer ? "border-amber-400/40 bg-amber-400/10 text-amber-300" : "border-teal-400/40 bg-teal-400/10 text-teal-300"}`}
|
||||
>
|
||||
{isPerformer ? "Artiest" : "Bezoeker"}
|
||||
{!isPerformer && (
|
||||
<span className="ml-1 opacity-80">
|
||||
— Drinkkaart €{data.drinkCardValue ?? 5}
|
||||
{viewGuests.length > 0 && ` (${1 + viewGuests.length} personen)`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 rounded-lg border border-white/20 bg-white/5 p-6 md:p-8">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
@@ -378,17 +651,51 @@ function ManageRegistrationPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-white/10 border-t pt-6">
|
||||
<p className="mb-2 text-sm text-white/50">Rol</p>
|
||||
<p className="text-lg text-white">
|
||||
{data.wantsToPerform
|
||||
? `Optreden${data.artForm ? ` — ${data.artForm}` : ""}`
|
||||
: "Toeschouwer"}
|
||||
</p>
|
||||
{data.wantsToPerform && data.experience && (
|
||||
<p className="mt-1 text-white/60">{data.experience}</p>
|
||||
)}
|
||||
</div>
|
||||
{isPerformer && (
|
||||
<div className="border-white/10 border-t pt-6">
|
||||
<p className="mb-2 text-sm text-white/50">Optreden</p>
|
||||
<p className="text-lg text-white">
|
||||
{data.artForm || "—"}
|
||||
{data.experience && (
|
||||
<span className="ml-2 text-white/60">
|
||||
({data.experience})
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-white/60">
|
||||
{data.isOver16 ? "16+ bevestigd" : "Leeftijd niet bevestigd"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isPerformer && viewGuests.length > 0 && (
|
||||
<div className="border-white/10 border-t pt-6">
|
||||
<p className="mb-3 text-sm text-white/50">
|
||||
Medebezoekers ({viewGuests.length})
|
||||
</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
{viewGuests.map((g, idx) => (
|
||||
<div
|
||||
key={`view-guest-${
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: stable index
|
||||
idx
|
||||
}`}
|
||||
className="rounded border border-teal-400/20 bg-teal-400/5 p-3"
|
||||
>
|
||||
<p className="text-white">
|
||||
{g.firstName} {g.lastName}
|
||||
</p>
|
||||
{g.email && (
|
||||
<p className="text-sm text-white/60">{g.email}</p>
|
||||
)}
|
||||
{g.phone && (
|
||||
<p className="text-sm text-white/60">{g.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.extraQuestions && (
|
||||
<div className="border-white/10 border-t pt-6">
|
||||
|
||||
Reference in New Issue
Block a user