diff --git a/chain.js b/chain.js index d2f37a6..34872b2 100644 --- a/chain.js +++ b/chain.js @@ -329,14 +329,21 @@ function isInTeam(id, rootId) { return false; } -function getPayoutsPublic() { - const recent = state ? state.payouts.slice(-40).reverse() : []; +function getPayoutsPublic(offset = 0, limit = 40) { + const off = Math.max(0, Number.isFinite(offset) ? Math.floor(offset) : 0); + const lim = Math.min(100, Math.max(1, Number.isFinite(limit) ? Math.floor(limit) : 40)); + const reversed = state ? state.payouts.slice().reverse() : []; // most recent first + const page = reversed.slice(off, off + lim); return { updatedAt: state && state.updatedAt, ready: !!(state && state.snapshotAt), totals: state ? { payouts: state.totals.count, pol: +state.totals.pol.toFixed(2), members: Object.keys(state.members).length } : null, contract: CONTRACT, - payouts: recent.map(p => ({ + total: reversed.length, + offset: off, + limit: lim, + hasMore: off + lim < reversed.length, + payouts: page.map(p => ({ key: p.key, kind: p.kind, toId: p.toId, fromId: p.fromId, level: p.level, levelName: levelName(p.level), pol: p.pol, tx: p.tx, ts: p.ts, desc: p.desc, toAccount: state.members[p.toId] ? state.members[p.toId].account : undefined, diff --git a/public/payouts.js b/public/payouts.js index b943a29..7f01b7b 100644 --- a/public/payouts.js +++ b/public/payouts.js @@ -55,20 +55,75 @@ const meta=p.kind==='referral'?`Direct referral reward from #${p.fromId}'s entry`:`${esc(p.desc||p.levelName)} — from #${p.fromId}`; return `
đź’¸
Member #${p.toId} received ${fmtPol(p.pol)} POL
${meta} · ${when}
${verify}
`; } - function render(d){ + // Paginated feed: keep a deduped store of payouts (by key), most-recent first. + // The live poll refreshes the top (recent) page; "Show more" pulls older pages. + const PAGE=12; + const store=new Map(); // key -> payout + let total=0, visible=PAGE, fetching=false; + const allSorted=()=>[...store.values()].sort((a,b)=>(b.ts||0)-(a.ts||0)); + + function ingest(d){ + if(d&&Array.isArray(d.payouts))for(const p of d.payouts)store.set(p.key,p); + if(d&&typeof d.total==='number')total=Math.max(total,d.total); + } + function updateTotals(d){ + const totals=document.getElementById('payoutTotals'); + if(totals&&d&&d.totals)totals.innerHTML=`
Members on-chain${d.totals.members}
Payouts recorded${d.totals.payouts}
POL paid to members${Math.round(d.totals.pol).toLocaleString()}
`; + } + + function ensureMoreBtn(){ + let btn=document.getElementById('ppMore'); + if(!btn){ + const feed=document.getElementById('payoutFeed'); + if(!feed)return null; + btn=document.createElement('button'); + btn.id='ppMore';btn.type='button';btn.className='btn btn-secondary pp-more'; + btn.addEventListener('click',onMoreClick); + feed.parentNode.insertBefore(btn,feed.nextSibling); + } + return btn; + } + function renderMoreBtn(loadedLen){ + const btn=ensureMoreBtn();if(!btn)return; + const cap=Math.max(total,loadedLen); + if(cap<=PAGE){btn.style.display='none';return} + btn.style.display=''; + const showingNow=Math.min(visible,cap); + if(showingNowMembers on-chain${d.totals.members}
Payouts recorded${d.totals.payouts}
POL paid to members${Math.round(d.totals.pol).toLocaleString()}
`; - if(!d.payouts||!d.payouts.length){feed.innerHTML='
Reading the blockchain… check back in a minute.
';return} - feed.innerHTML=d.payouts.slice(0,12).map(rowHtml).join(''); + const all=allSorted(); + if(!all.length){feed.innerHTML='
Reading the blockchain… check back in a minute.
';return} + feed.innerHTML=all.slice(0,visible).map(rowHtml).join(''); + renderMoreBtn(all.length); + } + + async function fetchPage(offset,limit){ + if(fetching)return null; + fetching=true; + try{const r=await fetch(`/api/public/payouts?offset=${offset}&limit=${limit}`);const d=await r.json();ingest(d);updateTotals(d);return d} + catch(e){return null} + finally{fetching=false} + } + async function onMoreClick(){ + const btn=document.getElementById('ppMore'); + if(btn&&btn.dataset.mode==='less'){ + visible=PAGE;renderFeed(); + const sec=document.getElementById('proof');if(sec)sec.scrollIntoView({behavior:'smooth',block:'start'}); + return; + } + visible+=PAGE; + if(visible>store.size&&store.sizep.ts&&(now-p.ts){remember(p.key);enqueue(p)}); diff --git a/public/styles.css b/public/styles.css index 7c1d004..bfe9cac 100644 --- a/public/styles.css +++ b/public/styles.css @@ -61,6 +61,7 @@ a{color:inherit}.wrap{width:min(1160px,calc(100% - 32px));margin:auto}.nav{heigh .pp-passed{color:#f2c768;font-size:12px;margin-top:3px} .pp-verify{flex:0 0 auto;font-size:13px;color:var(--teal);text-decoration:none;border:1px solid rgba(78,214,203,.35);border-radius:9px;padding:7px 10px;white-space:nowrap} .pp-verify:hover{background:rgba(78,214,203,.1)} +.pp-more{display:block;margin:14px auto 0} .pp-note{color:#8498aa;font-size:12.5px;margin-top:14px;line-height:1.5} .pp-stack{position:fixed;left:16px;bottom:16px;z-index:60;display:flex;flex-direction:column;gap:10px;pointer-events:none} .pp-toast{pointer-events:auto;display:block;width:min(330px,calc(100vw - 32px));background:linear-gradient(145deg,#132d45,#0a1b2b);border:1px solid #38556b;border-left:3px solid var(--ok);border-radius:14px;padding:13px 15px;box-shadow:var(--shadow);text-decoration:none;color:var(--text);opacity:0;transform:translateY(14px);transition:.4s ease} @@ -143,13 +144,13 @@ a.brand{text-decoration:none;color:inherit} .team-stat .num.ok{color:var(--ok)} .team-stat small{display:block;color:var(--muted);margin-top:9px;font-size:13px;line-height:1.45} @media(max-width:720px){.team-stats{grid-template-columns:1fr 1fr}} - -/* How-you-get-paid diagram page */ -.pay-entry{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;padding:8px 0} -.pay-node{background:#0d2236;border:1px solid #2a4a66;border-radius:14px;padding:16px 20px;text-align:center;font-weight:800;line-height:1.3} -.pay-you{border-color:var(--gold);color:var(--gold)} -.pay-join{color:var(--teal);border-color:rgba(78,214,203,.4)} -.pay-arrow-up{display:flex;flex-direction:column;align-items:center;color:var(--ok);font-weight:900;font-size:22px;line-height:1} -.pay-arrow-up span{font-size:13px;margin-bottom:2px;white-space:nowrap} -.pay-diagram-wrap{overflow-x:auto;margin-top:8px} -.pay-diagram{width:100%;min-width:640px;height:auto;display:block} + +/* How-you-get-paid diagram page */ +.pay-entry{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;padding:8px 0} +.pay-node{background:#0d2236;border:1px solid #2a4a66;border-radius:14px;padding:16px 20px;text-align:center;font-weight:800;line-height:1.3} +.pay-you{border-color:var(--gold);color:var(--gold)} +.pay-join{color:var(--teal);border-color:rgba(78,214,203,.4)} +.pay-arrow-up{display:flex;flex-direction:column;align-items:center;color:var(--ok);font-weight:900;font-size:22px;line-height:1} +.pay-arrow-up span{font-size:13px;margin-bottom:2px;white-space:nowrap} +.pay-diagram-wrap{overflow-x:auto;margin-top:8px} +.pay-diagram{width:100%;min-width:640px;height:auto;display:block} diff --git a/server.js b/server.js index b1e7996..d555d50 100644 --- a/server.js +++ b/server.js @@ -368,7 +368,10 @@ async function handleApi(req,res,pathname){ }catch(e){return json(res,502,{error:e.message||'Lookup failed'})} } if(req.method==='GET'&&pathname==='/api/public/payouts'){ - return json(res,200,chain.getPayoutsPublic(),{'Cache-Control':'public, max-age=20'}); + const q=new URL(req.url,'http://x').searchParams; + const offset=Number(q.get('offset')||0); + const limit=Number(q.get('limit')||40); + return json(res,200,chain.getPayoutsPublic(offset,limit),{'Cache-Control':'public, max-age=20'}); } if(req.method==='GET'&&pathname==='/api/public/org-stats'){ const root=Number(getConfig().orgRootId||21);