diff --git a/.gitignore b/.gitignore index 891e0344..3c8c2366 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ node_modules/ # Superpowers brainstorm mockups (local only) .superpowers/ +docs/superpowers/ .gstack/ 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 deleted file mode 100644 index 9d7e4744..00000000 --- a/docs/superpowers/plans/2026-06-02-user-onboarding-lead-gen.md +++ /dev/null @@ -1,1691 +0,0 @@ -# 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)} /> -
- -
- -