Files
rm-circle-team-router/public/join.js
T

141 lines
9.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.
// 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();
})();
// 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'}
};
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';
var s=document.createElement('section');
s.className='hero wrap';
s.innerHTML='<h1 style="margin-top:10px">'+a.h+'</h1>'+
'<div class="video-card" style="max-width:860px;margin:22px auto 0">'+
'<video id="hookPlayer" class="video-frame" style="display:block;width:100%;height:auto;aspect-ratio:16/9" controls preload="metadata" poster="'+a.p+'" src="'+a.v+'"></video></div>'+
'<div class="hero-actions" style="margin-top:26px"><a id="hookCta" class="btn btn-primary" style="font-size:19px;padding:16px 34px;font-weight:800" href="'+location.pathname+'">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'});
});
})();