29e0d6e791
Prospects who clicked a member's shared /my link landed on a bare stats dashboard with no explanation of the program. New /join/<id> page carries the full bridge-page story (video, strategy, matrix, live on-chain payout proof) personalized to one sponsor: their live position stats as proof, their ID pinned on the dApp join button, and the submit-your-ID form locked to them (source invite-<id>). Dashboard share buttons + QR codes now hand out the invite page; /my stays the member dashboard. Invalid/unregistered invite ids fall back to the /start rotation. Tracked as new 'join' event with an admin traffic column. Also: the chain indexer now auto-increments a rotation sponsor's directs when a registration names them as referrer (same op as the admin ⊕), announcing it on Telegram. At 2/2 it alerts to Qualify instead of auto-rotating — moving the team focus stays a human decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
90 lines
6.0 KiB
JavaScript
90 lines
6.0 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();
|
||
|
||
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.
|
||
const note=document.getElementById('invQualNote');
|
||
if(d.directCount>=2&&d.nextInLine){
|
||
note.innerHTML=`<div class="callout" style="margin-top:14px">This position is already <strong>qualified</strong> — joining here still works (you'll be placed in their team leg and your entry pays them directly). The team's current focus is helping <a href="/join/${d.nextInLine.id}" style="color:var(--teal)">Member #${d.nextInLine.id}</a> get their 2 — either way, you're on the same team.</div>`;
|
||
}
|
||
const jb=document.getElementById('joinBtn');
|
||
if(d.referralUrl)jb.href=d.referralUrl;
|
||
const cp=document.getElementById('copySponsor');
|
||
cp.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(String(d.id));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: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();
|
||
})();
|