Files
rm-circle-team-router/public/my.js
T
martbost 481423368b Add qualification badges to dashboard and matrix cards
Gold "★ Qualified" pill in the member-dashboard header (or an amber
"N more directs to qualify" pill), and a gold ✓ corner medallion on
every qualified position card in both the member tree and admin pyramid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 14:36:36 -05:00

75 lines
5.1 KiB
JavaScript

(function(){
const esc=s=>String(s??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]));
const fmt=n=>Number(n||0).toLocaleString(undefined,{maximumFractionDigits:2});
const date=ts=>ts?new Date(ts*1000).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}):'—';
const prompt=document.getElementById('idPrompt'),dash=document.getElementById('dash');
function pathId(){const m=location.pathname.match(/^\/my\/(\d+)$/);if(m)return m[1];const q=new URLSearchParams(location.search).get('id');if(q&&/^\d+$/.test(q))return q;try{return localStorage.getItem('ctb.myId')||''}catch(e){return ''}}
function card(n,focus){
if(!n)return '<div class="mtp-card mtp-open">open<br>slot</div>';
const badge=n.tier===2?'<span class="mt-badge mt-prem">P</span>':'<span class="mt-badge">S</span>';
const qmark=n.directCount>=2?'<span class="mtp-qmark" title="Qualified — 2/2 directs">✓</span>':'';
const down=n.downCount?`<span class="mtp-down">⬇ ${n.downCount} below · ${fmt(n.downPol)} POL</span>`:'<span class="mtp-down mtp-down-none">no downline yet</span>';
return `<div class="mtp-card${focus?' mtp-focus':''}" style="cursor:default">${qmark}${badge} <span class="mt-id">#${n.id}</span><span class="mtp-lvl">${esc(n.levelName)}</span><span class="mtp-sub">${n.directCount}/2 · ${fmt(n.earnedPol)} POL</span>${down}</div>`;
}
function renderTree(root){
if(!root){document.getElementById('dTree').innerHTML='<div class="empty">Team view is still indexing — check back in a few minutes.</div>';return}
const rows=[[root]];
for(let g=1;g<4;g++)rows.push(rows[g-1].flatMap(n=>n?[n.left||null,n.right||null]:[null,null]));
if(!rows[3].some(Boolean))rows.pop();
document.getElementById('dTree').innerHTML=`<div class="mtp">${rows.map((r,i)=>`<div class="mtp-row">${r.map(n=>card(n,i===0&&n&&true)).join('')}</div>`).join('')}</div>`;
}
function render(d){
prompt.classList.add('hidden');dash.classList.remove('hidden');
document.getElementById('dTitle').textContent='#'+d.id;
const qb=document.getElementById('dQualified');
if(qb)qb.outerHTML=d.directCount>=2
?'<span id="dQualified" class="q-badge q-yes">★ Qualified</span>'
:`<span id="dQualified" class="q-badge q-no">${2-d.directCount} more direct${2-d.directCount===1?'':'s'} to qualify</span>`;
document.getElementById('dFacts').innerHTML=
`<div class="fact"><small>Tier</small><strong>${esc(d.tierName)}</strong></div>`+
`<div class="fact"><small>Level</small><strong>${esc(d.levelName)}</strong></div>`+
`<div class="fact"><small>Directs</small><strong>${d.directCount}/2${d.directCount>=2?' ✓ qualified':''}</strong></div>`+
`<div class="fact"><small>Total received</small><strong style="color:var(--ok)">${fmt(d.totalEarnedPol)} POL</strong></div>`+
`<div class="fact"><small>Team below you</small><strong>${d.subtree?`${d.subtree.downCount} member${d.subtree.downCount===1?'':'s'}`:'—'}</strong></div>`+
`<div class="fact"><small>Member since</small><strong>${date(d.joinedAt)}</strong></div>`;
renderTree(d.subtree);
const inc=d.income||[];
document.getElementById('dIncome').innerHTML=inc.length
?`<div class="table-wrap"><table class="table" style="min-width:520px"><thead><tr><th>When</th><th>From member</th><th>For</th><th>Amount</th></tr></thead><tbody>${inc.map(p=>`<tr><td>${date(p.ts)}</td><td>#${p.fromId}</td><td>${esc(p.desc)}</td><td><strong>${fmt(p.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div>`
:'<div class="empty">No payments yet — they appear here the moment they land on-chain.</div>';
document.getElementById('dLineage').innerHTML=d.uplineChain&&d.uplineChain.length
?`<strong style="color:var(--text)">#${d.id}</strong> → ${d.uplineChain.map(i=>'#'+i).join(' → ')} <span style="color:#8498aa">(root)</span>`
:'You are at the top of your line.';
}
async function load(id){
try{
const r=await fetch('/api/public/member?id='+id);
const d=await r.json();
if(!r.ok)throw new Error(d.error||'Lookup failed');
if(!d.registered)throw new Error(`ID ${id} isn't registered on the smart contract — double-check the number.`);
try{localStorage.setItem('ctb.myId',String(id))}catch(e){}
if(!/^\/my\/\d+$/.test(location.pathname))history.replaceState(null,'','/my/'+id);
render(d);
}catch(x){
prompt.classList.remove('hidden');dash.classList.add('hidden');
document.getElementById('idError').textContent=x.message;
}
}
document.getElementById('idForm').addEventListener('submit',e=>{
e.preventDefault();
const v=document.getElementById('memberId').value.trim();
if(!/^\d{1,15}$/.test(v)){document.getElementById('idError').textContent='Numbers only — the ID from the RM Circle dApp.';return}
load(v);
});
document.getElementById('switchId').addEventListener('click',()=>{
try{localStorage.removeItem('ctb.myId')}catch(e){}
history.replaceState(null,'','/my');
dash.classList.add('hidden');prompt.classList.remove('hidden');
document.getElementById('memberId').value='';
});
const id=pathId();
if(id)load(id);
})();