diff --git a/public/assets/home.js b/public/assets/home.js
index 9b8234e..76ec918 100644
--- a/public/assets/home.js
+++ b/public/assets/home.js
@@ -133,6 +133,7 @@
recalc();
}
+ IAP.adSlot('banner', 'adSlotHome');
loadLadder();
loadStats();
loadTicker();
diff --git a/public/assets/my.js b/public/assets/my.js
index 4771e15..6bd9dfd 100644
--- a/public/assets/my.js
+++ b/public/assets/my.js
@@ -286,33 +286,42 @@
} catch (e) { return null; }
}
async function earnShowAd() {
- const st = await earnRefresh();
- if (!st || st.views >= st.target) return;
const type = earnState.types[earnState.i++ % earnState.types.length];
- let ad = null;
- try { ad = (await (await fetch('/api/ads/slot?type=' + type)).json()).ad; } catch (e) {}
+ let r = null;
+ try { r = await (await fetch('/api/my/earnview?type=' + type)).json(); } catch (e) {}
const box = $('earnAdBox');
- if (!ad) {
- box.innerHTML = 'No member ads are live in rotation right now. '
- + 'Views resume the moment a campaign is running.';
+ if (!r || !r.ad) {
+ await earnRefresh();
+ box.innerHTML = '' + (r && r.status && r.status.views >= r.status.target
+ ? 'Set complete for today.'
+ : 'No member ads are live in rotation right now. Views resume the moment a campaign is running.') + '';
return;
}
+ const ad = r.ad;
box.innerHTML = ad.imageUrl
? ''
: '' + ad.title + '' + (ad.body ? '
' + ad.body + '' : '') + '';
const btn = $('earnNextBtn');
btn.hidden = false;
btn.disabled = true;
- let left = st.dwell;
+ // the countdown only runs while this tab is visible AND focused; the
+ // server separately enforces the full dwell on its own clock
+ let left = r.status.dwell;
btn.textContent = 'Next ad (' + left + 's)';
clearInterval(earnState.timer);
earnState.timer = setInterval(async () => {
+ if (document.visibilityState !== 'visible' || !document.hasFocus()) {
+ btn.textContent = 'Paused: stay on this tab (' + left + 's)';
+ return;
+ }
left -= 1;
if (left > 0) { btn.textContent = 'Next ad (' + left + 's)'; return; }
clearInterval(earnState.timer);
btn.textContent = 'Next ad';
+ const v = await (await fetch('/api/my/adview', { method: 'POST',
+ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: r.token }) })).json();
+ if (v.error) { IAP.status(v.error, 'bad'); btn.disabled = false; return; }
btn.disabled = false;
- await fetch('/api/my/adview', { method: 'POST' }); // dwell served: count it
await earnRefresh();
}, 1000);
}
@@ -406,5 +415,11 @@
await render();
});
+ // ad surfaces: login ads greet the sign-in screen; members see live
+ // banner + text placements inside the back office (they ARE the audience)
+ IAP.adSlot('login', 'adSlotLogin');
+ IAP.adSlot('banner', 'adSlotOverview');
+ IAP.adSlot('text', 'adSlotSide');
+
render();
})();
diff --git a/public/contract.html b/public/contract.html
index e4da309..ff452d9 100644
--- a/public/contract.html
+++ b/public/contract.html
@@ -5,7 +5,7 @@