Coaching layer + tools: coach your directs (rungs, stalled, one-click nudges), automatic member nudges + weekly sponsor digest, prospects list, per-angle link stats, broadcast templates, Qualified Start calculator, Telegram proof feed, send-failed alerts, admin P&L pane, automatic credit burner (ethers), username lock, home-page comparison, printable checklist, wall link in Promo tools
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+93
-2
@@ -421,6 +421,10 @@
|
||||
if (d.username) { // wall link rides the username
|
||||
const wl = location.origin + '/wall/' + d.username;
|
||||
$('wallLine').textContent = wl;
|
||||
if ($('promoWallStrip')) { // the same wall link at the top of Promo tools, next to the invite link
|
||||
$('promoWallStrip').hidden = false; $('promoWallLink').textContent = wl; $('promoWallOpen').href = '/wall/' + d.username;
|
||||
$('promoWallCopy').onclick = async () => { try { await navigator.clipboard.writeText(wl); IAP.status('Wall link copied.', 'ok'); } catch (e) { IAP.status('Copy failed. Select the link and copy it.', 'bad'); } };
|
||||
}
|
||||
$('wallCopy').hidden = false;
|
||||
$('wallOpen').hidden = false;
|
||||
$('wallOpen').href = '/wall/' + d.username;
|
||||
@@ -460,7 +464,7 @@
|
||||
if ($('boTitle')) $('boTitle').textContent = TITLES[name];
|
||||
if (name === 'earn') setEarnSub(earnSub); // refresh whichever sub-tab is active
|
||||
if (name === 'profile') loadLineBanner();
|
||||
if (name === 'line') { loadLineage(); loadUplineMessages(); }
|
||||
if (name === 'line') { loadLineage(); loadUplineMessages(); loadCoach(); loadLinkStats(); loadProspects(); }
|
||||
if (name === 'campaigns') ['cTarget', 'cImage', 'cVideoUrl'].forEach(id => { if ($(id)) $(id).value = ''; }); // no residual URL between visits
|
||||
if (name === 'training') loadTraining();
|
||||
document.getElementById('memberArea').classList.remove('side-open'); // close mobile drawer
|
||||
@@ -525,8 +529,9 @@
|
||||
loadCampaigns();
|
||||
|
||||
// profile pane state
|
||||
$('pfCurrent').textContent = me.username ? 'Current username: @' + me.username : 'No username yet. Members see you as a number until you pick one.';
|
||||
$('pfCurrent').textContent = me.username ? '@' + me.username + ' is your permanent username. Your invite link, your public page and any banners you shared carry it, so it cannot be changed.' : 'No username yet. Members see you as a number until you pick one. Choose carefully: it is permanent once saved.';
|
||||
if (!$('pfUsername').value) $('pfUsername').value = me.username || '';
|
||||
$('pfUsername').disabled = !!me.username; $('pfSaveBtn').hidden = !!me.username;
|
||||
$('pfDetails').innerHTML = 'Email: ' + (me.email || 'none') + '<br>Wallet: '
|
||||
+ (me.address ? '<span class="mono">' + me.address.slice(0, 10) + '…' + me.address.slice(-6) + '</span>' : 'not linked yet')
|
||||
+ '<br>On-chain member: ' + (me.memberId ? '#' + me.memberId : 'not yet');
|
||||
@@ -961,6 +966,92 @@
|
||||
};
|
||||
}
|
||||
|
||||
// ── coaching: every direct's rung, stalled flag, one-click nudge ──
|
||||
async function loadCoach() {
|
||||
try {
|
||||
const r = await (await fetch('/api/my/coach')).json();
|
||||
const el = $('coachList'); if (!el || r.error) return;
|
||||
const d = r.directs || [];
|
||||
$('coachSummary').innerHTML = d.length ? '<b>' + d.length + '</b> direct' + (d.length === 1 ? '' : 's') + ' · <b>' + r.stalled + '</b> quiet for 3+ days' + (r.stalled ? ' · start at the top' : '') : '';
|
||||
if (!d.length) { el.innerHTML = '<p class="muted small">No directs yet. When someone joins through your link they show up here with their next step.</p>'; return; }
|
||||
el.innerHTML = d.map(x => '<div class="lin-row' + (x.stalled ? ' own' : '') + '"><span class="nm">' + esc(x.name) + (x.stalled ? ' <span class="badge amber">quiet ' + x.quietDays + 'd</span>' : '') + '</span>'
|
||||
+ '<span class="em">' + esc(x.label) + ' → ' + esc(x.next) + '</span>'
|
||||
+ '<span class="id">rung ' + x.rung + '/6</span>'
|
||||
+ '<span class="dt">' + (x.buyerCount ? x.buyerCount + ' buyer' + (x.buyerCount === 1 ? '' : 's') : '') + '</span>'
|
||||
+ '<button class="btn sec small" type="button" data-nudge="' + esc(x.email) + '" data-nname="' + esc(x.name) + '" data-say="' + esc(x.say) + '">Nudge</button></div>').join('');
|
||||
el.querySelectorAll('[data-nudge]').forEach(b => b.addEventListener('click', async () => {
|
||||
await openConvo(b.dataset.nudge, b.dataset.nname);
|
||||
const inp = $('chatInput'); if (inp) { inp.value = b.dataset.say.replace(/\{\{name\}\}/g, b.dataset.nname.replace(/^@/, '')); inp.focus(); }
|
||||
}));
|
||||
} catch (e) {}
|
||||
}
|
||||
// ── link stats: views, joins, buyers per angle ──
|
||||
async function loadLinkStats() {
|
||||
try {
|
||||
const r = await (await fetch('/api/my/linkstats')).json();
|
||||
const t = $('linkStatsTable'); if (!t || r.error) return;
|
||||
const rows = (r.angles || []).filter(a => a.views || a.joins || a.buyers);
|
||||
if (!rows.length) { t.innerHTML = '<tr><td class="muted small">No views yet. Share your link and the numbers start here.</td></tr>'; return; }
|
||||
t.innerHTML = '<tr><th>Hook</th><th>Views (30d)</th><th>Views (all)</th><th>Joined</th><th>Qualifying buyers</th></tr>'
|
||||
+ rows.map(a => '<tr><td>' + esc(a.angle === 'plain' ? 'plain link' : '?v=' + a.angle) + '</td><td class="mono">' + a.views30 + '</td><td class="mono">' + a.views + '</td><td class="mono">' + a.joins + '</td><td class="mono">' + a.buyers + '</td></tr>').join('');
|
||||
} catch (e) {}
|
||||
}
|
||||
// ── prospects: the member's own follow-up list ──
|
||||
let PP_STATUSES = ['new', 'contacted', 'interested', 'joined', 'bought', 'not now'];
|
||||
async function loadProspects() {
|
||||
try {
|
||||
const r = await (await fetch('/api/my/prospects')).json();
|
||||
if (r.error) return;
|
||||
PP_STATUSES = r.statuses || PP_STATUSES;
|
||||
const sel = $('ppStatus'); if (sel && !sel.options.length) sel.innerHTML = PP_STATUSES.map(s => '<option value="' + s + '">' + s + '</option>').join('');
|
||||
const list = r.prospects || []; const el = $('prospectList'); if (!el) return;
|
||||
if (!list.length) { el.innerHTML = '<p class="muted small">Nobody on the list yet.</p>'; return; }
|
||||
const today = new Date(); today.setHours(0, 0, 0, 0);
|
||||
el.innerHTML = list.map(p => { const due = p.nextTs && p.nextTs <= today.getTime() + 86399999; return '<div class="lin-row' + (due ? ' own' : '') + '" data-pid="' + p.id + '"><span class="nm">' + esc(p.name) + (due ? ' <span class="badge amber">follow up</span>' : '') + '</span>'
|
||||
+ '<span class="em">' + esc(p.contact || '') + (p.note ? ' · ' + esc(p.note) : '') + '</span>'
|
||||
+ '<select class="small" data-pstatus="' + p.id + '">' + PP_STATUSES.map(s => '<option' + (s === p.status ? ' selected' : '') + '>' + s + '</option>').join('') + '</select>'
|
||||
+ '<input type="date" class="small" data-pnext="' + p.id + '" value="' + (p.nextTs ? new Date(p.nextTs).toISOString().slice(0, 10) : '') + '">'
|
||||
+ '<button class="btn sec small" type="button" data-pdel="' + p.id + '">Remove</button></div>'; }).join('');
|
||||
const save = async (id, patch) => { const p = list.find(x => x.id === Number(id)); if (!p) return; try { await api('/api/my/prospects', Object.assign({}, p, patch)); } catch (e) { IAP.status(e.message, 'bad'); } };
|
||||
el.querySelectorAll('[data-pstatus]').forEach(s => s.addEventListener('change', () => save(s.dataset.pstatus, { status: s.value })));
|
||||
el.querySelectorAll('[data-pnext]').forEach(i => i.addEventListener('change', () => save(i.dataset.pnext, { next: i.value, nextTs: i.value ? Date.parse(i.value + 'T12:00:00') : null })));
|
||||
el.querySelectorAll('[data-pdel]').forEach(b => b.addEventListener('click', async () => { try { await api('/api/my/prospects/remove', { id: b.dataset.pdel }); loadProspects(); } catch (e) { IAP.status(e.message, 'bad'); } }));
|
||||
} catch (e) {}
|
||||
}
|
||||
if ($('prospectForm')) $('prospectForm').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api('/api/my/prospects', { name: $('ppName').value, contact: $('ppContact').value, status: $('ppStatus').value, nextTs: $('ppNext').value ? Date.parse($('ppNext').value + 'T12:00:00') : null });
|
||||
$('ppName').value = ''; $('ppContact').value = ''; $('ppNext').value = ''; loadProspects();
|
||||
} catch (err) { IAP.status(err.message, 'bad'); }
|
||||
});
|
||||
// ── broadcast templates ──
|
||||
const BC_TEMPLATES = [
|
||||
{ label: 'Welcome', subject: 'Welcome to my line: your first three moves', html: '<p>Glad you are in. Three things today, in this order:</p><ol><li>Pick your username on the Profile tab (it becomes your link).</li><li>Wallet tab: Connect and link wallet, then Switch on payouts. Both are free.</li><li>Copy your invite link from My line and send it to one person.</li></ol><p>Reply here if you get stuck on any of them. That is what I am here for.</p>' },
|
||||
{ label: 'Switch on payouts', subject: 'One free step so nothing passes you by', html: '<p>Quick reminder: if payouts are not switched on yet, do it now on the Wallet tab. One free transaction.</p><p>The contract locks each buyer to their sponsor at their first purchase, and payouts only route to wallets that are switched on. Ready early and you never miss one.</p>' },
|
||||
{ label: 'The $5 test', subject: 'See a payout land in real time', html: '<p>Want to see the whole thing work? Buy the $5 Micro package on Buy packages and watch the live ledger while you do it. You will see your credits mint and the split go out in the same transaction.</p><p>When you are ready to count as a qualifying buyer for me, the $20 Activation package is the one.</p>' },
|
||||
{ label: 'Qualified Start', subject: 'How to open level 2 today with your own positions', html: '<p>You can be your own first buyers, openly. On Buy packages, the Qualified Start card lets you link a second wallet you own as a position. When it buys a $20 package, it counts as a qualifying buyer, half comes straight back to your main wallet, and the credits pool with yours.</p><p>Two positions open level 2 the same day. The three Qualified Start videos in Training show every click.</p>' },
|
||||
{ label: 'Share your link', subject: 'One conversation a day is the whole job', html: '<p>Promo tools has posts, texts and emails that already carry your link. Pick one and send it to one person today.</p><p>Do not wait for the perfect moment. Nobody who waited ever built a line.</p>' }
|
||||
];
|
||||
(function () {
|
||||
const w = $('bcTemplates'); if (!w) return;
|
||||
w.innerHTML = BC_TEMPLATES.map((t, i) => '<button type="button" class="chip-t" data-bct="' + i + '">' + esc(t.label) + '</button>').join('');
|
||||
w.querySelectorAll('[data-bct]').forEach(b => b.addEventListener('click', () => { const t = BC_TEMPLATES[Number(b.dataset.bct)]; $('bcSubject').value = t.subject; $('bcEd').innerHTML = t.html; $('bcEd').focus(); }));
|
||||
})();
|
||||
// ── Qualified Start calculator ──
|
||||
(function () {
|
||||
const n = $('qcN'), pk = $('qcPkg'), out = $('qcOut'); if (!n || !pk || !out) return;
|
||||
const CR = { 20: 2000, 50: 5500, 100: 12000, 250: 32500 };
|
||||
const calc = () => {
|
||||
const k = Math.max(1, Math.min(10, Number(n.value) || 1)), usd = Number(pk.value) || 20;
|
||||
const gross = k * usd, back = gross / 2, net = gross - back, credits = k * (CR[usd] || 0);
|
||||
const level = k >= 5 ? 'Level 3 open (and level 2): the Nexus badge, wall position 3, full 50 / 20 / 10' : k >= 2 ? 'Level 2 open: 20% on your directs\' buyers, wall position 2' : 'Counts as one qualifying buyer. One more opens level 2.';
|
||||
out.innerHTML = '<div class="grid c2"><div><b>$' + gross + '</b> out across ' + k + ' position' + (k === 1 ? '' : 's') + '<br><b>$' + back + '</b> back to your main wallet in the same transactions (the 50% direct-sponsor share)<br><b>$' + net + '</b> net, plus a little POL for gas in each wallet</div>'
|
||||
+ '<div><b>' + credits.toLocaleString() + ' credits</b> pooled for your own ads<br>' + level + '<br><span class="muted">The 20% and 10% shares go to your upline if they are qualified, otherwise to the platform.</span></div></div>';
|
||||
};
|
||||
n.addEventListener('input', calc); pk.addEventListener('change', calc); calc();
|
||||
})();
|
||||
|
||||
// ── downline lineage + sponsor broadcast + upline messages ──
|
||||
async function loadLineage() {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user