// game-plan-form.jsx — custom multi-step lead form for the Amazon Game Plan page. // One question at a time (Typeform-style). Branches on "what brings you here". // Emails the lead + their requested day/time to EMAIL_ENDPOINT; you confirm by hand. // Exported to window.GamePlanForm and mounted by the page's inline script. /* ====================================================================== * * CONFIG — the one thing to wire up before going live. * * ====================================================================== */ // Where lead answers + the requested slot are sent. This is your Formspree // endpoint — every submission is emailed to you automatically. const EMAIL_ENDPOINT = "https://formspree.io/f/xjgdpzab"; /* ====================================================================== * * QUESTIONS — edit freely. {name} is replaced with their first name. * * type: text | longtext | email | tel | select | booking * * showIf: optional fn(answers) => bool (used for the new/existing fork) * * ====================================================================== */ const STEPS = [ { id: "name", type: "text", required: true, q: "Hey there — what should we call you?", help: "First name is perfect.", placeholder: "e.g. Jordan" }, { id: "reason", type: "select", required: true, q: "What brings you here, {name}?", help: "So your game plan is built around the right thing.", options: [ { value: "start", label: "I want to start selling on Amazon" }, { value: "grow", label: "I'm already selling and want to grow" }, ] }, // ---- branch: brand-new sellers ---- { id: "stage", type: "select", showIf: (a) => a.reason === "start", q: "How far along are you?", help: "No wrong answer — it just shapes the plan.", options: [ { value: "researching", label: "Just researching" }, { value: "picked", label: "Picked a product" }, { value: "sourcing", label: "Sourcing it now" }, { value: "ready", label: "Ready to launch" }, ] }, // ---- branch: existing sellers ---- { id: "revenue", type: "select", showIf: (a) => a.reason === "grow", q: "Roughly how much are you doing per month?", help: "Ballpark is fine — it stays private.", options: [ { value: "<10k", label: "Under $10k / mo" }, { value: "10-50k", label: "$10k – $50k / mo" }, { value: "50-250k", label: "$50k – $250k / mo" }, { value: "250k+", label: "$250k+ / mo" }, ] }, { id: "store", type: "text", showIf: (a) => a.reason === "grow", required: false, q: "Got a store or product link?", help: "Optional — your storefront or an ASIN link lets us look before we talk.", placeholder: "amazon.com/…" }, { id: "goal", type: "select", q: "Where do you want to be in 12 months?", help: "Your revenue goal.", options: [ { value: "<10k", label: "$0 – $10k / mo" }, { value: "10-50k", label: "$10k – $50k / mo" }, { value: "50-250k", label: "$50k – $250k / mo" }, { value: "250k+", label: "$250k+ / mo" }, ] }, { id: "challenge", type: "longtext", required: false, q: "What's the biggest thing in your way right now?", help: "One line is plenty — this shapes your plan more than anything else.", placeholder: "e.g. my listings get traffic but don't convert…" }, { id: "budget", type: "select", q: "Do you have budget set aside to invest in your Amazon?", help: "No figure needed — this just tells us what to plan toward.", options: [ { value: "now", label: "Ready to invest now" }, { value: "building", label: "Building toward it" }, { value: "notyet", label: "Not yet" }, ] }, { id: "location", type: "text", q: "Where are you based?", help: "Just so we get your timezone right for the call.", placeholder: "City, country" }, { id: "email", type: "email", required: true, q: "Where should we send your game plan, {name}?", help: "For your plan and the call confirmation. No spam, ever.", placeholder: "name@example.com" }, { id: "whatsapp", type: "tel", required: false, q: "WhatsApp number?", help: "Optional — usually the quickest way to reach you.", placeholder: "+1 555 000 0000" }, { id: "source", type: "text", required: false, q: "One last thing — how did you hear about us?", help: "Optional, but it helps us a lot.", placeholder: "e.g. Google, a friend, YouTube…" }, { id: "booking", type: "booking", q: "Last step, {name} — request your walkthrough", help: "20 focused minutes where we walk you through your plan — what to do, in what order, what it's worth. Pick a preferred day and any times that suit you; we'll work around your calendar and confirm by email." }, ]; /* ====================================================================== */ const { useState, useEffect, useRef, useMemo } = React; const firstName = (a) => (a.name || "").trim().split(/\s+/)[0] || "there"; const fill = (str, a) => (str || "").replace(/\{name\}/g, firstName(a)); const isEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((v || "").trim()); function submitLead(answers, stage) { const payload = { ...answers, _stage: stage, _form: "Amazon Game Plan", _subject: "New Amazon Game Plan request", _ts: new Date().toISOString() }; if (!EMAIL_ENDPOINT) { console.log("[GamePlanForm] lead (" + stage + "):", payload); return; } try { fetch(EMAIL_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(payload), keepalive: true, }).catch(() => {}); } catch (e) { /* preview sandbox can't reach the network — fine */ } } function fireConversion() { if (typeof window.gtag === "function") { window.gtag("event", "conversion", { send_to: "AW-16798512866/JjupCLPRq7wcEOLtk8o-" }); } } function ProgressBar({ value }) { return
; } function OptionList({ options, selected, onPick }) { const keys = ["A", "B", "C", "D", "E"]; return (
{options.map((o, i) => ( ))}
); } const MONTHS = ["January","February","March","April","May","June","July","August","September","October","November","December"]; const MON_SHORT = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; const DAY_SHORT = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]; const SLOTS = ["9:00 AM","10:00 AM","11:00 AM","12:00 PM","1:00 PM","2:00 PM","3:00 PM","4:00 PM","5:00 PM"]; /* ----- WhatsApp / phone field with a country-code dropdown ------------- */ const COUNTRY_CODES = [ { n: "United States", i: "US", d: "+1" }, { n: "Canada", i: "CA", d: "+1" }, { n: "United Kingdom", i: "GB", d: "+44" }, { n: "India", i: "IN", d: "+91" }, { n: "Australia", i: "AU", d: "+61" }, { n: "United Arab Emirates", i: "AE", d: "+971" }, { n: "Germany", i: "DE", d: "+49" }, { n: "France", i: "FR", d: "+33" }, { n: "Spain", i: "ES", d: "+34" }, { n: "Italy", i: "IT", d: "+39" }, { n: "Netherlands", i: "NL", d: "+31" }, { n: "Ireland", i: "IE", d: "+353" }, { n: "Portugal", i: "PT", d: "+351" }, { n: "Switzerland", i: "CH", d: "+41" }, { n: "Sweden", i: "SE", d: "+46" }, { n: "Norway", i: "NO", d: "+47" }, { n: "Denmark", i: "DK", d: "+45" }, { n: "Belgium", i: "BE", d: "+32" }, { n: "Poland", i: "PL", d: "+48" }, { n: "Mexico", i: "MX", d: "+52" }, { n: "Brazil", i: "BR", d: "+55" }, { n: "Argentina", i: "AR", d: "+54" }, { n: "Chile", i: "CL", d: "+56" }, { n: "Colombia", i: "CO", d: "+57" }, { n: "South Africa", i: "ZA", d: "+27" }, { n: "Nigeria", i: "NG", d: "+234" }, { n: "Kenya", i: "KE", d: "+254" }, { n: "Egypt", i: "EG", d: "+20" }, { n: "Saudi Arabia", i: "SA", d: "+966" }, { n: "Qatar", i: "QA", d: "+974" }, { n: "Israel", i: "IL", d: "+972" }, { n: "Turkey", i: "TR", d: "+90" }, { n: "Pakistan", i: "PK", d: "+92" }, { n: "Bangladesh", i: "BD", d: "+880" }, { n: "Sri Lanka", i: "LK", d: "+94" }, { n: "Singapore", i: "SG", d: "+65" }, { n: "Malaysia", i: "MY", d: "+60" }, { n: "Indonesia", i: "ID", d: "+62" }, { n: "Philippines", i: "PH", d: "+63" }, { n: "Thailand", i: "TH", d: "+66" }, { n: "Vietnam", i: "VN", d: "+84" }, { n: "Hong Kong", i: "HK", d: "+852" }, { n: "China", i: "CN", d: "+86" }, { n: "Japan", i: "JP", d: "+81" }, { n: "South Korea", i: "KR", d: "+82" }, { n: "New Zealand", i: "NZ", d: "+64" }, ]; // Splits a stored value ("+91 98765 43210") back into { dial, num }. function splitPhone(value) { const v = (value || "").trim(); if (v) { const match = COUNTRY_CODES .filter((c) => v.startsWith(c.d)) .sort((a, b) => b.d.length - a.d.length)[0]; if (match) return { dial: match.d, num: v.slice(match.d.length).trim() }; } return { dial: "+1", num: v }; } function PhoneField({ value, onChange, onEnter, placeholder, inputRef }) { const init = splitPhone(value); const [dial, setDial] = useState(init.dial); const [num, setNum] = useState(init.num); const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const wrapRef = useRef(null); const combine = (d, n) => (n.trim() ? d + " " + n.trim() : ""); function chooseDial(d) { setDial(d); setOpen(false); setQuery(""); onChange(combine(d, num)); } function changeNum(n) { setNum(n); onChange(combine(dial, n)); } useEffect(() => { if (!open) return; const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, [open]); const q = query.trim().toLowerCase(); const list = q ? COUNTRY_CODES.filter((c) => c.n.toLowerCase().includes(q) || c.d.includes(q) || c.i.toLowerCase().includes(q)) : COUNTRY_CODES; return (
changeNum(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); onEnter && onEnter(); } }} style={{ borderRadius: "0 12px 12px 0", flex: "1 1 auto", minWidth: 0 }} /> {open ? (
setQuery(e.target.value)} placeholder="Search country or code" style={{ width: "100%", height: 44, border: "0", borderBottom: "1px solid var(--line)", padding: "0 14px", font: "14px var(--sans)", color: "var(--ink)", outline: "none" }} />
{list.map((c) => ( ))} {list.length === 0 ? (
No match.
) : null}
) : null}
); } // Single-slot booking: visitor picks one day + one time. Sends a ready-to-use // start/end datetime + timezone so an automation can create a tentative // Google Calendar event at exactly that time (confirmed by hand afterward). function BookingStep({ onSubmit }) { const today = new Date(); const todayMid = new Date(today.getFullYear(), today.getMonth(), today.getDate()); // Earliest bookable moment: at least 24 hours from now. const minTime = new Date(today.getTime() + 24 * 60 * 60 * 1000); const [view, setView] = useState({ y: today.getFullYear(), m: today.getMonth() }); const [sel, setSel] = useState(null); const [time, setTime] = useState(null); const startWeekday = (new Date(view.y, view.m, 1).getDay() + 6) % 7; // Monday-first const daysInMonth = new Date(view.y, view.m + 1, 0).getDate(); const cells = []; for (let i = 0; i < startWeekday; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(view.y, view.m, d)); while (cells.length % 7 !== 0) cells.push(null); // Start Date for a given day + slot label, in the visitor's local time. const slotStart = (date, t) => { const mm = t.match(/(\d+):(\d+)\s*(AM|PM)/i); let h = parseInt(mm[1], 10); const min = parseInt(mm[2], 10); const ap = mm[3].toUpperCase(); if (ap === "PM" && h !== 12) h += 12; if (ap === "AM" && h === 12) h = 0; return new Date(date.getFullYear(), date.getMonth(), date.getDate(), h, min, 0); }; // A specific slot is bookable only if it's at least 24h out. const slotOk = (date, t) => slotStart(date, t) >= minTime; const pickable = (date) => { if (!date) return false; const wd = date.getDay(); // Weekday, today onward, and has at least one slot 24h+ from now. return date >= todayMid && wd >= 1 && wd <= 5 && SLOTS.some((t) => slotOk(date, t)); }; const atMonthStart = view.y === today.getFullYear() && view.m === today.getMonth(); function prev() { if (atMonthStart) return; const m = view.m - 1; setView(m < 0 ? { y: view.y - 1, m: 11 } : { y: view.y, m }); setSel(null); setTime(null); } function next() { const m = view.m + 1; setView(m > 11 ? { y: view.y + 1, m: 0 } : { y: view.y, m }); setSel(null); setTime(null); } const pad = (n) => (n < 10 ? "0" + n : "" + n); function parseSlot(t) { // "10:00 AM" -> {h, m} in 24h const mm = t.match(/(\d+):(\d+)\s*(AM|PM)/i); let h = parseInt(mm[1], 10); const min = parseInt(mm[2], 10); const ap = mm[3].toUpperCase(); if (ap === "PM" && h !== 12) h += 12; if (ap === "AM" && h === 12) h = 0; return { h: h, m: min }; } function localISO(d) { return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + "T" + pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":00"; } function send() { if (!sel || !time) return; const { h, m } = parseSlot(time); const start = new Date(sel.getFullYear(), sel.getMonth(), sel.getDate(), h, m, 0); const end = new Date(start.getTime() + 30 * 60000); const dateLabel = DAY_SHORT[sel.getDay()] + ", " + MON_SHORT[sel.getMonth()] + " " + sel.getDate() + ", " + sel.getFullYear(); onSubmit({ booking_date: dateLabel, booking_time: time, booking_label: dateLabel + " at " + time, booking_start: localISO(start), // e.g. 2026-06-15T10:00:00 (in visitor's tz) booking_end: localISO(end), booking_tz: tz, }); } const tz = (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) { return "your time"; } })(); return (
{MONTHS[view.m]} {view.y}
MTWTFSS
{cells.map((date, i) => { const a = pickable(date); const isSel = sel && date && date.getTime() === sel.getTime(); return ( ); })}
{sel ? "Pick a time · " + DAY_SHORT[sel.getDay()] + " " + MON_SHORT[sel.getMonth()] + " " + sel.getDate() : "Pick a day →"} {sel ? ( <>
{SLOTS.map((t) => { const ok = slotOk(sel, t); return ( ); })}
Times shown in {tz}.
) : (
Choose a weekday on the left, then pick the time that suits you best.
)}
); } function GamePlanForm() { const [answers, setAnswers] = useState({}); const [idx, setIdx] = useState(0); const [err, setErr] = useState(""); const [done, setDone] = useState(false); const [leadSent, setLeadSent] = useState(false); const inputRef = useRef(null); const steps = useMemo(() => STEPS.filter((s) => !s.showIf || s.showIf(answers)), [answers]); const step = steps[Math.min(idx, steps.length - 1)]; const progress = done ? 1 : (idx + 1) / steps.length; useEffect(() => { setErr(""); if (inputRef.current) inputRef.current.focus(); }, [idx, step && step.id]); function setVal(v) { setAnswers((a) => ({ ...a, [step.id]: v })); setErr(""); } function validate() { const v = answers[step.id]; if (step.required && (!v || !String(v).trim())) { setErr(step.type === "select" ? "Pick one to continue." : "This one's required."); return false; } if (step.type === "email" && v && !isEmail(v)) { setErr("That email doesn't look right."); return false; } return true; } function captureLeadOnce() { // fire the lead to email as soon as we have an email, so nothing is lost if (!leadSent && answers.email) { submitLead(answers, "lead"); setLeadSent(true); } } function advance() { if (!validate()) return; if (step.id === "email") captureLeadOnce(); if (idx >= steps.length - 1) { finish(); return; } setIdx((i) => i + 1); } function back() { if (idx > 0) setIdx((i) => i - 1); } function pick(value) { setAnswers((a) => ({ ...a, [step.id]: value })); setErr(""); const isLast = idx >= steps.length - 1; setTimeout(() => { isLast ? finish() : setIdx((i) => i + 1); }, 240); } function finish(extra) { const merged = extra ? { ...answers, ...extra } : { ...answers }; if (!leadSent && merged.email) { submitLead(merged, "lead"); setLeadSent(true); } submitLead(merged, "complete"); fireConversion(); setAnswers(merged); setDone(true); } function onKey(e) { if (e.key === "Enter" && step && step.type !== "longtext" && step.type !== "booking") { e.preventDefault(); advance(); } } if (done) { const slot = answers.booking_label; return (

Request received, {firstName(answers)}.

We've got your details and we're building your Amazon game plan by hand. {slot ? <> You asked for {slot} — we'll confirm a time : <> We'll confirm your walkthrough} {answers.email ? <> by email at {answers.email} : <> by email}, usually within a few hours.

Pending confirmation · No card charged · No obligation
); } const total = steps.length; const num = String(idx + 1).padStart(2, "0"); return (
Step {num} of {total} · takes ~2 min

{fill(step.q, answers)}

{step.help ?

{fill(step.help, answers)}

: null} {step.type === "select" ? ( ) : step.type === "booking" ? ( finish(payload)} /> ) : step.type === "longtext" ? (