initial commit
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
/* ============================================================================
|
||||
FORM PARSING AND VALIDATION - pure
|
||||
----------------------------------------------------------------------------
|
||||
Actions validate, components render. The action returns error CODES, never
|
||||
sentences, so the same result is correct on /en and /ar and the message
|
||||
catalogue stays the only place a wording lives.
|
||||
|
||||
Both forms work without JavaScript: they are plain <Form method="post">
|
||||
submissions, so validation has to be complete on the server. There is no
|
||||
client mirror of these rules to drift out of sync.
|
||||
========================================================================= */
|
||||
|
||||
import type { TranslationKey } from "~/i18n";
|
||||
|
||||
/**
|
||||
* The failures a reader can act on. Codes, not sentences.
|
||||
*
|
||||
* `consent` is its own code because "This field is required." is the wrong
|
||||
* sentence for an unticked checkbox: an unticked box is a decision, not an
|
||||
* omission, so it gets its own wording.
|
||||
*/
|
||||
export type FieldErrorCode = "required" | "email" | "consent";
|
||||
|
||||
export type FieldErrors = Readonly<Record<string, FieldErrorCode>>;
|
||||
|
||||
/**
|
||||
* Message catalogue key for each code - TOTAL over `FieldErrorCode`, so a new
|
||||
* code cannot be added without giving it a sentence in both languages. The
|
||||
* catalogue is the only place a wording lives (see the header), which is why
|
||||
* `consent` resolves here and not out of this section's own copy.
|
||||
*/
|
||||
export const FIELD_ERROR_KEYS: Readonly<
|
||||
Record<FieldErrorCode, TranslationKey>
|
||||
> = {
|
||||
required: "form.errorRequired",
|
||||
email: "form.errorEmail",
|
||||
consent: "form.errorConsent",
|
||||
};
|
||||
|
||||
/**
|
||||
* A field no human fills in. A bot that fills every input gets a 200 and an
|
||||
* acknowledgement, and nothing is written to the CMS.
|
||||
*/
|
||||
export const HONEYPOT_FIELD = "website";
|
||||
|
||||
/* Deliberately permissive: one @, no spaces, a dot in the domain. Anything
|
||||
stricter rejects valid addresses, and only a delivered email proves one. */
|
||||
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
||||
|
||||
/** Upper bounds, applied by truncation rather than by an error message. */
|
||||
const LIMITS = {
|
||||
name: 120,
|
||||
organisation: 160,
|
||||
email: 254,
|
||||
phone: 40,
|
||||
message: 4000,
|
||||
} as const;
|
||||
|
||||
function field(form: FormData, name: string, limit: number): string {
|
||||
const value = form.get(name);
|
||||
return typeof value === "string" ? value.trim().slice(0, limit) : "";
|
||||
}
|
||||
|
||||
export function isHoneypotFilled(form: FormData): boolean {
|
||||
const value = form.get(HONEYPOT_FIELD);
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
EVENT REGISTRATION - draft 4
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
export interface RegistrationValues {
|
||||
readonly fullName: string;
|
||||
readonly email: string;
|
||||
readonly organisation: string;
|
||||
readonly phone: string;
|
||||
/**
|
||||
* `event-registration.consentGdpr` is REQUIRED in the CMS: a submission
|
||||
* without it is rejected with a 400, so the checkbox is a real, labelled
|
||||
* control on the form and a real validation rule here - never a hidden
|
||||
* input that consents on the reader's behalf.
|
||||
*/
|
||||
readonly consentGdpr: boolean;
|
||||
}
|
||||
|
||||
/** The name of the consent checkbox, shared by the form and the validator. */
|
||||
export const CONSENT_FIELD = "consentGdpr";
|
||||
|
||||
/** A checkbox is either absent or carries its value ("on" by default). */
|
||||
function checkbox(form: FormData, name: string): boolean {
|
||||
const value = form.get(name);
|
||||
return typeof value === "string" && value.length > 0 && value !== "false";
|
||||
}
|
||||
|
||||
export interface Validated<T> {
|
||||
readonly values: T;
|
||||
readonly errors: FieldErrors;
|
||||
readonly valid: boolean;
|
||||
}
|
||||
|
||||
export function validateRegistration(
|
||||
form: FormData,
|
||||
): Validated<RegistrationValues> {
|
||||
const values: RegistrationValues = {
|
||||
fullName: field(form, "fullName", LIMITS.name),
|
||||
email: field(form, "email", LIMITS.email),
|
||||
organisation: field(form, "organisation", LIMITS.organisation),
|
||||
phone: field(form, "phone", LIMITS.phone),
|
||||
consentGdpr: checkbox(form, CONSENT_FIELD),
|
||||
};
|
||||
|
||||
const errors: Record<string, FieldErrorCode> = {};
|
||||
|
||||
if (!values.fullName) errors.fullName = "required";
|
||||
if (!values.email) errors.email = "required";
|
||||
else if (!EMAIL.test(values.email)) errors.email = "email";
|
||||
if (!values.consentGdpr) errors[CONSENT_FIELD] = "consent";
|
||||
|
||||
return { values, errors, valid: Object.keys(errors).length === 0 };
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
OPPORTUNITY APPLICATION - draft 8
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
export interface ApplicationValues {
|
||||
readonly fullName: string;
|
||||
readonly email: string;
|
||||
readonly organisation: string;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
export function validateApplication(
|
||||
form: FormData,
|
||||
): Validated<ApplicationValues> {
|
||||
const values: ApplicationValues = {
|
||||
fullName: field(form, "fullName", LIMITS.name),
|
||||
email: field(form, "email", LIMITS.email),
|
||||
organisation: field(form, "organisation", LIMITS.organisation),
|
||||
message: field(form, "message", LIMITS.message),
|
||||
};
|
||||
|
||||
const errors: Record<string, FieldErrorCode> = {};
|
||||
|
||||
if (!values.fullName) errors.fullName = "required";
|
||||
if (!values.email) errors.email = "required";
|
||||
else if (!EMAIL.test(values.email)) errors.email = "email";
|
||||
if (!values.message) errors.message = "required";
|
||||
|
||||
return { values, errors, valid: Object.keys(errors).length === 0 };
|
||||
}
|
||||
Reference in New Issue
Block a user