Files
rm-circle-team-router/public/my.js
T
martbost 800397cbdb Beef up /my cold-visitor "Start here" panel for promoted-link traffic
The dashboard serves two audiences — the member and prospects who click a
promoted link. Expanded the cold-visitor intro into a proper "New here? Start
here" panel at the top: what it is + 3 get-started steps + CTAs (join under this
member / watch training / strategy), and reframes the member detail below as
live proof. Member's own view still hides it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 06:45:43 -05:00

339 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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 ''}}
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');
// cold-visitor context banner: shown unless this looks like the member's own
// dashboard (their stored ID matches) or they dismissed it this session
try{
const intro=document.getElementById('coldIntro');
const seenBefore=d._priorId===String(d.id); // they've viewed this position before (likely their own)
const dismissed=sessionStorage.getItem('ctb.introDismissed')==='1';
if(intro){
if(!seenBefore&&!dismissed){intro.classList.remove('hidden');const cij=document.getElementById('coldIntroJoin');cij.href='/join/'+d.id;cij.textContent='See how to join under #'+d.id+' →';}
else intro.classList.add('hidden');
}
}catch(e){}
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>';
renderNextStep(d);
renderPipeline(d);
renderAlerts(d);
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 ''}
}
const LEVELS=['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona'];
// "My Next Step" — the single clearest action + the level ladder + a funded
// badge (server tells us funded true/false; it never sends the raw balance).
function renderNextStep(d){
const el=document.getElementById('dNextStep');if(!el)return;
const ns=d.nextStep||{},lvl=d.level||1;
const ladder=LEVELS.map((nm,i)=>{
const L=i+1;let cls='nlv';
if(L<lvl)cls+=' done';else if(L===lvl)cls+=' you';else if(ns.kind==='upgrade'&&L===ns.nextLevel)cls+=' next';
const tag=L===lvl?'<span class="nlvt">YOU</span>':(ns.kind==='upgrade'&&L===ns.nextLevel?'<span class="nlvt">NEXT</span>':'');
return `<div class="${cls}">${tag}<span class="nlvn">${nm}</span></div>`;
}).join('');
let head='',body='',badge='';
if(ns.kind==='qualify'){
head='Your next step — get qualified';
body=`You need <strong>${ns.need} more direct${ns.need===1?'':'s'}</strong> (people who join on <em>your</em> link) to unlock upgrade payments. Grab your link from <a href="#dShare" style="color:var(--teal)">Share this position</a> below and go get them.`;
}else if(ns.kind==='upgrade'){
const nm=LEVELS[ns.nextLevel-1]||('Level '+ns.nextLevel);
head=`Your next step — upgrade to ${esc(nm)}`;
body=`Stay one level ahead of your deepest active team so the pass-ups from below land on <em>you</em> instead of skipping past. Upgrade with your earned POL when you can.`;
if(ns.funded===true)badge='<span class="ns-badge ok">✓ Funded — you can upgrade now</span>';
else if(ns.funded===false)badge='<span class="ns-badge no">Keep building — not funded yet</span>';
}else if(ns.kind==='max'){
head="You're at the top level 🏆";
body='Keep helping your team duplicate — every level they climb still pays up to you.';
}else{el.innerHTML='';return;}
el.innerHTML=`<div class="ns-card"><div class="ns-top"><div class="ns-eyebrow">Your plan</div>${badge}</div><div class="ns-h">${head}</div><p class="ns-p">${body}</p><div class="nladder">${ladder}</div></div>`;
}
function renderPipeline(d){
const el=document.getElementById('dPipeline');
if(!el)return;
if(!d.subtree||(!d.subtree.left&&!d.subtree.right)){el.innerHTML='<div class="empty">Your pipeline starts when your first team members are placed below you.</div>';return}
if(!d.upgradeCosts){el.innerHTML='<div class="empty">Pipeline data is loading — check back in a minute.</div>';return}
const up=d.upgradeCosts;
// walk the subtree with depth: a member at depth D pays THIS position when
// they buy the upgrade OUT of level D (cost index D-1) — first in line
const ready=[],building=[],passedCount={n:0};
(function walk(n,depth){
if(!n||depth>16)return;
const lvl=n.level||1,tier=n.tier===2?2:1;
if(depth>=1){
if(lvl===depth){
const amt=(up[tier]||[])[depth-1];
if(amt)ready.push({id:n.id,depth,tier,amt,toLevel:LEVELS[depth]||('Level '+(depth+1)),needLevel:depth});
}else if(lvl<depth)building.push({id:n.id,depth,steps:depth-lvl});
else passedCount.n++;
}
walk(n.left,depth+1);walk(n.right,depth+1);
})(d.subtree,0);
ready.sort((a,b)=>a.depth-b.depth||a.id-b.id);
const eligible=r=>d.directCount>=2&&(d.level||1)>=r.needLevel;
const readyTotal=ready.filter(eligible).reduce((s,r)=>s+r.amt,0);
let html='';
if(ready.length){
html+=`<p style="margin:0 0 8px;font-weight:800;color:var(--ok)">Ready now — one upgrade from your wallet:</p>`;
html+=ready.slice(0,10).map(r=>{
const warn=!eligible(r)?(d.directCount<2?' <span style="color:var(--danger)">⚠ needs you qualified (2 directs)</span>':` <span style="color:var(--danger)">⚠ needs you at ${esc(LEVELS[r.needLevel-1])}+</span>`):'';
return `<div class="pp-row" style="padding:10px 14px"><div class="pp-icon">⏳</div><div class="pp-body"><strong>#${r.id}</strong> <span class="pp-meta" style="display:inline">· gen ${r.depth} · their ${esc(r.toLevel)} upgrade pays you <strong style="color:var(--gold)">${fmt(r.amt)} POL</strong>${warn}</span></div></div>`;
}).join('');
if(ready.length>10)html+=`<p class="micro" style="margin:6px 0 0">…and ${ready.length-10} more.</p>`;
if(readyTotal>0){
const myNext=(up[d.tier===2?2:1]||[])[(d.level||1)-1];
const fundNote=myNext?` Your next upgrade (${esc(LEVELS[d.level]||'max')}) costs ${fmt(myNext)} — ${readyTotal>=myNext?'the pipeline above covers it':'these payments put you '+fmt(readyTotal)+' toward it'}.`:'';
html+=`<p style="margin:12px 0 0;color:var(--muted)"><strong style="color:var(--text)">${fmt(readyTotal)} POL</strong> is one upgrade away from your wallet.${fundNote}</p>`;
}
}else html+='<p style="margin:0 0 8px;color:var(--muted)">Nobody is at their pay-you milestone yet — the members below are still climbing toward it.</p>';
if(building.length)html+=`<p class="micro" style="margin:10px 0 0">${building.length} more member${building.length===1?' is':'s are'} building toward their pay-you level${passedCount.n?`; ${passedCount.n} already passed theirs`:''}. Helping your leg upgrade IS your income.</p>`;
else if(passedCount.n)html+=`<p class="micro" style="margin:10px 0 0">${passedCount.n} member${passedCount.n===1?' has':'s have'} already passed their pay-you milestone.</p>`;
html+=`<div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap;align-items:center"><button id="dCopySummary" class="btn btn-secondary btn-sm">📋 Copy plain-English summary</button><span id="dCopyMsg" class="micro" style="margin:0"></span></div>`;
el.innerHTML=html;
const cs=document.getElementById('dCopySummary');
if(cs)cs.addEventListener('click',async()=>{
try{await navigator.clipboard.writeText(pipelineSummaryText(d));document.getElementById('dCopyMsg').textContent='Copied — paste it into your chat.';setTimeout(()=>{document.getElementById('dCopyMsg').textContent=''},4000);}catch(e){document.getElementById('dCopyMsg').textContent='Copy failed — long-press to select instead.'}
});
}
// Plain-English, chat-ready explainer of a member's upgrade pipeline — the
// "will I miss upgrades?" question answered from their real position.
function pipelineSummaryText(d){
const LV=LEVELS, up=(d.upgradeCosts&&d.upgradeCosts[d.tier===2?2:1])||[];
const money=n=>Math.round(n).toLocaleString();
const gens={};
(function walk(n,depth){if(!n||depth>8)return;if(depth>=1)(gens[depth]=gens[depth]||[]).push(n);walk(n.left,depth+1);walk(n.right,depth+1);})(d.subtree,0);
const myLevel=d.level||1, team=(d.subtree&&d.subtree.downCount)||0;
const L=[];
L.push(`RM Circle — your position #${d.id}, in plain English 👇`);
L.push('');
L.push(d.directCount>=2
? `✅ YOUR SPOT: ${d.tierName}, ${d.levelName} level, qualified (2/2 directs). ${team} ${team===1?'person':'people'} in your team so far.`
: `⚠️ YOUR SPOT: ${d.tierName}, ${d.levelName} level — NOT qualified yet (${d.directCount}/2 directs). Get ${2-d.directCount} more direct${2-d.directCount===1?'':'s'} of your own first — that's the ONE thing that unlocks everything below.`);
L.push('');
L.push(`HOW UPGRADES PAY YOU (the one rule that clears up all the confusion):`);
L.push(`Every layer of your team pays you ONCE, at ONE level. The people directly under you pay you when they upgrade to Ascensus. The next layer down pays you at Fabrica. The layer below that at Culmen. Each layer deeper pays you at the next level up the ladder.`);
L.push('');
L.push(`To CATCH each layer's payment, you only need to be that level yourself (and stay qualified). Your ladder right now:`);
const maxG=Math.min(6,Math.max(2,...Object.keys(gens).map(Number).concat([2])));
for(let g=1;g<=maxG;g++){
const amt=up[g-1]; if(!amt)continue;
const buys=LV[g]||'the top'; const need=LV[g-1];
const have=d.directCount>=2&&myLevel>=g;
const cnt=(gens[g]||[]).length;
L.push(`• Layer ${g}${cnt?` (${cnt} ${cnt===1?'person':'people'})`:''}: their ${buys} upgrade pays you ${money(amt)} POL each — needs you at ${need}${have?' ✅ you already are':' ← get here to catch it'}`);
}
L.push('');
L.push(`WILL YOU MISS THEM? Only if your team climbs past a level before you do. But nobody skips levels — they go up one at a time — so you always get warning. Stay one step ahead of your team's growth and you catch every single one. Fall behind and that one payment passes you to the next person up (it doesn't come back), so the whole game is simply: keep your level ahead of your deepest active layer.`);
L.push('');
// next concrete action
const myNext=up[myLevel-1];
const nextName=LV[myLevel]||'max level';
// shallowest layer you can't yet catch
let gap=0;
for(let g=1;g<=8;g++){ if((gens[g]||[]).length && !(d.directCount>=2&&myLevel>=g)){ gap=g; break; } }
// is anyone in the gap layer already close (at/near their pay-you level)?
const gapClose=gap&&(gens[gap]||[]).some(n=>(n.level||1)>=gap-1);
if(d.directCount<2) L.push(`YOUR NEXT MOVE: get your 2 directs. Until then none of this pipeline can pay you — spillover fills your team but only your own 2 qualify you.`);
else if(gap) L.push(`YOUR NEXT MOVE: your next uncovered layer is layer ${gap} — to catch their ${LV[gap]||'top'} upgrades you'll need to be at ${LV[gap-1]} (${myNext?money(myNext)+' POL':''}). They're still climbing toward it${gapClose?', and getting close — worth upgrading soon':", so no rush — just get there before they do"}. You're already eligible for everything shallower.`);
else if(myNext) L.push(`YOUR NEXT MOVE: you're currently eligible for every active layer. As your team goes deeper, upgrade to ${nextName} (${money(myNext)} POL) before that new layer climbs to its pay-you level.`);
L.push('');
L.push(`See it live and get a red alert the moment you'd miss one: rmcircle.team/my/${d.id}`);
return L.join('\n');
}
async function renderAlerts(d){
const el=document.getElementById('dAlerts');
if(!el)return;
let st={subscribed:false};
try{st=await (await fetch('/api/public/alert-status?id='+d.id)).json();}catch(e){}
function subscribedView(email){
el.innerHTML=`<p style="margin:0 0 10px"><span class="live-badge"><span class="dot"></span> Alerts ON</span> for <strong>${esc(email||'your email')}</strong></p><button id="alertOff" class="btn btn-secondary btn-sm">Turn off alerts</button><span id="alertMsg" class="micro" style="margin-left:8px"></span>`;
document.getElementById('alertOff').addEventListener('click',async()=>{
try{await fetch('/api/public/alert-signup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,email:''})});renderAlerts(d);}catch(e){document.getElementById('alertMsg').textContent='Try again.';}
});
}
function offView(){
el.innerHTML=`<form id="alertForm" style="display:flex;gap:8px;flex-wrap:wrap;max-width:460px"><input name="email" class="input" type="email" placeholder="you@example.com" required style="flex:1;min-width:200px"><button class="btn btn-primary btn-sm">Turn on alerts</button></form><div id="alertMsg" class="micro" style="margin-top:8px"></div>`;
document.getElementById('alertForm').addEventListener('submit',async e=>{
e.preventDefault();
const email=e.currentTarget.elements.email.value.trim(),msg=document.getElementById('alertMsg');
msg.style.color='var(--muted)';msg.textContent='Turning on…';
try{
const r=await fetch('/api/public/alert-signup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,email})});
const j=await r.json();
if(!r.ok)throw new Error(j.error||'Failed');
msg.style.color='var(--ok)';msg.textContent='Done — check your inbox for a confirmation.';
setTimeout(()=>renderAlerts(d),1200);
}catch(x){msg.style.color='var(--danger)';msg.textContent=x.message;}
});
}
if(st.subscribed)subscribedView(st.email);else offView();
}
function renderShare(d){
const el=document.getElementById('dShare');
if(!el)return;
const dashUrl=`https://rmcircle.team/join/${d.id}`;
if(d.directCount>=2){
const nx=d.nextInLine;
if(nx){
const nxDash=`https://rmcircle.team/join/${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 <strong>invite page</strong> (the full team-build pitch, personalized to them) or send prospects the join button, which carries <strong style="color:var(--gold)">#${nx.id}</strong> as sponsor. (If someone still joins through your own link, that's a bonus — the entry reward is yours and they spill down your leg — 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, share your <strong>personal invite page</strong> — the link and QR below. A prospect who opens it gets the full team-build story (video, strategy, live blockchain payouts) with <strong style="color:var(--gold)">your ID #${d.id}</strong> pinned as sponsor and your position's real results as proof. This dashboard stays yours; the invite page is what you share.</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 from the team's focus and promotes the next position in your leg. Any late signup on your own link is a bonus — it pays you and spills down as depth.</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{d._priorId=localStorage.getItem('ctb.myId');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;
}
}
const introX=document.getElementById('coldIntroClose');
if(introX)introX.addEventListener('click',()=>{try{sessionStorage.setItem('ctb.introDismissed','1')}catch(e){};document.getElementById('coldIntro').classList.add('hidden')});
document.getElementById('dJoinForm').addEventListener('submit',async e=>{
e.preventDefault();
const form=e.currentTarget,msg=document.getElementById('dJoinMsg');
const newId=form.elements.newId.value.trim(),memberName=form.elements.memberName.value.trim();
if(!/^\d{1,10}$/.test(newId)){msg.style.color='var(--danger)';msg.textContent='Numbers only — the ID shown by the RM Circle dApp.';return}
if(!memberName){msg.style.color='var(--danger)';msg.textContent='Add your name or Telegram handle.';return}
msg.style.color='var(--muted)';msg.textContent='Verifying on the blockchain…';
try{
const pageId=(location.pathname.match(/^\/my\/(\d+)$/)||[])[1]||'?';
const r=await fetch('/api/public/submit-id',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({newId,memberName,sponsorId:pageId,source:'dashboard-'+pageId,clickid:window.ctbGetClickId?window.ctbGetClickId():''})});
const d=await r.json();
if(!r.ok)throw new Error(d.error||'Submission failed');
form.classList.add('hidden');
msg.style.color='var(--ok)';
const dash=`<br><a href="/my/${newId}" style="color:var(--gold)">Open your own dashboard →</a>`;
if(d.duplicate)msg.innerHTML=`✓ ID <strong>${newId}</strong> was already submitted — you're on the list.${dash}`;
else if(d.onchain&&d.onchain.registered)msg.innerHTML=`✓ Verified on the blockchain! ID <strong>${newId}</strong> is registered under sponsor <strong>#${d.onchain.referrerId}</strong> (${d.onchain.tier}). The team has been notified — welcome aboard.${dash}`;
else if(d.path==='notfound'){msg.style.color='var(--danger)';msg.innerHTML=`⚠ ID <strong>${newId}</strong> isn't on the smart contract yet — double-check the number from the RM Circle dApp. Your submission is saved and the team will verify it manually.`;}
else msg.innerHTML=`✓ Got it — submitted. The team will verify your placement.${dash}`;
}catch(x){msg.style.color='var(--danger)';msg.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);
})();