/** * 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([ "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.", }; }