Files
instantadpay/public/assets/home.js
T
martbost 28ed925443 View-fraud protection + real ad placement surfaces
Earning views now run on single-use server-issued tokens: the view only
counts when the full dwell elapses on the server clock; instant, forged,
replayed, and stale posts are all rejected, and the client countdown
pauses whenever the tab loses visibility or focus. New placements: login
ads on the sign-in screen (the per-day format's real home), a banner slot
in the back-office Overview, a text slot in the member sidebar, and a
member-ads banner on the homepage. Assets v=20260905g.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-05 06:15:27 -05:00

142 lines
7.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.
// Landing page: live ladder, buy buttons, sponsor attribution line.
(async function () {
await IAP.renderNav('home');
const c = await IAP.getConfig();
IAP.$('contractLink').href = c.explorer + '/address/' + c.contract;
const sp = await (await fetch('/api/sponsor')).json();
if (sp.invited) {
const el = IAP.$('sponsorLine');
el.hidden = false;
el.textContent = (sp.sponsorId ? 'You were invited by member #' + sp.sponsorId + '.' : 'You arrived through a member’s invite.')
+ ' Your purchases pay their team, and your own link will do the same for you.';
}
async function loadLadder() {
const { products } = await (await fetch('/api/catalog')).json();
const wrap = document.getElementById('tiles');
wrap.innerHTML = '';
const NAMES = { 1: 'Micro', 2: 'Activation', 3: 'Builder', 4: 'Growth', 5: 'Leader' };
for (const p of products) {
const bonus = p.creditAmount - p.priceCents; // credits above 1cr/cent = bulk bonus
const div = document.createElement('div');
div.className = 'tile' + (p.priceCents === 5000 ? ' hot' : '');
div.innerHTML = '<div class="name">' + (NAMES[p.id] || 'Package ' + p.id) + '</div>'
+ '<div class="price">$' + Math.round(p.priceCents / 100) + '</div>'
+ '<div class="cr">' + p.creditAmount.toLocaleString() + ' credits</div>'
+ '<div class="bonus">' + (bonus > 0 ? '+' + bonus.toLocaleString() + ' bonus credits' : '&nbsp;') + '</div>'
+ '<div class="pol">' + (p.costWei ? IAP.fmtPol(p.costWei) + ' POL right now' : 'paused') + '</div>'
+ '<button class="btn small" data-id="' + p.id + '" data-cost="' + (p.costWei || '') + '"'
+ (p.costWei ? '' : ' disabled') + '>Buy</button>';
wrap.appendChild(div);
}
wrap.querySelectorAll('button[data-id]').forEach(b => b.addEventListener('click', () => buyPack(b)));
}
async function buyPack(btn) {
try {
btn.disabled = true;
// email members get their wallet linked to the account at buy time
const me = await (await fetch('/api/me')).json();
if (me.signedIn && me.email && !me.address) {
IAP.status('First, a free signature links your wallet to your account…');
await IAPWallet.signIn();
}
IAP.status('Confirm the purchase in your wallet…');
// resolve the sponsor at buy time: a code referrer who activated since
// page load still gets locked in
const spNow = await (await fetch('/api/sponsor')).json();
const r = await IAPWallet.buy(Number(btn.dataset.id), spNow.sponsorId || 0, btn.dataset.cost);
if (r.receipt.status !== '0x1') throw new Error('Transaction reverted. Check the explorer.');
IAP.status('Purchase settled on-chain. Credits are yours, payouts delivered. Watch it on the ledger.', 'ok');
IAP.refreshNavWallet();
} catch (e) {
IAP.status('Purchase failed: ' + (e.message || e), 'bad');
} finally { btn.disabled = false; }
}
async function loadStats() {
try {
const s = await (await fetch('/api/stats')).json();
IAP.$('stMembers').textContent = (s.onchainMembers || 0).toLocaleString();
IAP.$('stPurchases').textContent = (s.purchases || 0).toLocaleString();
IAP.$('stPaid').textContent = IAP.fmtPol(s.paidInWei || '0');
IAP.$('stPayouts').textContent = (s.payouts || 0).toLocaleString();
} catch (e) {}
}
async function loadTicker() {
try {
const { events } = await (await fetch('/api/feed?n=30')).json();
if (!events.length) return;
const inner = IAP.$('tickerInner');
inner.innerHTML = events.map(ev => '<span>' + IAP.describeEvent(ev, c) + '</span>').join('');
IAP.$('ticker').hidden = false;
} catch (e) {}
}
// level cycler: chips + generation highlighting + auto-advance
const LVL = {
1: { pct: '50%', desc: 'Level 1 is open to every member: your direct referrals each pay you 50 percent of every package they ever buy, straight to your wallet.' },
2: { pct: '20%', desc: 'Bring 2 buyers of $20 or more and level 2 unlocks: 20 percent of every package your referrals’ referrals buy, on every purchase, forever.' },
3: { pct: '10%', desc: 'At 5 qualifying buyers, level 3 opens the third generation: 10 percent of everything they buy. Eight positions deep in this picture, and it keeps growing.' }
};
const viz = document.getElementById('genViz');
if (viz) {
const chips = [...document.querySelectorAll('.chips [data-lvl]')];
const setLvl = n => {
viz.dataset.lvl = n;
document.getElementById('vizPct').textContent = LVL[n].pct;
document.getElementById('lvlDesc').textContent = LVL[n].desc;
chips.forEach(ch => ch.classList.toggle('on', ch.dataset.lvl === String(n)));
};
let cur = 1;
let auto = null;
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
auto = setInterval(() => { cur = cur % 3 + 1; setLvl(cur); }, 4200);
}
chips.forEach(ch => ch.addEventListener('click', () => {
if (auto) { clearInterval(auto); auto = null; } // a click takes the wheel
cur = Number(ch.dataset.lvl);
setLvl(cur);
}));
}
// what-if calculator: pure arithmetic on the locked constants
const dc = document.getElementById('dcDirects');
if (dc) {
const $id = x => document.getElementById(x);
const usd = n => '$' + n.toLocaleString(undefined, { maximumFractionDigits: 2 });
const recalc = () => {
const d = Number($id('dcDirects').value);
const p = Number($id('dcPkg').value);
const r = Number($id('dcSpread').value);
$id('dcDirectsV').textContent = d;
$id('dcSpreadV').textContent = r;
const qualifies = p >= 20; // sub-$20 packages never count toward qualification
const l2open = qualifies && d >= 2;
const l3open = qualifies && d >= 5;
const g2 = d * r, g3 = g2 * r;
const e1 = d * p * 0.5;
const e2 = l2open ? g2 * p * 0.2 : 0;
const e3 = l3open ? g3 * p * 0.1 : 0;
$id('dcN1').textContent = d; $id('dcN2').textContent = g2; $id('dcN3').textContent = g3;
$id('dcE1').textContent = usd(e1);
$id('dcE2').textContent = l2open ? usd(e2) : 'passes up';
$id('dcE3').textContent = l3open ? usd(e3) : 'passes up';
$id('dcTotal').textContent = usd(e1 + e2 + e3);
const setB = (el, open, need) => { el.textContent = open ? 'open' : 'locked: ' + need; el.className = 'badge' + (open ? '' : ' amber'); };
setB($id('dcB1'), true, '');
setB($id('dcB2'), l2open, qualifies ? (2 - d) + ' more buyer(s)' : 'needs $20+ buyers');
setB($id('dcB3'), l3open, qualifies ? (5 - d) + ' more buyer(s)' : 'needs $20+ buyers');
$id('dcQualNote').textContent = qualifies
? 'Buyers of $20 or more count toward your qualification. 2 unlock level 2, 5 unlock level 3.'
: 'Heads up: $5 packages pay your level 1 but do not qualify buyers, so levels 2 and 3 stay locked in this scenario.';
};
['dcDirects', 'dcPkg', 'dcSpread'].forEach(x => $id(x).addEventListener('input', recalc));
recalc();
}
IAP.adSlot('banner', 'adSlotHome');
loadLadder();
loadStats();
loadTicker();
setInterval(loadStats, 60000);
})();