// Traffic Desk client — pick size, pick approved creative, pick destination, // launch. Balance and running campaigns come from the server. (function () { 'use strict'; var $ = function (id) { return document.getElementById(id); }; var state = { size: null, creative: null, target: 'join', creatives: {}, remaining: 0, id: null }; var busy = false; function chip(label, on, fn) { var b = document.createElement('button'); b.type = 'button'; b.className = 'tf-chip' + (on ? ' on' : ''); b.textContent = label; b.addEventListener('click', fn); return b; } function renderSizes(sizes) { var host = $('tfSizes'); host.innerHTML = ''; sizes.forEach(function (s) { host.appendChild(chip(s, s === state.size, function () { state.size = s; state.creative = (state.creatives[s] || [])[0] || null; renderSizes(sizes); renderCreatives(); })); }); } function renderCreatives() { var host = $('tfCreatives'); host.innerHTML = ''; (state.creatives[state.size] || []).forEach(function (f) { var d = document.createElement('div'); d.className = 'tf-cre' + (f === state.creative ? ' on' : ''); var img = document.createElement('img'); img.src = '/banners/' + f; img.alt = 'Team banner ' + state.size; img.loading = 'lazy'; d.appendChild(img); d.addEventListener('click', function () { state.creative = f; renderCreatives(); }); host.appendChild(d); }); } function renderTargets() { var host = $('tfTargets'); host.innerHTML = ''; host.appendChild(chip('My invite page', state.target === 'join', function () { state.target = 'join'; renderTargets(); })); if (state.hasPage) { host.appendChild(chip('My personal page', state.target === 'page', function () { state.target = 'page'; renderTargets(); })); } else { // Don't let anyone point an ad at a page that doesn't exist yet. var d = document.createElement('span'); d.className = 'tf-hint'; d.style.cssText = 'margin:0;align-self:center'; d.innerHTML = 'Want to send traffic to your own page instead? Build it first →'; host.appendChild(d); } } function gate(msg) { $('tfGate').style.display = 'block'; $('tfGate').innerHTML = msg; $('tfCard').style.opacity = '.55'; $('tfGo').disabled = true; } function renderLive(campaigns, live) { if (!campaigns.length) { $('tfLiveWrap').style.display = 'none'; return; } var byId = {}; (live || []).forEach(function (l) { byId[l.ad_id] = l; }); var tb = $('tfLive'); tb.innerHTML = ''; campaigns.slice().reverse().forEach(function (c) { var l = byId[c.adId] || {}; var tr = document.createElement('tr'); var served = l.served != null ? l.served : 0; var statusCell = c.stopped ? 'stopped (' + Number(c.refunded || 0).toLocaleString() + ' returned)' : (l.live ? 'running' : 'finished'); tr.innerHTML = '' + c.size + '' + '' + (c.target.indexOf('/p/') !== -1 ? 'personal page' : 'invite page') + '' + '' + Number(c.bought != null ? c.bought : c.impressions).toLocaleString() + '' + '' + Number(c.stopped ? (c.served || 0) : served).toLocaleString() + '' + '' + (c.stopped ? '—' : (l.remaining != null ? Number(l.remaining).toLocaleString() : '—')) + '' + '' + (l.hits != null ? l.hits : '—') + '' + '' + statusCell + ''; var act = document.createElement('td'); if (!c.stopped && l.live) { var btn = document.createElement('button'); btn.className = 'btn btn-secondary btn-sm'; btn.textContent = 'Stop'; btn.title = 'Stop this banner and return the unserved impressions to your balance'; btn.addEventListener('click', function () { stopCampaign(c.adId, btn); }); act.appendChild(btn); } tr.appendChild(act); tb.appendChild(tr); }); $('tfLiveWrap').style.display = 'block'; } function applyStatus(d) { var st = d.status; state.creatives = st.creatives || {}; state.id = d.id; state.remaining = st.remaining; if (typeof d.hasPage === 'boolean') state.hasPage = d.hasPage; if (!state.hasPage && state.target === 'page') state.target = 'join'; if (!state.size) state.size = (st.sizes || [])[0] || null; if (!state.creative) state.creative = (state.creatives[state.size] || [])[0] || null; $('tfBal').style.display = 'flex'; $('tfRemain').textContent = Number(st.remaining).toLocaleString(); $('tfLimit').textContent = Number(st.limit).toLocaleString(); $('tfNote').innerHTML = 'Level ' + d.level + ' includes ' + Number(st.limit).toLocaleString() + ' impressions a month on the network. Each upgrade raises it — Apex is where it jumps to 50,000.'; $('tfImp').max = st.remaining; $('tfImp').value = Math.min(Number($('tfImp').value) || st.remaining, st.remaining) || 100; renderSizes(st.sizes || []); renderCreatives(); renderTargets(); renderLive(st.campaigns || [], d.live || []); if (st.remaining <= 0) { $('tfGo').disabled = true; $('tfGo').textContent = 'Allowance used — refills on the 1st'; } } async function stopCampaign(adId, btn) { if (btn) { btn.disabled = true; btn.textContent = 'Stopping…'; } try { var r = await fetch('/api/public/suite-traffic-stop', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ad_id: adId }) }); var d = await r.json(); if (!r.ok) { $('tfErr').textContent = d.error || 'Could not stop that banner.'; $('tfErr').style.display = 'block'; if (btn) { btn.disabled = false; btn.textContent = 'Stop'; } return; } $('tfOk').innerHTML = '✅ Banner stopped. ' + Number(d.stopped.refunded).toLocaleString() + ' unserved impressions went back into your balance' + (d.stopped.served ? ' — it had served ' + Number(d.stopped.served).toLocaleString() + '.' : '.'); $('tfOk').style.display = 'block'; boot(); } catch (e) { $('tfErr').textContent = 'Connection hiccup — try again.'; $('tfErr').style.display = 'block'; if (btn) { btn.disabled = false; btn.textContent = 'Stop'; } } } async function boot() { try { var r = await fetch('/api/public/suite-traffic'); if (r.status === 401) { gate('You’re not signed in yet. Open the Suite and connect the wallet that holds your position, then come back.'); return; } if (r.status === 403) { gate('The Traffic Desk isn’t open for this position yet. See your Suite.'); return; } if (!r.ok) return; var d = await r.json(); if (!d.configured) { gate('The ad network bridge is being finalized — this opens shortly.'); return; } applyStatus(d); } catch (e) {} } async function run() { if (busy) return; busy = true; $('tfErr').style.display = 'none'; $('tfOk').style.display = 'none'; $('tfGo').disabled = true; $('tfGo').innerHTML = 'Launching on the network…'; try { var r = await fetch('/api/public/suite-traffic', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ size: state.size, creative: state.creative, target: state.target, impressions: Number($('tfImp').value) || 0 }) }); var d = await r.json(); if (!r.ok) { $('tfErr').textContent = d.error || 'That didn’t go through — try again.'; $('tfErr').style.display = 'block'; } else { $('tfOk').innerHTML = '✅ Your banner is live on the network.
' + Number(d.campaign.impressions).toLocaleString() + ' impressions of ' + d.campaign.size + ' pointed at your ' + (d.campaign.target.indexOf('/p/') !== -1 ? 'personal page' : 'invite page') + '. It starts rotating immediately — check back here to watch it serve.'; $('tfOk').style.display = 'block'; applyStatus({ status: d.status, level: 0, id: state.id, live: [], configured: true }); // refresh from the server so the running-banners table picks it up setTimeout(boot, 800); } } catch (e) { $('tfErr').textContent = 'Connection hiccup — try again.'; $('tfErr').style.display = 'block'; } busy = false; if (state.remaining > 0) { $('tfGo').disabled = false; $('tfGo').textContent = '🚦 Launch my banner'; } } document.addEventListener('DOMContentLoaded', function () { boot(); $('tfGo').addEventListener('click', run); }); })();