Files
rm-circle-team-router/public/join.js
T
2026-09-01 09:40:07 -05:00

171 lines
12 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();
// Direct link (?direct=1): opt out of moving-link rotation — new joins land
// directly under THIS position (spillover in their team), instead of routing
// to the next-to-qualify member. Default (no flag) = the Team/moving link.
const _dq=new URLSearchParams(location.search).get('direct');
// ?direct=1 forces direct placement, ?direct=0 forces rotation. With
// neither, the position's own configured default decides (applied once
// the member data arrives, below).
let direct=_dq==='1';
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.
if(_dq!=='1'&&_dq!=='0'&&d.directDefault) direct=true;
const t=direct?{id:id,reason:'direct'}:(d.joinTarget||{id:d.id,referralUrl:d.referralUrl,reason:'self'});
joinTargetId=t.id;
const jb=document.getElementById('joinBtn');
jb.href='/join-now?ref='+id+(direct?'&direct=1':''); 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(direct){
note.innerHTML=`<div class="callout" style="margin-top:14px">You're joining <strong>directly under Member #${esc(id)}</strong> — a spillover placement in their team. Enter sponsor <strong>#${esc(id)}</strong> in the dApp.</div>`;
}else 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();
})();
// Angle-matched landing: ?v=<hook> renders a NON-DISTRACTING squeeze step —
// matched headline, the hook video, one proceed button. Nothing else. Clicking
// through loads the full invite page (same path, no query); attribution
// (angle:<hook>) is already stamped in sessionStorage by track.js.
(function(){
var ANGLES={
pocket:{h:'You already <span class="gold">found the money</span>.',
v:'/v/rmc-pocket-change-b403fb2f75.mp4',p:'/v/rmc-pocket-change-poster.jpg'},
phone:{h:'Time your phone started <span class="gold">paying YOU</span>.',
v:'/v/rmc-phone-32ac648844.mp4',p:'/v/rmc-phone-poster.jpg'},
two:{h:'You know two people.<br><span class="gold">That’s all this takes.</span>',
v:'/v/rmc-two-people-4102f2d719.mp4',p:'/v/rmc-two-people-poster.jpg'},
graveyard:{h:'This one runs on <span class="gold">teamwork</span> — not your transmission.',
v:'/v/rmc-graveyard-290fabee9d.mp4',p:'/v/rmc-graveyard-poster.jpg'},
stopwaiting:{h:'Stop <span class="gold">waiting</span>. Start <span class="gold">receiving</span>.',
v:'/v/rmc-combined-16284edbab.mp4',p:'/v/rmc-combined-poster.jpg'}
};
var a=ANGLES[new URLSearchParams(location.search).get('v')];
if(!a)return;
var main=document.querySelector('main');
if(!main)return;
// clear the stage: hide everything, including the nav's competing buttons
Array.prototype.forEach.call(main.children,function(el){el.style.display='none';});
var nav=document.querySelector('.nav-actions'); if(nav)nav.style.display='none';
// The brand mark stays as a signature, but "Premium Team Build" is insider
// vocabulary the hook hasn't earned yet, and the homepage link is an exit
// ramp off the sponsor's /join path.
var tag=document.getElementById('brandProgram'); if(tag)tag.style.display='none';
var brand=document.querySelector('.brand'); if(brand){brand.removeAttribute('href');brand.style.cursor='default';}
// Footer: keep the disclaimer text and the legal links only.
Array.prototype.forEach.call(document.querySelectorAll('.footer-links a'),function(l){
if(!/^\/(disclaimer|privacy|refunds)$/.test(l.getAttribute('href')||''))l.style.display='none';
});
// payouts.js holds its proof toasts until the hook video has finished.
document.documentElement.setAttribute('data-squeeze','1');
var s=document.createElement('section');
s.className='hero wrap';
// Headline, video and button must all sit above the fold: the video is
// sized from the viewport height (nav + headline + button ≈ 360px), and
// the card shrinks to fit it rather than holding the full-page 860px width.
s.style.padding='16px 0 28px';
s.innerHTML='<h1 style="font-size:clamp(30px,4.4vw,54px);letter-spacing:-1.5px;line-height:1.02;margin:0 auto 12px">'+a.h+'</h1>'+
'<div class="video-card" style="width:fit-content;max-width:min(860px,100%);margin:0 auto">'+
'<video id="hookPlayer" class="video-frame" style="display:block;width:auto;max-width:100%;height:auto;max-height:calc(100vh - 360px);min-height:180px;aspect-ratio:16/9" controls preload="metadata" poster="'+a.p+'" src="'+a.v+'"></video></div>'+
'<div class="hero-actions" style="margin-top:16px"><a id="hookCta" class="btn btn-primary" style="font-size:18px;padding:14px 30px;font-weight:800" href="'+location.pathname+(new URLSearchParams(location.search).get('direct')==='1'?'?direct=1':'')+'">Show me how it works →</a></div>'+
'<div class="micro" style="margin-top:14px">Independent team training resource • Participation involves risk • No income is guaranteed</div>';
main.appendChild(s);
var hp=document.getElementById('hookPlayer'),cta=document.getElementById('hookCta');
if(hp&&cta)hp.addEventListener('ended',function(){
cta.style.boxShadow='0 0 0 4px rgba(243,190,67,.45)';
cta.scrollIntoView({behavior:'smooth',block:'center'});
document.documentElement.removeAttribute('data-squeeze');
document.dispatchEvent(new Event('rmc:hookended'));
});
})();