Sponsor to downline messaging: lineage view, daily broadcast (email+inbox), login modal

- Downline resolver (3 levels); email visible for directs only, username+ID deeper
- Broadcast composer (rich editor) to directs or full downline, 1/day cap, sends email + on-site inbox
- Upline messages inbox in My line; unmissable sign-in modal for unread, ack marks read
- messages.js dual-mode store; sponsor_messages table; sanitizeRich reused

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-06 08:39:06 -05:00
parent 289538dbeb
commit 7701a29169
14 changed files with 344 additions and 27 deletions
+61
View File
@@ -146,6 +146,7 @@
const tc = $('dbTeamChip'), wk = (d.referrals || []).filter(r => Date.now() - new Date(r.joined) < 6048e5).length;
if (tc && wk) { tc.hidden = false; tc.textContent = '+' + wk + ' this week'; }
setInboxBadge(d.inboxUnread || 0);
if (d.sponsorMsg) showSponsorModal(d.sponsorMsg);
loadCharts(d);
$('nextMove').textContent = nextMove(d);
renderSteps(d);
@@ -228,6 +229,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(); }
document.getElementById('memberArea').classList.remove('side-open'); // close mobile drawer
if (location.hash !== '#' + name) history.replaceState(null, '', '#' + name);
}
@@ -537,6 +539,65 @@
}
$('vidStartBtn').addEventListener('click', () => loadVideoAd());
// ── unmissable sponsor-message modal on sign-in ──
function showSponsorModal(msg) {
if (!$('msgModal')) return;
$('mmFrom').textContent = 'A message from ' + (msg.fromName || 'your sponsor');
$('mmSubject').textContent = msg.subject || '';
$('mmBody').innerHTML = msg.body || ''; // server-sanitized
$('msgModal').hidden = false;
$('mmAck').onclick = async () => {
$('msgModal').hidden = true;
try { await fetch('/api/my/messages/' + msg.id + '/read', { method: 'POST' }); } catch (e) {}
};
}
// ── downline lineage + sponsor broadcast + upline messages ──
async function loadLineage() {
try {
const r = await (await fetch('/api/my/line')).json();
const el = $('lineageWrap');
if (r.error || !r.levels || !r.levels.every) return;
if (!r.levels.length || !r.levels.some(L => L.members.length)) {
el.innerHTML = '<p class="muted small">No one in your downline yet. Share your link and it fills in here.</p>';
return;
}
el.innerHTML = r.levels.map(L => !L.members.length ? '' :
'<div class="lin-lvl"><div class="cap">Level ' + L.level + ' · ' + L.members.length + (L.level === 1 ? ' direct' : '') + '</div>'
+ L.members.map(m => '<div class="lin-row"><span class="nm">' + esc(m.name) + '</span>'
+ (m.email ? '<span class="em">' + esc(m.email) + '</span>' : '<span class="id">#' + m.memberId + '</span>')
+ '<span class="id" style="margin-left:auto">' + new Date(m.joined).toLocaleDateString() + '</span></div>').join('')
+ '</div>').join('');
} catch (e) {}
}
async function loadUplineMessages() {
try {
const r = await (await fetch('/api/my/messages')).json();
if (r.error) return;
const card = $('upMsgCard'), list = $('upList');
if (!r.items || !r.items.length) { card.hidden = true; return; }
card.hidden = false;
list.innerHTML = r.items.map(i => '<div class="promo-block" style="margin-bottom:10px">'
+ '<div style="display:flex;justify-content:space-between;gap:10px"><b>' + esc(i.subject) + '</b>'
+ '<span class="small muted">' + esc(i.fromName) + ' · ' + new Date(i.sent).toLocaleDateString() + '</span></div>'
+ '<div class="ib-rich" style="margin-top:8px">' + (i.body || '') + '</div></div>').join('');
// opening the pane marks them read
for (const i of r.items) if (!i.read) fetch('/api/my/messages/' + i.id + '/read', { method: 'POST' }).catch(() => {});
} catch (e) {}
}
// broadcast composer editor (its own small rich editor, server sanitizes)
document.querySelectorAll('[data-bc]').forEach(b =>
b.addEventListener('click', () => { $('bcEd').focus(); document.execCommand(b.dataset.bc, false, null); }));
document.querySelectorAll('[data-bcblock]').forEach(b =>
b.addEventListener('click', () => { $('bcEd').focus(); document.execCommand('formatBlock', false, b.dataset.bcblock); }));
if ($('bcLinkBtn')) $('bcLinkBtn').addEventListener('click', () => { const u = prompt('Link URL (https://…)'); if (u) { $('bcEd').focus(); document.execCommand('createLink', false, u); } });
if ($('bcSendBtn')) $('bcSendBtn').addEventListener('click', busy2($('bcSendBtn'), async () => {
const r = await api('/api/my/broadcast', { scope: $('bcScope').value, subject: $('bcSubject').value, body: $('bcEd').innerHTML });
IAP.status('Broadcast sent to ' + r.sent + ' member' + (r.sent === 1 ? '' : 's') + '.', 'ok');
$('bcSubject').value = ''; $('bcEd').innerHTML = '';
$('bcHint').textContent = 'Sent. You can send your next broadcast in 24 hours.';
}));
// ── solo-ads inbox: list, read view, dwell-gated read reward ──
let ibTimer = null;
function setInboxBadge(n) {