Add the Workshops page and keep each sign-up on the site.

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>
This commit is contained in:
adilalloandCursor committed 2026-11-03 19:56:49 -07:00
1 parent 974a5ace4c
commit c0ba3a3798
50 files changed
+2773 -1

No files matched your search

@@ -0,0 +1,115 @@
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),
});
}