Files
rm-circle-team-router/public/join.js
T
martbost 629df6336c Wire the join funnel to the live /join-now self-enroll page
/join-now now honors ?ref=<id> (resolves that member's moving-link joinTarget;
falls back to the global rotation sponsor). Repointed: /start 'Join With Current
Sponsor' -> /join-now; /my pitch buttons -> /join-now?ref=<id>; invite page
/join/<id> join button -> /join-now?ref=<id>; bare /join redirect -> /join-now.
'Get Started' learn CTAs still go to /start for onboarding. When the flag is off,
/join-now gracefully redirects to /start, so the wiring is safe either way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 07:37:19 -05:00

104 lines
7.2 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.
// 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=>({'&':'&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'}):'—';
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');
jb.href='/join-now?ref='+id; jb.removeAttribute('target'); // self-enroll page, placed in this leg
// the join CTA + "join under" header + dApp-sponsor mentions reflect the
// ACTUAL sponsor the entry lands on, not the inviter (only the top
// "invited by #X" line keeps the inviter's id)
document.getElementById('invIdBtn').textContent='#'+t.id;
document.getElementById('invIdJoin').textContent='#'+t.id;
document.querySelectorAll('.inv-id-inline').forEach(el=>el.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();
})();