diff --git a/public/join.js b/public/join.js index a895d62..961a3c4 100644 --- a/public/join.js +++ b/public/join.js @@ -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 @@ `
Member since${date(d.joinedAt)}
`; // 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=`
This position is already qualified — 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 Member #${d.nextInLine.id} get their 2 — either way, you're on the same team.
`; - } + // 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=`
You were invited by Member #${esc(d.id)}, who's already qualified. So your entry goes to Member #${esc(t.id)} — 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 #${esc(t.id)} in the dApp.
`; + }else if(d.directCount>=2){ + note.innerHTML=`
This position is already qualified — joining here still works: your entry pays them and you're placed in their team leg as depth.
`; + } 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'); diff --git a/server.js b/server.js index 3e5baae..cf7ee5d 100644 --- a/server.js +++ b/server.js @@ -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'});