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:
1 parent
974a5ace4c
commit
c0ba3a3798
50 files changed
+2773
-1
No files matched your search
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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.",
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user