import { useState, useRef } from “react”; // ─── Constants ─── const TEMP_OPTIONS = [ { value: “hot”, label: “🔥 Hot”, color: “#E8572A” }, { value: “warm”, label: “☀️ Warm”, color: “#FF9F1C” }, { value: “cool”, label: “💧 Cool”, color: “#2EC4B6” }, ]; const REP_ACTIONS = [ { value: “send_info”, label: “📩 Send info/asset” }, { value: “book_meeting”, label: “📅 Book a meeting” }, { value: “intro”, label: “🤝 Make an intro” }, { value: “proposal”, label: “📝 Send proposal” }, { value: “personal_note”, label: “✉️ Personal follow-up” }, { value: “none”, label: “— None” }, ]; const MKTG_ACTIONS = [ { value: “nurture”, label: “📬 Add to post-event nurture” }, { value: “help_followup”, label: “🙋 Help me with my follow-up” }, { value: “pause_30”, label: “⏸️ Pause nurture 30 days” }, { value: “none”, label: “— None” }, ]; const fmt = d => d.toLocaleTimeString([], { hour: “numeric”, minute: “2-digit” }); const durMin = (s, e) => e ? Math.round((e - s) / 60000) : null; // ─── Shared UI ─── const Pill = ({ selected, onClick, children, color }) => ( ); const SectionLabel = ({ color, children }) => (
{children}
); const Input = ({ value, onChange, placeholder, type, autoComplete, onFocus, onBlur }) => ( e.target.style.borderColor = “#FF6B35”} onBlur={e => e.target.style.borderColor = “#e8e8e8”} /> ); // ─── Lead Card ─── function LeadCard({ lead, onEnd, isActive }) { const dur = durMin(lead.startTime, lead.endTime); const temp = TEMP_OPTIONS.find(t => t.value === lead.temperature); const rActs = REP_ACTIONS.filter(r => lead.repActions?.includes(r.value) && r.value !== “none”); const mActs = MKTG_ACTIONS.filter(m => lead.mktgActions?.includes(m.value) && m.value !== “none”); return (
{lead.name || “No name”}
{lead.company &&
{lead.company}
}
{fmt(lead.startTime)}
{dur !== null &&
{dur} min
} {isActive &&
● LIVE
} {lead.source === “import” &&
IMPORTED
}
{temp && {temp.label}} {rActs.map(a => {a.label})} {mActs.map(a => {a.label})}
{lead.repNote &&
”{lead.repNote}”
} {lead.notes &&
{lead.notes}
} {isActive && ( )}
); } // ─── Dashboard ─── function Dashboard({ leads, eventName, costs, setCosts }) { const reps = […new Set(leads.map(l => l.staffName || “Unassigned”))]; const totalLeads = leads.length; const hot = leads.filter(l => l.temperature === “hot”).length; const warm = leads.filter(l => l.temperature === “warm”).length; const cool = totalLeads - hot - warm; const qualifiedLeads = hot + warm; const avgDwell = leads.filter(l => l.endTime).length > 0 ? Math.round(leads.filter(l => l.endTime).reduce((s, l) => s + durMin(l.startTime, l.endTime), 0) / leads.filter(l => l.endTime).length) : null; const repOwes = leads.filter(l => l.repActions?.length > 0).length; const mktgQueue = leads.filter(l => l.mktgActions?.length > 0).length; const helpRequests = leads.filter(l => l.mktgActions?.includes(“help_followup”)).length; const totalCost = Object.values(costs).reduce((s, v) => s + (Number(v) || 0), 0); const cpl = qualifiedLeads > 0 && totalCost > 0 ? Math.round(totalCost / qualifiedLeads) : null; const grade = cpl === null ? { g: “—”, c: “#666” } : cpl <= 150 ? { g: “A+”, c: “#1A936F” } : cpl <= 250 ? { g: “A”, c: “#2EC4B6” } : cpl <= 400 ? { g: “B”, c: “#FF9F1C” } : cpl <= 600 ? { g: “C”, c: “#FF6B35” } : { g: “D”, c: “#E8572A” }; const Stat = ({ label, value, sub, color }) => (
{label}
{value}
{sub &&
{sub}
}
); const RepRow = ({ name }) => { const rl = leads.filter(l => (l.staffName || “Unassigned”) === name); const rHot = rl.filter(l => l.temperature === “hot”).length; const rWarm = rl.filter(l => l.temperature === “warm”).length; const rOwes = rl.filter(l => l.repActions?.length > 0).length; const rDwell = rl.filter(l => l.endTime).length > 0 ? Math.round(rl.filter(l => l.endTime).reduce((s, l) => s + durMin(l.startTime, l.endTime), 0) / rl.filter(l => l.endTime).length) : null; ``` return (
{name}
{rl.length}
🔥 {rHot} ☀️ {rWarm} 💧 {rl.length - rHot - rWarm} {rDwell && ⏱ {rDwell} min avg} {rOwes > 0 && ⚡ {rOwes} follow-ups owed}
); ``` }; const CostInput = ({ id, label }) => (
$ setCosts(p => ({ …p, [id]: e.target.value }))} type=“number” placeholder=“0” style={{ width: 100, padding: “8px 10px”, border: “1px solid #ffffff22”, borderRadius: 8, fontSize: 14, background: “#ffffff0a”, color: “#fff”, outline: “none”, fontFamily: “‘JetBrains Mono’, monospace”, textAlign: “right”, }} />
); return (
{/* Hero grade */}
{eventName || “Event”} · Cost Per Qualified Lead
{grade.g}
{cpl !== null &&
${cpl} per qualified lead
} {cpl === null &&
Add costs below to calculate grade
}
``` {/* Stats grid */}
0 ? `${helpRequests} need help` : ""} />
{/* Rep breakdown */} {reps.length > 0 && (
Activity by Rep
{reps.map(r => )}
)} {/* Cost inputs */}
Event Investment
{totalCost > 0 && (
Total ${totalCost.toLocaleString()}
)}
{/* Marketing queue summary */}
Marketing Queue
{leads.filter(l => l.mktgActions?.includes("nurture")).length} → post-event nurture
{helpRequests} → need follow-up help
{leads.filter(l => l.mktgActions?.includes("pause_30")).length} → paused 30 days
{/* Footer */}
BoothSpark
You're already in the room. Stop wasting it.
``` ); } // ─── Main App ─── export default function BoothSparkApp() { const [tab, setTab] = useState(“capture”); // capture | import | dashboard const [leads, setLeads] = useState([]); const [showForm, setShowForm] = useState(false); const [activeId, setActiveId] = useState(null); const [formStep, setFormStep] = useState(1); const [costs, setCosts] = useState({}); const fileRef = useRef(); // Form fields const [name, setName] = useState(””); const [company, setCompany] = useState(””); const [email, setEmail] = useState(””); const [phone, setPhone] = useState(””); const [temperature, setTemperature] = useState(””); const [repActions, setRepActions] = useState([]); const [repNote, setRepNote] = useState(””); const [mktgActions, setMktgActions] = useState([]); const [notes, setNotes] = useState(””); const [eventName, setEventName] = useState(””); const [staffName, setStaffName] = useState(””); // Import state const [importPreview, setImportPreview] = useState(null); const [importMapping, setImportMapping] = useState({}); const [importCount, setImportCount] = useState(0); const resetForm = () => { setName(””); setCompany(””); setEmail(””); setPhone(””); setTemperature(””); setRepActions([]); setRepNote(””); setMktgActions([]); setNotes(””); setFormStep(1); }; const toggle = (arr, setArr, val) => { if (val === “none”) return setArr([“none”]); const w = arr.filter(v => v !== “none”); setArr(w.includes(val) ? w.filter(v => v !== val) : […w, val]); }; const saveLead = () => { const l = { id: Date.now(), name, company, email, phone, temperature, repActions: repActions.filter(v => v !== “none”), repNote, mktgActions: mktgActions.filter(v => v !== “none”), notes, startTime: new Date(), endTime: null, staffName, source: “manual”, }; setLeads(p => [l, …p]); setActiveId(l.id); resetForm(); setShowForm(false); }; const endConvo = id => { setLeads(p => p.map(l => l.id === id ? { …l, endTime: new Date() } : l)); setActiveId(null); }; // CSV Import const handleFile = e => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = ev => { const text = ev.target.result; const lines = text.split(”\n”).map(l => l.split(”,”).map(c => c.replace(/^”|”$/g, “”).trim())); if (lines.length < 2) return; const headers = lines[0]; const rows = lines.slice(1).filter(r => r.some(c => c)); setImportPreview({ headers, rows }); // Auto-map common field names const m = {}; const nameMap = { name: “name”, “full name”: “name”, “first name”: “name”, “contact name”: “name”, company: “company”, organization: “company”, email: “email”, “email address”: “email”, phone: “phone”, “phone number”: “phone” }; headers.forEach((h, i) => { const key = h.toLowerCase().trim(); if (nameMap[key]) m[nameMap[key]] = i; }); setImportMapping(m); setImportCount(rows.length); }; reader.readAsText(file); }; const doImport = () => { if (!importPreview) return; const newLeads = importPreview.rows.map((row, i) => ({ id: Date.now() + i, name: row[importMapping.name] || “”, company: row[importMapping.company] || “”, email: row[importMapping.email] || “”, phone: row[importMapping.phone] || “”, temperature: “”, repActions: [], repNote: “”, mktgActions: [], notes: “”, startTime: new Date(), endTime: new Date(), staffName: staffName || “Import”, source: “import”, })); setLeads(p => […newLeads, …p]); setImportPreview(null); setImportCount(0); }; const exportCSV = () => { const h = [“bs_name”,“bs_company”,“bs_email”,“bs_phone”,“bs_temperature”,“bs_rep_actions”,“bs_rep_note”,“bs_mktg_actions”,“bs_notes”,“bs_start_time”,“bs_end_time”,“bs_dwell_min”,“bs_staff”,“bs_event”,“bs_source”]; const rows = leads.map(l => [ `"${(l.name||"").replace(/"/g,'""')}"`, `"${(l.company||"").replace(/"/g,'""')}"`, l.email, l.phone, l.temperature, `"${(l.repActions||[]).join("; ")}"`, `"${(l.repNote||"").replace(/"/g,'""')}"`, `"${(l.mktgActions||[]).join("; ")}"`, `"${(l.notes||"").replace(/"/g,'""')}"`, l.startTime.toISOString(), l.endTime ? l.endTime.toISOString() : “”, durMin(l.startTime, l.endTime) || “”, l.staffName, eventName, l.source || “manual”, ]); const csv = [h, …rows].map(r => r.join(”,”)).join(”\n”); const blob = new Blob([csv], { type: “text/csv” }); const url = URL.createObjectURL(blob); const a = document.createElement(“a”); a.href = url; a.download = `boothspark_${eventName.replace(/\s/g, "_") || "export"}.csv`; a.click(); URL.revokeObjectURL(url); }; const totalCount = leads.length; const hot = leads.filter(l => l.temperature === “hot”).length; const warm = leads.filter(l => l.temperature === “warm”).length; const TABS = [ { id: “capture”, label: “Capture”, icon: “✏️” }, { id: “import”, label: “Import”, icon: “📥” }, { id: “dashboard”, label: “Dashboard”, icon: “📊” }, ]; return (
``` {/* Header */}
BoothSpark
{eventName || "Lead Capture"}
{totalCount > 0 && (
🔥{hot} ☀️{warm} 💧{totalCount - hot - warm} {totalCount}
)}
{/* Tab bar */}
{TABS.map(t => ( ))}
{/* ─── CAPTURE TAB ─── */} {tab === "capture" && (
{totalCount === 0 && !showForm && (
Quick Setup
setEventName(e.target.value)} placeholder="Event name" /> setStaffName(e.target.value)} placeholder="Your name" />
)} {showForm && (
{formStep === 1 ? "Who'd you meet?" : "What happens next?"}
{[1, 2].map(s =>
)}
{formStep === 1 ? ( <> setName(e.target.value)} placeholder="Name *" autoComplete="name" /> setCompany(e.target.value)} placeholder="Company" autoComplete="organization" /> setEmail(e.target.value)} placeholder="Email" type="email" autoComplete="email" /> setPhone(e.target.value)} placeholder="Phone" type="tel" autoComplete="tel" /> Temperature
{TEMP_OPTIONS.map(t => setTemperature(t.value)}>{t.label})}
) : ( <> What did you promise?
{REP_ACTIONS.map(a => toggle(repActions, setRepActions, a.value)}>{a.label})}
{repActions.length > 0 && !repActions.includes("none") && ( setRepNote(e.target.value)} placeholder="Specifics — what exactly?" style={{ width: "100%", padding: "10px 14px", border: "2px solid #9B5DE522", borderRadius: 10, fontSize: 13, marginTop: 8, outline: "none", background: "#9B5DE506", boxSizing: "border-box" }} /> )} What should marketing do?
{MKTG_ACTIONS.map(a => toggle(mktgActions, setMktgActions, a.value)}>{a.label})}
Notes