// 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; } function fmtPol(wei) { const s = BigInt(wei).toString().padStart(19, '0'); const whole = s.slice(0, -18) || '0'; const frac = s.slice(-18, -12).replace(/0+$/, ''); return whole + (frac ? '.' + frac : ''); } 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 = '
' + '' + '' + 'How it works' + 'Ad packages' + 'Live ledger' + 'The contract' + 'Members' + 'โ€ฆ
'; document.body.prepend(nav); if (c.rehearsal) { const b = document.createElement('div'); b.className = 'rehearsal'; b.innerHTML = 'Testnet rehearsal: 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(); } 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 ? '@' + String(me.username).replace(/[&<>]/g, '') + '' : (me.email ? String(me.email).replace(/[&<>]/g, '') : (me.address ? '' + me.address.slice(0, 6) + 'โ€ฆ' + me.address.slice(-4) + '' : 'signed in')); el.innerHTML = (me.memberId ? 'member #' + me.memberId + ' ' : '') + who; } else { el.innerHTML = 'Sign in'; } 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; div.innerHTML = '' + describeEvent(ev, c) + '' + (c.explorer ? 'verify โ†—' : 'verify โ†—'); // built-in viewer when the chain has no public explorer return div; } // Render one served ad into #. 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 => ' โš  report'; 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) { try { const { ad } = await (await fetch('/api/ads/slot?type=' + type)).json(); const el = $(elId); if (!ad || !el) return; if (ad.imageUrl) { el.style.textAlign = 'center'; el.innerHTML = '' + 'advertisement' + '
member ad' + reportTag(ad) + '
'; } else { el.innerHTML = '' + ad.title + '' + (ad.body ? ' ยท ' + ad.body : '') + ' member ad' + reportTag(ad) + ''; } wireReport(el); el.hidden = false; } catch (e) {} } return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, $ }; })();