350d50798f
- "Your team" now covers the member's FULL leg with click-to-drill pyramid, breadcrumb, and a list-view toggle (mirrors the admin matrix). - Legend under both the member tree and admin matrix explaining the qualified checkmark, tier badges, and downline rollup. - "Share this position" card: while under 2/2 it's a personal recruiting tool — per-member QR (vendored qrcode-generator, CSP-safe), copy link, and a join CTA carrying the member's ID as sponsor. At 2/2 the page automatically retires the link and redirects sharing to the team rotator, so personal referrals can't undermine the moving-link strategy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
132 lines
10 KiB
JavaScript
132 lines
10 KiB
JavaScript
(function(){
|
||
const esc=s=>String(s??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[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 ''}}
|
||
|
||
const treeUI={byId:{},parent:{},homeId:0,rootId:0,mode:'pyramid'};
|
||
function indexTree(n,par){if(!n)return;treeUI.byId[n.id]=n;if(par)treeUI.parent[n.id]=par.id;indexTree(n.left,n);indexTree(n.right,n)}
|
||
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 `<button class="mtp-card${focus?' mtp-focus':''}" data-tid="${n.id}">${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}</button>`;
|
||
}
|
||
function renderPyramid(){
|
||
const out=document.getElementById('dTree'),nav=document.getElementById('dTreeNav');
|
||
const root=treeUI.byId[treeUI.rootId];
|
||
if(!root){out.innerHTML='<div class="empty">Team view is still indexing — check back in a few minutes.</div>';nav.innerHTML='';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();
|
||
out.innerHTML=`<div class="mtp">${rows.map((r,i)=>`<div class="mtp-row">${r.map(n=>card(n,i===0)).join('')}</div>`).join('')}</div>`;
|
||
const path=[];let cur=treeUI.rootId;
|
||
while(cur){path.unshift(cur);if(cur===treeUI.homeId)break;cur=treeUI.parent[cur]}
|
||
nav.innerHTML=path.length>1?`<span class="micro" style="margin:0">Viewing:</span> `+path.map((id,i)=>i===path.length-1?`<strong class="mt-id">#${id}</strong>`:`<button class="btn btn-secondary btn-sm" data-tid="${id}">#${id}</button>`).join(' <span style="color:var(--muted)">›</span> ')+` <button class="btn btn-secondary btn-sm" data-tid="${treeUI.parent[treeUI.rootId]}">↑ Up one</button>`:'';
|
||
const wire=el=>el.querySelectorAll('[data-tid]').forEach(b=>b.addEventListener('click',()=>{const t=Number(b.dataset.tid);if(t&&treeUI.byId[t]){treeUI.rootId=t;renderTree()}}));
|
||
wire(out);wire(nav);
|
||
}
|
||
function renderList(){
|
||
const out=document.getElementById('dTree');
|
||
document.getElementById('dTreeNav').innerHTML='';
|
||
const home=treeUI.byId[treeUI.homeId];
|
||
if(!home){out.innerHTML='<div class="empty">Team view is still indexing — check back in a few minutes.</div>';return}
|
||
const node=(n,depth)=>{
|
||
if(!n)return '';
|
||
const ch=[n.left,n.right].filter(Boolean);
|
||
const qmark=n.directCount>=2?'<span class="mtp-qmark" style="position:static;display:inline-grid;width:16px;height:16px;font-size:10px;vertical-align:middle">✓</span> ':'';
|
||
const badge=n.tier===2?'<span class="mt-badge mt-prem">P</span>':'<span class="mt-badge">S</span>';
|
||
const label=`${qmark}${badge} <span class="mt-id">#${n.id}</span> <span class="mt-meta">${esc(n.levelName)} · ${n.directCount}/2 directs · ${fmt(n.earnedPol)} POL${n.downCount?` · ⬇ ${n.downCount} below (${fmt(n.downPol)} POL)`:''}</span>`;
|
||
const kids=ch.length?`<ul class="mt-kids">${ch.map(c=>node(c,depth+1)).join('')}</ul>`:'';
|
||
return ch.length?`<li class="mt-node"><details${depth<3?' open':''}><summary>${label}</summary>${kids}</details></li>`:`<li class="mt-node mt-leaf">${label}</li>`;
|
||
};
|
||
out.innerHTML=`<ul class="mt-tree">${node(home,0)}</ul>`;
|
||
}
|
||
function renderTree(){
|
||
const tb=document.getElementById('dTreeToggle');
|
||
if(tb)tb.textContent=treeUI.mode==='pyramid'?'List view':'Pyramid view';
|
||
if(treeUI.mode==='pyramid')renderPyramid();else renderList();
|
||
}
|
||
document.getElementById('dTreeToggle').addEventListener('click',()=>{
|
||
treeUI.mode=treeUI.mode==='pyramid'?'list':'pyramid';
|
||
renderTree();
|
||
});
|
||
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>`;
|
||
treeUI.byId={};treeUI.parent={};treeUI.homeId=d.id;treeUI.rootId=d.id;
|
||
indexTree(d.subtree,null);
|
||
renderTree();
|
||
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>';
|
||
renderShare(d);
|
||
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.';
|
||
}
|
||
function qrSvg(url){
|
||
try{
|
||
const q=qrcode(0,'M');q.addData(url);q.make();
|
||
return q.createSvgTag({cellSize:4,margin:3,scalable:true});
|
||
}catch(e){return ''}
|
||
}
|
||
function renderShare(d){
|
||
const el=document.getElementById('dShare');
|
||
if(!el)return;
|
||
const dashUrl=`https://rmcircle.saasy.top/my/${d.id}`;
|
||
if(d.directCount>=2){
|
||
el.innerHTML=`<p style="color:var(--muted);font-size:14px;line-height:1.6;margin:6px 0 14px"><span class="q-badge q-yes" style="margin-right:6px">★ Qualified</span> This position has its 2 directs, so its referral link is retired under the team strategy. Send new members through the team rotation — the current sponsor is always live on the start page.</p><div style="display:flex;gap:16px;align-items:center;flex-wrap:wrap"><img src="/qr-start.svg" alt="Team QR code" class="qr-img"><div><a class="btn btn-primary" href="/start">Open the Team Sponsor Page →</a><p class="micro" style="margin:10px 0 0">This QR sends prospects to the team rotation — show it or print it.</p></div></div>`;
|
||
return;
|
||
}
|
||
const need=2-d.directCount;
|
||
el.innerHTML=`<p style="color:var(--muted);font-size:14px;line-height:1.6;margin:6px 0 14px">You need <strong style="color:var(--text)">${need} more direct${need===1?'':'s'}</strong> to qualify. Until then this is your personal recruiting page — share the link or let a prospect scan the code. They'll see this position's live results, and the join button below carries <strong style="color:var(--gold)">your ID #${d.id}</strong> as sponsor.</p><div style="display:flex;gap:16px;align-items:center;flex-wrap:wrap"><span id="dQr" class="qr-img"></span><div style="min-width:220px"><div style="display:flex;gap:8px;flex-wrap:wrap"><button id="copyShare" class="btn btn-secondary btn-sm">Copy my page link</button><a class="btn btn-primary btn-sm" href="${esc(d.referralUrl||'#')}" target="_blank" rel="noopener noreferrer">Join under #${d.id} →</a></div><p class="micro" style="margin:10px 0 0;word-break:break-all">${esc(dashUrl)}</p><p class="micro" style="margin:6px 0 0">When you reach 2/2 this page automatically retires your link and points new members to the team rotation.</p></div></div>`;
|
||
const qr=document.getElementById('dQr');if(qr)qr.innerHTML=qrSvg(dashUrl);
|
||
const cp=document.getElementById('copyShare');
|
||
if(cp)cp.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(dashUrl);cp.textContent='Copied ✓';setTimeout(()=>cp.textContent='Copy my page link',1400)}catch(e){}});
|
||
}
|
||
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);
|
||
})();
|