// My account: email-first join/login, wallet link at purchase time, // free payout activation, invite link, on-chain activity. (async function () { // back-office shell: no marketing nav here; rehearsal notice lives in the top bar const $ = IAP.$; try { const c = await IAP.getConfig(); if (c.rehearsal && $('boRehearsal')) { $('boRehearsal').hidden = false; $('boRehearsal').innerHTML = 'Testnet rehearsal · ' + c.chainName; } } catch (e) {} async function api(path, body) { const r = await (await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) })).json(); if (r.error) throw new Error(r.error); return r; } function nextMove(d) { if (!d.address) return 'Grab your link below and start sharing today. Then link your wallet (one free signature) so every payment can lock to you before your people start buying.'; if (!d.memberId) return 'Your wallet is linked. Switch on payouts above (one free transaction) and every purchase in your line pays you the moment it happens.'; if (d.buyerCount === 0) return 'Payouts are on and your link is live. Your next milestone: your first buyer of $20 or more. Every direct pays you 50 percent from their very first package.'; if (d.buyerCount === 1) return 'One qualifying buyer down, one to go. Your next $20+ buyer unlocks level 2: 20 percent of everything your people’s people buy.'; if (d.buyerCount < 5) return 'Level 2 is open. ' + (5 - d.buyerCount) + ' more qualifying buyer(s) unlock level 3 and the full three-level flow.'; return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.'; } // milestone stepper under "Your next move": lit nodes for what's done, // amber glow on the current target — the same ladder the contract pays function renderSteps(d) { const el = $('ncSteps'); if (!el) return; const b = d.buyerCount || 0; const steps = [ { label: 'Joined', sub: 'free account', hit: true }, { label: 'Payouts on', sub: 'wallet linked', hit: !!d.memberId }, { label: 'First buyer', sub: '50% of every pack', hit: b >= 1 }, { label: 'Level 2', sub: '2 buyers · +20%', hit: b >= 2 }, { label: 'Level 3', sub: '5 buyers · +10%', hit: b >= 5 } ]; const cur = steps.findIndex(s => !s.hit); el.innerHTML = steps.map((s, i) => '
' + '' + (s.hit ? '✓' : i + 1) + '' + '' + s.label + '' + s.sub + '
').join(''); } // ── overview v3 charts: hand-rolled SVG/CSS, real data only ── const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const CH = { mint: '#43e8c3', cyan: '#54ccff', violet: '#9d7dff', amber: '#ffb238', track: 'rgba(139,166,156,.18)' }; function bars(el, xel, data) { // data: [{v,label,tip,alt}] if (!el) return; const max = Math.max(1, ...data.map(d => d.v)); el.innerHTML = data.length ? data.map(d => '
').join('') : '
'.repeat(8); if (xel) xel.innerHTML = data.map(d => '' + esc(d.label) + '').join(''); } function donut(el, segs, center) { // segs sum to the ring; r=15.9155 → circumference 100 if (!el) return; const total = segs.reduce((s, x) => s + x.v, 0); let off = 25, out = ''; if (total > 0) for (const s of segs) { const len = s.v / total * 100; if (len <= 0 || s.color === 'none') { off -= Math.max(0, len); continue; } out += ''; off -= len; } out += '' + esc(center.big) + '' + '' + esc(center.small) + ''; el.innerHTML = out; } function legend(el, rows) { if (el) el.innerHTML = rows.map(r => '
' + esc(r.label) + ' ' + esc(r.v) + '
').join(''); } async function loadCharts(d) { // credit composition donut + today's viewing ring (live balances) try { const st = await (await fetch('/api/my/earn')).json(); if (!st.error) { const purchased = d.credits || 0, earned = st.earned || 0, wc = Math.min(d.welcomeCredits || 0, earned); $('dbCredits').textContent = (purchased + earned).toLocaleString(); $('dbCreditsSub').textContent = purchased.toLocaleString() + ' purchased · ' + earned.toLocaleString() + ' earned'; donut($('chDonut'), [{ v: purchased, color: CH.mint }, { v: wc, color: CH.amber }, { v: earned - wc, color: CH.cyan }], { big: (purchased + earned).toLocaleString(), small: 'spendable' }); legend($('chDonutLegend'), [ { color: CH.mint, label: 'Purchased', v: purchased.toLocaleString() }, { color: CH.amber, label: 'Welcome', v: wc.toLocaleString() }, { color: CH.cyan, label: 'Earned by viewing', v: Math.max(0, earned - wc).toLocaleString() }]); const done = Math.min(st.views || 0, st.target || 5), left = Math.max(0, (st.target || 5) - done); donut($('chRing'), [{ v: done, color: st.claimed ? CH.mint : CH.cyan }, { v: left, color: 'none' }], { big: done + '/' + (st.target || 5), small: st.claimed ? 'claimed' : 'ads viewed' }); legend($('chRingLegend'), [ { color: st.claimed ? CH.mint : CH.cyan, label: 'Viewed today', v: done }, { color: CH.track, label: 'To go', v: left }, { color: CH.amber, label: 'Claim pays', v: '+' + (st.claimCredits || 0) + ' credits' }]); } } catch (e) {} // recent on-chain payouts, sized to scale, oldest→newest try { let evs = []; if (d.memberId) { const a = await (await fetch('/api/my/activity')).json(); evs = (a.earnings || []).filter(e => e.amountWei).sort((x, y) => x.block - y.block).slice(-12); } bars($('chEarnBars'), $('chEarnX'), evs.map(e => ({ v: Number(BigInt(e.amountWei) / 1000000000000n) / 1e6, label: e.type === 'AwardPaid' ? 'award' : 'L' + (e.tier || 1), tip: IAP.fmtPol(e.amountWei) + ' POL', alt: e.type === 'AwardPaid' }))); if (!evs.length) $('chEarnSub').textContent = d.memberId ? 'no payouts yet — share your link' : 'activate a package to start earning'; } catch (e) {} // campaign delivery: impressions per campaign try { const r = await (await fetch('/api/my/campaigns')).json(); const cs = (r.campaigns || []).slice(0, 8); bars($('chCampBars'), $('chCampX'), cs.map(c => ({ v: c.imps || 0, label: String(c.name || c.type).slice(0, 9), tip: (c.name || c.type) + ': ' + (c.imps || 0) + ' imps · ' + (c.clicks || 0) + ' clicks', alt: c.type === 'text' }))); if (!cs.length) $('chCampSub').textContent = 'no campaigns yet — place your first ad'; } catch (e) {} } async function loadDashboard() { try { const d = await (await fetch('/api/my/dashboard')).json(); if (d.error) return; const wc = d.welcomeCredits || 0; $('dbCredits').textContent = ((d.credits || 0) + wc).toLocaleString(); $('dbCreditsSub').textContent = wc ? (d.credits || 0).toLocaleString() + ' purchased · ' + wc + ' welcome' : ''; $('dbEarned').textContent = IAP.fmtPol(d.earnedWei || '0'); $('dbBuyers').textContent = d.buyerCount || 0; $('dbTeam').textContent = (d.referrals || []).length; // trend chips: only real, computable facts const ec = $('dbEarnedChip'); if (ec && d.earnCount) { ec.hidden = false; ec.textContent = d.earnCount + ' instant payout' + (d.earnCount > 1 ? 's' : ''); } const bc = $('dbBuyersChip'); if (bc && d.memberId) { bc.hidden = false; bc.textContent = (d.buyerCount || 0) >= 5 ? 'level 3 open' : (d.buyerCount || 0) >= 2 ? 'level 2 open' : 'level 2 at 2 buyers'; } const tc = $('dbTeamChip'), wk = (d.referrals || []).filter(r => Date.now() - new Date(r.joined) < 6048e5).length; if (tc && wk) { tc.hidden = false; tc.textContent = '+' + wk + ' this week'; } setInboxBadge(d.inboxUnread || 0); loadCharts(d); $('nextMove').textContent = nextMove(d); renderSteps(d); const wrap = $('rosterWrap'); if ((d.referrals || []).length) { $('rosterEmpty').hidden = true; let t = wrap.querySelector('table'); if (t) t.remove(); t = document.createElement('table'); t.className = 'roster'; t.innerHTML = d.referrals.map(r => '' + String(r.name || r.email || '').replace(/[&<>]/g, '') + '' + '' + new Date(r.joined).toLocaleDateString() + '' + '' + r.status + '').join(''); wrap.appendChild(t); } // overview recent-activity widget: chain events + line joins, newest first try { const c = await IAP.getConfig(); const ov = $('ovFeed'); const rows = []; if (d.memberId) { const a = await (await fetch('/api/my/activity')).json(); for (const ev of [...(a.earnings || []), ...(a.purchases || [])] .sort((x, y) => y.block - x.block).slice(0, 5)) rows.push(IAP.feedRow(ev, c)); } for (const r of (d.referrals || []).slice(0, 3)) { const div = document.createElement('div'); div.className = 'row'; div.innerHTML = '🤝 ' + r.email + ' joined your line' + new Date(r.joined).toLocaleDateString() + ''; rows.push(div); } if (rows.length) { ov.innerHTML = ''; rows.slice(0, 6).forEach(r => ov.appendChild(r)); } } catch (e) {} // ready-to-send share message + promo tools, personalized const link = location.origin + '/join/' + (d.username || d.refCode || d.memberId || ''); fillPromo(link); if (d.username) { // wall link rides the username const wl = location.origin + '/wall/' + d.username; $('wallLine').textContent = wl; $('wallCopy').hidden = false; $('wallOpen').hidden = false; $('wallOpen').href = '/wall/' + d.username; $('wallCopy').onclick = async () => { try { await navigator.clipboard.writeText(wl); IAP.status('Wall link copied.', 'ok'); } catch (e) { IAP.status('Copy failed. Select the link text instead.', 'bad'); } }; } if (d.refCode || d.memberId) { const pitch = 'I found an advertising site that pays referrals instantly to your own wallet. ' + 'No withdrawals, no waiting, and every payment is public on a blockchain ledger you can check yourself. ' + 'Free to join and look around: ' + link; $('copyPitch').hidden = false; $('pitchPreview').hidden = false; $('pitchPreview').textContent = '"' + pitch + '"'; $('copyPitch').onclick = async () => { try { await navigator.clipboard.writeText(pitch); IAP.status('Message copied. Paste it anywhere.', 'ok'); } catch (e) { IAP.status('Copy failed. Select the preview text instead.', 'bad'); } }; } } catch (e) {} } // ── back-office menu: hash-routed panes ─────────────── const PANES = ['overview', 'line', 'buy', 'campaigns', 'earn', 'earnings', 'promo', 'wallet', 'profile']; const TITLES = { overview: 'Overview', line: 'My line', buy: 'Buy packages', campaigns: 'Campaigns', earn: 'Earn credits', earnings: 'Earnings', promo: 'Promo tools', wallet: 'Wallet & account', profile: 'Profile' }; function setPane(name) { if (!PANES.includes(name)) name = 'overview'; for (const p of PANES) { const el = $('pane-' + p); if (el) el.hidden = p !== name; } document.querySelectorAll('.bo-menu [data-pane]').forEach(b => b.classList.toggle('on', b.dataset.pane === name)); if ($('boTitle')) $('boTitle').textContent = TITLES[name]; if (name === 'earn') setEarnSub(earnSub); // refresh whichever sub-tab is active if (name === 'profile') loadLineBanner(); document.getElementById('memberArea').classList.remove('side-open'); // close mobile drawer if (location.hash !== '#' + name) history.replaceState(null, '', '#' + name); } document.querySelectorAll('.bo-menu [data-pane]').forEach(b => b.addEventListener('click', () => setPane(b.dataset.pane))); document.querySelectorAll('.qa [data-goto]').forEach(b => b.addEventListener('click', () => setPane(b.dataset.goto))); const qaCopy = document.getElementById('qaCopyInvite'); if (qaCopy) qaCopy.addEventListener('click', async () => { const link = document.getElementById('inviteLine').textContent; if (!link || !link.startsWith('http')) { setPane('line'); return; } try { await navigator.clipboard.writeText(link); IAP.status('Invite link copied.', 'ok'); } catch (e) { setPane('line'); } }); window.addEventListener('hashchange', () => setPane(location.hash.slice(1))); if ($('boBurger')) $('boBurger').addEventListener('click', () => document.getElementById('memberArea').classList.toggle('side-open')); async function render() { const me = await IAP.refreshNavWallet(); const signedIn = me && me.signedIn; $('authArea').hidden = !!signedIn; $('memberArea').hidden = !signedIn; if (!signedIn) return; setPane(location.hash.slice(1) || 'overview'); $('campGate').hidden = !!me.memberId; $('earnGate').hidden = !!me.memberId; loadDashboard(); const who = []; if (me.email) who.push(me.email); if (me.address) who.push('wallet ' + me.address.slice(0, 8) + '…' + me.address.slice(-6) + ''); else who.push('no wallet linked yet'); if (me.memberId) who.push('on-chain member #' + me.memberId + '' + (me.onchainSponsorId ? ', sponsored by #' + me.onchainSponsorId : '')); else if (me.sponsorId) who.push('invited by member #' + me.sponsorId); $('posLine').innerHTML = who.join('
'); $('creditLine').textContent = (me.credits || 0).toLocaleString(); $('linkCard').hidden = !!me.address; $('activateCard').hidden = !(me.address && !me.memberId); $('activityArea').hidden = !me.memberId; $('campGate').hidden = true; // earned credits fund campaigns for everyone $('campaignCard').hidden = false; loadCampaigns(); // profile pane state $('pfCurrent').textContent = me.username ? 'Current username: @' + me.username : 'No username yet. Members see you as a number until you pick one.'; if (!$('pfUsername').value) $('pfUsername').value = me.username || ''; $('pfDetails').innerHTML = 'Email: ' + (me.email || 'none') + '
Wallet: ' + (me.address ? '' + me.address.slice(0, 10) + '…' + me.address.slice(-6) + '' : 'not linked yet') + '
On-chain member: ' + (me.memberId ? '#' + me.memberId : 'not yet'); // the share link works from day one; usernames make it a vanity link if (me.username || me.refCode || me.memberId) { $('inviteLine').textContent = location.origin + '/join/' + (me.username || me.refCode || me.memberId); $('copyInvite').hidden = false; } else { $('inviteLine').textContent = 'Sign in with your email to get your link.'; $('copyInvite').hidden = true; } if (me.memberId) { const bc = me.buyerCount || 0; $('qualLine').innerHTML = '' + bc + ' qualifying buyer(s) referred
' + (bc >= 5 ? 'Level 3 unlocked: full three-level earnings' : bc >= 2 ? 'Level 2 unlocked · ' + (5 - bc) + ' more for level 3' : (2 - bc) + ' more buyer(s) of $20+ unlock level 2'); loadActivity(); } else { $('qualLine').innerHTML = 'Share your link now. Then switch on payouts (free, above) ' + 'before your people start buying: the contract locks each buyer to their sponsor at ' + 'their first purchase, and payments only route to wallets that are switched on.'; } } async function loadActivity() { try { const c = await IAP.getConfig(); const a = await (await fetch('/api/my/activity')).json(); const fill = (id, evs, empty) => { const el = $(id); el.innerHTML = ''; if (!evs || !evs.length) { el.innerHTML = '
' + empty + '
'; return; } for (const ev of evs) el.appendChild(IAP.feedRow(ev, c)); }; fill('earnFeed', a.earnings, 'No payouts yet. They appear here the moment one lands.'); fill('refFeed', a.referrals, 'No referral activity yet. Share your invite link.'); fill('buyFeed', a.purchases, 'No purchases from your wallet yet.'); } catch (e) {} } async function loadCampaigns() { try { const r = await (await fetch('/api/my/campaigns')).json(); if (r.error) return; lastRates = r.rates; // populate the banner-size dropdown once (ids map to NAS width/height) if (r.bannerSizes && $('cSize') && !$('cSize').options.length) $('cSize').innerHTML = r.bannerSizes.map(s => '').join(''); soloHint(); $('rateLine').textContent = 'Available to spend: ' + r.availableCredits.toLocaleString() + (r.earnedCredits ? ' (' + r.purchasedCredits.toLocaleString() + ' purchased + ' + r.earnedCredits + ' earned)' : '') + ' credits · rates: banner ' + r.rates.bannerCreditsPerBatch + 'cr/' + r.rates.bannerBatch + ' views, text ' + r.rates.textCreditsPerBatch + 'cr/' + r.rates.textBatch + ' views, login ' + r.rates.loginCreditsPerDay + 'cr/day, solo ' + (r.rates.soloCostPerRecipient || 5) + 'cr/delivery'; const el = $('campList'); el.innerHTML = ''; if (!r.campaigns.length) { el.innerHTML = '

