feat:simplify dx and improve payment functions
This commit is contained in:
200
apps/web/src/components/registration/GuestList.tsx
Normal file
200
apps/web/src/components/registration/GuestList.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export function GuestList({
|
||||
guests,
|
||||
errors,
|
||||
onChange,
|
||||
onAdd,
|
||||
onRemove,
|
||||
headerNote,
|
||||
}: Props) {
|
||||
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>
|
||||
|
||||
{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={() => onRemove(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>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user