3f9dc3cc94
The dashboard card was too easy to scroll past, so this is a real overlay: once per browser session, on every page that loads common.js, dismissed with the X, Escape or the backdrop. It leads with the mechanic rather than the promotion, because the ask was to remind people what the site is about: the ad spend in your line pays you, in the same transaction. Five Dollar Friday rides underneath as the reason to act, and the copy is driven by the live /api/friday, so it reads "tomorrow", "today" or nothing at all without anyone editing text on a Friday morning. Not shown on /join or /admin: an overlay in the middle of signing up costs sign-ups, and staff do not need reminding what the product is. QA: the overlay appears after an async fetch, so hiding it at load time missed it and every click in the harness timed out. The harness now pre-sets the same sessionStorage flag a real dismissal uses, before page scripts run. Green again, 0 bugs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
6.7 KiB
JavaScript
116 lines
6.7 KiB
JavaScript
/* InstantAdPay session pop-up (Marty, 2026-09-24).
|
|
|
|
One card, once per browser session, on every page that loads common.js. It exists because the
|
|
dashboard card was too easy to scroll past: this one is a proper overlay you have to dismiss.
|
|
|
|
It leads with WHAT THE SITE IS, not the promotion, because the ask was "remind them what it is
|
|
about". Five Dollar Friday rides along underneath as the reason to act this week, and the card
|
|
reads the live /api/friday so it says "tomorrow", "today" or nothing at all without anyone
|
|
editing copy.
|
|
|
|
Rules it follows:
|
|
- once per SESSION, not per day: sessionStorage, so closing the tab resets it
|
|
- a new `key` (change of promo, new campaign) shows it again even in the same session
|
|
- never on the sign-up or sign-in flow: an overlay in the middle of joining costs sign-ups
|
|
- Escape and the backdrop both close it, and focus is trapped while it is open
|
|
- if sessionStorage is unavailable (private mode) it simply shows once and does not throw
|
|
*/
|
|
(function () {
|
|
var KEY = 'iap.welcome.v1';
|
|
// Pages where an overlay would get in the way of something more important.
|
|
// /join is the sign-up flow and /admin is staff only; an overlay on either is pure friction.
|
|
var SKIP = /^\/(join|signup|register|login|auth|admin)(\/|$)/i;
|
|
|
|
function seen() { try { return sessionStorage.getItem(KEY) === '1'; } catch (e) { return false; } }
|
|
function mark() { try { sessionStorage.setItem(KEY, '1'); } catch (e) {} }
|
|
|
|
function fridayLine(f) {
|
|
if (!f || !f.on) return null;
|
|
if (f.live) return { tag: 'TODAY', text: 'It is Five Dollar Friday right now. Every package of $5 or more gets <b>20% bonus credits</b>, automatically, until midnight Central.' };
|
|
if (f.nextLabel) return { tag: f.nextLabel, text: 'Five Dollar Friday: any package of $5 or more gets <b>' + (f.pct || 20) + '% bonus credits</b>, automatically. No code, nothing to claim.' };
|
|
return null;
|
|
}
|
|
|
|
function show(f) {
|
|
if (document.querySelector('.iap-welcome')) return;
|
|
var fri = fridayLine(f);
|
|
|
|
var back = document.createElement('div');
|
|
back.className = 'iap-welcome';
|
|
back.setAttribute('role', 'dialog');
|
|
back.setAttribute('aria-modal', 'true');
|
|
back.setAttribute('aria-label', 'About InstantAdPay');
|
|
back.style.cssText = 'position:fixed;inset:0;z-index:400;background:rgba(2,6,5,.72);' +
|
|
'display:flex;align-items:center;justify-content:center;padding:18px;overflow:auto;' +
|
|
'-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);';
|
|
|
|
var card = document.createElement('div');
|
|
card.style.cssText = 'position:relative;width:100%;max-width:470px;background:#0b1512;color:#eef7f3;' +
|
|
'border:1px solid rgba(67,232,195,.28);border-radius:18px;padding:26px 24px 22px;' +
|
|
'box-shadow:0 26px 70px rgba(0,0,0,.6);font-size:15px;line-height:1.55;';
|
|
|
|
var html =
|
|
'<button type="button" class="iap-w-x" aria-label="Close" style="position:absolute;top:10px;right:12px;' +
|
|
'background:none;border:0;color:#8ba69c;font-size:26px;line-height:1;cursor:pointer;padding:4px 8px;">×</button>' +
|
|
'<div style="letter-spacing:2px;text-transform:uppercase;font-size:11px;font-weight:700;color:#43e8c3;">InstantAdPay</div>' +
|
|
'<h2 style="margin:8px 0 12px;font-size:25px;line-height:1.25;color:#fff;">The ad spend in your line pays you, in the same transaction.</h2>' +
|
|
'<p style="margin:0 0 14px;color:#c8d6e2;">You buy advertising you were going to buy anyway. When someone in your line buys a package, the contract splits it and your share lands in your wallet before the page reloads. No back office, no payday to wait for.</p>' +
|
|
'<ul style="margin:0 0 16px;padding-left:18px;color:#c8d6e2;">' +
|
|
'<li style="margin-bottom:5px;">Every payout is on the public ledger, and you can read it yourself.</li>' +
|
|
'<li style="margin-bottom:5px;">Free members earn ad credits by viewing ads, videos and visits.</li>' +
|
|
'<li>Credits buy your own advertising. They are delivery, not a balance to hoard.</li>' +
|
|
'</ul>';
|
|
|
|
if (fri) {
|
|
html += '<div style="margin:0 0 16px;padding:12px 14px;border:1px solid rgba(255,178,56,.38);' +
|
|
'background:rgba(255,178,56,.08);border-radius:12px;color:#eef7f3;">' +
|
|
'<div style="font-weight:800;color:#ffb238;font-size:12px;letter-spacing:1.4px;text-transform:uppercase;margin-bottom:4px;">' + fri.tag + '</div>' +
|
|
'<div>' + fri.text + '</div></div>';
|
|
}
|
|
|
|
html += '<div style="display:flex;gap:9px;flex-wrap:wrap;">' +
|
|
'<a class="iap-w-go" href="/my#buy" style="flex:1 1 auto;text-align:center;text-decoration:none;background:linear-gradient(180deg,#5ff0cf,#43e8c3);color:#04120e;font-weight:800;padding:12px 16px;border-radius:11px;">See the packages</a>' +
|
|
'<a class="iap-w-go" href="/ledger" style="flex:0 1 auto;text-align:center;text-decoration:none;border:1px solid rgba(67,232,195,.4);color:#43e8c3;font-weight:700;padding:12px 16px;border-radius:11px;">Live ledger</a>' +
|
|
'</div>';
|
|
|
|
card.innerHTML = html;
|
|
back.appendChild(card);
|
|
document.body.appendChild(back);
|
|
mark();
|
|
|
|
var prev = document.activeElement;
|
|
var closeBtn = card.querySelector('.iap-w-x');
|
|
function close() {
|
|
back.remove();
|
|
document.removeEventListener('keydown', onKey);
|
|
try { if (prev && prev.focus) prev.focus(); } catch (e) {}
|
|
}
|
|
function onKey(e) {
|
|
if (e.key === 'Escape') { close(); return; }
|
|
if (e.key !== 'Tab') return;
|
|
var f = card.querySelectorAll('button,a[href]');
|
|
if (!f.length) return;
|
|
var first = f[0], last = f[f.length - 1];
|
|
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
}
|
|
closeBtn.addEventListener('click', close);
|
|
back.addEventListener('click', function (e) { if (e.target === back) close(); });
|
|
// following a link is a deliberate exit, so let it through but tidy up
|
|
card.querySelectorAll('.iap-w-go').forEach(function (a) { a.addEventListener('click', close); });
|
|
document.addEventListener('keydown', onKey);
|
|
try { closeBtn.focus(); } catch (e) {}
|
|
}
|
|
|
|
function boot() {
|
|
if (seen() || SKIP.test(location.pathname)) return;
|
|
// the Friday state decides one paragraph; a failure there must not stop the card
|
|
fetch('/api/friday').then(function (r) { return r.ok ? r.json() : null; })
|
|
.catch(function () { return null; })
|
|
.then(function (f) { show(f); });
|
|
}
|
|
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
|
|
else boot();
|
|
})();
|