6a9cd96b8c
Instead of only pointing at the team rotator, a qualified member's share card now runs the moving-link wave automatically: breadth-first, left-to-right, the first position below them with fewer than 2 directs becomes "next in line" — their QR, page link, and join CTA all carry that position's ID. When it qualifies, the card rotates to the next one. Falls back to the team rotator when the whole leg is qualified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
141 lines
12 KiB
JavaScript
141 lines
12 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){
|
||
const nx=d.nextInLine;
|
||
if(nx){
|
||
const nxDash=`https://rmcircle.saasy.top/my/${nx.id}`;
|
||
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> Your link is retired — now the effort moves down. <strong style="color:var(--text)">Next in line: #${nx.id}</strong> (${nx.directCount}/2 directs). Help them get their 2 — share their page or send prospects the join button, which carries <strong style="color:var(--gold)">#${nx.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 #${nx.id}'s page link</button><a class="btn btn-primary btn-sm" href="${esc(nx.referralUrl||'#')}" target="_blank" rel="noopener noreferrer">Join under #${nx.id} →</a></div><p class="micro" style="margin:10px 0 0;word-break:break-all">${esc(nxDash)}</p><p class="micro" style="margin:6px 0 0">When #${nx.id} reaches 2/2 this rotates to the next position in your leg — the qualification wave keeps moving down. General traffic can still go through the <a href="/start" style="color:var(--teal)">team rotation</a>.</p></div></div>`;
|
||
const qr=document.getElementById('dQr');if(qr)qr.innerHTML=qrSvg(nxDash);
|
||
const cp=document.getElementById('copyShare');
|
||
if(cp)cp.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(nxDash);cp.textContent='Copied ✓';setTimeout(()=>cp.textContent=`Copy #${nx.id}'s page link`,1400)}catch(e){}});
|
||
return;
|
||
}
|
||
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 and everyone in your leg is qualified too — outstanding. 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);
|
||
})();
|