Achievement badges + milestone credit bonuses

- Idempotent one-time credit bonus per milestone (payouts/firstBuyer/level2/level3): 10/25/50/100 cr
- Overview badges strip (unlockable medals) with canvas share-to-image (username overlaid)
- Newly-unlocked milestones toast the bonus on next dashboard load

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-06 08:43:50 -05:00
parent 7701a29169
commit 9e05711f4e
12 changed files with 120 additions and 26 deletions
+65
View File
@@ -26,6 +26,70 @@
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.';
}
// achievement badges: the milestone ladder as unlockable medals + share-to-image
const BADGES = [
{ key: 'payouts', label: 'Payouts On', sub: 'wallet linked', icon: '<circle cx="12" cy="12" r="8"/><path d="M9 12l2 2 4-4"/>' },
{ key: 'firstBuyer', label: 'First Buyer', sub: 'first qualified referral', icon: '<path d="M6 7h12l-1 13H7z"/><path d="M9 7a3 3 0 0 1 6 0"/>' },
{ key: 'level2', label: 'Level 2', sub: '2 qualifying buyers', icon: '<path d="M12 3l2.5 5 5.5.8-4 3.9.9 5.5L12 21l-4.9 2.6.9-5.5-4-3.9 5.5-.8z"/>' },
{ key: 'level3', label: 'Level 3', sub: 'fully qualified', icon: '<path d="M5 4h14v5a7 7 0 0 1-14 0z"/><path d="M9 20h6M12 16v4"/><path d="M5 6H3v2a3 3 0 0 0 3 3M19 6h2v2a3 3 0 0 1-3 3"/>' }
];
let lastMilestones = [];
function renderBadges(d) {
const reached = d.milestonesReached || [];
lastMilestones = reached;
const strip = $('badgeStrip');
if (!strip) return;
$('badgeCard').hidden = false;
strip.innerHTML = BADGES.map(b => {
const got = reached.includes(b.key);
return '<div class="badge-a' + (got ? '' : ' locked') + '">'
+ '<div class="medal"><svg viewBox="0 0 24 24">' + b.icon + '</svg></div>'
+ '<div class="bl">' + b.label + '</div><div class="bs">' + b.sub + '</div>'
+ (got ? '<button class="share" data-badge="' + b.key + '">Share</button>' : '') + '</div>';
}).join('');
strip.querySelectorAll('[data-badge]').forEach(btn =>
btn.addEventListener('click', () => shareBadge(btn.dataset.badge, d)));
// celebrate anything newly granted this load
if (d.milestonesGranted && d.milestonesGranted.length) {
const total = d.milestonesGranted.reduce((s, g) => s + g.credited, 0);
const names = d.milestonesGranted.map(g => (BADGES.find(b => b.key === g.key) || {}).label).filter(Boolean).join(', ');
IAP.status('🏆 Achievement unlocked: ' + names + ' — +' + total + ' bonus credits!', 'ok');
}
}
// compose a shareable badge image on a canvas (zero-dep) and download it
function shareBadge(key, d) {
const b = BADGES.find(x => x.key === key); if (!b) return;
const c = document.createElement('canvas'); c.width = 1200; c.height = 630;
const x = c.getContext('2d');
const g = x.createLinearGradient(0, 0, 1200, 630);
g.addColorStop(0, '#041109'); g.addColorStop(1, '#0b2018');
x.fillStyle = g; x.fillRect(0, 0, 1200, 630);
x.strokeStyle = 'rgba(67,232,195,.5)'; x.lineWidth = 6; x.strokeRect(24, 24, 1152, 582);
x.fillStyle = '#43e8c3'; x.font = 'bold 34px Sora, sans-serif'; x.fillText('InstantAdPay', 70, 100);
x.fillStyle = '#8ba69c'; x.font = '22px Sora, sans-serif'; x.fillText('ACHIEVEMENT UNLOCKED', 70, 150);
// medal circle
x.beginPath(); x.arc(600, 320, 110, 0, Math.PI * 2);
const mg = x.createRadialGradient(600, 290, 20, 600, 320, 110);
mg.addColorStop(0, 'rgba(67,232,195,.45)'); mg.addColorStop(1, 'rgba(9,26,19,.9)');
x.fillStyle = mg; x.fill(); x.strokeStyle = '#43e8c3'; x.lineWidth = 5; x.stroke();
x.fillStyle = '#eef7f3'; x.textAlign = 'center';
x.font = 'bold 64px Sora, sans-serif'; x.fillText(b.label, 600, 500);
x.font = '26px Sora, sans-serif'; x.fillStyle = '#8ba69c'; x.fillText(b.sub, 600, 545);
const who = d.username ? '@' + d.username : d.memberId ? 'member #' + d.memberId : '';
if (who) { x.fillStyle = '#43e8c3'; x.font = 'bold 30px Sora, sans-serif'; x.fillText(who, 600, 590); }
x.textAlign = 'left';
try {
c.toBlob(bl => {
const a = document.createElement('a');
a.href = URL.createObjectURL(bl);
a.download = 'instantadpay-' + key + '-badge.png';
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
IAP.status('Badge image saved — share it anywhere.', 'ok');
}, 'image/png');
} catch (e) { IAP.status('Could not generate the image.', 'bad'); }
}
// milestone stepper under "Your next move": lit nodes for what's done,
// amber glow on the current target — the same ladder the contract pays
function renderSteps(d) {
@@ -147,6 +211,7 @@
if (tc && wk) { tc.hidden = false; tc.textContent = '+' + wk + ' this week'; }
setInboxBadge(d.inboxUnread || 0);
if (d.sponsorMsg) showSponsorModal(d.sponsorMsg);
renderBadges(d);
loadCharts(d);
$('nextMove').textContent = nextMove(d);
renderSteps(d);
+12
View File
@@ -259,6 +259,18 @@ textarea{resize:vertical;font:inherit}
#boBurger{display:block}
.bo-content{padding:18px}
}
/* ── achievement badges ── */
.badges{display:flex;gap:16px;flex-wrap:wrap;margin-top:12px}
.badge-a{display:flex;flex-direction:column;align-items:center;gap:8px;width:120px;text-align:center}
.badge-a .medal{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;position:relative;
background:radial-gradient(circle at 50% 35%,rgba(67,232,195,.28),rgba(9,26,19,.9));
border:2px solid var(--mint);box-shadow:0 0 18px rgba(67,232,195,.3)}
.badge-a.locked .medal{background:rgba(139,166,156,.08);border-color:var(--line-strong);box-shadow:none;filter:grayscale(1);opacity:.5}
.badge-a .medal svg{width:38px;height:38px;stroke:var(--mint);fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.badge-a.locked .medal svg{stroke:var(--muted)}
.badge-a .bl{font-size:12px;font-weight:700;line-height:1.2}
.badge-a .bs{font-size:10.5px;color:var(--muted)}
.badge-a .share{font-size:11px;color:var(--mint);cursor:pointer;background:none;border:0;padding:0;text-decoration:underline}
/* ── "Your next move": milestone stepper card ── */
.next-card{position:relative;overflow:hidden;border-color:rgba(67,232,195,.28)}
.next-card::before{content:"";position:absolute;inset:0;pointer-events:none;background:
+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=20260905z">
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
</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=20260905z"></script>
<script src="/assets/contract.js?v=20260905z"></script>
<script src="/assets/chat.js?v=20260905z"></script>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/contract.js?v=20260906a"></script>
<script src="/assets/chat.js?v=20260906a"></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=20260905z">
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
</head>
<body>
@@ -410,9 +410,9 @@
</div>
</section>
<script src="/assets/common.js?v=20260905z"></script>
<script src="/assets/wallet.js?v=20260905z"></script>
<script src="/assets/home.js?v=20260905z"></script>
<script src="/assets/chat.js?v=20260905z"></script>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/wallet.js?v=20260906a"></script>
<script src="/assets/home.js?v=20260906a"></script>
<script src="/assets/chat.js?v=20260906a"></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=20260905z">
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
</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=20260905z"></script>
<script src="/assets/ledger.js?v=20260905z"></script>
<script src="/assets/chat.js?v=20260905z"></script>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/ledger.js?v=20260906a"></script>
<script src="/assets/chat.js?v=20260906a"></script>
</body>
</html>
+9 -5
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=20260905z">
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
</head>
<body class="bo-body">
@@ -186,6 +186,10 @@
</div>
<div class="nc-steps" id="ncSteps"></div>
</div>
<div class="card" id="badgeCard" hidden>
<div class="card-head"><h3>Your achievements</h3><span class="sub">unlock as you build</span></div>
<div class="badges" id="badgeStrip"></div>
</div>
<div class="grid c2">
<div class="card">
<h3>Recent activity</h3>
@@ -529,9 +533,9 @@
</div>
</div>
<script src="/assets/common.js?v=20260905z"></script>
<script src="/assets/wallet.js?v=20260905z"></script>
<script src="/assets/my.js?v=20260905z"></script>
<script src="/assets/chat.js?v=20260905z"></script>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/wallet.js?v=20260906a"></script>
<script src="/assets/my.js?v=20260906a"></script>
<script src="/assets/chat.js?v=20260906a"></script>
</body>
</html>
+3 -3
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Transaction | 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=20260905z">
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
</head>
<body>
<div id="nav"></div>
@@ -33,7 +33,7 @@
<p><a href="/ledger">← Back to the live ledger</a> · <a href="/contract">Read the contract review</a></p>
</div>
</section>
<script src="/assets/common.js?v=20260905z"></script>
<script src="/assets/tx.js?v=20260905z"></script>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/tx.js?v=20260906a"></script>
</body>
</html>
+2 -2
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Viewing ad — InstantAdPay</title>
<link rel="stylesheet" href="/assets/site.css?v=20260905z">
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
<style>
html,body{height:100%;margin:0;overflow:hidden}
.vw{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--bg,#04110c);color:var(--ink,#e8fff7)}
@@ -43,6 +43,6 @@
</div>
<iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe>
</div>
<script src="/assets/view.js?v=20260905z"></script>
<script src="/assets/view.js?v=20260906a"></script>
</body>
</html>
+3 -3
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Banner wall | 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=20260905z">
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
</head>
<body>
<div id="nav"></div>
@@ -26,7 +26,7 @@
</div>
</div>
</section>
<script src="/assets/common.js?v=20260905z"></script>
<script src="/assets/wall.js?v=20260905z"></script>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/wall.js?v=20260906a"></script>
</body>
</html>