From d7c74d74473108b6fd089c7db17f8312553fae64 Mon Sep 17 00:00:00 2001 From: Pritesh Date: Tue, 2 Jun 2026 22:20:38 +0530 Subject: [PATCH 01/23] docs: design spec for lead-gen surfaces (Credits & Billing, Hire-an-Expert, Top-up, Enterprise) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add brainstorming spec for: sidebar OBSERVE→MANAGE rename + Credits & Billing link + Hire-an-Expert footer button; new /billing page with extracted Dograh Model Credits card + CTAs; Top-up / Hire-an-Expert / Enterprise intake modals with inline math captcha; and a workflow-builder Hire-an-Expert nudge. Frontend only; submissions fire PostHog events via a submitLead() seam for a future MongoDB endpoint. Also gitignore .superpowers/ brainstorm mockups. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 3 + .../2026-06-02-lead-gen-surfaces-design.md | 147 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-02-lead-gen-surfaces-design.md diff --git a/.gitignore b/.gitignore index e4ccaf3f..8fa4c055 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ coturn/ *.wav dograh_pcm_cache/ node_modules/ + +# Superpowers brainstorm mockups (local only) +.superpowers/ diff --git a/docs/superpowers/specs/2026-06-02-lead-gen-surfaces-design.md b/docs/superpowers/specs/2026-06-02-lead-gen-surfaces-design.md new file mode 100644 index 00000000..ed4af1b4 --- /dev/null +++ b/docs/superpowers/specs/2026-06-02-lead-gen-surfaces-design.md @@ -0,0 +1,147 @@ +# Lead-gen surfaces: Credits & Billing, Hire-an-Expert, Top-up & Enterprise intake + +**Date:** 2026-06-02 +**Status:** Approved for planning +**Scope:** Frontend only (`ui/`). No backend in this milestone. Form submissions fire PostHog events now; a MongoDB-backed endpoint will be wired later via a single `submitLead()` seam. + +## Guiding constraints + +- **Minimum diff, maximum reuse.** Touch existing files as little as possible; extract rather than rewrite; never duplicate logic. Existing functional code must not regress. +- **Follow established repo patterns** (verified against the codebase): + - shadcn primitives in `src/components/ui/` (all needed ones exist: `dialog`, `select`, `textarea`, `radio-group`, `input`, `label`, `button`, `card`, `progress`, `tooltip`). + - PostHog: `import posthog from "posthog-js"` + `import { PostHogEvent } from "@/constants/posthog-events"`, then `posthog.capture(PostHogEvent.X, { ...props })`. **All new event names are registered in `src/constants/posthog-events.ts`** (the central registry) — no string literals. + - Auto-generated client returns `{ data, error }` and does NOT throw — always check `response.error` and use `detailFromError` from `@/lib/apiError`. + - React Context providers live in `src/context/` (e.g. `OnboardingContext`); sidebar collapse uses the `isCollapsed` pattern; toasts use `sonner`. + +## Summary of changes + +| # | Change | Primary files | +|---|--------|---------------| +| 1 | Rename sidebar section `OBSERVE` → `MANAGE`; add `Credits & Billing` link → `/billing`; add `Hire an Expert` footer button | `src/components/layout/AppSidebar.tsx` | +| 2 | New `/billing` page; extract Dograh Model Credits card into a component; add footer CTAs; remove card from Agent Runs | `src/app/billing/page.tsx` (new), `src/components/billing/DograhCreditsCard.tsx` (new), `src/app/usage/page.tsx` (delete card) | +| 3 | Three lead modals + shared building blocks | `src/components/lead-forms/*` (new) | +| 4 | Workflow-builder Hire-an-Expert nudge | `src/components/lead-forms/HireExpertNudge.tsx` (new), `src/app/workflow/[workflowId]/RenderWorkflow.tsx` (mount) | +| 5 | Shared modal state provider | `src/context/LeadFormsContext.tsx` (new), `src/components/layout/AppLayout.tsx` (mount) | +| 6 | New PostHog event names | `src/constants/posthog-events.ts` | + +--- + +## 1. Sidebar (`AppSidebar.tsx`) + +Data + footer changes only — no structural rewrite. + +- In `NAV_SECTIONS`, rename the section `label` `"OBSERVE"` → `"MANAGE"`. +- Add one item to that section, after `Reports`: + `{ title: "Credits & Billing", url: "/billing", icon: CircleDollarSign }` (`CircleDollarSign` is already imported). + Final MANAGE order: Agent Runs, Reports, Credits & Billing. +- In the existing `SidebarFooter`, change the avatar row to `justify-between` and add a **Hire an Expert** button to the right of the avatar circle: + - **Person icon** (`UserRound` from `lucide-react`), **solid primary** `Button` (default variant), label **"Hire an Expert" always visible** when sidebar is expanded. + - When `isCollapsed` is true, render **icon-only** with the existing `Tooltip` pattern (mirror how `ThemeToggle`/nav items collapse). Text hides only on sidebar collapse. + - `onClick`: `openHireExpert("sidebar")` from `useLeadForms()`. Fires `PostHogEvent.HIRE_EXPERT_OPENED` with `{ source: "sidebar" }` inside the provider. + - Placed for both `provider === "stack"` and `provider !== "stack"` avatar rows (both exist in the footer today). + +## 2. `/billing` page + extracted credits card + +### `src/components/billing/DograhCreditsCard.tsx` (new) +- Move the existing "Dograh Model Credits" JSX out of `usage/page.tsx` (≈ line 412) verbatim, including its `getMpsCreditsApiV1OrganizationsUsageMpsCreditsGet` fetch, loading skeleton, `Card`/`CardHeader`/`CardContent`, the used/quota numbers, remaining, and `Progress` bar. +- The component owns its own fetch (same call, same `{data,error}` check, same `isLoadingCredits` state) so both `/billing` and any future host stay self-contained. +- **Add a card footer** (mockup option A), inside the existing `Card`: + - Left: helper text `Running low?` (muted, small). + - Right-aligned button group: `Hire an Expert` (`variant="outline"`, person icon) + `Request top-up` (default/primary). + - `Hire an Expert` → `openHireExpert("billing_card")`; `Request top-up` → `openTopUp("billing_card")`. +- Fires `TOPUP_REQUEST_OPENED` / `HIRE_EXPERT_OPENED` with `{ source: "billing_card" }` via the provider. + +### `src/app/billing/page.tsx` (new) +- Thin App Router page. Header title **"Credits & Billing"** + short description, then ``. Matches the existing page-header style used on `usage`/`reports` pages. +- Auth-guarded fetch pattern per `ui/AGENTS.md` (guard on `authLoading`/`user`) — inherited from the card component. + +### `src/app/usage/page.tsx` (edit — deletion) +- Remove the "MPS Credits Card" block and its now-unused credits state/fetch (`mpsCredits`, `isLoadingCredits`, `fetchMpsCredits`, the import of `getMpsCredits...` and `MpsCreditsResponse`) **only if** nothing else on the page uses them. Agent Runs keeps the runs table, filters, and timezone selector untouched. +- Net: a clean removal; credits logic now lives in the extracted component. + +## 3. Three lead modals (`src/components/lead-forms/`) + +Shared building blocks (new, small): +- `MathCaptcha.tsx` — randomized "What is X + Y?" generated client-side (seeded on mount), validates the numeric answer before submit. Zero deps. +- `submitLead.ts` — single async seam: `submitLead(kind, payload)` fires the appropriate `PostHog` capture with all field values as props, returns success. **Designed so a future MongoDB `POST` is added in one place** without touching the forms. +- `isPersonalEmail.ts` — blocklist of common free domains (`gmail.com`, `yahoo.com`, `outlook.com`, `hotmail.com`, `icloud.com`, `proton.me`/`protonmail.com`, `aol.com`, `gmx.*`, `mail.com`, etc.); returns true for personal. Inline error copy: "Please use your work email." +- `leadFieldOptions.ts` — dropdown option constants reused across forms (timelines, volume buckets, industry, company size, current stage). + +Modals (each: shadcn `Dialog`, fields, `MathCaptcha`, Cancel + Submit; on success → sonner `toast.success(...)` then close): + +### `TopUpModal.tsx` +- Fields: **How many credits?** (number `Input`), **What's the use case?** (`Input`/short `Textarea`), **Expected monthly call volume** (`Select`: `0–5k`, `5k–20k`, `>20k`). +- **Conditional block** (rendered only when volume === `>20k`): heading "Talk to us about volume pricing", three qualifier fields — **Work email** (validated via `isPersonalEmail`, rejects personal), **Company name**, **Company size** (`Select`: Only me / 2–10 / 10–100 / 100–1000 / 1000+) — and a dashed-underline link **"Need enterprise deployment? (SSO, on-prem, SOC2, data residency)"** → `openEnterprise("topup")`. Below 20k, none of this renders, and those fields are not required. +- Submit → `submitLead("topup", { credits, useCase, volume, workEmail?, company?, companySize?, wantsVolumePricing })`; fires `PostHogEvent.TOPUP_REQUESTED`. + +### `HireExpertModal.tsx` +- Fields: **Company name**; **What does your business do?** (1 line); **What do you want the agent to do?** (optional, 2–3 line `Textarea`); **Phone / WhatsApp** (optional); **Timeline** (`Select`: ASAP / 2–4 weeks / 1–2 months / Flexible / Exploring); **Expected monthly call volume** (`Select`: 0–5k / 5k–100k / 100k+ / Not sure); **Existing call scripts/workflows to share?** (`RadioGroup` Yes/No); **Current stage** (`Select`: Have a live process we want to automate / Have an idea, no process yet / Just researching / Already built something, need help fixing). +- Bottom: dashed-underline **enterprise link** → `openEnterprise("hire_expert")`. +- Submit → `submitLead("hire_expert", {...})`; fires `PostHogEvent.HIRE_EXPERT_SUBMITTED`. + +### `EnterpriseModal.tsx` +- Fields: **Company name**; **Industry** (`Select`: Financial services / Healthcare / Insurance / Government / Telecom / BPO / Other); **Company size** (`Select`: 50–200 / 200–1000 / 1000–5000 / 5000+); **Timeline** (`Select`: This quarter / Next quarter / 6 months / Exploring); **Work email** (single field, personal-domain rejected); **Phone** (optional); **Anything else we should know?** (optional `Textarea`). +- Submit → `submitLead("enterprise", {...})`; fires `PostHogEvent.ENTERPRISE_LEAD_SUBMITTED`. +- Can be opened standalone or stacked on top of TopUp/Hire (its open state is independent in the provider, so the originating modal can stay open behind it or close — default: open enterprise on top, leave the trigger modal open). + +## 4. Workflow-builder nudge (`HireExpertNudge.tsx`) + +Mounted inside `RenderWorkflow.tsx` (keyed by `workflowId`). +- A `setTimeout` arms at **5 minutes** of being on the builder. On fire (if eligible), shows a **bottom floating toast/banner** (fixed, bottom-center or bottom-right of the canvas, above the React Flow controls): person icon, **"Hire an Expert"** bold + subtitle **"We'll build your agent for you"**, and a dismiss **×**. Slides in; **auto-fades after 30s**. +- **Eligibility / frequency:** once per `workflowId`, tracked in `localStorage` (`dograh:hireNudge:`). Suppressed entirely if the Hire modal has ever been opened this session (provider exposes an `everOpenedHire` flag) or if the localStorage flag is set. +- **Dismiss semantics:** only an explicit **× dismiss or a click** sets the localStorage "shown/done" flag. **Auto-expiry does NOT** set it — a user who never noticed it remains eligible on a later qualifying visit. +- Clicking the banner → `openHireExpert("builder_nudge")`. +- Events (all with `{ workflowId }`): `HIRE_NUDGE_SHOWN` (on display), `HIRE_NUDGE_CLICKED`, `HIRE_NUDGE_DISMISSED` (×), `HIRE_NUDGE_EXPIRED` (auto-fade). +- Cleanup: clear the timer on unmount / workflow change to avoid firing after navigation. + +## 5. Shared modal state (`LeadFormsContext.tsx`) + +- New provider in `src/context/`, mounted once in `AppLayout` (wraps the app shell like other providers). Renders the three modals a single time. +- Exposes via `useLeadForms()`: + - `openHireExpert(source)`, `openTopUp(source)`, `openEnterprise(source)` — each sets its modal open and fires the corresponding `*_OPENED` PostHog event with `{ source }`. + - `everOpenedHire: boolean` — read by the nudge for suppression. +- Triggers (sidebar button, card buttons, nudge) call the hook — no prop-drilling, no duplicate modal mounts. + +## 6. PostHog events (append to `src/constants/posthog-events.ts`) + +```ts +HIRE_EXPERT_OPENED: "hire_expert_opened", +HIRE_EXPERT_SUBMITTED: "hire_expert_submitted", +TOPUP_REQUEST_OPENED: "topup_request_opened", +TOPUP_REQUESTED: "topup_requested", +ENTERPRISE_LEAD_OPENED: "enterprise_lead_opened", +ENTERPRISE_LEAD_SUBMITTED: "enterprise_lead_submitted", +HIRE_NUDGE_SHOWN: "hire_nudge_shown", +HIRE_NUDGE_CLICKED: "hire_nudge_clicked", +HIRE_NUDGE_DISMISSED: "hire_nudge_dismissed", +HIRE_NUDGE_EXPIRED: "hire_nudge_expired", +``` + +Common props: `source` ("sidebar" | "billing_card" | "builder_nudge" | "topup" | "hire_expert"), plus submitted forms include their (non-sensitive) field values; nudge events include `workflowId`. + +## Decisions locked during brainstorming + +- Route: new `/billing` page; sidebar label **"Credits & Billing"**. +- Credits card **moves** from Agent Runs to `/billing`. +- Card footer = option A (both CTAs in footer, top-up primary, "Running low?" helper left). +- Sidebar footer button = person icon + solid primary, text always visible, collapses with sidebar. +- Hire modal has 3 entry points (sidebar, card, builder nudge) → one shared modal via context. +- Top-up volume gate at **`>20k`** reveals work email + company + enterprise link; hidden below. +- Top-up extra qualifier fields (under `>20k` gate) = **Work email + Company name + Company size** (Only me / 2–10 / 10–100 / 100–1000 / 1000+). Note this company-size scale differs from the Enterprise form's (which is 50–200 / 200–1000 / 1000–5000 / 5000+) — intentional, different audiences. +- Enterprise form uses a **single Work email** field (merged the user's two email lines). +- Captcha = inline client-side math question on all three forms. +- Submissions = PostHog now; one `submitLead()` seam for the future MongoDB endpoint. +- Personal-email rejection via free-domain blocklist. +- Nudge: 5 min trigger, 30s auto-fade, once per `workflowId`, auto-expiry does not consume the eligibility. + +## Out of scope (this milestone) + +- Backend endpoint / MongoDB persistence (next milestone — wire into `submitLead()`). +- Calendar/Calendly email automation for volume-pricing leads (future). +- Real captcha service (Turnstile/hCaptcha) — inline math is sufficient for OSS for now. +- Any change to the credits/MPS backend API. + +## Testing approach + +- Type-check (`tsc --noEmit`) and lint clean on all changed/new files. +- Manual dogfood (browse/QA): sidebar rename + new link + footer button (expanded & collapsed); `/billing` renders the card with footer CTAs; Agent Runs no longer shows the card; each modal opens from each trigger, captcha blocks bad answers, personal email rejected, `>20k` reveals the volume block + enterprise link; enterprise opens from both dashed links; nudge appears after the timer (with a shortened dev timer), auto-fades, and respects the once-per-workflow rule. Verify PostHog events fire with correct props. From 67ec161899ca8ab11d006c4490e05d6f4e659563 Mon Sep 17 00:00:00 2001 From: Pritesh Date: Wed, 3 Jun 2026 04:10:53 +0530 Subject: [PATCH 02/23] docs: implementation plan for user-onboarding lead-gen surfaces 14 bite-sized tasks: PostHog events, shared helpers (field options, work-email blocklist, submitLead seam, math captcha), three intake modals (enterprise/hire/top-up), LeadFormsProvider context, AppLayout mount, sidebar MANAGE rename + Credits & Billing link + footer Hire button, extracted DograhCreditsCard, /billing page, credits removal from Agent Runs, builder nudge, and a full verification/dogfood pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-02-user-onboarding-lead-gen.md | 1691 +++++++++++++++++ 1 file changed, 1691 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-02-user-onboarding-lead-gen.md diff --git a/docs/superpowers/plans/2026-06-02-user-onboarding-lead-gen.md b/docs/superpowers/plans/2026-06-02-user-onboarding-lead-gen.md new file mode 100644 index 00000000..9d7e4744 --- /dev/null +++ b/docs/superpowers/plans/2026-06-02-user-onboarding-lead-gen.md @@ -0,0 +1,1691 @@ +# User Onboarding & Lead-Gen Surfaces Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add lead-gen surfaces to the Dograh UI — rename the sidebar OBSERVE section to MANAGE with a new Credits & Billing link, move the Dograh Model Credits card to a new /billing page with Top-up + Hire-an-Expert CTAs, three intake modals (Top-up, Hire-an-Expert, Enterprise) with an inline math captcha, and a delayed Hire-an-Expert nudge on the workflow builder — all firing PostHog events. + +**Architecture:** Frontend-only (`ui/`). A single `LeadFormsProvider` context (mounted in `AppLayout`) renders the three modals once and exposes `openHireExpert/openTopUp/openEnterprise(source)` plus an `everOpenedHire` flag. All triggers call the hook — no duplicate modal mounts, no prop-drilling. Form submission goes through one `submitLead()` seam that fires PostHog today and will gain a MongoDB POST later. The credits card is *extracted* from `usage/page.tsx` into a reusable component (not rewritten). + +**Tech Stack:** Next.js 15 App Router, React 19, TypeScript, Tailwind, shadcn/ui primitives (`dialog`, `select`, `textarea`, `radio-group`, `input`, `label`, `button`, `card`, `progress`, `tooltip` — all already present), `posthog-js` with names in `src/constants/posthog-events.ts`, `sonner` toasts, `lucide-react` icons. + +**Branch:** `feat/user-onboarding` (already checked out). + +**Working directory for all commands:** `/Users/pk/Documents/WORK Tech/dograh-oss-repo/dograh/ui` + +**Verification commands (no test runner for UI components in this repo — verification is type-check + lint + manual):** +- Type-check: `npx tsc --noEmit` +- Lint (per ui/AGENTS.md): `npm run fix-lint` (auto-fixes) then re-run to confirm clean +- Manual dogfood at the end (see final task) + +--- + +## File Structure + +| File | Responsibility | Action | +|------|----------------|--------| +| `src/constants/posthog-events.ts` | Central PostHog event-name registry | Modify (append 10 events) | +| `src/components/lead-forms/leadFieldOptions.ts` | Shared dropdown option arrays + types | Create | +| `src/components/lead-forms/isPersonalEmail.ts` | Free-domain blocklist helper | Create | +| `src/components/lead-forms/submitLead.ts` | Single submit seam (PostHog now, API later) | Create | +| `src/components/lead-forms/MathCaptcha.tsx` | Inline math captcha field | Create | +| `src/components/lead-forms/TopUpModal.tsx` | Top-up dialog with >20k volume gate | Create | +| `src/components/lead-forms/HireExpertModal.tsx` | Hire-an-Expert dialog | Create | +| `src/components/lead-forms/EnterpriseModal.tsx` | Enterprise intake dialog | Create | +| `src/components/lead-forms/HireExpertNudge.tsx` | Delayed builder nudge banner | Create | +| `src/context/LeadFormsContext.tsx` | Shared modal state provider | Create | +| `src/components/layout/AppLayout.tsx` | Mount the provider | Modify | +| `src/components/billing/DograhCreditsCard.tsx` | Extracted credits card + footer CTAs | Create | +| `src/app/billing/page.tsx` | New Credits & Billing page | Create | +| `src/app/usage/page.tsx` | Remove credits card + its now-unused state | Modify | +| `src/components/layout/AppSidebar.tsx` | OBSERVE→MANAGE, new link, footer button | Modify | +| `src/app/workflow/[workflowId]/RenderWorkflow.tsx` | Mount the nudge | Modify | + +--- + +## Task 1: PostHog event names + +**Files:** +- Modify: `src/constants/posthog-events.ts` + +- [ ] **Step 1: Append the 10 new event names** + +Edit `src/constants/posthog-events.ts` — add these keys inside the `PostHogEvent` object, after the existing `SLACK_COMMUNITY_CLICKED` line and before the closing `} as const;`: + +```ts + HIRE_EXPERT_OPENED: "hire_expert_opened", + HIRE_EXPERT_SUBMITTED: "hire_expert_submitted", + TOPUP_REQUEST_OPENED: "topup_request_opened", + TOPUP_REQUESTED: "topup_requested", + ENTERPRISE_LEAD_OPENED: "enterprise_lead_opened", + ENTERPRISE_LEAD_SUBMITTED: "enterprise_lead_submitted", + HIRE_NUDGE_SHOWN: "hire_nudge_shown", + HIRE_NUDGE_CLICKED: "hire_nudge_clicked", + HIRE_NUDGE_DISMISSED: "hire_nudge_dismissed", + HIRE_NUDGE_EXPIRED: "hire_nudge_expired", +``` + +- [ ] **Step 2: Verify type-check passes** + +Run: `npx tsc --noEmit` +Expected: no errors (the file is a plain const object; new keys are valid). + +- [ ] **Step 3: Commit** + +```bash +git add src/constants/posthog-events.ts +git commit -m "feat(lead-gen): register PostHog events for lead-gen surfaces" +``` + +--- + +## Task 2: Shared field options + helpers (leadFieldOptions, isPersonalEmail, submitLead) + +**Files:** +- Create: `src/components/lead-forms/leadFieldOptions.ts` +- Create: `src/components/lead-forms/isPersonalEmail.ts` +- Create: `src/components/lead-forms/submitLead.ts` + +- [ ] **Step 1: Create `leadFieldOptions.ts`** + +```ts +// 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; +``` + +- [ ] **Step 2: Create `isPersonalEmail.ts`** + +```ts +// 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; +} +``` + +- [ ] **Step 3: Create `submitLead.ts`** + +```ts +// 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 = { + 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; +} + +export async function submitLead({ kind, source, payload }: SubmitLeadArgs): Promise { + // 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")); +} +``` + +- [ ] **Step 4: Verify type-check passes** + +Run: `npx tsc --noEmit` +Expected: no errors. (Imports resolve; `PostHogEvent` keys exist from Task 1.) + +- [ ] **Step 5: Commit** + +```bash +git add src/components/lead-forms/leadFieldOptions.ts src/components/lead-forms/isPersonalEmail.ts src/components/lead-forms/submitLead.ts +git commit -m "feat(lead-gen): shared field options, work-email validation, and submit seam" +``` + +--- + +## Task 3: MathCaptcha component + +**Files:** +- Create: `src/components/lead-forms/MathCaptcha.tsx` + +- [ ] **Step 1: Create `MathCaptcha.tsx`** + +```tsx +"use client"; + +import { useEffect, useState } from "react"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +interface MathCaptchaProps { + // Called whenever validity changes, so the parent can enable/disable submit. + onValidChange: (valid: boolean) => void; + id?: string; +} + +// Dead-simple anti-spam: "What is X + Y?". Generated client-side on mount. +// Math.random is allowed in browser runtime (this is not a workflow script). +export function MathCaptcha({ onValidChange, id = "math-captcha" }: MathCaptchaProps) { + const [a, setA] = useState(0); + const [b, setB] = useState(0); + const [answer, setAnswer] = useState(""); + + useEffect(() => { + setA(Math.floor(Math.random() * 8) + 1); + setB(Math.floor(Math.random() * 8) + 1); + }, []); + + useEffect(() => { + onValidChange(answer.trim() !== "" && parseInt(answer, 10) === a + b); + }, [answer, a, b, onValidChange]); + + return ( +
+ + setAnswer(e.target.value)} + placeholder="Answer" + className="w-32" + /> +
+ ); +} +``` + +- [ ] **Step 2: Verify type-check passes** + +Run: `npx tsc --noEmit` +Expected: no errors. + +- [ ] **Step 3: Commit** + +```bash +git add src/components/lead-forms/MathCaptcha.tsx +git commit -m "feat(lead-gen): inline math captcha field" +``` + +--- + +## Task 4: EnterpriseModal (built first — the other two link to it) + +**Files:** +- Create: `src/components/lead-forms/EnterpriseModal.tsx` + +- [ ] **Step 1: Create `EnterpriseModal.tsx`** + +```tsx +"use client"; + +import { useState } from "react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; + +import { validateWorkEmail } from "./isPersonalEmail"; +import { + ENTERPRISE_COMPANY_SIZE_OPTIONS, + ENTERPRISE_INDUSTRY_OPTIONS, + ENTERPRISE_TIMELINE_OPTIONS, + type LeadSource, +} from "./leadFieldOptions"; +import { MathCaptcha } from "./MathCaptcha"; +import { submitLead } from "./submitLead"; + +interface EnterpriseModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + source: LeadSource; +} + +export function EnterpriseModal({ open, onOpenChange, source }: EnterpriseModalProps) { + const [company, setCompany] = useState(""); + const [industry, setIndustry] = useState(""); + const [companySize, setCompanySize] = useState(""); + const [timeline, setTimeline] = useState(""); + const [workEmail, setWorkEmail] = useState(""); + const [phone, setPhone] = useState(""); + const [notes, setNotes] = useState(""); + const [emailError, setEmailError] = useState(null); + const [captchaValid, setCaptchaValid] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const reset = () => { + setCompany(""); setIndustry(""); setCompanySize(""); setTimeline(""); + setWorkEmail(""); setPhone(""); setNotes(""); setEmailError(null); + setCaptchaValid(false); setSubmitting(false); + }; + + const handleSubmit = async () => { + const err = validateWorkEmail(workEmail); + if (err) { setEmailError(err); return; } + if (!company.trim() || !industry || !companySize || !timeline) { + toast.error("Please fill in all required fields"); + return; + } + if (!captchaValid) { toast.error("Please answer the quick check"); return; } + + setSubmitting(true); + try { + await submitLead({ + kind: "enterprise", + source, + payload: { company, industry, companySize, timeline, workEmail, phone, notes }, + }); + toast.success("Thanks — our team will reach out about enterprise deployment."); + reset(); + onOpenChange(false); + } catch { + toast.error("Something went wrong. Please try again."); + setSubmitting(false); + } + }; + + return ( + { if (!o) reset(); onOpenChange(o); }}> + + + Enterprise deployment + + SSO, on-prem, SOC2, data residency. Tell us about your environment. + + + +
+
+ + setCompany(e.target.value)} /> +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + { setWorkEmail(e.target.value); setEmailError(null); }} + placeholder="you@company.com" + /> + {emailError &&

{emailError}

} +
+ +
+ + setPhone(e.target.value)} /> +
+ +
+ +