Traffic Desk ships: signed NAS bridge client, monthly impression allowance by level (2.5k Scintilla -> 150k Corona), team-kit creative only (no member uploads = no moderation risk), local grant ledger, live serve stats; tile moved to L1 since credits are included at every paid level
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
// 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 = '';
|
||||
[['join', 'My invite page'], ['page', 'My personal page (/p/' + state.id + ')']].forEach(function (t) {
|
||||
host.appendChild(chip(t[1], state.target === t[0], function () {
|
||||
state.target = t[0]; renderTargets();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
tr.innerHTML = '<td><b>' + c.size + '</b></td>' +
|
||||
'<td>' + (c.target.indexOf('/p/') !== -1 ? 'personal page' : 'invite page') + '</td>' +
|
||||
'<td>' + Number(c.impressions).toLocaleString() + '</td>' +
|
||||
'<td><b>' + Number(served).toLocaleString() + '</b></td>' +
|
||||
'<td>' + (l.remaining != null ? Number(l.remaining).toLocaleString() : '—') + '</td>' +
|
||||
'<td>' + (l.hits != null ? l.hits : '—') + '</td>' +
|
||||
'<td>' + (l.live ? '<span style="color:var(--teal)">running</span>' : 'finished') + '</td>';
|
||||
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 (!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 <b style="color:var(--text)">' +
|
||||
Number(st.limit).toLocaleString() + '</b> 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 boot() {
|
||||
try {
|
||||
var r = await fetch('/api/public/suite-traffic');
|
||||
if (r.status === 401) { gate('You’re not signed in yet. <a href="/suite" style="color:var(--gold);font-weight:700">Open the Suite</a> 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. <a href="/suite" style="color:var(--gold);font-weight:700">See your Suite</a>.'); 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 = '<span class="spin"></span>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 = '✅ <b>Your banner is live on the network.</b><br>' +
|
||||
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);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user