Member dashboard: stat cards, next-move guidance, line roster, share kit
/api/my/dashboard aggregates credits, earned POL (from the chain index), qualification count, and the referral roster (privacy-masked emails, joined-free vs wallet-linked status, code+id referrer match). Members area leads with four stat cards, a next-move card with a qualification progress bar (2 and 5 buyer marks), the line roster, and a copy-ready share message beside the invite link. Assets v=20260904j. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+16
-1
@@ -82,6 +82,13 @@ const J = {
|
||||
async byEmail(e) { return pub(this.db.byEmail[e]); },
|
||||
async byAddress(a) { const e = this.db.byAddress[a]; return e ? pub(this.db.byEmail[e]) : null; },
|
||||
async byCode(c) { const e = this.db.byCode[c]; return e ? pub(this.db.byEmail[e]) : null; },
|
||||
async listByReferrer(refs) {
|
||||
const set = new Set(refs.filter(Boolean).map(String));
|
||||
return Object.values(this.db.byEmail)
|
||||
.filter(a => set.has(String(a.sponsorRef || '')))
|
||||
.sort((a, b) => b.created - a.created)
|
||||
.slice(0, 200).map(pub);
|
||||
},
|
||||
async linkWallet(e, a) {
|
||||
const acct = this.db.byEmail[e];
|
||||
if (!acct) return { error: 'No such account.' };
|
||||
@@ -133,6 +140,13 @@ const D = {
|
||||
async byEmail(e) { const r = await db.q('SELECT * FROM accounts WHERE email=?', [e]); return rowPub(r[0]); },
|
||||
async byAddress(a) { const r = await db.q('SELECT * FROM accounts WHERE address=?', [a]); return rowPub(r[0]); },
|
||||
async byCode(c) { const r = await db.q('SELECT * FROM accounts WHERE code=?', [c]); return rowPub(r[0]); },
|
||||
async listByReferrer(refs) {
|
||||
const clean = refs.filter(Boolean).map(String);
|
||||
if (!clean.length) return [];
|
||||
const rows = await db.q('SELECT * FROM accounts WHERE sponsor_ref IN (' + clean.map(() => '?').join(',')
|
||||
+ ') ORDER BY created DESC LIMIT 200', clean);
|
||||
return rows.map(rowPub);
|
||||
},
|
||||
async linkWallet(e, a) {
|
||||
const cur = await this.byEmail(e);
|
||||
if (!cur) return { error: 'No such account.' };
|
||||
@@ -166,6 +180,7 @@ async function ensure(email, sponsorRef) {
|
||||
async function byEmail(email) { return impl().byEmail(normEmail(email)); }
|
||||
async function byAddress(address) { return impl().byAddress(normAddr(address)); }
|
||||
async function byCode(code) { return impl().byCode(String(code || '').toLowerCase()); }
|
||||
async function listByReferrer(refs) { return impl().listByReferrer(refs || []); }
|
||||
async function linkWallet(email, address) {
|
||||
const a = normAddr(address);
|
||||
if (!/^0x[0-9a-f]{40}$/.test(a)) return { error: 'Bad wallet address.' };
|
||||
@@ -173,4 +188,4 @@ async function linkWallet(email, address) {
|
||||
}
|
||||
async function count() { return impl().count(); }
|
||||
|
||||
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, linkWallet, count };
|
||||
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, listByReferrer, linkWallet, count };
|
||||
|
||||
@@ -11,12 +11,60 @@
|
||||
return r;
|
||||
}
|
||||
|
||||
function nextMove(d) {
|
||||
if (!d.address) return 'Grab your link below and start sharing today. Then link your wallet (one free signature) so every payment can lock to you before your people start buying.';
|
||||
if (!d.memberId) return 'Your wallet is linked. Switch on payouts above (one free transaction) and every purchase in your line pays you the moment it happens.';
|
||||
if (d.buyerCount === 0) return 'Payouts are on and your link is live. Your next milestone: your first buyer of $20 or more. Every direct pays you 50 percent from their very first package.';
|
||||
if (d.buyerCount === 1) return 'One qualifying buyer down, one to go. Your next $20+ buyer unlocks level 2: 20 percent of everything your people’s people buy.';
|
||||
if (d.buyerCount < 5) return 'Level 2 is open. ' + (5 - d.buyerCount) + ' more qualifying buyer(s) unlock level 3 and the full three-level flow.';
|
||||
return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.';
|
||||
}
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const d = await (await fetch('/api/my/dashboard')).json();
|
||||
if (d.error) return;
|
||||
$('dbCredits').textContent = (d.credits || 0).toLocaleString();
|
||||
$('dbEarned').textContent = IAP.fmtPol(d.earnedWei || '0');
|
||||
$('dbBuyers').textContent = d.buyerCount || 0;
|
||||
$('dbTeam').textContent = (d.referrals || []).length;
|
||||
$('nextMove').textContent = nextMove(d);
|
||||
$('qualFill').style.width = Math.min(100, (d.buyerCount || 0) * 20) + '%';
|
||||
const wrap = $('rosterWrap');
|
||||
if ((d.referrals || []).length) {
|
||||
$('rosterEmpty').hidden = true;
|
||||
let t = wrap.querySelector('table');
|
||||
if (t) t.remove();
|
||||
t = document.createElement('table');
|
||||
t.className = 'roster';
|
||||
t.innerHTML = d.referrals.map(r => '<tr><td>' + r.email + '</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);
|
||||
}
|
||||
// ready-to-send share message
|
||||
const link = location.origin + '/join/' + (d.refCode || d.memberId || '');
|
||||
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. '
|
||||
+ 'Free to join and look around: ' + link;
|
||||
$('copyPitch').hidden = false;
|
||||
$('pitchPreview').hidden = false;
|
||||
$('pitchPreview').textContent = '"' + pitch + '"';
|
||||
$('copyPitch').onclick = async () => {
|
||||
try { await navigator.clipboard.writeText(pitch); IAP.status('Message copied. Paste it anywhere.', 'ok'); }
|
||||
catch (e) { IAP.status('Copy failed. Select the preview text instead.', 'bad'); }
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function render() {
|
||||
const me = await IAP.refreshNavWallet();
|
||||
const signedIn = me && me.signedIn;
|
||||
$('authArea').hidden = !!signedIn;
|
||||
$('memberArea').hidden = !signedIn;
|
||||
if (!signedIn) return;
|
||||
loadDashboard();
|
||||
|
||||
const who = [];
|
||||
if (me.email) who.push(me.email);
|
||||
|
||||
@@ -220,6 +220,16 @@ input[type=range]{padding:0;border:0;background:transparent;accent-color:var(--m
|
||||
input:focus,select:focus{border-color:var(--mint)}
|
||||
:focus-visible{outline:2px solid var(--mint);outline-offset:2px}
|
||||
.hero-note{font-family:var(--mono);font-size:12px;color:var(--muted);margin-top:26px}
|
||||
/* member dashboard: qualification progress + roster */
|
||||
.qualbar{position:relative;height:12px;border-radius:999px;background:rgba(4,8,7,.7);border:1px solid var(--line-strong);
|
||||
margin:26px 0 30px;max-width:520px}
|
||||
.qualbar #qualFill{height:100%;border-radius:999px;background:linear-gradient(90deg,var(--mint),var(--mint-hi));
|
||||
box-shadow:0 0 18px rgba(67,232,195,.4);width:0;transition:width .6s ease}
|
||||
.qb-mark{position:absolute;top:16px;transform:translateX(-50%);font-family:var(--mono);font-size:11px;color:var(--muted);white-space:nowrap}
|
||||
.roster{width:100%;border-collapse:collapse;font-size:13.5px}
|
||||
.roster td{padding:8px 10px;border-bottom:1px solid var(--line)}
|
||||
.roster td:first-child{font-family:var(--mono)}
|
||||
.roster tr:last-child td{border-bottom:0}
|
||||
/* chat widget */
|
||||
#iapChatBtn{position:fixed;right:20px;bottom:20px;z-index:40;width:56px;height:56px;border-radius:50%;
|
||||
border:0;background:var(--mint);color:var(--mint-ink);font-size:24px;cursor:pointer;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<title>The contract | InstantAdPay</title>
|
||||
<meta name="description" content="Plain-language review of the InstantAdPay settlement contract: what it does, what nobody can change, what the operator can and cannot touch, and how to verify all of it yourself.">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260904i">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260904j">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
@@ -129,8 +129,8 @@
|
||||
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="/assets/common.js?v=20260904i"></script>
|
||||
<script src="/assets/contract.js?v=20260904i"></script>
|
||||
<script src="/assets/chat.js?v=20260904i"></script>
|
||||
<script src="/assets/common.js?v=20260904j"></script>
|
||||
<script src="/assets/contract.js?v=20260904j"></script>
|
||||
<script src="/assets/chat.js?v=20260904j"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+5
-5
@@ -5,7 +5,7 @@
|
||||
<title>InstantAdPay: advertise and earn, locked in code</title>
|
||||
<meta name="description" content="Real ad packages with instant on-chain settlement. Every purchase pays the sponsor line in the same transaction, verifiable by anyone on the live ledger.">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260904i">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260904j">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -398,9 +398,9 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="/assets/common.js?v=20260904i"></script>
|
||||
<script src="/assets/wallet.js?v=20260904i"></script>
|
||||
<script src="/assets/home.js?v=20260904i"></script>
|
||||
<script src="/assets/chat.js?v=20260904i"></script>
|
||||
<script src="/assets/common.js?v=20260904j"></script>
|
||||
<script src="/assets/wallet.js?v=20260904j"></script>
|
||||
<script src="/assets/home.js?v=20260904j"></script>
|
||||
<script src="/assets/chat.js?v=20260904j"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+4
-4
@@ -5,7 +5,7 @@
|
||||
<title>Live ledger | InstantAdPay</title>
|
||||
<meta name="description" content="Every purchase, payout, and pass-up on InstantAdPay, streamed straight from the blockchain with a verify link on every line.">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260904i">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260904j">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
@@ -25,8 +25,8 @@
|
||||
<div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="/assets/common.js?v=20260904i"></script>
|
||||
<script src="/assets/ledger.js?v=20260904i"></script>
|
||||
<script src="/assets/chat.js?v=20260904i"></script>
|
||||
<script src="/assets/common.js?v=20260904j"></script>
|
||||
<script src="/assets/ledger.js?v=20260904j"></script>
|
||||
<script src="/assets/chat.js?v=20260904j"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+35
-6
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>My account | InstantAdPay</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260904i">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260904j">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
@@ -46,6 +46,24 @@
|
||||
</div>
|
||||
|
||||
<div id="memberArea" hidden>
|
||||
<div class="stats" style="margin-top:0">
|
||||
<div class="stat"><div class="n" id="dbCredits">0</div><div class="l">Ad credits</div></div>
|
||||
<div class="stat"><div class="n mono" id="dbEarned">0</div><div class="l">POL earned</div></div>
|
||||
<div class="stat"><div class="n" id="dbBuyers">0</div><div class="l">Qualifying buyers</div></div>
|
||||
<div class="stat"><div class="n" id="dbTeam">0</div><div class="l">Joined your line</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="nextCard">
|
||||
<h3>Your next move</h3>
|
||||
<p class="muted" id="nextMove">…</p>
|
||||
<div id="qualBarWrap">
|
||||
<div class="qualbar"><div id="qualFill"></div>
|
||||
<span class="qb-mark" style="left:40%">2 · level 2</span>
|
||||
<span class="qb-mark" style="left:100%">5 · level 3</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid c3">
|
||||
<div class="card"><h3>Your account</h3>
|
||||
<p id="posLine" class="muted small">…</p></div>
|
||||
@@ -56,6 +74,13 @@
|
||||
<p id="qualLine" class="muted small">…</p></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Your line</h3>
|
||||
<p class="muted small">Everyone who joined through your link, newest first. When they buy their
|
||||
first package of $20 or more, they count toward your qualification.</p>
|
||||
<div id="rosterWrap"><p class="muted small" id="rosterEmpty">Nobody yet. Your link is ready below; share it and this list starts filling.</p></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="linkCard" hidden>
|
||||
<h3>Link your wallet</h3>
|
||||
<p class="muted small">Your earnings pay straight to your own wallet, so we need to know which one is yours.
|
||||
@@ -76,7 +101,11 @@
|
||||
your line, and you earn 50 percent of every ad package they ever buy, plus levels 2 and 3 as you
|
||||
qualify. Just switch on payouts before your people start buying so every payment locks to you.</p>
|
||||
<p class="mono" id="inviteLine">…</p>
|
||||
<button class="btn small sec" id="copyInvite" hidden>Copy link</button>
|
||||
<p>
|
||||
<button class="btn small sec" id="copyInvite" hidden>Copy link</button>
|
||||
<button class="btn small sec" id="copyPitch" hidden>Copy a ready-to-send message</button>
|
||||
</p>
|
||||
<p class="muted small" id="pitchPreview" hidden></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -131,9 +160,9 @@
|
||||
|
||||
<footer><div>InstantAdPay · <a href="/ledger">live ledger</a></div></footer>
|
||||
</div>
|
||||
<script src="/assets/common.js?v=20260904i"></script>
|
||||
<script src="/assets/wallet.js?v=20260904i"></script>
|
||||
<script src="/assets/my.js?v=20260904i"></script>
|
||||
<script src="/assets/chat.js?v=20260904i"></script>
|
||||
<script src="/assets/common.js?v=20260904j"></script>
|
||||
<script src="/assets/wallet.js?v=20260904j"></script>
|
||||
<script src="/assets/my.js?v=20260904j"></script>
|
||||
<script src="/assets/chat.js?v=20260904j"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -300,6 +300,42 @@ const server = http.createServer(async (req, res) => {
|
||||
return json(res, 200, out);
|
||||
}
|
||||
|
||||
if (p === '/api/my/dashboard' && req.method === 'GET') {
|
||||
const s = await auth.fromRequest(req);
|
||||
if (!s) return json(res, 401, { error: 'Sign in first.' });
|
||||
const memberId = await auth.refreshMemberId(s);
|
||||
const acct = (s.email && await accounts.byEmail(s.email)) || (s.address && await accounts.byAddress(s.address)) || null;
|
||||
const out = { memberId, email: s.email || (acct && acct.email) || null,
|
||||
address: s.address || (acct && acct.address) || null,
|
||||
refCode: (acct && acct.code) || null, credits: 0, buyerCount: 0,
|
||||
earnedWei: '0', earnCount: 0, referrals: [] };
|
||||
if (memberId) {
|
||||
try {
|
||||
const mm = await chain.member(memberId);
|
||||
out.buyerCount = mm.buyerCount;
|
||||
out.credits = await ads.availableCredits(memberId);
|
||||
} catch (e) { out.chainReadError = true; }
|
||||
let earned = 0n, n = 0;
|
||||
for (const ev of chain.recentEvents(600)) {
|
||||
if ((ev.type === 'TierPaid' && ev.recipientId === memberId) || (ev.type === 'AwardPaid' && ev.toId === memberId)) {
|
||||
earned += BigInt(ev.amountWei); n += 1;
|
||||
}
|
||||
}
|
||||
out.earnedWei = earned.toString();
|
||||
out.earnCount = n;
|
||||
}
|
||||
// who joined through this member (code and, once on-chain, numeric id)
|
||||
const refs = [];
|
||||
if (acct && acct.code) refs.push(acct.code);
|
||||
if (memberId) refs.push(String(memberId));
|
||||
const joined = await accounts.listByReferrer(refs);
|
||||
out.referrals = joined.map(r => ({
|
||||
email: r.email.replace(/^(.).*(@.*)$/, '$1***$2'), // privacy mask
|
||||
joined: r.created,
|
||||
status: r.address ? 'wallet linked' : 'joined free'
|
||||
}));
|
||||
return json(res, 200, out);
|
||||
}
|
||||
if (p === '/api/my/activity' && req.method === 'GET') {
|
||||
const s = await auth.fromRequest(req);
|
||||
if (!s) return json(res, 401, { error: 'Sign in first.' });
|
||||
|
||||
Reference in New Issue
Block a user