72c8f137ae
The old copy claimed qualified links must be retired to avoid "extra shallow legs" — factually wrong for this contract: extra signups on a qualified link spill DOWNWARD (BFS) as depth, and the referrer keeps earning the full entry reward on every signup with no cap at 2. Updated the strategy flow, team-principle notice, strategy card, start-page callout, chatbot (canned + AI prompt), training article, and the qualified share card: the team play is still helping your 2 get their 2 (only own directs qualify a position), but continued personal promotion is accurately described as paid depth-building, and spillover recipients earn only when the spilled member upgrades — if qualified and leveled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
154 lines
14 KiB
JavaScript
154 lines
14 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 isSpill(n){return !!(n&&treeUI.parent[n.id]&&n.referrerId&&n.referrerId!==treeUI.parent[n.id])}
|
||
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 spill=isSpill(n)?`<span class="mtp-spill" title="Placed here by spillover — referred by #${n.referrerId}">↧ spillover · ref #${n.referrerId}</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>${spill}${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)`:''}${isSpill(n)?` · <span style="color:var(--teal)">↧ spillover (ref #${n.referrerId})</span>`:''}</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();
|
||
// spillover explainer — shown exactly when the confusing state exists:
|
||
// positions sitting under you while you're not yet qualified
|
||
const note=document.getElementById('dSpillNote');
|
||
if(note){
|
||
const kids=d.subtree?[d.subtree.left,d.subtree.right].filter(Boolean):[];
|
||
const spills=kids.filter(k=>k.referrerId&&k.referrerId!==d.id).length;
|
||
if(d.directCount<2&&kids.length&&spills){
|
||
note.innerHTML=`<div class="callout" style="margin-top:14px"><strong>Why am I not qualified when people sit under me?</strong> ${spills===kids.length?'The positions':'Some positions'} under you arrived by <strong>spillover</strong> — when your upline recruits, the contract fills the next open slot downward, which can land members in your matrix. Spillover grows your team and your future level payments, but qualification only counts your <strong>directs</strong> — people who join using <em>your</em> ID. You have ${d.directCount}/2 directs; share your link above to get ${2-d.directCount===1?'your last one':'your 2'}.</div>`;
|
||
note.classList.remove('hidden');
|
||
}else{note.classList.add('hidden');note.innerHTML='';}
|
||
}
|
||
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> The team play now 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. (Your own link still works too — extra signups pay you the entry reward and spill down your leg as depth — but only #${nx.id}'s own directs can qualify them.)</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);
|
||
})();
|