/* 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 && (
| Name |
Kind |
Status |
Recipients |
Sent |
Replied |
|
{campaigns.map((c) => (
goto('campaign-builder', c.id)} style={{ borderBottom: '1px solid var(--divider)', cursor: 'pointer' }}>
|
{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 (
{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) => (
))}
{isSeq && (
)}
);
}
Object.assign(window, { Campaigns, CampaignBuilder });