mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-13 08:15:21 +02:00
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:
parent
10e17ea221
commit
3ccbc14549
3 changed files with 161 additions and 0 deletions
49
ui/src/components/lead-forms/isPersonalEmail.ts
Normal file
49
ui/src/components/lead-forms/isPersonalEmail.ts
Normal 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;
|
||||
}
|
||||
81
ui/src/components/lead-forms/leadFieldOptions.ts
Normal file
81
ui/src/components/lead-forms/leadFieldOptions.ts
Normal 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: "0–5k calls/month" },
|
||||
{ value: "5k-20k", label: "5k–20k 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: "2–10" },
|
||||
{ value: "10-100", label: "10–100" },
|
||||
{ value: "100-1000", label: "100–1000" },
|
||||
{ value: "1000+", label: "1000+" },
|
||||
] as const;
|
||||
|
||||
// Hire-an-Expert timeline.
|
||||
export const HIRE_TIMELINE_OPTIONS = [
|
||||
{ value: "asap", label: "ASAP" },
|
||||
{ value: "2-4_weeks", label: "2–4 weeks" },
|
||||
{ value: "1-2_months", label: "1–2 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: "0–5k" },
|
||||
{ value: "5k-100k", label: "5k–100k" },
|
||||
{ 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: "50–200" },
|
||||
{ value: "200-1000", label: "200–1000" },
|
||||
{ value: "1000-5000", label: "1000–5000" },
|
||||
{ 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;
|
||||
31
ui/src/components/lead-forms/submitLead.ts
Normal file
31
ui/src/components/lead-forms/submitLead.ts
Normal 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"));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue