/* global React, Icon, StatusPill */ const { useState, useEffect } = React; const epTh = { padding: '10px 20px', textAlign: 'left', fontSize: 9, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-dim)' }; const epTd = { padding: '12px 20px', verticalAlign: 'middle', color: 'var(--text-secondary)' }; function _epRel(iso) { if (!iso) return '—'; const d = new Date(iso); const diff = (Date.now() - d.getTime()) / 1000; if (diff < 3600) return `${Math.round(diff / 60)}m`; if (diff < 86400) return `${Math.round(diff / 3600)}h`; return `${Math.round(diff / 86400)}d`; } function Campaigns({ goto }) { const [campaigns, setCampaigns] = useState(null); const [picker, setPicker] = useState(false); const [creating, setCreating] = useState(false); const load = () => fetch('/api/campaigns', { credentials: 'include' }) .then((r) => r.ok ? r.json() : Promise.reject(r.statusText)) .then((d) => setCampaigns((d.items || []).filter((c) => c.kind !== 'linkedin_outreach'))); useEffect(() => { load(); }, []); const create = async (kind) => { setCreating(true); setPicker(false); try { const r = await fetch('/api/campaigns', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: kind === 'email_sequence' ? 'New email sequence' : 'New email campaign', kind, daily_cap: 100, }), }); const data = await r.json(); goto('campaign-builder', data.id); } finally { setCreating(false); } }; return (
Outreach · Email

Email Campaign

{campaigns ? `${campaigns.length} campaigns` : '—'}
{campaigns === null &&
Loading…
} {campaigns && campaigns.length === 0 && (
No email campaigns yet.
)} {campaigns && campaigns.length > 0 && ( {campaigns.map((c) => ( goto('campaign-builder', c.id)} style={{ borderBottom: '1px solid var(--divider)', cursor: 'pointer' }}> ))}
Name Kind Status Recipients Sent Replied
{c.name}
created {_epRel(c.created_at)}
{c.kind.replace('email_', '').replace('_', ' ')} {c.recipient_count} {c.sent_count} {c.replied_count}
)}
{picker && (
setPicker(false)}>
e.stopPropagation()} style={{ background: 'var(--surface)', border: '1px solid var(--divider)', width: 520, padding: 28 }}>
New email campaign

Pick campaign type

)}
); } function CampaignBuilder({ campaignId, goto }) { const [campaign, setCampaign] = useState(null); const [steps, setSteps] = useState([]); const [leadLists, setLeadLists] = useState([]); const [error, setError] = useState(null); const reload = async () => { const c = await fetch(`/api/campaigns/${campaignId}`, { credentials: 'include' }).then((r) => r.json()); setCampaign(c); const s = await fetch(`/api/campaigns/${campaignId}/steps`, { credentials: 'include' }).then((r) => r.json()); setSteps((s.items && s.items.length) ? s.items : [{ step_order: 0, kind: 'email', subject: '', body: '', delay_hours: 0 }]); const ll = await fetch('/api/lead-lists', { credentials: 'include' }).then((r) => r.json()); setLeadLists(ll.items || []); }; useEffect(() => { reload(); }, [campaignId]); if (!campaign) return
Loading…
; const isSeq = campaign.kind === 'email_sequence'; const patch = async (body) => { await fetch(`/api/campaigns/${campaignId}`, { method: 'PATCH', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); }; const saveSteps = () => fetch(`/api/campaigns/${campaignId}/steps`, { method: 'PUT', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify(steps.map((s, i) => ({ step_order: i, kind: 'email', subject: s.subject || '', body: s.body || '', delay_hours: parseInt(s.delay_hours || 0), }))), }); const start = async () => { setError(null); try { await saveSteps(); const r = await fetch(`/api/campaigns/${campaignId}/start`, { method: 'POST', credentials: 'include' }); if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.detail || 'start failed'); } reload(); } catch (e) { setError(e.message); } }; const pause = async () => { await fetch(`/api/campaigns/${campaignId}/pause`, { method: 'POST', credentials: 'include' }); reload(); }; const onChangeStep = (i, field, val) => { const next = [...steps]; next[i] = { ...next[i], [field]: val }; setSteps(next); }; return (
setCampaign({ ...campaign, name: e.target.value })} onBlur={() => patch({ name: campaign.name })} style={{ fontFamily: 'var(--font-display)', fontStretch: '75%', fontWeight: 800, fontSize: 22, padding: '4px 8px', minWidth: 320 }} /> {campaign.kind.replace('email_', '').replace('_', ' ')}
{campaign.status !== 'active' && ( )} {campaign.status === 'active' && ( )}
{error && (
{error}
)}
Audience
Pick a lead list. Segment-based audiences come in a later phase.
{steps.map((s, i) => (
STEP {i + 1} {isSeq && i > 0 && ( wait onChangeStep(i, 'delay_hours', e.target.value)} onBlur={saveSteps} style={{ width: 50, margin: '0 6px', padding: '2px 6px', background: 'var(--bg)', border: '1px solid var(--divider)', color: 'var(--text)' }} /> hours )} {isSeq && i > 0 && ( )}
onChangeStep(i, 'subject', e.target.value)} onBlur={saveSteps} />