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>
166 lines
4.0 KiB
TypeScript
166 lines
4.0 KiB
TypeScript
/**
|
|
* Shared workshop-signup constants and routing. Safe for the form and the server.
|
|
* Member count never maps to a price.
|
|
*/
|
|
|
|
export const WORKSHOP_SIGNUP_ANCHOR_ID = "workshop-signup";
|
|
|
|
export const WORKSHOP_HONEYPOT_FIELD = "company_website";
|
|
|
|
export const WORKSHOP_STARTED_AT_FIELD = "startedAt";
|
|
|
|
/** Ignore submits that arrive faster than a person can fill the form. */
|
|
export const WORKSHOP_MIN_FILL_MS = 4_000;
|
|
|
|
/** Proposed retention. Inquiries that do not lead to a workshop are deleted after this. */
|
|
export const WORKSHOP_INQUIRY_RETENTION_MS = 365 * 24 * 60 * 60 * 1000;
|
|
|
|
export const WORKSHOP_CHALLENGE_IDS = [
|
|
"members-rarely-vote",
|
|
"preparing-the-board",
|
|
"board-and-management",
|
|
"committees-unsure",
|
|
"disagreement",
|
|
"big-change",
|
|
"new-directors",
|
|
"co-op-principles",
|
|
"something-else",
|
|
] as const;
|
|
|
|
export type WorkshopChallengeId = (typeof WORKSHOP_CHALLENGE_IDS)[number];
|
|
|
|
export const WORKSHOP_ORGANIZATION_KINDS = [
|
|
"credit-union",
|
|
"mutual",
|
|
"consumer-coop",
|
|
"utility-coop",
|
|
"agricultural-coop",
|
|
"housing-coop",
|
|
"worker-coop",
|
|
"league",
|
|
"volunteer",
|
|
"something-else",
|
|
] as const;
|
|
|
|
export type WorkshopOrganizationKind = (typeof WORKSHOP_ORGANIZATION_KINDS)[number];
|
|
|
|
export const WORKSHOP_MEMBER_COUNTS = [
|
|
"under-1000",
|
|
"1000-10000",
|
|
"10000-100000",
|
|
"100000-1000000",
|
|
"over-1000000",
|
|
"not-sure",
|
|
] as const;
|
|
|
|
export type WorkshopMemberCount = (typeof WORKSHOP_MEMBER_COUNTS)[number];
|
|
|
|
export const WORKSHOP_ROLES = [
|
|
"board",
|
|
"executive",
|
|
"staff",
|
|
"member",
|
|
"something-else",
|
|
] as const;
|
|
|
|
export type WorkshopRole = (typeof WORKSHOP_ROLES)[number];
|
|
|
|
const LARGE_MEMBER_COUNTS = new Set<WorkshopMemberCount>([
|
|
"1000-10000",
|
|
"10000-100000",
|
|
"100000-1000000",
|
|
"over-1000000",
|
|
]);
|
|
|
|
export type WorkshopFieldErrorCode = "required" | "invalid" | "tooLong";
|
|
|
|
export type WorkshopFieldErrors = Partial<
|
|
Record<
|
|
| "challenges"
|
|
| "challengeOther"
|
|
| "unspokenQuestion"
|
|
| "organizationName"
|
|
| "organizationKind"
|
|
| "memberCount"
|
|
| "contactName"
|
|
| "email"
|
|
| "role",
|
|
WorkshopFieldErrorCode
|
|
>
|
|
>;
|
|
|
|
export interface WorkshopFormValues {
|
|
challenges: string[];
|
|
challengeOther: string;
|
|
unspokenQuestion: string;
|
|
organizationName: string;
|
|
organizationKind: string;
|
|
memberCount: string;
|
|
contactName: string;
|
|
email: string;
|
|
role: string;
|
|
sourcePage: string;
|
|
sourceUrl: string;
|
|
}
|
|
|
|
export const EMPTY_WORKSHOP_FORM_VALUES: WorkshopFormValues = {
|
|
challenges: [],
|
|
challengeOther: "",
|
|
unspokenQuestion: "",
|
|
organizationName: "",
|
|
organizationKind: "",
|
|
memberCount: "",
|
|
contactName: "",
|
|
email: "",
|
|
role: "",
|
|
sourcePage: "/workshops",
|
|
sourceUrl: "",
|
|
};
|
|
|
|
export interface WorkshopRouteSuggestion {
|
|
/** True when the organization has 1,000 members or more. Never a price. */
|
|
largeOrganization: boolean;
|
|
/** Inbox note. Names the next conversation, never a fee. */
|
|
note: string;
|
|
}
|
|
|
|
export function isLargeMemberCount(memberCount: string): boolean {
|
|
return LARGE_MEMBER_COUNTS.has(memberCount as WorkshopMemberCount);
|
|
}
|
|
|
|
/**
|
|
* Inbox routing. Volunteer-run groups go to the kit. Organizations of 1,000
|
|
* members or more are flagged large and go to the facilitator lead. Leagues
|
|
* go to a partnership call. Everyone else gets the one-page brief.
|
|
*/
|
|
export function suggestWorkshopRoute(input: {
|
|
organizationKind: string;
|
|
memberCount: string;
|
|
}): WorkshopRouteSuggestion {
|
|
if (input.organizationKind === "volunteer") {
|
|
return {
|
|
largeOrganization: false,
|
|
note: "Volunteer-run group: send the kit and office hours.",
|
|
};
|
|
}
|
|
|
|
if (isLargeMemberCount(input.memberCount)) {
|
|
return {
|
|
largeOrganization: true,
|
|
note: "Large organization: call with the facilitator lead.",
|
|
};
|
|
}
|
|
|
|
if (input.organizationKind === "league") {
|
|
return {
|
|
largeOrganization: false,
|
|
note: "League, association or co-op developer: partnership call.",
|
|
};
|
|
}
|
|
|
|
return {
|
|
largeOrganization: false,
|
|
note: "Send the one-page brief, then offer a call.",
|
|
};
|
|
}
|