5b4fe133eb
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
150 lines
7.8 KiB
JavaScript
150 lines
7.8 KiB
JavaScript
// 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' : ' ') + '</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;
|
||
// readable pace (Marty, 2026-09-12): about 70 px per second no matter how much text is loaded,
|
||
// instead of a fixed 42 s for the whole strip; pause while a finger or pointer rests on it
|
||
const wrap = IAP.$('ticker');
|
||
const secs = Math.max(30, Math.round((inner.scrollWidth + wrap.clientWidth) / 70));
|
||
inner.style.animationDuration = secs + 's';
|
||
const pause = on => { inner.style.animationPlayState = on ? 'paused' : 'running'; };
|
||
wrap.addEventListener('mouseenter', () => pause(true)); wrap.addEventListener('mouseleave', () => pause(false));
|
||
wrap.addEventListener('touchstart', () => pause(true), { passive: true }); wrap.addEventListener('touchend', () => pause(false), { passive: true });
|
||
} 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);
|
||
})();
|