Ornate achievement badges (Spark/Surge/Circuit/Nexus) — kie.ai mint-teal trophies + name overlay

- Named the milestone ladder: Spark (payouts) / Surge (first buyer) / Circuit (L2) / Nexus (L3)
- Overview badges strip now shows the ornate badge art (locked = dimmed); milestone stepper renamed
- Share button composites the badge template with the member's @username on the ribbon (JPEG, same-origin canvas)
- Badge art in public/badges/ (optimized JPG ~300KB each)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-06 14:11:44 -05:00
parent 3c0e94f20f
commit 23136f4ba0
14 changed files with 68 additions and 71 deletions
+36 -41
View File
@@ -42,10 +42,10 @@
// achievement badges: the milestone ladder as unlockable medals + share-to-image
const BADGES = [
{ key: 'payouts', label: 'Payouts On', sub: 'wallet linked', emblem: '⚡', icon: '<circle cx="12" cy="12" r="8"/><path d="M9 12l2 2 4-4"/>' },
{ key: 'firstBuyer', label: 'First Buyer', sub: 'first qualified referral', emblem: '🎯', 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', emblem: '⭐', 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', emblem: '🏆', 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"/>' }
{ key: 'payouts', label: 'Spark', sub: 'payouts on', img: '/badges/badge-spark.jpg' },
{ key: 'firstBuyer', label: 'Surge', sub: 'first qualifying buyer', img: '/badges/badge-surge.jpg' },
{ key: 'level2', label: 'Circuit', sub: '2 qualifying buyers', img: '/badges/badge-circuit.jpg' },
{ key: 'level3', label: 'Nexus', sub: 'fully qualified', img: '/badges/badge-nexus.jpg' }
];
let lastMilestones = [];
function renderBadges(d) {
@@ -57,9 +57,9 @@
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>'
+ '<img class="badge-img" src="' + b.img + '" alt="' + b.label + ' badge" loading="lazy">'
+ '<div class="bl">' + b.label + '</div><div class="bs">' + b.sub + '</div>'
+ (got ? '<button class="share" data-badge="' + b.key + '">Share</button>' : '') + '</div>';
+ (got ? '<button class="share" data-badge="' + b.key + '">Share</button>' : '<div class="bs">🔒 locked</div>') + '</div>';
}).join('');
strip.querySelectorAll('[data-badge]').forEach(btn =>
btn.addEventListener('click', () => shareBadge(btn.dataset.badge, d)));
@@ -71,40 +71,35 @@
}
}
// compose a shareable badge image on a canvas (zero-dep) and download it
// composite the ornate badge template with the member's @username on the ribbon
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 with the milestone emblem
x.beginPath(); x.arc(600, 300, 112, 0, Math.PI * 2);
const mg = x.createRadialGradient(600, 270, 20, 600, 300, 112);
mg.addColorStop(0, 'rgba(67,232,195,.5)'); mg.addColorStop(1, 'rgba(9,26,19,.92)');
x.fillStyle = mg; x.fill(); x.strokeStyle = '#43e8c3'; x.lineWidth = 6; x.stroke();
x.save(); x.shadowColor = 'rgba(67,232,195,.6)'; x.shadowBlur = 24;
x.font = '90px "Segoe UI Emoji","Noto Color Emoji",sans-serif'; x.textAlign = 'center'; x.textBaseline = 'middle';
x.fillText(b.emblem || '', 600, 300); x.restore(); x.textBaseline = 'alphabetic';
x.fillStyle = '#eef7f3'; x.textAlign = 'center';
x.font = 'bold 60px Sora, sans-serif'; x.fillText(b.label, 600, 485);
x.font = '25px Sora, sans-serif'; x.fillStyle = '#8ba69c'; x.fillText(b.sub, 600, 527);
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, 575); }
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'); }
const img = new Image();
img.onload = () => {
const c = document.createElement('canvas'); c.width = img.width; c.height = img.height;
const x = c.getContext('2d');
x.drawImage(img, 0, 0);
if (who) { // the blank ribbon sits at ~76% down; name it in dark ink on the mint banner
x.textAlign = 'center';
x.fillStyle = '#04231b';
x.font = 'bold ' + Math.round(c.width * 0.052) + 'px Sora, "Segoe UI", sans-serif';
x.fillText(who, c.width / 2, c.height * 0.78);
x.textAlign = 'left';
}
try {
c.toBlob(bl => {
const a = document.createElement('a');
a.href = URL.createObjectURL(bl);
a.download = 'instantadpay-' + b.label.toLowerCase() + '-badge.jpg';
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
IAP.status('Your ' + b.label + ' badge is saved — share it anywhere.', 'ok');
}, 'image/jpeg', 0.9);
} catch (e) { IAP.status('Could not generate the image.', 'bad'); }
};
img.onerror = () => IAP.status('Could not load the badge art.', 'bad');
img.src = b.img; // same-origin, canvas stays untainted
}
// milestone stepper under "Your next move": lit nodes for what's done,
@@ -115,10 +110,10 @@
const b = d.buyerCount || 0;
const steps = [
{ label: 'Joined', sub: 'free account', hit: true },
{ label: 'Payouts on', sub: 'wallet linked', hit: !!d.memberId },
{ label: 'First buyer', sub: '50% of every pack', hit: b >= 1 },
{ label: 'Level 2', sub: '2 buyers · +20%', hit: b >= 2 },
{ label: 'Level 3', sub: '5 buyers · +10%', hit: b >= 5 }
{ label: 'Spark', sub: 'payouts on', hit: !!d.memberId },
{ label: 'Surge', sub: 'first buyer · 50%', hit: b >= 1 },
{ label: 'Circuit', sub: '2 buyers · +20%', hit: b >= 2 },
{ label: 'Nexus', sub: '5 buyers · +10%', hit: b >= 5 }
];
const cur = steps.findIndex(s => !s.hit);
el.innerHTML = steps.map((s, i) =>