418 lines
12 KiB
TypeScript
418 lines
12 KiB
TypeScript
import { useState } from "react";
|
|
import {
|
|
type GuestEntry,
|
|
type GuestErrors,
|
|
inputCls,
|
|
MAX_GUESTS,
|
|
} from "@/lib/registration";
|
|
|
|
interface Props {
|
|
guests: GuestEntry[];
|
|
errors: GuestErrors[];
|
|
onChange: (index: number, field: keyof GuestEntry, value: string) => void;
|
|
onAdd: () => void;
|
|
onRemove: (index: number) => void;
|
|
/** Optional suffix rendered after the guest count header (e.g. price warning) */
|
|
headerNote?: React.ReactNode;
|
|
/** Whether payment has already been made (disables removal without warning) */
|
|
isPaid?: boolean;
|
|
}
|
|
|
|
export function GuestList({
|
|
guests,
|
|
errors,
|
|
onChange,
|
|
onAdd,
|
|
onRemove,
|
|
headerNote,
|
|
isPaid,
|
|
}: Props) {
|
|
const [removingIndex, setRemovingIndex] = useState<number | null>(null);
|
|
|
|
const handleRemoveClick = (index: number) => {
|
|
if (isPaid) {
|
|
setRemovingIndex(index);
|
|
} else {
|
|
onRemove(index);
|
|
}
|
|
};
|
|
|
|
const confirmRemove = () => {
|
|
if (removingIndex !== null) {
|
|
onRemove(removingIndex);
|
|
setRemovingIndex(null);
|
|
}
|
|
};
|
|
|
|
const cancelRemove = () => {
|
|
setRemovingIndex(null);
|
|
};
|
|
|
|
return (
|
|
<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">
|
|
({guests.length}/{MAX_GUESTS})
|
|
</span>
|
|
</p>
|
|
{headerNote}
|
|
</div>
|
|
{guests.length < MAX_GUESTS && (
|
|
<button
|
|
type="button"
|
|
onClick={onAdd}
|
|
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>
|
|
{guests.length === 0 ? "Medebezoeker toevoegen" : "Toevoegen"}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Removal confirmation modal for paid registrations */}
|
|
{removingIndex !== null && (
|
|
<div className="mb-4 rounded border border-orange-400/30 bg-orange-400/10 p-4">
|
|
<p className="mb-2 font-medium text-orange-300 text-sm">
|
|
Let op: gast verwijderen
|
|
</p>
|
|
<p className="mb-4 text-sm text-white/70">
|
|
Als je al betaald hebt voor deze gast krijg je geen terugbetaling
|
|
bij het verwijderen.
|
|
</p>
|
|
<div className="flex gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={confirmRemove}
|
|
className="rounded bg-orange-400/20 px-3 py-1.5 text-orange-300 text-sm hover:bg-orange-400/30"
|
|
>
|
|
Toch verwijderen
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={cancelRemove}
|
|
className="rounded border border-white/20 px-3 py-1.5 text-sm text-white/60 hover:bg-white/5"
|
|
>
|
|
Annuleren
|
|
</button>
|
|
</div>
|
|
</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-4">
|
|
{guests.map((guest, idx) => (
|
|
<div
|
|
key={`guest-${
|
|
// biome-ignore lint/suspicious/noArrayIndexKey: stable positional 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={() => handleRemoveClick(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) => onChange(idx, "firstName", e.target.value)}
|
|
placeholder="Voornaam"
|
|
autoComplete="off"
|
|
className={inputCls(!!errors[idx]?.firstName)}
|
|
/>
|
|
{errors[idx]?.firstName && (
|
|
<span className="text-red-300 text-sm" role="alert">
|
|
{errors[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) => onChange(idx, "lastName", e.target.value)}
|
|
placeholder="Achternaam"
|
|
autoComplete="off"
|
|
className={inputCls(!!errors[idx]?.lastName)}
|
|
/>
|
|
{errors[idx]?.lastName && (
|
|
<span className="text-red-300 text-sm" role="alert">
|
|
{errors[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) => onChange(idx, "email", e.target.value)}
|
|
placeholder="optioneel@email.be"
|
|
autoComplete="off"
|
|
inputMode="email"
|
|
className={inputCls(!!errors[idx]?.email)}
|
|
/>
|
|
{errors[idx]?.email && (
|
|
<span className="text-red-300 text-sm" role="alert">
|
|
{errors[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) => onChange(idx, "phone", e.target.value)}
|
|
placeholder="06-12345678"
|
|
autoComplete="off"
|
|
inputMode="tel"
|
|
className={inputCls(!!errors[idx]?.phone)}
|
|
/>
|
|
{errors[idx]?.phone && (
|
|
<span className="text-red-300 text-sm" role="alert">
|
|
{errors[idx].phone}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Birthdate */}
|
|
<div className="flex flex-col gap-2 md:col-span-2">
|
|
<p className="text-white/80">
|
|
Geboortedatum <span className="text-red-300">*</span>
|
|
</p>
|
|
<fieldset className="m-0 flex gap-2 border-0 p-0">
|
|
<legend className="sr-only">
|
|
Geboortedatum medebezoeker {idx + 1}
|
|
</legend>
|
|
<div className="flex flex-1 flex-col gap-1">
|
|
<label
|
|
htmlFor={`guest-${idx}-birthdateDay`}
|
|
className="sr-only"
|
|
>
|
|
Dag
|
|
</label>
|
|
<select
|
|
id={`guest-${idx}-birthdateDay`}
|
|
value={guest.birthdate?.split("-")[2] ?? ""}
|
|
onChange={(e) => {
|
|
const parts = (guest.birthdate || "--").split("-");
|
|
onChange(
|
|
idx,
|
|
"birthdate",
|
|
`${parts[0] || ""}-${parts[1] || ""}-${e.target.value.padStart(2, "0")}`,
|
|
);
|
|
}}
|
|
aria-label="Dag"
|
|
className={`border-b bg-transparent pb-2 text-sm text-white transition-colors focus:outline-none ${errors[idx]?.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
|
|
>
|
|
<option value="" className="bg-[#214e51]">
|
|
DD
|
|
</option>
|
|
{Array.from({ length: 31 }, (_, i) => i + 1).map((d) => (
|
|
<option
|
|
key={d}
|
|
value={String(d).padStart(2, "0")}
|
|
className="bg-[#214e51]"
|
|
>
|
|
{String(d).padStart(2, "0")}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="flex flex-1 flex-col gap-1">
|
|
<label
|
|
htmlFor={`guest-${idx}-birthdateMonth`}
|
|
className="sr-only"
|
|
>
|
|
Maand
|
|
</label>
|
|
<select
|
|
id={`guest-${idx}-birthdateMonth`}
|
|
value={guest.birthdate?.split("-")[1] ?? ""}
|
|
onChange={(e) => {
|
|
const parts = (guest.birthdate || "--").split("-");
|
|
onChange(
|
|
idx,
|
|
"birthdate",
|
|
`${parts[0] || ""}-${e.target.value.padStart(2, "0")}-${parts[2] || ""}`,
|
|
);
|
|
}}
|
|
aria-label="Maand"
|
|
className={`border-b bg-transparent pb-2 text-sm text-white transition-colors focus:outline-none ${errors[idx]?.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
|
|
>
|
|
<option value="" className="bg-[#214e51]">
|
|
MM
|
|
</option>
|
|
{[
|
|
"Jan",
|
|
"Feb",
|
|
"Mrt",
|
|
"Apr",
|
|
"Mei",
|
|
"Jun",
|
|
"Jul",
|
|
"Aug",
|
|
"Sep",
|
|
"Okt",
|
|
"Nov",
|
|
"Dec",
|
|
].map((m, i) => (
|
|
<option
|
|
key={m}
|
|
value={String(i + 1).padStart(2, "0")}
|
|
className="bg-[#214e51]"
|
|
>
|
|
{m}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="flex flex-1 flex-col gap-1">
|
|
<label
|
|
htmlFor={`guest-${idx}-birthdateYear`}
|
|
className="sr-only"
|
|
>
|
|
Jaar
|
|
</label>
|
|
<select
|
|
id={`guest-${idx}-birthdateYear`}
|
|
value={guest.birthdate?.split("-")[0] ?? ""}
|
|
onChange={(e) => {
|
|
const parts = (guest.birthdate || "--").split("-");
|
|
onChange(
|
|
idx,
|
|
"birthdate",
|
|
`${e.target.value}-${parts[1] || ""}-${parts[2] || ""}`,
|
|
);
|
|
}}
|
|
aria-label="Jaar"
|
|
className={`border-b bg-transparent pb-2 text-sm text-white transition-colors focus:outline-none ${errors[idx]?.birthdate ? "border-red-400" : "border-white/30 focus:border-white"}`}
|
|
>
|
|
<option value="" className="bg-[#214e51]">
|
|
JJJJ
|
|
</option>
|
|
{Array.from(
|
|
{ length: 100 },
|
|
(_, i) => new Date().getFullYear() - i,
|
|
).map((y) => (
|
|
<option
|
|
key={y}
|
|
value={String(y)}
|
|
className="bg-[#214e51]"
|
|
>
|
|
{y}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</fieldset>
|
|
{errors[idx]?.birthdate && (
|
|
<span className="text-red-300 text-sm" role="alert">
|
|
{errors[idx].birthdate}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Postcode */}
|
|
<div className="flex flex-col gap-2">
|
|
<label
|
|
htmlFor={`guest-${idx}-postcode`}
|
|
className="text-white/80"
|
|
>
|
|
Postcode <span className="text-red-300">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id={`guest-${idx}-postcode`}
|
|
value={guest.postcode}
|
|
onChange={(e) => onChange(idx, "postcode", e.target.value)}
|
|
placeholder="bv. 9000"
|
|
autoComplete="off"
|
|
inputMode="numeric"
|
|
className={inputCls(!!errors[idx]?.postcode)}
|
|
/>
|
|
{errors[idx]?.postcode && (
|
|
<span className="text-red-300 text-sm" role="alert">
|
|
{errors[idx].postcode}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|