Overview v3: plated stat cards, trend chips, payout bars, credit donut, view ring, delivery chart

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-05 06:41:01 -05:00
parent e22021c266
commit fee86d296a
7 changed files with 193 additions and 24 deletions
+91
View File
@@ -26,6 +26,86 @@
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.';
}
// ── overview v3 charts: hand-rolled SVG/CSS, real data only ──
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const CH = { mint: '#43e8c3', cyan: '#54ccff', violet: '#9d7dff', amber: '#ffb238', track: 'rgba(139,166,156,.18)' };
function bars(el, xel, data) { // data: [{v,label,tip,alt}]
if (!el) return;
const max = Math.max(1, ...data.map(d => d.v));
el.innerHTML = data.length
? data.map(d => '<div class="bar' + (d.alt ? ' alt' : '') + (d.v ? '' : ' empty') + '" style="height:'
+ Math.max(4, Math.round(d.v / max * 100)) + '%" data-v="' + esc(d.tip) + '"></div>').join('')
: '<div class="bar empty" style="height:4%"></div>'.repeat(8);
if (xel) xel.innerHTML = data.map(d => '<span>' + esc(d.label) + '</span>').join('');
}
function donut(el, segs, center) { // segs sum to the ring; r=15.9155 → circumference 100
if (!el) return;
const total = segs.reduce((s, x) => s + x.v, 0);
let off = 25, out = '<circle cx="21" cy="21" r="15.9155" fill="none" stroke="' + CH.track + '" stroke-width="5"></circle>';
if (total > 0) for (const s of segs) {
const len = s.v / total * 100;
if (len <= 0 || s.color === 'none') { off -= Math.max(0, len); continue; }
out += '<circle cx="21" cy="21" r="15.9155" fill="none" stroke="' + s.color + '" stroke-width="5" '
+ 'stroke-dasharray="' + len + ' ' + (100 - len) + '" stroke-dashoffset="' + off + '"></circle>';
off -= len;
}
out += '<text x="21" y="20.5" text-anchor="middle" fill="currentColor" font-size="6.5" font-weight="800" class="donut-center">' + esc(center.big) + '</text>'
+ '<text x="21" y="26.5" text-anchor="middle" fill="#8ba69c" font-size="3.1">' + esc(center.small) + '</text>';
el.innerHTML = out;
}
function legend(el, rows) {
if (el) el.innerHTML = rows.map(r => '<div><i style="background:' + r.color + '"></i>'
+ esc(r.label) + ' <b>' + esc(r.v) + '</b></div>').join('');
}
async function loadCharts(d) {
// credit composition donut + today's viewing ring (live balances)
try {
const st = await (await fetch('/api/my/earn')).json();
if (!st.error) {
const purchased = d.credits || 0, earned = st.earned || 0, wc = Math.min(d.welcomeCredits || 0, earned);
$('dbCredits').textContent = (purchased + earned).toLocaleString();
$('dbCreditsSub').textContent = purchased.toLocaleString() + ' purchased · ' + earned.toLocaleString() + ' earned';
donut($('chDonut'),
[{ v: purchased, color: CH.mint }, { v: wc, color: CH.amber }, { v: earned - wc, color: CH.cyan }],
{ big: (purchased + earned).toLocaleString(), small: 'spendable' });
legend($('chDonutLegend'), [
{ color: CH.mint, label: 'Purchased', v: purchased.toLocaleString() },
{ color: CH.amber, label: 'Welcome', v: wc.toLocaleString() },
{ color: CH.cyan, label: 'Earned by viewing', v: Math.max(0, earned - wc).toLocaleString() }]);
const done = Math.min(st.views || 0, st.target || 5), left = Math.max(0, (st.target || 5) - done);
donut($('chRing'), [{ v: done, color: st.claimed ? CH.mint : CH.cyan }, { v: left, color: 'none' }],
{ big: done + '/' + (st.target || 5), small: st.claimed ? 'claimed' : 'ads viewed' });
legend($('chRingLegend'), [
{ color: st.claimed ? CH.mint : CH.cyan, label: 'Viewed today', v: done },
{ color: CH.track, label: 'To go', v: left },
{ color: CH.amber, label: 'Claim pays', v: '+' + (st.claimCredits || 0) + ' credits' }]);
}
} catch (e) {}
// recent on-chain payouts, sized to scale, oldest→newest
try {
let evs = [];
if (d.memberId) {
const a = await (await fetch('/api/my/activity')).json();
evs = (a.earnings || []).filter(e => e.amountWei).sort((x, y) => x.block - y.block).slice(-12);
}
bars($('chEarnBars'), $('chEarnX'), evs.map(e => ({
v: Number(BigInt(e.amountWei) / 1000000000000n) / 1e6,
label: e.type === 'AwardPaid' ? 'award' : 'L' + (e.tier || 1),
tip: IAP.fmtPol(e.amountWei) + ' POL', alt: e.type === 'AwardPaid' })));
if (!evs.length) $('chEarnSub').textContent = d.memberId
? 'no payouts yet — share your link' : 'activate a package to start earning';
} catch (e) {}
// campaign delivery: impressions per campaign
try {
const r = await (await fetch('/api/my/campaigns')).json();
const cs = (r.campaigns || []).slice(0, 8);
bars($('chCampBars'), $('chCampX'), cs.map(c => ({
v: c.imps || 0, label: String(c.name || c.type).slice(0, 9),
tip: (c.name || c.type) + ': ' + (c.imps || 0) + ' imps · ' + (c.clicks || 0) + ' clicks',
alt: c.type === 'text' })));
if (!cs.length) $('chCampSub').textContent = 'no campaigns yet — place your first ad';
} catch (e) {}
}
async function loadDashboard() {
try {
const d = await (await fetch('/api/my/dashboard')).json();
@@ -36,6 +116,17 @@
$('dbEarned').textContent = IAP.fmtPol(d.earnedWei || '0');
$('dbBuyers').textContent = d.buyerCount || 0;
$('dbTeam').textContent = (d.referrals || []).length;
// trend chips: only real, computable facts
const ec = $('dbEarnedChip');
if (ec && d.earnCount) { ec.hidden = false; ec.textContent = d.earnCount + ' instant payout' + (d.earnCount > 1 ? 's' : ''); }
const bc = $('dbBuyersChip');
if (bc && d.memberId) {
bc.hidden = false;
bc.textContent = (d.buyerCount || 0) >= 5 ? 'level 3 open' : (d.buyerCount || 0) >= 2 ? 'level 2 open' : 'level 2 at 2 buyers';
}
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'; }
loadCharts(d);
$('nextMove').textContent = nextMove(d);
$('qualFill').style.width = Math.min(100, (d.buyerCount || 0) * 20) + '%';
const wrap = $('rosterWrap');
+37 -1
View File
@@ -258,7 +258,43 @@ 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 */
/* ── overview v3: stat cards w/ plates+chips, hand-rolled charts ── */
.statx{display:flex;gap:14px;align-items:flex-start;background:var(--panel);border:1px solid var(--line);
border-radius:var(--radius);padding:18px;position:relative;overflow:hidden}
.statx .pl{flex:0 0 auto;width:46px;height:46px;border-radius:13px;display:grid;place-items:center;
background:rgba(67,232,195,.1);border:1px solid rgba(67,232,195,.3)}
.statx .pl svg{width:22px;height:22px;stroke:var(--mint);fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.statx.c-cyan .pl{background:rgba(84,204,255,.1);border-color:rgba(84,204,255,.3)}
.statx.c-cyan .pl svg{stroke:var(--cyan)}
.statx.c-violet .pl{background:rgba(157,125,255,.12);border-color:rgba(157,125,255,.32)}
.statx.c-violet .pl svg{stroke:var(--violet)}
.statx.c-amber .pl{background:rgba(255,178,56,.1);border-color:rgba(255,178,56,.3)}
.statx.c-amber .pl svg{stroke:var(--amber)}
.statx .nv{font-family:var(--disp);font-size:26px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1.1}
.statx .lb{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-top:2px}
.chip{display:inline-block;margin-top:7px;font-size:11.5px;font-weight:700;border-radius:999px;padding:2px 10px;
background:rgba(67,232,195,.1);color:var(--mint)}
.chip.flat{background:rgba(139,166,156,.12);color:var(--muted)}
.chip.warm{background:rgba(255,178,56,.12);color:var(--amber)}
.card-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
.card-head .sub{font-size:12px;color:var(--muted)}
/* bar chart (divs, real data) */
.barchart{display:flex;align-items:flex-end;gap:6px;height:120px;margin-top:14px}
.barchart .bar{flex:1;min-width:6px;background:linear-gradient(180deg,var(--mint),rgba(67,232,195,.25));
border-radius:5px 5px 2px 2px;position:relative;transition:height .5s ease}
.barchart .bar.alt{background:linear-gradient(180deg,var(--violet),rgba(157,125,255,.25))}
.barchart .bar:hover::after{content:attr(data-v);position:absolute;bottom:calc(100% + 4px);left:50%;
transform:translateX(-50%);background:var(--panel-solid);border:1px solid var(--line-strong);border-radius:7px;
padding:2px 8px;font-family:var(--mono);font-size:11px;white-space:nowrap;z-index:3}
.barchart .bar.empty{background:rgba(139,166,156,.15);height:4px!important}
.chart-x{display:flex;gap:6px;margin-top:6px}
.chart-x span{flex:1;text-align:center;font-family:var(--mono);font-size:10px;color:var(--muted);overflow:hidden;white-space:nowrap}
/* donut + ring (SVG) */
.donut-wrap{display:flex;gap:22px;align-items:center;margin-top:10px;flex-wrap:wrap}
.donut-legend{display:flex;flex-direction:column;gap:8px;font-size:13px}
.donut-legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:8px}
.donut-center{font-family:var(--disp);font-weight:800}
/* ── 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)}
+4 -4
View File
@@ -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=20260905i">
<link rel="stylesheet" href="/assets/site.css?v=20260905j">
</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=20260905i"></script>
<script src="/assets/contract.js?v=20260905i"></script>
<script src="/assets/chat.js?v=20260905i"></script>
<script src="/assets/common.js?v=20260905j"></script>
<script src="/assets/contract.js?v=20260905j"></script>
<script src="/assets/chat.js?v=20260905j"></script>
</body>
</html>
+5 -5
View File
@@ -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=20260905i">
<link rel="stylesheet" href="/assets/site.css?v=20260905j">
</head>
<body>
@@ -405,9 +405,9 @@
</div>
</section>
<script src="/assets/common.js?v=20260905i"></script>
<script src="/assets/wallet.js?v=20260905i"></script>
<script src="/assets/home.js?v=20260905i"></script>
<script src="/assets/chat.js?v=20260905i"></script>
<script src="/assets/common.js?v=20260905j"></script>
<script src="/assets/wallet.js?v=20260905j"></script>
<script src="/assets/home.js?v=20260905j"></script>
<script src="/assets/chat.js?v=20260905j"></script>
</body>
</html>
+4 -4
View File
@@ -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=20260905i">
<link rel="stylesheet" href="/assets/site.css?v=20260905j">
</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=20260905i"></script>
<script src="/assets/ledger.js?v=20260905i"></script>
<script src="/assets/chat.js?v=20260905i"></script>
<script src="/assets/common.js?v=20260905j"></script>
<script src="/assets/ledger.js?v=20260905j"></script>
<script src="/assets/chat.js?v=20260905j"></script>
</body>
</html>
+51 -10
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Member area | 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=20260905i">
<link rel="stylesheet" href="/assets/site.css?v=20260905k">
</head>
<body class="bo-body">
@@ -85,11 +85,52 @@
<main class="bo-content">
<div class="pane" id="pane-overview">
<div class="stats" style="margin-top:0">
<div class="stat"><div class="n" id="dbCredits">0</div><div class="l">Ad credits</div><div class="small muted" id="dbCreditsSub"></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 class="grid" style="grid-template-columns:repeat(auto-fit,minmax(215px,1fr));margin-top:0">
<div class="statx">
<div class="pl"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5v9M9.2 10c0-1 1.2-1.7 2.8-1.7s2.8.7 2.8 1.7-1.2 1.5-2.8 1.7-2.8.7-2.8 1.7 1.2 1.7 2.8 1.7 2.8-.7 2.8-1.7"/></svg></div>
<div><div class="nv" id="dbCredits">0</div><div class="lb">Ad credits</div><span class="chip flat" id="dbCreditsSub"></span></div>
</div>
<div class="statx c-cyan">
<div class="pl"><svg viewBox="0 0 24 24"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18"/><circle cx="16.5" cy="14.5" r="1.4"/></svg></div>
<div><div class="nv mono" id="dbEarned">0</div><div class="lb">POL earned</div><span class="chip" id="dbEarnedChip" hidden></span></div>
</div>
<div class="statx c-violet">
<div class="pl"><svg viewBox="0 0 24 24"><circle cx="12" cy="7" r="3.2"/><circle cx="5" cy="17" r="2.6"/><circle cx="19" cy="17" r="2.6"/><path d="M12 10v3M12 13l-5 2M12 13l5 2"/></svg></div>
<div><div class="nv" id="dbBuyers">0</div><div class="lb">Qualifying buyers</div><span class="chip" id="dbBuyersChip" hidden></span></div>
</div>
<div class="statx c-amber">
<div class="pl"><svg viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/></svg></div>
<div><div class="nv" id="dbTeam">0</div><div class="lb">Joined your line</div><span class="chip warm" id="dbTeamChip" hidden></span></div>
</div>
</div>
<div class="grid c2" style="margin-top:16px">
<div class="card">
<div class="card-head"><h3>Recent payouts</h3><span class="sub" id="chEarnSub">on-chain, newest last</span></div>
<div class="barchart" id="chEarnBars"></div>
<div class="chart-x" id="chEarnX"></div>
</div>
<div class="card">
<div class="card-head"><h3>Credit composition</h3><span class="sub">live balances</span></div>
<div class="donut-wrap">
<svg id="chDonut" width="150" height="150" viewBox="0 0 42 42" role="img" aria-label="Credit composition"></svg>
<div class="donut-legend" id="chDonutLegend"></div>
</div>
</div>
</div>
<div class="grid c2">
<div class="card">
<div class="card-head"><h3>Today's viewing</h3><span class="sub">daily claim progress</span></div>
<div class="donut-wrap">
<svg id="chRing" width="130" height="130" viewBox="0 0 42 42" role="img" aria-label="Daily view progress"></svg>
<div class="donut-legend" id="chRingLegend"></div>
</div>
</div>
<div class="card">
<div class="card-head"><h3>Campaign delivery</h3><span class="sub" id="chCampSub">impressions per campaign</span></div>
<div class="barchart" id="chCampBars"></div>
<div class="chart-x" id="chCampX"></div>
</div>
</div>
<div class="card" id="nextCard">
<h3>Your next move</h3>
@@ -289,9 +330,9 @@
</div>
</div>
<script src="/assets/common.js?v=20260905i"></script>
<script src="/assets/wallet.js?v=20260905i"></script>
<script src="/assets/my.js?v=20260905i"></script>
<script src="/assets/chat.js?v=20260905i"></script>
<script src="/assets/common.js?v=20260905k"></script>
<script src="/assets/wallet.js?v=20260905k"></script>
<script src="/assets/my.js?v=20260905k"></script>
<script src="/assets/chat.js?v=20260905k"></script>
</body>
</html>