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:
+16
-11
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user