Files
rm-circle-team-router/public/start.js
T
martbost 096c12ab46 Fix scope bug that dropped the referral from the join button
The placement flag was declared inside the invited-visitor branch but read
further down when building the join button href, so it threw a
ReferenceError there. The display was right — the page named the inviter
correctly — but the button fell back to a bare /join-now, which is the one
place that actually matters.

Caught by walking the live paths rather than trusting the earlier pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 11:30:30 -05:00

118 lines
8.6 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.
let currentSponsor=null;
// Who invited this visitor, if anyone. A prospect who arrived on /join/<id> and then tapped
// a nav link used to land here and be offered the COMPANY rotation position instead — the
// inviter lost the referral just because the prospect clicked "Training" first. The invite
// is remembered in the rmc_ref cookie (set when the invite page was served) and can also be
// passed explicitly as ?ref=<id>.
function invitedBy(){
try{
const q=new URLSearchParams(location.search).get('ref');
if(q&&/^\d{1,15}$/.test(q))return q;
const m=document.cookie.match(/(?:^|;\s*)rmc_ref=(\d{1,15})/);
if(m)return m[1];
}catch(e){}
return null;
}
function placementChoice(){
try{
const q=new URLSearchParams(location.search).get('direct');
if(q==='1'||q==='0')return q;
const m=document.cookie.match(/(?:^|;\s*)rmc_direct=([01])/);
if(m)return m[1];
}catch(e){}
return null;
}
async function load(){
try{
const ref=invitedBy();
// an explicit ?direct=1/0 from the invite link wins over the position's default, and it
// has to be in scope for the join button further down — not just the display block
const dq=placementChoice();
const [cr,sr]=await Promise.all([
fetch('/api/public/config'),
fetch(ref?('/api/public/member?id='+encodeURIComponent(ref)):'/api/public/current-sponsor')
]);
const c=await cr.json(); const s=await sr.json();
if(!sr.ok)throw new Error(s.error||'No sponsor assigned');
if(ref&&s.registered){
// Route exactly the way that member's own invite page would: their moving link sends
// the join to the next position in their leg, unless their position is set to take
// direct placements. Same rules, so the two pages can never disagree.
const goDirect=dq==='1'||(dq!=='0'&&(s.directDefault||!s.joinTarget));
const t=goDirect?{id:s.id,referralUrl:s.referralUrl,directCount:s.directCount}:s.joinTarget;
currentSponsor={id:t.id,name:null,directs:(t.directCount!==undefined?t.directCount:0),goal:2,referralUrl:t.referralUrl};
// say whose team this is, so it never reads as an anonymous "current placement"
const badge=document.querySelector('.live-badge');
if(badge){
const dot=badge.querySelector('.dot');
badge.textContent=' '+(String(t.id)===String(ref)?('Invited by Member #'+ref):('Member #'+ref+"'s team"));
if(dot)badge.insertBefore(dot,badge.firstChild);
}
const jb=document.getElementById('joinButton');
if(jb)jb.textContent=(String(t.id)===String(ref)?('Join Member #'+ref+"'s Team →"):('Join '+'#'+ref+"'s Team →"));
const note=document.getElementById('queueText');
if(note&&String(t.id)!==String(ref)){
note.textContent='Member #'+ref+' is already qualified, so your entry fills the next open spot in their team (#'+t.id+').';
}
}else{
currentSponsor=s.sponsor;
}
if(c.siteName){document.title=`Get Started | ${c.siteName}`;const bn=document.getElementById('brandName');if(bn)bn.textContent=c.siteName}
document.getElementById('sponsorId').textContent=`ID ${currentSponsor.id}`;
document.getElementById('sponsorIdInline').textContent=currentSponsor.id;
document.getElementById('sponsorName').textContent=currentSponsor.name||'Current RM Circle placement';
document.getElementById('progressText').textContent=`${currentSponsor.directs} / 2`;
document.getElementById('progressBar').style.width=`${Math.min(100,(currentSponsor.directs/2)*100)}%`;
document.getElementById('entryPol').textContent=c.premiumEntryPol;
document.getElementById('entryPol2').textContent=c.premiumEntryPol;
if(c.polUsd>0){
var usd=Math.round(c.premiumEntryPol*c.polUsd);
['entryUsd','entryUsd2'].forEach(function(id){var el=document.getElementById(id);if(el)el.textContent=' (~$'+usd+' today — POL’s price moves)';});
}
// carry the inviter through to enrolment — /join-now already knows how to place a
// ?ref properly (direct placement or next open spot in their leg). Without this the
// button dropped the referral on the floor and enrolled under the company rotation.
document.getElementById('joinButton').href='/join-now'+(ref?('?ref='+encodeURIComponent(ref)+(dq?('&direct='+dq):'')):'');
{const dl=document.getElementById('dappJoinLink');if(dl&&currentSponsor.referralUrl){dl.href=currentSponsor.referralUrl;dl.classList.remove('hidden');}}
// the company-queue line only means anything on the company-rotation path
// only on the company-rotation path — the invited path already wrote its own line here
if(!ref)document.getElementById('queueText').textContent=(c.showQueueProgress&&s.waitingCount!==undefined)
?`${s.waitingCount} team placement${s.waitingCount===1?'':'s'} waiting behind the current sponsor.`:'';
document.getElementById('supportBox').textContent=c.supportLabel||'Contact your team sponsor if you need help before joining.';
if(c.telegramUrl){const w=document.getElementById('supportLinkWrap'),a=document.getElementById('supportLink');a.href=c.telegramUrl;w.classList.remove('hidden')}
}catch(e){
document.getElementById('sponsorName').textContent=e.message;
document.getElementById('joinButton').classList.add('hidden');
document.getElementById('copyButton').classList.add('hidden');
}
}
document.getElementById('joinButton').addEventListener('click',()=>{try{if(currentSponsor)sessionStorage.setItem('ctb.joinedSponsor',currentSponsor.id)}catch(e){};fetch('/api/public/join-click',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({source:window.ctbGetSource?window.ctbGetSource():'(direct)',clickid:window.ctbGetClickId?window.ctbGetClickId():''})}).catch(()=>{})});
document.getElementById('idSubmitForm').addEventListener('submit',async e=>{
e.preventDefault();
const form=e.currentTarget,input=form.elements.newId,msg=document.getElementById('idSubmitMsg');
const newId=input.value.trim();
const memberName=form.elements.memberName.value.trim();
if(!/^[0-9]{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 so the team can reach you.';return}
let joinedSponsor='';try{joinedSponsor=sessionStorage.getItem('ctb.joinedSponsor')||''}catch(e){}
const sponsorId=joinedSponsor||(currentSponsor?currentSponsor.id:'?');
msg.style.color='var(--muted)';msg.textContent='Submitting…';
try{
const r=await fetch('/api/public/submit-id',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({newId,memberName,sponsorId,source:window.ctbGetSource?window.ctbGetSource():'(direct)',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 Member Dashboard →</a>`;
if(d.duplicate)msg.innerHTML=`✓ ID <strong>${newId}</strong> was already submitted — you're on the list.${dash}`;
else if(d.path==='rotation')msg.innerHTML=`✓ Verified on the blockchain! ID <strong>${newId}</strong> joined through the team rotation under sponsor <strong>#${d.onchain.referrerId}</strong> (${d.onchain.tier}). The team has been notified and you're in line for the rotation that gets <strong>your</strong> 2.${dash}`;
else if(d.path==='leg')msg.innerHTML=`✓ Verified on the blockchain! ID <strong>${newId}</strong> is registered under sponsor <strong>#${d.onchain.referrerId}</strong>'s team build (${d.onchain.tier}) — your placement is recorded and the team has been notified. Heads up: the sponsor shown on this page is the site's team rotation, a separate group effort — your own team hub is your dashboard.${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! ID <strong>${newId}</strong> is submitted. The team has been notified and will verify your placement.${dash}`;
}catch(x){msg.style.color='var(--danger)';msg.textContent=x.message}
});
document.getElementById('copyButton').addEventListener('click',async()=>{
if(!currentSponsor)return; await navigator.clipboard.writeText(currentSponsor.id); const b=document.getElementById('copyButton'); const old=b.textContent;b.textContent='Copied ✓';setTimeout(()=>b.textContent=old,1400)
});
load();