147 lines
10 KiB
JavaScript
147 lines
10 KiB
JavaScript
// 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=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[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 direct=new URLSearchParams(location.search).get('direct')==='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.
|
||
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'}
|
||
};
|
||
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+(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'});
|
||
});
|
||
})();
|