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 }) => (
);
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 (
🔥 {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 && (
)}
{showForm && (
{formStep === 1 ? "Who'd you meet?" : "What happens next?"}
{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
)}
{leads.map(l =>
endConvo(l.id)} />)}
{totalCount === 0 && !showForm && (
👋
No leads yet
Tap below to start capturing
)}
{/* Bottom bar */}
{totalCount > 0 && (
)}
)}
{/* ─── IMPORT TAB ─── */}
{tab === "import" && (
{/* CSV Upload */}
Upload Badge Scan or Lead List
CSV export from your badge scanner, lead retrieval app, or spreadsheet. We'll auto-detect name, company, email, and phone columns.
{/* Import Preview */}
{importPreview && (
Preview: {importCount} leads found
Column mapping (auto-detected):
{["name", "company", "email", "phone"].map(field => (
{field}
→
))}
{/* Sample rows */}
{importPreview.rows.slice(0, 3).map((row, i) => (
{importMapping.name !== undefined && {row[importMapping.name]}}
{importMapping.company !== undefined && · {row[importMapping.company]}}
{importMapping.email !== undefined && · {row[importMapping.email]}}
))}
{importCount > 3 &&
...and {importCount - 3} more
}
Imported leads need temperature + actions tagged on the Capture tab
)}
{/* Business card stub */}
📷 Scan Business Card
Take a photo of a business card and auto-fill the lead form. Coming soon in the BoothSpark app.
)}
{/* ─── DASHBOARD TAB ─── */}
{tab === "dashboard" && (
)}
```
);
}
// JavaScript Document