No campaigns yet. Launch your first below.

'; return; } const tbl = document.createElement('div'); tbl.className = 'tablewrap'; tbl.innerHTML = '' + '' + r.campaigns.map(c => '' + '' + '' + '' + '' + '').join('') + '
NameTypeViewsClicksSpentBudgetStatus
' + c.name + '' + c.type + (c.type === 'banner' && c.width ? ' ' + c.width + '×' + c.height + '' : '') + '' + c.imps.toLocaleString() + (c.impsNas ? ' +' + c.impsNas.toLocaleString() + ' net' : '') + '' + c.clicks + '' + c.spent + '' + c.budget + '' + (c.status === 'out' ? 'budget spent' : c.status) + '' + (c.status === 'active' ? '' : c.status === 'paused' ? '' : '') + '
'; el.appendChild(tbl); el.querySelectorAll('button[data-camp]').forEach(b => b.addEventListener('click', async () => { try { await api('/api/my/campaigns/' + b.dataset.camp + '/' + b.dataset.act); await loadCampaigns(); } catch (e) { IAP.status(e.message, 'bad'); } })); } catch (e) {} } let lastRates = null; function soloHint() { if (!lastRates || $('cType').value !== 'solo') return; const cost = lastRates.soloCostPerRecipient || 5; const n = Math.floor((Number($('cBudget').value) || 0) / cost); $('cSoloHint').textContent = cost + ' credits per guaranteed inbox delivery' + (n ? ' — this budget reaches ' + n + ' members' : '') + '. Readers earn ' + (lastRates.soloReadCredits || 2) + ' credits for a real read, so your message gets opened.'; } $('cBudget').addEventListener('input', soloHint); // rich solo editor: small toolbar over contenteditable (CSP allows no external editor); // the server whitelist-sanitizes whatever HTML arrives, this is just authoring comfort document.querySelectorAll('.ed-bar [data-cmd]').forEach(btn => btn.addEventListener('click', () => { $('cSoloEd').focus(); document.execCommand(btn.dataset.cmd, false, null); })); document.querySelectorAll('.ed-bar [data-block]').forEach(btn => btn.addEventListener('click', () => { $('cSoloEd').focus(); document.execCommand('formatBlock', false, btn.dataset.block); })); $('edLinkBtn').addEventListener('click', () => { const url = prompt('Link URL (https://…)'); if (!url) return; $('cSoloEd').focus(); document.execCommand('createLink', false, url); }); // inline media: upload, then drop the element at the cursor (BV-style) let mediaMode = 'image'; function insertHtmlAtCursor(html) { const ed = $('cSoloEd'); ed.focus(); if (!document.execCommand('insertHTML', false, html)) ed.insertAdjacentHTML('beforeend', html); } $('edImgBtn').addEventListener('click', () => { mediaMode = 'image'; $('cSoloFile').accept = 'image/png,image/jpeg,image/webp,image/gif'; $('cSoloFile').click(); }); $('edVidBtn').addEventListener('click', () => { mediaMode = 'video'; $('cSoloFile').accept = 'video/mp4,video/webm'; $('cSoloFile').click(); }); $('cSoloFile').addEventListener('change', async () => { const f = $('cSoloFile').files[0]; if (!f) return; $('edMediaInfo').textContent = 'Uploading ' + f.name + '…'; try { const r = await (await fetch('/api/my/upload', { method: 'POST', headers: { 'Content-Type': f.type }, body: f })).json(); if (r.error) { $('edMediaInfo').textContent = r.error; $('cSoloFile').value = ''; return; } insertHtmlAtCursor(r.type === 'video' ? '


' : '


'); $('edMediaInfo').textContent = f.name + ' inserted'; } catch (e) { $('edMediaInfo').textContent = 'Upload failed. Try again.'; } $('cSoloFile').value = ''; }); // raw-text toggle: swap the WYSIWYG surface for the underlying HTML and back $('edRawBtn').addEventListener('click', () => { const ed = $('cSoloEd'), raw = $('cSoloRaw'); if (raw.hidden) { raw.value = ed.innerHTML; raw.hidden = false; ed.hidden = true; $('edRawBtn').textContent = 'Visual'; } else { ed.innerHTML = raw.value; ed.hidden = false; raw.hidden = true; $('edRawBtn').textContent = 'Raw text'; } }); $('cType').addEventListener('change', () => { const t = $('cType').value; $('cImageRow').hidden = t !== 'banner'; // only banners carry a creative; login frames its URL $('cSizeRow').hidden = t !== 'banner'; $('cTitleRow').hidden = t !== 'text' && t !== 'solo'; $('cBodyRow').hidden = t !== 'text'; $('cSoloRow').hidden = t !== 'solo'; $('cSoloHint').hidden = t !== 'solo'; $('cVideoRow').hidden = t !== 'video'; $('cTitle').placeholder = t === 'solo' ? 'Subject line (max 80)' : 'Headline (max 60)'; soloHint(); if (t === 'video') videoHint(); }); // video composer: tier dropdown + upload + price hint function videoHint() { if (!lastRates || !lastRates.videoTiers) return; if ($('cWatchSecs') && !$('cWatchSecs').options.length) $('cWatchSecs').innerHTML = lastRates.videoTiers.map(t => '').join(''); const tier = lastRates.videoTiers.find(t => t.secs === Number($('cWatchSecs').value)) || lastRates.videoTiers[0]; const n = tier ? Math.floor((Number($('cBudget').value) || 0) / tier.cost) : 0; $('cVideoHint').textContent = tier ? (tier.cost + ' credits per completed ' + tier.secs + 's view' + (n ? ' — this budget buys ' + n + ' views' : '') + '. Viewers earn ' + tier.reward + ' credits each, so they watch.') : ''; } document.addEventListener('change', e => { if (e.target && e.target.id === 'cWatchSecs') videoHint(); }); $('cBudget').addEventListener('input', () => { if ($('cType').value === 'video') videoHint(); }); $('cVideoUploadBtn').addEventListener('click', () => $('cVideoFile').click()); $('cVideoFile').addEventListener('change', async () => { const f = $('cVideoFile').files[0]; if (!f) return; $('cVideoInfo').textContent = 'Uploading ' + f.name + '… (large files take a moment)'; try { const r = await (await fetch('/api/my/upload', { method: 'POST', headers: { 'Content-Type': f.type }, body: f })).json(); if (r.error) { $('cVideoInfo').textContent = r.error; $('cVideoFile').value = ''; return; } $('cVideoUrl').value = r.url; $('cVideoInfo').textContent = f.name + ' uploaded'; $('cVideoPrev').hidden = false; $('cVideoPrev').innerHTML = ''; } catch (e) { $('cVideoInfo').textContent = 'Upload failed. Try again.'; } $('cVideoFile').value = ''; }); $('createCampBtn').addEventListener('click', busy2($('createCampBtn'), async () => { // in raw mode the source of truth is the textarea; sync it back first let soloBody = $('cSoloEd').innerHTML; if ($('cSoloRaw') && !$('cSoloRaw').hidden) soloBody = $('cSoloRaw').value; const isVideo = $('cType').value === 'video'; await api('/api/my/campaigns', { type: $('cType').value, name: $('cName').value, targetUrl: $('cTarget').value, imageUrl: $('cImage').value, size: $('cSize').value, title: isVideo ? $('cVideoTitle').value : $('cTitle').value, body: $('cType').value === 'solo' ? soloBody : $('cBody').value, videoUrl: $('cVideoUrl').value, watchSecs: Number($('cWatchSecs').value), ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value, budget: Number($('cBudget').value) }); IAP.status('Campaign is live. It starts serving right away.', 'ok'); $('cName').value = ''; $('cBudget').value = ''; $('cSoloEd').innerHTML = ''; $('cSoloRaw').value = ''; $('cCtaLabel').value = ''; $('cVideoUrl').value = ''; $('cVideoTitle').value = ''; $('cVideoCta').value = ''; $('cVideoInfo').textContent = ''; $('cVideoPrev').hidden = true; $('cVideoPrev').innerHTML = ''; $('edMediaInfo').textContent = ''; await loadCampaigns(); })); // defers the busy() lookup to click time (busy is declared below) function busy2(btn, fn) { return (...a) => busy(btn, fn)(...a); } // ── watch-to-earn videos: escape-proof player, server-clock reward ── const vidState = { token: null, secs: 0, maxSeen: 0, done: false, credited: false }; async function loadVideoStatus() { try { const st = await (await fetch('/api/my/videos')).json(); if (st.error) return; $('vidProgress').textContent = 'today: ' + (st.status.count || 0) + ' / ' + st.status.cap + ' videos watched'; if (st.status.left <= 0) { $('vidBox').innerHTML = 'That is today\'s video set. Come back tomorrow.'; $('vidStartBtn').hidden = true; return; } $('vidStartBtn').hidden = false; } catch (e) {} } async function loadVideoAd() { let r = null; try { r = await (await fetch('/api/my/videos')).json(); } catch (e) {} if (!r || !r.ad) { $('vidBox').innerHTML = '' + (r && r.status && r.status.left <= 0 ? 'That is today\'s video set. Come back tomorrow.' : 'No member videos are live right now. Check back when a campaign is running.') + ''; $('vidWrap').hidden = true; return; } const ad = r.ad; vidState.token = r.token; vidState.secs = ad.watchSecs; vidState.maxSeen = 0; vidState.done = false; vidState.credited = false; $('vidBox').hidden = true; $('vidWrap').hidden = false; $('vidCta').hidden = true; $('vidHint').textContent = ad.title ? 'Now playing: ' + ad.title : ''; const p = $('vidPlayer'); p.src = ad.videoUrl; p.currentTime = 0; // escape-proof: no seeking past what's been watched; track max reached p.onseeking = () => { if (p.currentTime > vidState.maxSeen + 0.5) p.currentTime = vidState.maxSeen; }; p.ontimeupdate = () => { if (p.currentTime > vidState.maxSeen) vidState.maxSeen = p.currentTime; const left = Math.max(0, Math.ceil(vidState.secs - vidState.maxSeen)); $('vidTimer').textContent = left > 0 ? 'Watch ' + left + 's more to earn' : 'Watch time met — finishing…'; if (!vidState.done && vidState.maxSeen >= vidState.secs) { vidState.done = true; completeVideo(ad); } }; $('vidStartBtn').textContent = 'Playing…'; $('vidStartBtn').disabled = true; try { await p.play(); } catch (e) { $('vidStartBtn').disabled = false; $('vidStartBtn').textContent = 'Tap to play'; } $('vidCta').href = ad.ctaUrl; $('vidCta').textContent = ad.ctaLabel || 'Learn more'; $('vidCta').hidden = false; } async function completeVideo(ad) { if (vidState.credited) return; vidState.credited = true; try { const r = await (await fetch('/api/my/videowatch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: vidState.token }) })).json(); if (r.error) { IAP.status(r.error, 'bad'); $('vidHint').textContent = r.error; } else if (r.credited) { IAP.status('+' + r.credited + ' credits earned for watching.', 'ok'); $('vidHint').textContent = '+' + r.credited + ' credits earned. Load the next one.'; loadDashboard(); } else $('vidHint').textContent = 'That video just ran out of budget — load another.'; $('vidProgress').textContent = 'today: ' + ((r.status && r.status.count) || 0) + ' / ' + ((r.status && r.status.cap) || 0) + ' videos watched'; } catch (e) { IAP.status('Could not confirm that watch. Try the next one.', 'bad'); } $('vidStartBtn').disabled = false; $('vidStartBtn').textContent = 'Next video'; } $('vidStartBtn').addEventListener('click', () => loadVideoAd()); // ── solo-ads inbox: list, read view, dwell-gated read reward ── let ibTimer = null; function setInboxBadge(n) { for (const id of ['inboxBadge', 'inboxBadge2']) { const b = $(id); if (b) { b.hidden = !n; b.textContent = n; } } } // Earn credits sub-tabs: Watch ads | Inbox let earnSub = 'watch'; function setEarnSub(which) { earnSub = ['inbox', 'videos'].includes(which) ? which : 'watch'; const w = $('earn-watch'), i = $('earn-inbox'), v = $('earn-videos'); if (w) w.hidden = earnSub !== 'watch'; if (i) i.hidden = earnSub !== 'inbox'; if (v) v.hidden = earnSub !== 'videos'; document.querySelectorAll('.subtabs [data-earn]').forEach(b => b.classList.toggle('on', b.dataset.earn === earnSub)); if (earnSub === 'inbox') loadInbox(); else if (earnSub === 'videos') loadVideoStatus(); else earnRefresh(); } document.querySelectorAll('.subtabs [data-earn]').forEach(b => b.addEventListener('click', () => setEarnSub(b.dataset.earn))); async function loadInbox() { try { const r = await (await fetch('/api/my/inbox')).json(); if (r.error) return; $('ibRewardNote').textContent = '+' + r.readCredits + ' credits per real read (up to ' + r.readCap + ' rewarded reads a day)'; const el = $('ibList'); $('inboxReadCard').hidden = true; $('inboxListCard').hidden = false; setInboxBadge(r.unread); if (!r.items.length) { el.innerHTML = '

No solo ads yet. When a member sends one, it lands here — and reading it pays.

'; return; } el.innerHTML = ''; for (const i of r.items) { const d = document.createElement('div'); d.className = 'ib-row' + (i.read ? '' : ' unread'); d.innerHTML = '' + (i.rewarded ? 'claimed' : i.read ? '' : 'new') + '' + new Date(i.delivered).toLocaleDateString() + ''; d.querySelector('.sub').textContent = i.subject || '(no subject)'; d.querySelector('.from').textContent = 'from ' + (i.fromName || 'a member'); d.addEventListener('click', () => openInboxItem(i.id)); el.appendChild(d); } } catch (e) {} } async function openInboxItem(id) { try { const r = await (await fetch('/api/my/inbox/' + id)).json(); if (r.error) { IAP.status(r.error, 'bad'); return; } $('inboxListCard').hidden = true; $('inboxReadCard').hidden = false; $('ibSubject').textContent = r.subject || '(no subject)'; $('ibMeta').textContent = 'from ' + (r.fromName || 'a member') + ' · ' + new Date(r.delivered).toLocaleString(); $('ibBody').innerHTML = r.body || ''; // whitelist-sanitized on the server at submit const mv = $('ibMedia'); mv.hidden = !r.mediaUrl; mv.innerHTML = !r.mediaUrl ? '' : r.mediaType === 'video' ? '' : 'attachment'; // the read reward needs BOTH the dwell AND an actual click-through to the // advertiser — the visit is what makes the ad worth the sender's credits const visit = $('ibVisit'); visit.href = r.url; visit.textContent = r.ctaLabel || 'Learn more'; visit.target = '_blank'; const btn = $('ibClaimBtn'); clearInterval(ibTimer); if (r.rewarded) { btn.hidden = true; visit.classList.remove('cta-need'); $('ibHint').textContent = 'Read reward already claimed for this one.'; return; } let dwellDone = false; let visited = !!r.visited; btn.hidden = false; btn.disabled = true; visit.classList.toggle('cta-need', !visited); let left = r.dwell; const refresh = () => { if (!dwellDone) { btn.textContent = 'Read it — claim in ' + left + 's'; return; } if (!visited) { btn.textContent = 'Claim +' + r.reward + ' — visit the ad first'; btn.disabled = true; return; } btn.textContent = 'Claim +' + r.reward + ' credits'; btn.disabled = false; }; $('ibHint').textContent = 'Read the message, click through to the advertiser, then claim your credits.'; refresh(); // countdown pauses off-tab; the server separately enforces the dwell on its own clock ibTimer = setInterval(() => { if (document.visibilityState !== 'visible' || !document.hasFocus()) return; left -= 1; if (left > 0) { refresh(); return; } clearInterval(ibTimer); dwellDone = true; refresh(); }, 1000); // clicking the CTA records the visit (and counts the advertiser's click) visit.onclick = async () => { visited = true; visit.classList.remove('cta-need'); try { await fetch('/api/my/inbox/' + id + '/visit', { method: 'POST' }); } catch (e2) {} refresh(); }; btn.onclick = async () => { try { const c = await api('/api/my/inbox/' + id + '/claim'); IAP.status('+' + c.credited + ' credits for reading. They spend like any earned credits.', 'ok'); btn.hidden = true; $('ibHint').textContent = 'Claimed. Head back for the next one.'; loadDashboard(); } catch (e2) { IAP.status(e2.message, 'bad'); } }; } catch (e) {} } $('ibBack').addEventListener('click', ev => { ev.preventDefault(); clearInterval(ibTimer); loadInbox(); }); // ── promo tools: Branded Voice copy, personalized with the member link ── const PROMO_POSTS = [ 'A membership site where money is handled by code, not people. Every purchase splits instantly to sponsor wallets on the Polygon blockchain. Nothing to withdraw. The money just lands in your wallet. Plus you earn ad credits for viewing ads while you\'re there. {{LINK}}', 'No withdrawal button. Think about that. A smart contract on Polygon splits every payment the second it hits. 50% to the sponsor. 20% to the next level. 10% to the next. Lands straight in your own wallet. No button to push. No waiting. Just money where it belongs. See how it works: {{LINK}}', 'Every payment is public on the blockchain. You can watch the ledger move in real time. Every split, every wallet, every transaction. Nothing hidden. Nothing you have to take on faith. That\'s the difference between a platform that asks for trust and one where trust isn\'t needed. See for yourself: {{LINK}}' ]; const PROMO_SWIPE = { subject: 'Your Wallet Gets Paid Instantly..', body: 'You know the usual drill. Someone buys on your link, you wait for a payout. Maybe days. Maybe an approval hold. Maybe a "your account is under review."\n\nInstantAdPay doesn\'t work like that.\n\nA smart contract on the Polygon blockchain handles every purchase the second it happens. 50% to the sponsor. 20% to the next level. 10% to the one after that. 20% to the platform. Each split lands directly in your own wallet. No withdrawal button. No "request payout." No approval queue.\n\nThe money just shows up.\n\nYou can watch every transaction on the public ledger. Real time. Anyone can verify it.\n\nFree to join. Packages from $5 to $250. No income promises. It\'s advertising, not investing.\n\n{{LINK}}' }; function promoBlock(text) { const div = document.createElement('div'); div.className = 'promo-block'; div.textContent = text; const btn = document.createElement('button'); btn.className = 'btn small sec'; btn.textContent = 'Copy'; btn.addEventListener('click', async () => { try { await navigator.clipboard.writeText(text); IAP.status('Copied. Paste it anywhere.', 'ok'); } catch (e) { IAP.status('Copy failed. Select the text instead.', 'bad'); } }); div.appendChild(btn); return div; } function fillPromo(link) { const posts = $('promoPosts'); if (!posts || posts.dataset.filled === link) return; posts.dataset.filled = link; posts.innerHTML = ''; for (const p of PROMO_POSTS) posts.appendChild(promoBlock(p.replace('{{LINK}}', link))); const sw = $('promoSwipeWrap'); sw.innerHTML = ''; sw.appendChild(promoBlock('Subject: ' + PROMO_SWIPE.subject + '\n\n' + PROMO_SWIPE.body.replace('{{LINK}}', link))); } // ── profile ── (busy2 defers the busy lookup past its TDZ) $('pfSaveBtn').addEventListener('click', busy2($('pfSaveBtn'), async () => { const r = await api('/api/my/profile', { username: $('pfUsername').value }); IAP.status('You are @' + r.account.username + ' now.', 'ok'); await render(); })); // ── in-dashboard package buying ── const PKG = { 1: 'Micro', 2: 'Activation', 3: 'Builder', 4: 'Growth', 5: 'Leader' }; async function loadBuyTiles() { try { const { products } = await (await fetch('/api/catalog')).json(); const wrap = $('boTiles'); wrap.innerHTML = ''; for (const p of products) { const bonus = p.creditAmount - p.priceCents; const div = document.createElement('div'); div.className = 'tile' + (p.priceCents === 5000 ? ' hot' : ''); div.innerHTML = '
' + (PKG[p.id] || 'Package ' + p.id) + '
' + '
$' + Math.round(p.priceCents / 100) + '
' + '
' + p.creditAmount.toLocaleString() + ' credits
' + '
' + (bonus > 0 ? '+' + bonus.toLocaleString() + ' bonus credits' : ' ') + '
' + '
' + (p.costWei ? IAP.fmtPol(p.costWei) + ' POL right now' : 'paused') + '
' + ''; wrap.appendChild(div); } wrap.querySelectorAll('button[data-id]').forEach(b => b.addEventListener('click', async () => { try { b.disabled = true; const spNow = await (await fetch('/api/sponsor')).json(); IAP.status('Confirm the purchase in your wallet…'); const r = await IAPWallet.buy(Number(b.dataset.id), spNow.sponsorId || 0, b.dataset.cost); if (r.status !== '0x1' && r.receipt && r.receipt.status !== '0x1') throw new Error('Transaction reverted.'); IAP.status('Purchase settled on-chain. Credits are in your account.', 'ok'); await render(); loadBuyTiles(); } catch (e) { IAP.status('Purchase failed: ' + ((e && e.message) || e), 'bad'); } finally { b.disabled = false; } })); } catch (e) {} } loadBuyTiles(); // ── earn-by-viewing: daily ad set with dwell, then claim ── const earnState = { types: ['banner', 'text'], i: 0, timer: null }; async function earnRefresh() { try { const st = await (await fetch('/api/my/earn')).json(); if (st.error) return null; $('earnProgress').textContent = 'today: ' + st.views + ' / ' + st.target + ' ads viewed'; $('earnBalance').textContent = st.earned + ' earned credits'; const done = st.views >= st.target; $('earnClaimBtn').hidden = !(done && !st.claimed); if (st.claimed) $('earnHint').textContent = 'Claimed for today. Come back tomorrow, or put those credits to work in Campaigns.'; else if (done) $('earnHint').textContent = 'Set complete. Claim your ' + st.claimCredits + ' credits.'; return st; } catch (e) { return null; } } async function earnShowAd() { // each view happens full screen in its own tab: /view/ frames the // advertiser's site, runs the countdown, then a human check credits it const type = earnState.types[earnState.i++ % earnState.types.length]; let r = null; try { r = await (await fetch('/api/my/earnview?type=' + type)).json(); } catch (e) {} const box = $('earnAdBox'); if (!r || !r.ad || !r.viewUrl) { 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 w = window.open(r.viewUrl, '_blank'); box.innerHTML = 'Ad open in its own tab. Watch the countdown, pass the quick ' + 'check, and the view credits itself — this page updates the moment it does.' + (w ? '' : '
Pop-up blocked — open the ad here.'); $('earnStartBtn').textContent = 'View next ad'; } $('earnStartBtn').addEventListener('click', () => earnShowAd()); // the viewer tab pings localStorage when a view credits; refresh instantly window.addEventListener('storage', e => { if (e.key === 'iap-view-done') { earnRefresh(); loadDashboard(); } }); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && $('pane-earn') && !$('pane-earn').hidden) earnRefresh(); }); $('earnClaimBtn').addEventListener('click', async () => { try { const r = await api('/api/my/claim'); IAP.status('+' + r.credited + ' credits earned. Spend them in Campaigns.', 'ok'); await earnRefresh(); loadDashboard(); } catch (e) { IAP.status(e.message, 'bad'); } }); const busy = (btn, fn) => async () => { try { btn.disabled = true; await fn(); } catch (e) { IAP.status((e && e.message) || String(e), 'bad'); } finally { btn.disabled = false; } }; // passwordless (feature-flagged on config.emailAuth): code replaces passwords (async () => { const cfg = await IAP.getConfig(); if (!cfg.emailAuth) return; $('passCards').hidden = true; $('magicCard').hidden = false; const start = busy($('mcSendBtn'), async () => { const r = await api('/api/auth/email/start', { email: $('mcEmail').value }); $('mcCodeRow').hidden = false; $('mcVerifyBtn').hidden = false; $('mcSendBtn').hidden = true; $('mcResend').hidden = false; if (r.devCode) { $('mcCode').value = r.devCode; IAP.status('Dev mode: code filled in for you.', 'ok'); } else IAP.status('Code sent. Check your inbox (and spam, the first time).', 'ok'); $('mcCode').focus(); }); $('mcSendBtn').addEventListener('click', start); $('mcResend').addEventListener('click', busy($('mcResend'), async () => { const r = await api('/api/auth/email/start', { email: $('mcEmail').value }); if (r.devCode) $('mcCode').value = r.devCode; IAP.status('Fresh code sent.', 'ok'); })); $('mcVerifyBtn').addEventListener('click', busy($('mcVerifyBtn'), async () => { await api('/api/auth/email/verify', { email: $('mcEmail').value, code: $('mcCode').value }); IAP.status('You are in.', 'ok'); if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad await render(); })); })(); $('signupBtn').addEventListener('click', busy($('signupBtn'), async () => { await api('/api/signup', { email: $('suEmail').value, password: $('suPass').value }); IAP.status('Welcome aboard. You are in.', 'ok'); await render(); })); $('loginBtn').addEventListener('click', busy($('loginBtn'), async () => { await api('/api/login', { email: $('liEmail').value, password: $('liPass').value }); IAP.status('Logged in.', 'ok'); if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad await render(); })); $('walletSigninLink').addEventListener('click', async e => { e.preventDefault(); try { IAP.status('Check your wallet for the free sign-in signature…'); await IAPWallet.signIn(); IAP.status('Signed in with your wallet.', 'ok'); if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad await render(); } catch (err) { IAP.status((err && err.message) || String(err), 'bad'); } }); $('linkBtn').addEventListener('click', busy($('linkBtn'), async () => { IAP.status('Check your wallet for the free link signature…'); await IAPWallet.signIn(); // server binds the wallet to the signed-in email account IAP.status('Wallet linked. Earnings pay there from now on.', 'ok'); await render(); })); $('activateBtn').addEventListener('click', busy($('activateBtn'), async () => { const me = await (await fetch('/api/me')).json(); IAP.status('Confirm the free activation in your wallet…'); const r = await IAPWallet.activate(me.sponsorId || 0); if (r.receipt.status !== '0x1') throw new Error('Transaction reverted. Check the explorer.'); IAP.status('Payouts are on. Your invite link is live.', 'ok'); await render(); })); $('copyInvite').addEventListener('click', async () => { try { await navigator.clipboard.writeText($('inviteLine').textContent); IAP.status('Link copied.', 'ok'); } catch (e) { IAP.status('Copy failed. Select and copy the link text.', 'bad'); } }); $('logoutLink').addEventListener('click', async e => { e.preventDefault(); await fetch('/api/auth/logout', { method: 'POST' }); IAP.status('Logged out.', 'ok'); await render(); }); // ── welcome tour (viral banner gauntlet): a new member meets their 3-level // upline's sites, 10 focus-paused seconds each, then unlocks welcome credits. // Same three levels the contract pays — the tour IS the org chart. async function showGauntlet() { try { const g = await (await fetch('/api/my/gauntlet')).json(); if (!g.pending || !g.slides || !g.slides.length) return false; const gate = $('gauntGate'); gate.hidden = false; for (let i = 0; i < g.slides.length; i++) { const s = g.slides[i]; $('ggWho').textContent = 'Position ' + (i + 1) + ': ' + s.name + (i === 0 ? ' — the person who invited you' : ''); $('ggProgress').textContent = 'Meeting your line: ' + (i + 1) + ' of ' + g.slides.length; $('ggFrame').src = s.targetUrl; let left = g.dwell || 10; $('ggTimer').textContent = left + 's'; await new Promise(done => { const t = setInterval(() => { if (document.visibilityState !== 'visible' || !document.hasFocus()) return; left -= 1; $('ggTimer').textContent = Math.max(0, left) + 's'; if (left <= 0) { clearInterval(t); done(); } }, 1000); }); } $('ggFrame').src = 'about:blank'; $('ggTimer').textContent = '✓'; $('ggWho').textContent = 'That is your line. When you grow, they earn — and yours starts the day you share.'; $('ggClaim').hidden = false; await new Promise(done => { $('ggClaim').onclick = async () => { try { const r = await api('/api/my/gauntlet/complete', { token: g.token }); IAP.status('+' + r.credited + ' welcome credits unlocked. They spend on real campaigns.', 'ok'); } catch (e) { IAP.status(e.message, 'bad'); } done(); }; }); gate.hidden = true; $('ggClaim').hidden = true; return true; } catch (e) { return false; } } // ── line banner (profile): the member's slot on welcome tours + their wall ── function fillLineBanner(a) { if (!a) return; if (a.lineTargetUrl) $('lbTarget').value = a.lineTargetUrl; if (a.lineBannerUrl) { $('lbBanner').value = a.lineBannerUrl; $('lbPreview').hidden = false; $('lbPreview').innerHTML = 'line banner'; } $('lbCurrent').textContent = a.lineTargetUrl ? 'Live: your next three levels meet ' + a.lineTargetUrl + ' on their welcome tour.' : 'Not set yet. Until you set one, your tour slot is skipped.'; } async function loadLineBanner() { try { fillLineBanner(await (await fetch('/api/me')).json()); } catch (e) {} } $('lbUploadBtn').addEventListener('click', () => $('lbFile').click()); $('lbFile').addEventListener('change', async () => { const f = $('lbFile').files[0]; if (!f) return; $('lbUpInfo').textContent = 'Uploading…'; try { const r = await (await fetch('/api/my/upload', { method: 'POST', headers: { 'Content-Type': f.type }, body: f })).json(); if (r.error) { $('lbUpInfo').textContent = r.error; $('lbFile').value = ''; return; } $('lbBanner').value = r.url; $('lbUpInfo').textContent = 'Uploaded.'; $('lbPreview').hidden = false; $('lbPreview').innerHTML = 'line banner'; } catch (e) { $('lbUpInfo').textContent = 'Upload failed. Try again.'; } $('lbFile').value = ''; }); $('lbSaveBtn').addEventListener('click', busy2($('lbSaveBtn'), async () => { const r = await api('/api/my/linebanner', { bannerUrl: $('lbBanner').value, targetUrl: $('lbTarget').value }); IAP.status('Line banner saved. Your next three levels will meet it.', 'ok'); if (r.account) fillLineBanner(r.account); })); // ── login ad interstitial (ClickBaitPays pattern): after a successful // sign-in the sponsor card appears; "Open Ad" opens the CTA link in a NEW // tab (a real, counted click) while the timer counts down on THIS page — // deliberately not paused, the member is expected to be in the ad tab. // At zero, "Go to dashboard" appears. async function showLoginAd() { try { const { ad } = await (await fetch('/api/ads/slot?type=login')).json(); if (!ad || !ad.targetUrl) return; const gate = $('loginGate'); const openBtn = $('lgOpen'); const goBtn = $('lgContinue'); const timer = $('lgTimer'); $('lgCreative').innerHTML = ad.imageUrl ? 'sponsor ad' : '' + (ad.title ? String(ad.title).replace(/[&<>]/g, '') : 'Visit today\'s sponsor') + ''; $('lgStatus').textContent = 'Login ad sponsor — click "Open Ad" to begin.'; timer.hidden = true; goBtn.hidden = true; openBtn.disabled = false; gate.hidden = false; await new Promise(done => { openBtn.onclick = () => { window.open(ad.targetUrl, '_blank'); // the click relay counts it openBtn.disabled = true; $('lgStatus').textContent = 'Ad open in a new tab. View it and come back — the timer runs here.'; let left = ad.dwell || 10; timer.hidden = false; timer.textContent = 'Time remaining: ' + left + 's'; const t = setInterval(() => { left -= 1; if (left > 0) { timer.textContent = 'Time remaining: ' + left + 's'; return; } clearInterval(t); timer.textContent = 'Time is up'; timer.classList.add('done'); $('lgStatus').textContent = 'Thanks for the look. Your dashboard is ready.'; goBtn.hidden = false; }, 1000); goBtn.onclick = () => done(); }; }); gate.hidden = true; $('lgTimer').classList.remove('done'); } catch (e) {} } // ad surfaces: a banner greets the sign-in screen; members see live // banner + text placements inside the back office (they ARE the audience) IAP.adSlot('banner', 'adSlotLogin'); IAP.adSlot('banner', 'adSlotOverview'); IAP.adSlot('text', 'adSlotSide'); render(); })();