feat(lead-gen): shared field options, work-email validation, and submit seam

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pritesh 2026-06-03 04:19:21 +05:30
parent 10e17ea221
commit 3ccbc14549
3 changed files with 161 additions and 0 deletions

View file

@ -0,0 +1,49 @@
// Returns true if the email uses a common free/personal domain.
// Used to gate "work email" fields on lead forms.
const PERSONAL_EMAIL_DOMAINS = new Set([
"gmail.com",
"googlemail.com",
"yahoo.com",
"yahoo.co.in",
"yahoo.co.uk",
"ymail.com",
"outlook.com",
"hotmail.com",
"hotmail.co.uk",
"live.com",
"msn.com",
"icloud.com",
"me.com",
"mac.com",
"proton.me",
"protonmail.com",
"pm.me",
"aol.com",
"gmx.com",
"gmx.net",
"mail.com",
"zoho.com",
"yandex.com",
"fastmail.com",
]);
export function isValidEmail(email: string): boolean {
// Pragmatic check — not RFC-perfect, but rejects obvious garbage.
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
}
export function isPersonalEmail(email: string): boolean {
const at = email.trim().toLowerCase().split("@");
if (at.length !== 2) return false;
return PERSONAL_EMAIL_DOMAINS.has(at[1]);
}
// Convenience validator for work-email fields.
// Returns an error string, or null if valid.
export function validateWorkEmail(email: string): string | null {
if (!email.trim()) return "Work email is required";
if (!isValidEmail(email)) return "Please enter a valid email address";
if (isPersonalEmail(email)) return "Please use your work email";
return null;
}

View file

@ -0,0 +1,81 @@
// Shared dropdown options + lead source/kind types for the lead-gen forms.
export type LeadSource =
| "sidebar"
| "billing_card"
| "builder_nudge"
| "topup"
| "hire_expert";
export type LeadKind = "topup" | "hire_expert" | "enterprise";
// Top-up: expected monthly call volume. ">20k" unlocks the volume-pricing block.
export const TOPUP_VOLUME_OPTIONS = [
{ value: "0-5k", label: "05k calls/month" },
{ value: "5k-20k", label: "5k20k calls/month" },
{ value: ">20k", label: ">20k calls/month" },
] as const;
// The value that gates the volume-pricing qualifier block.
export const VOLUME_PRICING_GATE = ">20k";
// Top-up volume-pricing qualifier: company size (small-business scale).
export const TOPUP_COMPANY_SIZE_OPTIONS = [
{ value: "only_me", label: "Only me" },
{ value: "2-10", label: "210" },
{ value: "10-100", label: "10100" },
{ value: "100-1000", label: "1001000" },
{ value: "1000+", label: "1000+" },
] as const;
// Hire-an-Expert timeline.
export const HIRE_TIMELINE_OPTIONS = [
{ value: "asap", label: "ASAP" },
{ value: "2-4_weeks", label: "24 weeks" },
{ value: "1-2_months", label: "12 months" },
{ value: "flexible", label: "Flexible" },
{ value: "exploring", label: "Exploring" },
] as const;
// Hire-an-Expert expected monthly call volume.
export const HIRE_VOLUME_OPTIONS = [
{ value: "0-5k", label: "05k" },
{ value: "5k-100k", label: "5k100k" },
{ value: "100k+", label: "100k+" },
{ value: "not_sure", label: "Not sure" },
] as const;
// Hire-an-Expert current stage.
export const HIRE_STAGE_OPTIONS = [
{ value: "live_process", label: "Have a live process we want to automate" },
{ value: "idea_no_process", label: "Have an idea, no process yet" },
{ value: "researching", label: "Just researching" },
{ value: "built_need_help", label: "Already built something, need help fixing" },
] as const;
// Enterprise industry.
export const ENTERPRISE_INDUSTRY_OPTIONS = [
{ value: "financial_services", label: "Financial services" },
{ value: "healthcare", label: "Healthcare" },
{ value: "insurance", label: "Insurance" },
{ value: "government", label: "Government" },
{ value: "telecom", label: "Telecom" },
{ value: "bpo", label: "BPO" },
{ value: "other", label: "Other" },
] as const;
// Enterprise company size (enterprise scale — intentionally different from top-up's).
export const ENTERPRISE_COMPANY_SIZE_OPTIONS = [
{ value: "50-200", label: "50200" },
{ value: "200-1000", label: "2001000" },
{ value: "1000-5000", label: "10005000" },
{ value: "5000+", label: "5000+" },
] as const;
// Enterprise timeline.
export const ENTERPRISE_TIMELINE_OPTIONS = [
{ value: "this_quarter", label: "This quarter" },
{ value: "next_quarter", label: "Next quarter" },
{ value: "6_months", label: "6 months" },
{ value: "exploring", label: "Exploring" },
] as const;

View file

@ -0,0 +1,31 @@
// Single submission seam for all lead forms.
// Today: fires a PostHog capture. Later: add a POST to the backend
// (MongoDB) endpoint here — no form component will need to change.
import posthog from "posthog-js";
import { PostHogEvent } from "@/constants/posthog-events";
import type { LeadKind, LeadSource } from "./leadFieldOptions";
const SUBMIT_EVENT: Record<LeadKind, string> = {
topup: PostHogEvent.TOPUP_REQUESTED,
hire_expert: PostHogEvent.HIRE_EXPERT_SUBMITTED,
enterprise: PostHogEvent.ENTERPRISE_LEAD_SUBMITTED,
};
export interface SubmitLeadArgs {
kind: LeadKind;
source: LeadSource;
// Field values, already validated by the caller. Non-sensitive lead data.
payload: Record<string, unknown>;
}
export async function submitLead({ kind, source, payload }: SubmitLeadArgs): Promise<void> {
// PostHog capture — the durable record until the backend endpoint lands.
posthog.capture(SUBMIT_EVENT[kind], { source, ...payload });
// FUTURE: when the MongoDB endpoint exists, POST here, e.g.
// const res = await submitLeadApiV1LeadsPost({ body: { kind, source, ...payload } });
// if (res.error) throw new Error(detailFromError(res.error, "Failed to submit"));
}