Member identity, promo tools, back-office color, seeded inventory

Real people, not numbers: usernames (unique, profile pane to set them),
shown across the ledger, activity, rosters, and the sidebar chip; vanity
invite links (/join/<username>) with late chain binding intact. Promo tools
pane ships Branded Voice share posts and an email swipe personalized with
each member's link. The earn viewer excludes a member's own campaigns, so
nobody earns from their own spend. Back office gains the cyan/violet/amber
accent family over the green base. Three house campaigns seeded so every
surface shows live inventory. Assets v=20260905i.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-05 06:28:17 -05:00
parent 28ed925443
commit e22021c266
11 changed files with 257 additions and 53 deletions
+16 -11
View File
@@ -52,10 +52,11 @@ window.IAP = (function () {
const el = $('navWallet');
if (!el) return;
if (me.signedIn) {
// email-only members have no wallet address yet
const who = me.address
? '<span class="mono">' + me.address.slice(0, 6) + '…' + me.address.slice(-4) + '</span>'
: (me.email ? String(me.email).replace(/[&<>]/g, '') : 'signed in');
// identity order: username, then email, then wallet
const who = me.username
? '<b>@' + String(me.username).replace(/[&<>]/g, '') + '</b>'
: (me.email ? String(me.email).replace(/[&<>]/g, '')
: (me.address ? '<span class="mono">' + me.address.slice(0, 6) + '…' + me.address.slice(-4) + '</span>' : 'signed in'));
el.innerHTML = (me.memberId ? '<span class="badge">member #' + me.memberId + '</span> ' : '') + who;
} else {
el.innerHTML = '<a href="/my">Sign in</a>';
@@ -65,17 +66,21 @@ window.IAP = (function () {
}
function describeEvent(ev, c) {
const pol = w => fmtPol(w) + ' POL';
// real people, not numbers: use usernames when the site knows them
const nm = id => (ev.names && ev.names[id])
? String(ev.names[id]).replace(/[&<>]/g, '')
: 'member #' + id;
switch (ev.type) {
case 'Purchase': return '🧾 member #' + ev.buyerId + ' bought package #' + ev.productId
case 'Purchase': return '🧾 ' + nm(ev.buyerId) + ' bought package #' + ev.productId
+ ' (' + fmtUsd(ev.priceCents) + ') for ' + pol(ev.paidWei) + ' → +' + ev.creditAmount.toLocaleString() + ' credits';
case 'TierPaid': return '💸 level ' + ev.tier + ' payout → member #' + ev.recipientId + ': ' + pol(ev.amountWei)
case 'TierPaid': return '💸 level ' + ev.tier + ' payout → ' + nm(ev.recipientId) + ': ' + pol(ev.amountWei)
+ (ev.hops ? ' (passed up ' + ev.hops + ')' : '');
case 'PassedUp': return '↷ level ' + ev.tier + ' passed over #' + ev.skippedId + ' (' + ev.reason + ')';
case 'PassedUp': return '↷ level ' + ev.tier + ' passed over ' + nm(ev.skippedId) + ' (' + ev.reason + ')';
case 'AdminPaid': return '🏛 platform fee settled: ' + pol(ev.amountWei);
case 'BuyerCounted': return '⭐ member #' + ev.sponsorId + ' now has ' + ev.newCount + ' qualifying buyer(s)';
case 'MemberActivated': return '👤 member #' + ev.id + ' activated a payout wallet';
case 'AwardPaid': return '🎁 award: ' + pol(ev.amountWei) + ' → member #' + ev.toId;
case 'CreditsConsumed': return '📣 member #' + ev.memberId + ' ran ads: −' + ev.amount.toLocaleString() + ' credits';
case 'BuyerCounted': return '⭐ ' + nm(ev.sponsorId) + ' now has ' + ev.newCount + ' qualifying buyer(s)';
case 'MemberActivated': return '👤 ' + nm(ev.id) + ' activated a payout wallet';
case 'AwardPaid': return '🎁 award: ' + pol(ev.amountWei) + ' → ' + nm(ev.toId);
case 'CreditsConsumed': return '📣 ' + nm(ev.memberId) + ' ran ads: −' + ev.amount.toLocaleString() + ' credits';
case 'PriceCached': return '🔮 oracle price refreshed';
case 'FallbackPriceUsed': return '🔮 cached price bridged an oracle gap';
default: return '· ' + ev.type;
+57 -8
View File
@@ -45,7 +45,7 @@
if (t) t.remove();
t = document.createElement('table');
t.className = 'roster';
t.innerHTML = d.referrals.map(r => '<tr><td>' + r.email + '</td>'
t.innerHTML = d.referrals.map(r => '<tr><td>' + String(r.name || r.email || '').replace(/[&<>]/g, '') + '</td>'
+ '<td>' + new Date(r.joined).toLocaleDateString() + '</td>'
+ '<td><span class="badge' + (r.status === 'joined free' ? ' amber' : '') + '">' + r.status + '</span></td></tr>').join('');
wrap.appendChild(t);
@@ -72,8 +72,9 @@
rows.slice(0, 6).forEach(r => ov.appendChild(r));
}
} catch (e) {}
// ready-to-send share message
const link = location.origin + '/join/' + (d.refCode || d.memberId || '');
// ready-to-send share message + promo tools, personalized
const link = location.origin + '/join/' + (d.username || d.refCode || d.memberId || '');
fillPromo(link);
if (d.refCode || d.memberId) {
const pitch = 'I found an advertising site that pays referrals instantly to your own wallet. '
+ 'No withdrawals, no waiting, and every payment is public on a blockchain ledger you can check yourself. '
@@ -90,9 +91,9 @@
}
// ── back-office menu: hash-routed panes ───────────────
const PANES = ['overview', 'line', 'buy', 'campaigns', 'earn', 'earnings', 'wallet'];
const PANES = ['overview', 'line', 'buy', 'campaigns', 'earn', 'earnings', 'promo', 'wallet', 'profile'];
const TITLES = { overview: 'Overview', line: 'My line', buy: 'Buy packages', campaigns: 'Campaigns',
earn: 'Earn credits', earnings: 'Earnings', wallet: 'Wallet & account' };
earn: 'Earn credits', earnings: 'Earnings', promo: 'Promo tools', wallet: 'Wallet & account', profile: 'Profile' };
function setPane(name) {
if (!PANES.includes(name)) name = 'overview';
for (const p of PANES) {
@@ -148,9 +149,15 @@
$('campaignCard').hidden = false;
loadCampaigns();
// the share link works from day one; codes resolve to your chain id later
if (me.refCode || me.memberId) {
$('inviteLine').textContent = location.origin + '/join/' + (me.refCode || me.memberId);
// profile pane state
$('pfCurrent').textContent = me.username ? 'Current username: @' + me.username : 'No username yet. Members see you as a number until you pick one.';
if (!$('pfUsername').value) $('pfUsername').value = 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');
// the share link works from day one; usernames make it a vanity link
if (me.username || me.refCode || me.memberId) {
$('inviteLine').textContent = location.origin + '/join/' + (me.username || me.refCode || me.memberId);
$('copyInvite').hidden = false;
} else {
$('inviteLine').textContent = 'Sign in with your email to get your link.';
@@ -233,6 +240,48 @@
// defers the busy() lookup to click time (busy is declared below)
function busy2(btn, fn) { return (...a) => busy(btn, fn)(...a); }
// ── promo tools: Branded Voice copy, personalized with the member link ──
const PROMO_POSTS = [
'A membership site where money is handled by code, not people. Every purchase splits instantly to sponsor wallets on the Polygon blockchain. Nothing to withdraw. The money just lands in your wallet. Plus you earn ad credits for viewing ads while you\'re there. {{LINK}}',
'No withdrawal button. Think about that. A smart contract on Polygon splits every payment the second it hits. 50% to the sponsor. 20% to the next level. 10% to the next. Lands straight in your own wallet. No button to push. No waiting. Just money where it belongs. See how it works: {{LINK}}',
'Every payment is public on the blockchain. You can watch the ledger move in real time. Every split, every wallet, every transaction. Nothing hidden. Nothing you have to take on faith. That\'s the difference between a platform that asks for trust and one where trust isn\'t needed. See for yourself: {{LINK}}'
];
const PROMO_SWIPE = {
subject: 'Your Wallet Gets Paid Instantly..',
body: 'You know the usual drill. Someone buys on your link, you wait for a payout. Maybe days. Maybe an approval hold. Maybe a "your account is under review."\n\nInstantAdPay doesn\'t work like that.\n\nA smart contract on the Polygon blockchain handles every purchase the second it happens. 50% to the sponsor. 20% to the next level. 10% to the one after that. 20% to the platform. Each split lands directly in your own wallet. No withdrawal button. No "request payout." No approval queue.\n\nThe money just shows up.\n\nYou can watch every transaction on the public ledger. Real time. Anyone can verify it.\n\nFree to join. Packages from $5 to $250. No income promises. It\'s advertising, not investing.\n\n{{LINK}}'
};
function promoBlock(text) {
const div = document.createElement('div');
div.className = 'promo-block';
div.textContent = text;
const btn = document.createElement('button');
btn.className = 'btn small sec';
btn.textContent = 'Copy';
btn.addEventListener('click', async () => {
try { await navigator.clipboard.writeText(text); IAP.status('Copied. Paste it anywhere.', 'ok'); }
catch (e) { IAP.status('Copy failed. Select the text instead.', 'bad'); }
});
div.appendChild(btn);
return div;
}
function fillPromo(link) {
const posts = $('promoPosts');
if (!posts || posts.dataset.filled === link) return;
posts.dataset.filled = link;
posts.innerHTML = '';
for (const p of PROMO_POSTS) posts.appendChild(promoBlock(p.replace('{{LINK}}', link)));
const sw = $('promoSwipeWrap');
sw.innerHTML = '';
sw.appendChild(promoBlock('Subject: ' + PROMO_SWIPE.subject + '\n\n' + PROMO_SWIPE.body.replace('{{LINK}}', link)));
}
// ── profile ── (busy2 defers the busy lookup past its TDZ)
$('pfSaveBtn').addEventListener('click', busy2($('pfSaveBtn'), async () => {
const r = await api('/api/my/profile', { username: $('pfUsername').value });
IAP.status('You are @' + r.account.username + ' now.', 'ok');
await render();
}));
// ── in-dashboard package buying ──
const PKG = { 1: 'Micro', 2: 'Activation', 3: 'Builder', 4: 'Growth', 5: 'Leader' };
async function loadBuyTiles() {
+20
View File
@@ -9,6 +9,7 @@
--ink:#eef7f3; --muted:#8ba69c;
--mint:#43e8c3; --mint-hi:#8ffbe3; --mint-ink:#03211a;
--mint-soft:rgba(67,232,195,.09); --bad:#ff8f7d;
--cyan:#54ccff; --violet:#9d7dff; --amber:#ffb238;
--mono:"Consolas","JetBrains Mono",monospace;
--disp:"Sora","Segoe UI",system-ui,sans-serif;
--radius:18px;
@@ -257,6 +258,25 @@ input:focus,select:focus{border-color:var(--mint)}
#boBurger{display:block}
.bo-content{padding:18px}
}
/* back-office accent family: green leads, cyan/violet/amber season the cards */
.bo .stats .stat:nth-child(2) .n{color:var(--cyan)}
.bo .stats .stat:nth-child(2)::before{background:linear-gradient(90deg,transparent,var(--cyan),transparent)}
.bo .stats .stat:nth-child(3) .n{color:var(--violet)}
.bo .stats .stat:nth-child(3)::before{background:linear-gradient(90deg,transparent,var(--violet),transparent)}
.bo .stats .stat:nth-child(4) .n{color:var(--amber)}
.bo .stats .stat:nth-child(4)::before{background:linear-gradient(90deg,transparent,var(--amber),transparent)}
.bo .card h3::before{content:"";display:inline-block;width:9px;height:9px;border-radius:2.5px;
background:var(--mint);margin-right:10px;transform:rotate(45deg);vertical-align:1px}
#pane-line .card h3::before{background:var(--cyan)}
#pane-buy .card h3::before,#pane-campaigns .card h3::before{background:var(--amber)}
#pane-earn .card h3::before,#pane-earnings .card h3::before{background:var(--violet)}
#pane-promo .card h3::before{background:var(--cyan)}
#pane-wallet .card h3::before,#pane-profile .card h3::before{background:var(--mint)}
.bo .card{background:linear-gradient(165deg,rgba(24,36,31,.6),rgba(13,19,17,.66))}
#pane-line .qualbar,#nextCard{border-left:3px solid rgba(67,232,195,.4)}
.promo-block{background:rgba(4,8,7,.55);border:1px solid var(--line);border-radius:12px;
padding:14px 16px;margin:0 0 12px;font-size:13.5px;line-height:1.6;white-space:pre-wrap}
.promo-block .btn{margin-top:10px}
/* back-office polish: pane transitions, quick actions */
@media(prefers-reduced-motion:no-preference){
.pane:not([hidden]){animation:panein .25s ease}