f0ea4469a4
Fixes the spillover leak where a qualified member promoting their own link had joins land back on themselves (bonus reward) instead of qualifying their team. - /api/public/member now returns joinTarget: unqualified member -> self; qualified -> next-to-qualify in their leg (reuses rotation-aligned nextInLine); whole leg qualified -> global rotation sponsor; else -> spillover (prior behavior) - /join page: the Join button, "join under" header, sponsor-ID copy, and the submitted sponsorId all follow joinTarget; a note explains "invited by #X, joining to help #Y qualify" so the new member knows their real sponsor - Member's SHARE link is unchanged (/join/<id>); resolution happens when a visitor opens it, so it self-corrects even for links already in the wild Dry-run verified: /join/27 -> #34, /join/34 -> #34 (self), /join/21 -> #34. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
7.0 KiB
JavaScript
101 lines
7.0 KiB
JavaScript
// Personal invite page (/join/<id>): the full bridge-page story, personalized
|
||
// to one sponsor. Pulls the sponsor's live position via /api/public/member,
|
||
// pins their ID on every CTA, and submits new IDs with sponsorId locked to
|
||
// the page. Prospects who land on a bad/unregistered ID get routed to /start
|
||
// (the team rotation) instead of a dead end.
|
||
(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'}):'—';
|
||
|
||
function pathId(){const m=location.pathname.match(/^\/join\/(\d+)$/);return m?m[1]:''}
|
||
const id=pathId();
|
||
let joinTargetId=id; // where the entry actually lands after moving-link routing
|
||
|
||
function setIds(idStr){
|
||
document.getElementById('invId').textContent='#'+idStr;
|
||
document.getElementById('invIdJoin').textContent='#'+idStr;
|
||
document.getElementById('invIdBtn').textContent='#'+idStr;
|
||
document.querySelectorAll('.inv-id-inline').forEach(el=>el.textContent='#'+idStr);
|
||
}
|
||
|
||
function fallbackToRotation(msg){
|
||
const facts=document.getElementById('invFacts');
|
||
facts.innerHTML=`<div class="empty" style="grid-column:1/-1">${esc(msg)} <a href="/start" style="color:var(--teal)">Open the team rotation instead →</a></div>`;
|
||
const jb=document.getElementById('joinBtn');jb.textContent='Open the Team Sponsor Page →';jb.href='/start';jb.removeAttribute('target');
|
||
}
|
||
|
||
async function load(){
|
||
if(!id){fallbackToRotation('This invite link is missing its member ID.');return}
|
||
setIds(id);
|
||
try{
|
||
const r=await fetch('/api/public/member?id='+id);
|
||
const d=await r.json();
|
||
if(!r.ok||!d.registered)throw new Error('This invite ID isn’t registered on the smart contract.');
|
||
render(d);
|
||
}catch(x){fallbackToRotation(x.message)}
|
||
}
|
||
|
||
function render(d){
|
||
const q=document.getElementById('invQualified');
|
||
q.outerHTML=d.directCount>=2
|
||
?'<span id="invQualified" class="q-badge q-yes">★ Qualified</span>'
|
||
:`<span id="invQualified" class="q-badge q-no">Building — ${d.directCount}/2 directs</span>`;
|
||
document.getElementById('invFacts').innerHTML=
|
||
`<div class="fact"><small>Level</small><strong>${esc(d.levelName)}</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 them</small><strong>${d.subtree?`${d.subtree.downCount} member${d.subtree.downCount===1?'':'s'}`:'—'}</strong></div>`+
|
||
`<div class="fact"><small>Directs</small><strong>${d.directCount}/2</strong></div>`+
|
||
`<div class="fact"><small>Tier</small><strong>${esc(d.tierName)}</strong></div>`+
|
||
`<div class="fact"><small>Member since</small><strong>${date(d.joinedAt)}</strong></div>`;
|
||
// Qualified sponsor: still a valid join (doctrine: late signups pay them
|
||
// and spill down), but surface the team wave so nobody's surprised.
|
||
// Moving-link routing: the entry lands on whoever the smart link resolves to.
|
||
// A qualified inviter's link routes to the next-to-qualify in their leg, so
|
||
// the join builds the team down in order instead of spilling onto the inviter.
|
||
const t=d.joinTarget||{id:d.id,referralUrl:d.referralUrl,reason:'self'};
|
||
joinTargetId=t.id;
|
||
const jb=document.getElementById('joinBtn');
|
||
if(t.referralUrl)jb.href=t.referralUrl;
|
||
// the join CTA + "join under" header reflect the ACTUAL sponsor, not the inviter
|
||
document.getElementById('invIdBtn').textContent='#'+t.id;
|
||
document.getElementById('invIdJoin').textContent='#'+t.id;
|
||
const note=document.getElementById('invQualNote');
|
||
if(t.reason==='leg'||t.reason==='global'){
|
||
note.innerHTML=`<div class="callout" style="margin-top:14px">You were invited by <strong>Member #${esc(d.id)}</strong>, who's already <strong>qualified</strong>. So your entry goes to <strong>Member #${esc(t.id)}</strong> — the next spot in the team that needs its 2 — and you'll join directly under them. Same team, filling the next open position in order (no spillover). Enter sponsor <strong>#${esc(t.id)}</strong> in the dApp.</div>`;
|
||
}else if(d.directCount>=2){
|
||
note.innerHTML=`<div class="callout" style="margin-top:14px">This position is already <strong>qualified</strong> — joining here still works: your entry pays them and you're placed in their team leg as depth.</div>`;
|
||
}
|
||
const cp=document.getElementById('copySponsor');
|
||
cp.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(String(joinTargetId));cp.textContent='Copied ✓';setTimeout(()=>cp.textContent='Copy Sponsor ID',1400)}catch(e){}});
|
||
}
|
||
|
||
document.getElementById('joinForm').addEventListener('submit',async e=>{
|
||
e.preventDefault();
|
||
const form=e.currentTarget,msg=document.getElementById('joinMsg');
|
||
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 r=await fetch('/api/public/submit-id',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({newId,memberName,sponsorId:joinTargetId||id||'?',source:'invite-'+(id||'?'),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>${esc(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>${esc(newId)}</strong> is registered under sponsor <strong>#${d.onchain.referrerId}</strong> (${esc(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>${esc(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}
|
||
});
|
||
|
||
// matrix dots (same as bridge)
|
||
for(const pid of ['p8','p16']){
|
||
const node=document.getElementById(pid);if(!node)continue;
|
||
const count=pid==='p8'?8:16;for(let i=0;i<count;i++){const s=document.createElement('span');s.className='person';node.appendChild(s)}
|
||
}
|
||
load();
|
||
})();
|