Self-rotating personal links: qualified member's join routes to next-to-qualify
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>
This commit is contained in:
+18
-7
@@ -10,6 +10,7 @@
|
||||
|
||||
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;
|
||||
@@ -49,14 +50,24 @@
|
||||
`<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.
|
||||
const note=document.getElementById('invQualNote');
|
||||
if(d.directCount>=2&&d.nextInLine){
|
||||
note.innerHTML=`<div class="callout" style="margin-top:14px">This position is already <strong>qualified</strong> — joining here still works (you'll be placed in their team leg and your entry pays them directly). The team's current focus is helping <a href="/join/${d.nextInLine.id}" style="color:var(--teal)">Member #${d.nextInLine.id}</a> get their 2 — either way, you're on the same team.</div>`;
|
||||
}
|
||||
// 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(d.referralUrl)jb.href=d.referralUrl;
|
||||
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(d.id));cp.textContent='Copied ✓';setTimeout(()=>cp.textContent='Copy Sponsor ID',1400)}catch(e){}});
|
||||
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=>{
|
||||
@@ -67,7 +78,7 @@
|
||||
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:id||'?',source:'invite-'+(id||'?'),clickid:window.ctbGetClickId?window.ctbGetClickId():''})});
|
||||
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');
|
||||
|
||||
@@ -363,6 +363,28 @@ async function handleApi(req,res,pathname){
|
||||
}catch(e){}
|
||||
}
|
||||
if(r.nextInLine)r.nextInLine.referralUrl=`${getConfig().dappReferralBaseUrl}${encodeURIComponent(r.nextInLine.id)}`;
|
||||
// Moving-link routing: where a join through THIS member's personal link
|
||||
// actually lands. Not-yet-qualified -> their own position (still filling
|
||||
// their 2). Qualified -> the next-to-qualify in their leg (nextInLine,
|
||||
// already aligned to the curated rotation above), so their promotion
|
||||
// builds the team down in order instead of spilling onto themselves.
|
||||
// Whole leg qualified -> the global rotation sponsor. Last resort -> their
|
||||
// own position (spillover, the prior behavior).
|
||||
if(r.registered){
|
||||
const base=getConfig().dappReferralBaseUrl;
|
||||
if((r.directCount||0)<2){
|
||||
r.joinTarget={id,referralUrl:r.referralUrl,reason:'self'};
|
||||
}else if(r.nextInLine){
|
||||
r.joinTarget={id:r.nextInLine.id,referralUrl:r.nextInLine.referralUrl,invitedBy:id,reason:'leg',directCount:r.nextInLine.directCount};
|
||||
}else{
|
||||
let g=null;try{g=activeSponsor(getSponsors());}catch(e){}
|
||||
if(g&&String(g.id)!==String(id)&&(g.directs||0)<2){
|
||||
r.joinTarget={id:g.id,referralUrl:`${base}${encodeURIComponent(g.id)}`,invitedBy:id,reason:'global',directCount:g.directs};
|
||||
}else{
|
||||
r.joinTarget={id,referralUrl:r.referralUrl,reason:'spillover'};
|
||||
}
|
||||
}
|
||||
}
|
||||
memberCache.set(id,{data:r,ts:Date.now()});
|
||||
if(memberCache.size>500)memberCache.delete(memberCache.keys().next().value);
|
||||
return json(res,200,r,{'Cache-Control':'public, max-age=60'});
|
||||
|
||||
Reference in New Issue
Block a user