Badge share sheet: public badge page with preview image (/b/<user>/<badge>), X/Facebook/Telegram/WhatsApp/LinkedIn/native share, copy link, save image
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+50
-34
@@ -142,43 +142,59 @@
|
||||
}, 'image/jpeg', 0.86);
|
||||
});
|
||||
}
|
||||
// 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
|
||||
// Share: a picker (Marty, 2026-09-13). The composed badge is stored once so a public page at
|
||||
// /b/<username>/<badge> carries it as the preview image; every network gets that link.
|
||||
function shareBadge(key, d) {
|
||||
const b = BADGES.find(x => x.key === key); if (!b) return;
|
||||
const who = d.username ? d.username : d.memberId ? 'member #' + d.memberId : '';
|
||||
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) { // name it on the blank ribbon: gold with a dark outline so it reads on any badge color
|
||||
x.textAlign = 'center';
|
||||
x.textBaseline = 'middle';
|
||||
x.font = 'bold ' + Math.round(c.width * 0.055) + 'px Sora, "Segoe UI", sans-serif';
|
||||
const y = c.height * (b.ribbonY || 0.75);
|
||||
x.lineJoin = 'round';
|
||||
x.lineWidth = Math.max(3, Math.round(c.width * 0.008));
|
||||
x.strokeStyle = 'rgba(4,20,15,.9)';
|
||||
x.strokeText(who, c.width / 2, y);
|
||||
x.fillStyle = '#ffd15c';
|
||||
x.fillText(who, c.width / 2, y);
|
||||
x.textAlign = 'left';
|
||||
x.textBaseline = 'alphabetic';
|
||||
}
|
||||
if (!d.username) { IAP.status('Pick a username first; your share page carries it.', 'bad'); return; }
|
||||
IAP.status('Preparing your ' + b.label + ' badge…', 'ok');
|
||||
composeBadge(key, d, c => {
|
||||
if (!c) { IAP.status('Could not load the badge art.', 'bad'); return; }
|
||||
c.toBlob(async bl => {
|
||||
let page = null;
|
||||
try { const r = await (await fetch('/api/my/badge-image?key=' + encodeURIComponent(key), { method: 'POST', headers: { 'Content-Type': 'image/jpeg' }, body: bl })).json(); if (r.error) throw new Error(r.error); page = r.page; }
|
||||
catch (e) { IAP.status(e.message || 'Could not prepare the share page.', 'bad'); return; }
|
||||
openShareSheet(b, d, bl, page);
|
||||
}, 'image/jpeg', 0.9);
|
||||
});
|
||||
}
|
||||
function openShareSheet(b, d, blob, page) {
|
||||
const text = 'I just unlocked the ' + b.label + ' badge on InstantAdPay (' + b.sub + '). Advertising that pays you back, on-chain, the same second a package sells.';
|
||||
const enc = encodeURIComponent;
|
||||
const links = [
|
||||
['X', 'https://x.com/intent/tweet?text=' + enc(text) + '&url=' + enc(page)],
|
||||
['Facebook', 'https://www.facebook.com/sharer/sharer.php?u=' + enc(page)],
|
||||
['Telegram', 'https://t.me/share/url?url=' + enc(page) + '&text=' + enc(text)],
|
||||
['WhatsApp', 'https://wa.me/?text=' + enc(text + ' ' + page)],
|
||||
['LinkedIn', 'https://www.linkedin.com/sharing/share-offsite/?url=' + enc(page)]
|
||||
];
|
||||
const back = document.createElement('div'); back.className = 'modal-back'; back.style.zIndex = '200';
|
||||
back.innerHTML = '<div class="modal-card" role="dialog" aria-modal="true" style="max-width:420px">'
|
||||
+ '<h3 style="margin:0 0 4px">Share your ' + b.label + ' badge</h3><p class="muted small" style="margin:0 0 12px">Each option shares your badge page, which shows the image and your join link.</p>'
|
||||
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">'
|
||||
+ links.map(l => '<a class="btn sec small" target="_blank" rel="noopener" href="' + l[1] + '" style="text-align:center">' + l[0] + '</a>').join('')
|
||||
+ (navigator.share ? '<button type="button" class="btn sec small" data-sh="native">More…</button>' : '')
|
||||
+ '<button type="button" class="btn sec small" data-sh="copy">Copy link</button>'
|
||||
+ '<button type="button" class="btn sec small" data-sh="save">Save image</button>'
|
||||
+ '</div><p class="small muted" style="margin:12px 0 0;word-break:break-all">' + page + '</p>'
|
||||
+ '<div style="text-align:right;margin-top:12px"><button type="button" class="btn sec small" data-sh="close">Close</button></div></div>';
|
||||
document.body.appendChild(back);
|
||||
const close = () => back.remove();
|
||||
back.addEventListener('click', e => { if (e.target === back) close(); });
|
||||
back.querySelector('[data-sh="close"]').addEventListener('click', close);
|
||||
back.querySelector('[data-sh="copy"]').addEventListener('click', async () => { try { await navigator.clipboard.writeText(page); IAP.status('Link copied.', 'ok'); } catch (e) { IAP.status('Copy this: ' + page, 'ok'); } });
|
||||
back.querySelector('[data-sh="save"]').addEventListener('click', () => {
|
||||
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'instantadpay-' + b.label.toLowerCase() + '-badge.jpg'; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
||||
IAP.status('Your ' + b.label + ' badge is saved.', 'ok');
|
||||
});
|
||||
const nat = back.querySelector('[data-sh="native"]');
|
||||
if (nat) nat.addEventListener('click', async () => {
|
||||
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
|
||||
const file = new File([blob], 'instantadpay-' + b.label.toLowerCase() + '-badge.jpg', { type: 'image/jpeg' });
|
||||
if (navigator.canShare && navigator.canShare({ files: [file] })) await navigator.share({ files: [file], title: b.label + ' badge', text: text + ' ' + page });
|
||||
else await navigator.share({ title: b.label + ' badge', text, url: page });
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
// milestone stepper under "Your next move": lit nodes for what's done,
|
||||
|
||||
Reference in New Issue
Block a user