Files
instantadpay/public/assets/common.js
T

148 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Shared page runtime: site config, nav, formatting. Zero dependencies.
window.IAP = (function () {
let config = null;
const $ = id => document.getElementById(id);
async function getConfig() {
if (!config) config = await (await fetch('/api/config')).json();
return config;
}
// POL amounts display with two decimals (rounded half-up), e.g. 523.39
function fmtPol(wei) {
const cents = (BigInt(wei) + 5000000000000000n) / 10000000000000000n; // wei -> hundredths of a POL
const s = cents.toString().padStart(3, '0');
return s.slice(0, -2) + '.' + s.slice(-2);
}
const fmtUsd = cents => '$' + (cents / 100).toFixed(2);
function status(msg, cls) {
let el = $('status');
if (!el) { el = document.createElement('div'); el.id = 'status'; document.body.appendChild(el); }
el.textContent = msg; el.className = cls || ''; el.hidden = false;
clearTimeout(status._t);
if (cls === 'ok') status._t = setTimeout(() => { el.hidden = true; }, 6000);
}
async function renderNav(active) {
const c = await getConfig();
const nav = document.createElement('nav');
nav.innerHTML = '<div class="wrap">'
+ '<span class="logo-wrap"><a class="logo" href="/"><img src="/logo.png" alt="InstantAdPay" style="height:30px;display:block"></a><span class="byline">Brought to you by the <b>Crypto Team Build Network</b></span></span>'
+ '<span class="links">'
+ '<a href="/" data-p="home">How it works</a>'
+ '<a href="/#ladder" data-p="pricing">Ad packages</a>'
+ '<a href="/ledger" data-p="ledger">Live ledger</a>'
+ '<a href="/contract" data-p="contract">The contract</a>'
+ '<a href="/my" data-p="my">Members</a>'
+ '</span><span id="navWallet" class="muted">…</span></div>';
document.body.prepend(nav);
if (c.rehearsal) {
const b = document.createElement('div');
b.className = 'rehearsal';
b.innerHTML = '<b>Testnet rehearsal</b>: running on ' + c.chainName + '. Purchases use valueless test POL while we prove every payout in public.';
document.body.prepend(b);
}
const a = nav.querySelector('[data-p="' + active + '"]');
if (a) a.className = 'active';
refreshNavWallet();
renderFooter();
}
function renderFooter() {
if (document.getElementById('iapFooter')) return;
const f = document.createElement('footer'); f.id = 'iapFooter';
f.style.cssText = 'border-top:1px solid var(--line);margin-top:48px;padding:26px 22px;text-align:center;color:var(--muted);font-size:13px';
f.innerHTML = '<div>© ' + new Date().getFullYear() + ' InstantAdPay</div>'
+ '<div style="margin-top:8px;display:flex;gap:16px;justify-content:center;flex-wrap:wrap">'
+ '<a href="/">How it works</a><a href="/ledger">Live ledger</a><a href="/contract">The contract</a>'
+ '<a href="/terms">Terms</a><a href="/privacy">Privacy</a><a href="/disclaimer">Disclaimer</a></div>';
document.body.appendChild(f);
}
async function refreshNavWallet() {
try {
const me = await (await fetch('/api/me')).json();
const el = $('navWallet');
if (!el) return;
if (me.signedIn) {
// identity order: username, then email, then wallet
const who = me.username
? '<b>@' + String(me.username).replace(/[&<>]/g, '') + '</b>'
: (me.email ? String(me.email).replace(/[&<>]/g, '')
: (me.address ? '<span class="mono">' + me.address.slice(0, 6) + '…' + me.address.slice(-4) + '</span>' : 'signed in'));
el.innerHTML = (me.memberId ? '<span class="badge">member #' + me.memberId + '</span> ' : '') + who;
} else {
el.innerHTML = '<a href="/my">Sign in</a>';
}
return me;
} catch (e) { return null; }
}
function describeEvent(ev, c) {
const pol = w => fmtPol(w) + ' POL';
// real people, not numbers: use usernames when the site knows them
const nm = id => (ev.names && ev.names[id])
? String(ev.names[id]).replace(/[&<>]/g, '')
: 'member #' + id;
switch (ev.type) {
case 'Purchase': return '🧾 ' + nm(ev.buyerId) + ' bought package #' + ev.productId
+ ' (' + fmtUsd(ev.priceCents) + ') for ' + pol(ev.paidWei) + ' → +' + ev.creditAmount.toLocaleString() + ' credits';
case 'TierPaid': return '💸 level ' + ev.tier + ' payout → ' + nm(ev.recipientId) + ': ' + pol(ev.amountWei)
+ (ev.hops ? ' (passed up ' + ev.hops + ')' : '');
case 'PassedUp': return '↷ level ' + ev.tier + ' passed over ' + nm(ev.skippedId) + ' (' + ev.reason + ')';
case 'AdminPaid': return '🏛 platform fee settled: ' + pol(ev.amountWei);
case 'BuyerCounted': return '⭐ ' + nm(ev.sponsorId) + ' now has ' + ev.newCount + ' qualifying buyer(s)';
case 'MemberActivated': return '👤 ' + nm(ev.id) + ' activated a payout wallet';
case 'AwardPaid': return '🎁 award: ' + pol(ev.amountWei) + ' → ' + nm(ev.toId);
case 'CreditsConsumed': return '📣 ' + nm(ev.memberId) + ' ran ads: −' + ev.amount.toLocaleString() + ' credits';
case 'PriceCached': return '🔮 oracle price refreshed';
case 'FallbackPriceUsed': return '🔮 cached price bridged an oracle gap';
default: return '· ' + ev.type;
}
}
function feedRow(ev, c) {
const div = document.createElement('div');
div.className = 'row t-' + ev.type;
const when = ev.ts ? new Date(ev.ts).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : '';
div.innerHTML = (when ? '<span class="when" title="' + new Date(ev.ts).toLocaleString() + '">' + when + '</span>' : '') + '<span>' + describeEvent(ev, c) + '</span>'
+ (c.explorer
? '<span class="tx"><a target="_blank" rel="noopener" href="' + c.explorer + '/tx/' + ev.tx + '">verify ↗</a></span>'
: '<span class="tx"><a href="/tx/' + ev.tx + '">verify ↗</a></span>'); // built-in viewer when the chain has no public explorer
return div;
}
// Render one served ad into #<elId>. Silent if no inventory.
// report an ad (auto-approved ads need a member-facing flag → admin notified)
function reportAd(campaignId) {
if (!campaignId) return;
const reason = (prompt('Report this ad. Reason: broken, inappropriate, spam, scam, or other', 'broken') || '').trim().toLowerCase();
if (!reason) return;
const note = prompt('Anything to add? (optional)') || '';
fetch('/api/report-ad', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ campaignId, reason, note }) })
.then(() => status('Thanks — this ad was reported to the admin for review.', 'ok'))
.catch(() => status('Could not send the report. Try again.', 'bad'));
}
const reportTag = ad => ' <a class="ad-report small muted" href="#" data-cid="' + ad.id + '" style="margin-left:8px">⚠ report</a>';
function wireReport(el) {
const rl = el.querySelector('.ad-report');
if (rl) rl.addEventListener('click', e => { e.preventDefault(); reportAd(Number(rl.dataset.cid)); });
}
async function adSlot(type, elId, opts) {
try {
const q = '/api/ads/slot?type=' + type + (opts && opts.width ? '&w=' + opts.width + '&h=' + opts.height : '');
const { ad } = await (await fetch(q)).json();
const el = $(elId);
if (!ad || !el) return;
el.hidden = false;
if (ad.imageUrl) {
el.style.textAlign = 'center';
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow">'
+ '<img src="' + ad.imageUrl + '" alt="advertisement" style="max-width:min(100%,728px);height:auto;display:block;margin:0 auto;border-radius:8px"></a>'
+ '<div class="small muted">member ad' + reportTag(ad) + '</div>';
} else {
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow"><b>' + ad.title + '</b>'
+ (ad.body ? ' · ' + ad.body : '') + '</a> <span class="small muted">member ad' + reportTag(ad) + '</span>';
}
wireReport(el);
el.hidden = false;
} catch (e) {}
}
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, $ };
})();