The form is built once to the settled field set, and a valid send is saved before anyone is emailed. Co-authored-by: Cursor <cursoragent@cursor.com>
116 lines
4.3 KiB
TypeScript
116 lines
4.3 KiB
TypeScript
import {
|
|
WORKSHOP_CHALLENGE_IDS,
|
|
WORKSHOP_HONEYPOT_FIELD,
|
|
WORKSHOP_MEMBER_COUNTS,
|
|
WORKSHOP_ORGANIZATION_KINDS,
|
|
WORKSHOP_ROLES,
|
|
WORKSHOP_STARTED_AT_FIELD,
|
|
type WorkshopFieldErrors,
|
|
type WorkshopFormValues,
|
|
} from "../../workshopSignup";
|
|
|
|
const CHALLENGE_SET = new Set<string>(WORKSHOP_CHALLENGE_IDS);
|
|
const KIND_SET = new Set<string>(WORKSHOP_ORGANIZATION_KINDS);
|
|
const MEMBER_SET = new Set<string>(WORKSHOP_MEMBER_COUNTS);
|
|
const ROLE_SET = new Set<string>(WORKSHOP_ROLES);
|
|
|
|
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
|
export interface ParsedWorkshopSignup {
|
|
values: WorkshopFormValues;
|
|
fieldErrors: WorkshopFieldErrors;
|
|
honeypot: string;
|
|
startedAt: number | null;
|
|
}
|
|
|
|
function asString(value: unknown): string {
|
|
return typeof value === "string" ? value : "";
|
|
}
|
|
|
|
function asStringList(value: unknown): string[] {
|
|
if (Array.isArray(value)) {
|
|
return value.filter((item): item is string => typeof item === "string");
|
|
}
|
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
return [value];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
export function parseWorkshopSignupPayload(raw: unknown): ParsedWorkshopSignup {
|
|
const record =
|
|
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
|
|
|
const challenges = [
|
|
...new Set(asStringList(record.challenges).map((item) => item.trim())),
|
|
].filter((item) => CHALLENGE_SET.has(item));
|
|
|
|
const values: WorkshopFormValues = {
|
|
challenges,
|
|
challengeOther: asString(record.challengeOther).trim().slice(0, 200),
|
|
unspokenQuestion: asString(record.unspokenQuestion).trim(),
|
|
organizationName: asString(record.organizationName).trim(),
|
|
organizationKind: asString(record.organizationKind).trim(),
|
|
memberCount: asString(record.memberCount).trim(),
|
|
contactName: asString(record.contactName).trim(),
|
|
email: asString(record.email).trim().toLowerCase(),
|
|
role: asString(record.role).trim(),
|
|
sourcePage: asString(record.sourcePage).trim() || "/workshops",
|
|
sourceUrl: asString(record.sourceUrl).trim().slice(0, 2000),
|
|
};
|
|
|
|
const fieldErrors: WorkshopFieldErrors = {};
|
|
|
|
if (values.challenges.length === 0) fieldErrors.challenges = "required";
|
|
if (
|
|
values.challenges.includes("something-else") &&
|
|
values.challengeOther.length === 0
|
|
) {
|
|
fieldErrors.challengeOther = "required";
|
|
}
|
|
if (values.unspokenQuestion.length > 1000) fieldErrors.unspokenQuestion = "tooLong";
|
|
if (values.organizationName.length === 0) fieldErrors.organizationName = "required";
|
|
else if (values.organizationName.length > 150) fieldErrors.organizationName = "tooLong";
|
|
if (!KIND_SET.has(values.organizationKind)) fieldErrors.organizationKind = "required";
|
|
if (!MEMBER_SET.has(values.memberCount)) fieldErrors.memberCount = "required";
|
|
if (values.contactName.length === 0) fieldErrors.contactName = "required";
|
|
else if (values.contactName.length > 100) fieldErrors.contactName = "tooLong";
|
|
if (values.email.length === 0 || !EMAIL_PATTERN.test(values.email) || values.email.length > 254) {
|
|
fieldErrors.email = values.email.length === 0 ? "required" : "invalid";
|
|
}
|
|
if (values.role.length > 0 && !ROLE_SET.has(values.role)) fieldErrors.role = "invalid";
|
|
|
|
const startedRaw = record[WORKSHOP_STARTED_AT_FIELD];
|
|
const startedAt =
|
|
typeof startedRaw === "number"
|
|
? startedRaw
|
|
: typeof startedRaw === "string" && startedRaw.trim().length > 0
|
|
? Number(startedRaw)
|
|
: null;
|
|
|
|
return {
|
|
values,
|
|
fieldErrors,
|
|
honeypot: asString(record[WORKSHOP_HONEYPOT_FIELD]).trim(),
|
|
startedAt: startedAt !== null && Number.isFinite(startedAt) ? startedAt : null,
|
|
};
|
|
}
|
|
|
|
export function workshopSignupFromFormData(formData: FormData): ParsedWorkshopSignup {
|
|
return parseWorkshopSignupPayload({
|
|
challenges: formData.getAll("challenges"),
|
|
challengeOther: formData.get("challengeOther"),
|
|
unspokenQuestion: formData.get("unspokenQuestion"),
|
|
organizationName: formData.get("organizationName"),
|
|
organizationKind: formData.get("organizationKind"),
|
|
memberCount: formData.get("memberCount"),
|
|
contactName: formData.get("contactName"),
|
|
email: formData.get("email"),
|
|
role: formData.get("role"),
|
|
sourcePage: formData.get("sourcePage"),
|
|
sourceUrl: formData.get("sourceUrl"),
|
|
[WORKSHOP_STARTED_AT_FIELD]: formData.get(WORKSHOP_STARTED_AT_FIELD),
|
|
[WORKSHOP_HONEYPOT_FIELD]: formData.get(WORKSHOP_HONEYPOT_FIELD),
|
|
});
|
|
}
|