feat:simplify dx and improve payment functions
This commit is contained in:
117
apps/web/src/lib/registration.ts
Normal file
117
apps/web/src/lib/registration.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
// Shared types, validators, and helpers for the registration workflow.
|
||||
// Single source of truth — used by EventRegistrationForm, manage page, and API router.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Drink card pricing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const DRINK_CARD_BASE = 5; // €5 for primary registrant
|
||||
export const DRINK_CARD_PER_GUEST = 2; // €2 per additional guest
|
||||
export const MAX_GUESTS = 9;
|
||||
|
||||
/** Returns drink card value in euros for a given number of extra guests. */
|
||||
export function calculateDrinkCard(guestCount: number): number {
|
||||
return DRINK_CARD_BASE + guestCount * DRINK_CARD_PER_GUEST;
|
||||
}
|
||||
|
||||
/** Returns drink card value in cents for payment processing. */
|
||||
export function calculateDrinkCardCents(guestCount: number): number {
|
||||
return calculateDrinkCard(guestCount) * 100;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Guest types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface GuestEntry {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export interface GuestErrors {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parses the JSON blob stored in the `guests` DB column.
|
||||
* Returns an empty array on any parse failure.
|
||||
*/
|
||||
export function parseGuests(raw: string | null | undefined): GuestEntry[] {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.map((g) => ({
|
||||
firstName: g.firstName ?? "",
|
||||
lastName: g.lastName ?? "",
|
||||
email: g.email ?? "",
|
||||
phone: g.phone ?? "",
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module-level validators (pure functions — no hook deps)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function 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;
|
||||
}
|
||||
|
||||
export function 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;
|
||||
}
|
||||
|
||||
export function validatePhone(value: string): string | undefined {
|
||||
if (value && !/^[\d\s\-+()]{10,}$/.test(value.replace(/\s/g, "")))
|
||||
return "Voer een geldig telefoonnummer in";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Validates all guests and returns errors array + overall validity flag. */
|
||||
export function validateGuests(guests: GuestEntry[]): {
|
||||
errors: GuestErrors[];
|
||||
valid: boolean;
|
||||
} {
|
||||
const errors: 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,
|
||||
}));
|
||||
const valid = !errors.some((e) => Object.values(e).some(Boolean));
|
||||
return { errors, valid };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared CSS helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Input class string with error-state variant. */
|
||||
export function inputCls(hasError: boolean): string {
|
||||
return `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"}`;
|
||||
}
|
||||
Reference in New Issue
Block a user