Personal invite pages (/join/<id>) + auto-count queue directs from the chain
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>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// 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();
|
||||
})();
|
||||
Reference in New Issue
Block a user