diff --git a/chain.js b/chain.js index 8d6bb7d..9434c16 100644 --- a/chain.js +++ b/chain.js @@ -194,6 +194,7 @@ async function tail() { for (const lg of logs) { const ev = decodeLog(lg); if (!ev) continue; + ev.ts = Date.now(); // indexed-at time: powers honest time-series charts state.events.push(ev); tally(ev); if (onEvent) { try { onEvent(ev); } catch (e) { console.error('chain onEvent', e.message); } } diff --git a/public/assets/my.js b/public/assets/my.js index 606fb64..f928a3e 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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 => '
').join('') + : '
'.repeat(8); + if (xel) xel.innerHTML = data.map(d => '' + esc(d.label) + '').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 = ''; + 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 += ''; + off -= len; + } + out += '' + esc(center.big) + '' + + '' + esc(center.small) + ''; + el.innerHTML = out; + } + function legend(el, rows) { + if (el) el.innerHTML = rows.map(r => '
' + + esc(r.label) + ' ' + esc(r.v) + '
').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'); diff --git a/public/assets/site.css b/public/assets/site.css index e77ea42..83943b4 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -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)} diff --git a/public/contract.html b/public/contract.html index 2885673..a40972f 100644 --- a/public/contract.html +++ b/public/contract.html @@ -5,7 +5,7 @@ The contract | InstantAdPay - +
@@ -129,8 +129,8 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.
- - - + + + diff --git a/public/index.html b/public/index.html index 2090ea3..ce943d6 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ InstantAdPay: advertise and earn, locked in code - + @@ -405,9 +405,9 @@ - - - - + + + + diff --git a/public/ledger.html b/public/ledger.html index 130a977..5567845 100644 --- a/public/ledger.html +++ b/public/ledger.html @@ -5,7 +5,7 @@ Live ledger | InstantAdPay - +
@@ -25,8 +25,8 @@
InstantAdPay · how it works · contract source ↗
- - - + + + diff --git a/public/my.html b/public/my.html index 35ff4b1..aa401ed 100644 --- a/public/my.html +++ b/public/my.html @@ -4,7 +4,7 @@ Member area | InstantAdPay - + @@ -85,11 +85,52 @@
-
-
0
Ad credits
-
0
POL earned
-
0
Qualifying buyers
-
0
Joined your line
+
+
+
+
0
Ad credits
+
+
+
+
0
POL earned
+
+
+
+
0
Qualifying buyers
+
+
+
+
0
Joined your line
+
+
+ +
+
+

Recent payouts

on-chain, newest last
+
+
+
+
+

Credit composition

live balances
+
+ +
+
+
+
+
+
+

Today's viewing

daily claim progress
+
+ +
+
+
+
+

Campaign delivery

impressions per campaign
+
+
+

Your next move

@@ -289,9 +330,9 @@
- - - - + + + +