Paginate the live payment proof feed (Show more / Show fewer)
The feed showed only the latest 12 of 136 recorded payouts. Now: - chain.getPayoutsPublic(offset,limit) returns a page of the full reversed history plus total/offset/limit/hasMore (limit capped 100). - /api/public/payouts accepts ?offset & ?limit (defaults 0/40, so the live poll + toast detection are unchanged). - payouts.js keeps a deduped store keyed by payout key; the live poll refreshes the recent page while "Show more" pulls older pages 12 at a time (with slight overlap so a newly-arrived payout can't open a gap), and "Show fewer" collapses back. Verified in-browser: 12 → 48 → 136 rows, button flips to "Show fewer", collapses to 12, 0 console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -329,14 +329,21 @@ function isInTeam(id, rootId) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPayoutsPublic() {
|
function getPayoutsPublic(offset = 0, limit = 40) {
|
||||||
const recent = state ? state.payouts.slice(-40).reverse() : [];
|
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 {
|
return {
|
||||||
updatedAt: state && state.updatedAt,
|
updatedAt: state && state.updatedAt,
|
||||||
ready: !!(state && state.snapshotAt),
|
ready: !!(state && state.snapshotAt),
|
||||||
totals: state ? { payouts: state.totals.count, pol: +state.totals.pol.toFixed(2), members: Object.keys(state.members).length } : null,
|
totals: state ? { payouts: state.totals.count, pol: +state.totals.pol.toFixed(2), members: Object.keys(state.members).length } : null,
|
||||||
contract: CONTRACT,
|
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,
|
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,
|
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,
|
toAccount: state.members[p.toId] ? state.members[p.toId].account : undefined,
|
||||||
|
|||||||
+63
-8
@@ -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}`;
|
const meta=p.kind==='referral'?`Direct referral reward from #${p.fromId}'s entry`:`${esc(p.desc||p.levelName)} — from #${p.fromId}`;
|
||||||
return `<div class="pp-row"><div class="pp-icon">💸</div><div class="pp-body"><strong>Member #${p.toId} received ${fmtPol(p.pol)} POL</strong><div class="pp-meta">${meta} · ${when}</div></div>${verify}</div>`;
|
return `<div class="pp-row"><div class="pp-icon">💸</div><div class="pp-body"><strong>Member #${p.toId} received ${fmtPol(p.pol)} POL</strong><div class="pp-meta">${meta} · ${when}</div></div>${verify}</div>`;
|
||||||
}
|
}
|
||||||
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=`<div class="fact"><small>Members on-chain</small><strong>${d.totals.members}</strong></div><div class="fact"><small>Payouts recorded</small><strong>${d.totals.payouts}</strong></div><div class="fact"><small>POL paid to members</small><strong>${Math.round(d.totals.pol).toLocaleString()}</strong></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(showingNow<cap){btn.dataset.mode='more';btn.textContent=`Show more · showing ${showingNow} of ${cap}`;}
|
||||||
|
else{btn.dataset.mode='less';btn.textContent='Show fewer';}
|
||||||
|
}
|
||||||
|
function renderFeed(){
|
||||||
const feed=document.getElementById('payoutFeed');
|
const feed=document.getElementById('payoutFeed');
|
||||||
if(!feed)return;
|
if(!feed)return;
|
||||||
const totals=document.getElementById('payoutTotals');
|
const all=allSorted();
|
||||||
if(totals&&d.totals)totals.innerHTML=`<div class="fact"><small>Members on-chain</small><strong>${d.totals.members}</strong></div><div class="fact"><small>Payouts recorded</small><strong>${d.totals.payouts}</strong></div><div class="fact"><small>POL paid to members</small><strong>${Math.round(d.totals.pol).toLocaleString()}</strong></div>`;
|
if(!all.length){feed.innerHTML='<div class="empty">Reading the blockchain… check back in a minute.</div>';return}
|
||||||
if(!d.payouts||!d.payouts.length){feed.innerHTML='<div class="empty">Reading the blockchain… check back in a minute.</div>';return}
|
feed.innerHTML=all.slice(0,visible).map(rowHtml).join('');
|
||||||
feed.innerHTML=d.payouts.slice(0,12).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.size<total){await fetchPage(Math.max(0,store.size-4),PAGE+8);}
|
||||||
|
renderFeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function poll(){
|
async function poll(){
|
||||||
let d;
|
const d=await fetchPage(0,40); // recent page drives the live feed + toasts
|
||||||
try{const r=await fetch('/api/public/payouts');d=await r.json()}catch(e){return}
|
|
||||||
if(!d||!d.payouts)return;
|
if(!d||!d.payouts)return;
|
||||||
render(d);
|
renderFeed();
|
||||||
const now=Date.now()/1000;
|
const now=Date.now()/1000;
|
||||||
const fresh=d.payouts.filter(p=>p.ts&&(now-p.ts)<TOAST_FRESH_S&&!seen.has(p.key));
|
const fresh=d.payouts.filter(p=>p.ts&&(now-p.ts)<TOAST_FRESH_S&&!seen.has(p.key));
|
||||||
fresh.slice(0,TOAST_MAX_QUEUE).reverse().forEach(p=>{remember(p.key);enqueue(p)});
|
fresh.slice(0,TOAST_MAX_QUEUE).reverse().forEach(p=>{remember(p.key);enqueue(p)});
|
||||||
|
|||||||
+11
-10
@@ -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-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{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-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-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-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}
|
.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 .num.ok{color:var(--ok)}
|
||||||
.team-stat small{display:block;color:var(--muted);margin-top:9px;font-size:13px;line-height:1.45}
|
.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}}
|
@media(max-width:720px){.team-stats{grid-template-columns:1fr 1fr}}
|
||||||
|
|
||||||
/* How-you-get-paid diagram page */
|
/* 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-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-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-you{border-color:var(--gold);color:var(--gold)}
|
||||||
.pay-join{color:var(--teal);border-color:rgba(78,214,203,.4)}
|
.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{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-arrow-up span{font-size:13px;margin-bottom:2px;white-space:nowrap}
|
||||||
.pay-diagram-wrap{overflow-x:auto;margin-top:8px}
|
.pay-diagram-wrap{overflow-x:auto;margin-top:8px}
|
||||||
.pay-diagram{width:100%;min-width:640px;height:auto;display:block}
|
.pay-diagram{width:100%;min-width:640px;height:auto;display:block}
|
||||||
|
|||||||
@@ -368,7 +368,10 @@ async function handleApi(req,res,pathname){
|
|||||||
}catch(e){return json(res,502,{error:e.message||'Lookup failed'})}
|
}catch(e){return json(res,502,{error:e.message||'Lookup failed'})}
|
||||||
}
|
}
|
||||||
if(req.method==='GET'&&pathname==='/api/public/payouts'){
|
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'){
|
if(req.method==='GET'&&pathname==='/api/public/org-stats'){
|
||||||
const root=Number(getConfig().orgRootId||21);
|
const root=Number(getConfig().orgRootId||21);
|
||||||
|
|||||||
Reference in New Issue
Block a user