From 3c0e94f20fc6c261f9dde5837a3e0111fa7ef038 Mon Sep 17 00:00:00 2001 From: martbost Date: Sun, 6 Sep 2026 12:21:53 -0500 Subject: [PATCH] Badge emblems, profile social links, and SEO/OG social previews MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Achievement badge share-image now draws a milestone emblem (βš‘πŸŽ―β­πŸ†) in the medal - Profile: Facebook/X/YouTube/Instagram/TikTok/Telegram/LinkedIn/Website links, shown on the public bio page - OG + Twitter Card tags on homepage/ledger/contract (hero banner as share image) - Per-member OG tags server-injected into /wall/ so shared bio links preview with name/bio/avatar - robots.txt + sitemap.xml; .txt/.xml MIME types Co-Authored-By: Claude Fable 5 --- accounts.js | 12 ++++++---- db.js | 1 + public/assets/my.js | 33 ++++++++++++++++----------- public/assets/site.css | 4 ++++ public/assets/wall.js | 8 +++++++ public/contract.html | 19 +++++++++++---- public/index.html | 26 ++++++++++++++++----- public/ledger.html | 19 +++++++++++---- public/my.html | 23 ++++++++++++++----- public/robots.txt | 5 ++++ public/shorts.html | 4 ++-- public/sitemap.xml | 6 +++++ public/tx.html | 6 ++--- public/view.html | 4 ++-- public/wall.html | 8 ++++--- server.js | 52 ++++++++++++++++++++++++++++++++++++++---- 16 files changed, 178 insertions(+), 52 deletions(-) create mode 100644 public/robots.txt create mode 100644 public/sitemap.xml diff --git a/accounts.js b/accounts.js index 9b3f623..aab3bd9 100644 --- a/accounts.js +++ b/accounts.js @@ -35,7 +35,7 @@ function newCode(taken) { const pub = a => a ? { email: a.email, sponsorRef: a.sponsorRef || '', code: a.code || null, username: a.username || null, memberId: a.memberId || 0, lineBannerUrl: a.lineBannerUrl || null, lineTargetUrl: a.lineTargetUrl || null, - avatarUrl: a.avatarUrl || null, bio: a.bio || null, + avatarUrl: a.avatarUrl || null, bio: a.bio || null, socials: a.socials || null, address: a.address || null, created: a.created } : null; const USER_RE = /^[a-zA-Z0-9_]{3,20}$/; const normUser = u => String(u || '').trim().toLowerCase(); @@ -108,11 +108,12 @@ const J = { this.save(); return { ok: true, account: pub(acct) }; }, - async setProfile(e, avatarUrl, bio) { + async setProfile(e, avatarUrl, bio, socials) { const acct = this.db.byEmail[e]; if (!acct) return { error: 'No such account.' }; if (avatarUrl !== undefined) acct.avatarUrl = avatarUrl || null; if (bio !== undefined) acct.bio = bio || null; + if (socials !== undefined) acct.socials = socials || null; this.save(); return { ok: true, account: pub(acct) }; }, @@ -155,7 +156,7 @@ const J = { const rowPub = r => r ? pub({ email: r.email, sponsorRef: r.sponsor_ref, code: r.code, username: r.username, memberId: r.member_id || 0, lineBannerUrl: r.line_banner_url, lineTargetUrl: r.line_target_url, - avatarUrl: r.avatar_url, bio: r.bio, + avatarUrl: r.avatar_url, bio: r.bio, socials: r.socials, address: r.address, created: Number(r.created) }) : null; const D = { async signup(e, password, ref) { @@ -205,9 +206,10 @@ const D = { await db.q('UPDATE accounts SET line_banner_url=?, line_target_url=? WHERE email=?', [bannerUrl || null, targetUrl || null, e]); return { ok: true, account: await this.byEmail(e) }; }, - async setProfile(e, avatarUrl, bio) { + async setProfile(e, avatarUrl, bio, socials) { if (avatarUrl !== undefined) await db.q('UPDATE accounts SET avatar_url=? WHERE email=?', [avatarUrl || null, e]); if (bio !== undefined) await db.q('UPDATE accounts SET bio=? WHERE email=?', [bio || null, e]); + if (socials !== undefined) await db.q('UPDATE accounts SET socials=? WHERE email=?', [socials || null, e]); return { ok: true, account: await this.byEmail(e) }; }, async byMemberId(id) { @@ -310,5 +312,5 @@ async function count() { return impl().count(); } module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername, setUsername, setMemberId, namesForMembers, listByReferrer, downline, linkWallet, count, setLineBanner: (e, b, t) => impl().setLineBanner(String(e || '').toLowerCase(), b, t), - setProfile: (e, a, bio) => impl().setProfile(String(e || '').toLowerCase(), a, bio), + setProfile: (e, a, bio, socials) => impl().setProfile(String(e || '').toLowerCase(), a, bio, socials), byMemberId: id => impl().byMemberId(id) }; diff --git a/db.js b/db.js index 22fc33b..53726ab 100644 --- a/db.js +++ b/db.js @@ -83,6 +83,7 @@ async function bootstrap() { await alterSafe('ALTER TABLE accounts ADD KEY idx_member (member_id)'); await alterSafe('ALTER TABLE accounts ADD COLUMN avatar_url VARCHAR(500) NULL'); await alterSafe('ALTER TABLE accounts ADD COLUMN bio VARCHAR(600) NULL'); + await alterSafe('ALTER TABLE accounts ADD COLUMN socials VARCHAR(1200) NULL'); // JSON {platform:url} await alterSafe('ALTER TABLE accounts ADD COLUMN line_banner_url VARCHAR(500) NULL'); await alterSafe('ALTER TABLE accounts ADD COLUMN line_target_url VARCHAR(500) NULL'); await q(`CREATE TABLE IF NOT EXISTS daily_views ( diff --git a/public/assets/my.js b/public/assets/my.js index cd51540..89c94b6 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -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', icon: '' }, - { key: 'firstBuyer', label: 'First Buyer', sub: 'first qualified referral', icon: '' }, - { key: 'level2', label: 'Level 2', sub: '2 qualifying buyers', icon: '' }, - { key: 'level3', label: 'Level 3', sub: 'fully qualified', icon: '' } + { key: 'payouts', label: 'Payouts On', sub: 'wallet linked', emblem: '⚑', icon: '' }, + { key: 'firstBuyer', label: 'First Buyer', sub: 'first qualified referral', emblem: '🎯', icon: '' }, + { key: 'level2', label: 'Level 2', sub: '2 qualifying buyers', emblem: '⭐', icon: '' }, + { key: 'level3', label: 'Level 3', sub: 'fully qualified', emblem: 'πŸ†', icon: '' } ]; let lastMilestones = []; function renderBadges(d) { @@ -81,16 +81,19 @@ 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(); + // 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 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); + 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, 590); } + if (who) { x.fillStyle = '#43e8c3'; x.font = 'bold 30px Sora, sans-serif'; x.fillText(who, 600, 575); } x.textAlign = 'left'; try { c.toBlob(bl => { @@ -1228,10 +1231,13 @@ fillProfileDetails(a); } catch (e) {} } + const SOCIALS = ['facebook', 'twitter', 'youtube', 'instagram', 'tiktok', 'telegram', 'linkedin', 'website']; function fillProfileDetails(a) { if (!a) return; if (a.bio) $('pfBio').value = a.bio; if (a.avatarUrl) { const p = $('pfAvatarPrev'); p.src = a.avatarUrl; p.hidden = false; } + let soc = {}; try { soc = a.socials ? JSON.parse(a.socials) : {}; } catch (e) {} + for (const p of SOCIALS) if ($('soc-' + p)) $('soc-' + p).value = soc[p] || ''; if (a.username) { const link = location.origin + '/wall/' + a.username; $('pfBioLink').textContent = link; @@ -1255,7 +1261,8 @@ $('pfAvatarFile').value = ''; }); $('pfDetailsSave').addEventListener('click', busy2($('pfDetailsSave'), async () => { - const body = { bio: $('pfBio').value }; + const body = { bio: $('pfBio').value, socials: {} }; + for (const p of SOCIALS) body.socials[p] = ($('soc-' + p) && $('soc-' + p).value.trim()) || ''; if (pfAvatar) body.avatarUrl = pfAvatar; const r = await api('/api/my/profile-details', body); IAP.status('Profile saved.', 'ok'); diff --git a/public/assets/site.css b/public/assets/site.css index 0d1b3f0..3cc55b6 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -373,6 +373,10 @@ textarea{resize:vertical;font:inherit} .bio-head{display:flex;gap:22px;align-items:center;flex-wrap:wrap;padding:8px 0 4px} .bio-avatar{width:96px;height:96px;border-radius:50%;object-fit:cover;border:2px solid var(--mint);flex:0 0 auto} .bio-meta{flex:1;min-width:220px} +.bio-socials{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px} +.bio-socials a{font-size:12px;font-weight:700;color:var(--mint);border:1px solid var(--line-strong); + border-radius:999px;padding:4px 12px;text-decoration:none} +.bio-socials a:hover{border-color:var(--mint)} .bio-qr{flex:0 0 auto;background:var(--panel);border:1px solid var(--line-strong);border-radius:14px;padding:10px} .bio-qr img{display:block;border-radius:8px;background:#eef7f3} /* ── wall page ── */ diff --git a/public/assets/wall.js b/public/assets/wall.js index 6498d75..5a10168 100644 --- a/public/assets/wall.js +++ b/public/assets/wall.js @@ -18,6 +18,14 @@ if (w.avatarUrl) { const av = IAP.$('bioAvatar'); av.src = w.avatarUrl; av.hidden = false; } IAP.$('bioText').textContent = w.bio || 'Building a team on InstantAdPay β€” join through this page and you\'re in my line.'; if (w.qrUrl) IAP.$('bioQr').src = w.qrUrl; + // social links + const SOC = { facebook: 'Facebook', twitter: 'X', youtube: 'YouTube', instagram: 'Instagram', tiktok: 'TikTok', telegram: 'Telegram', linkedin: 'LinkedIn', website: 'Website' }; + const sc = IAP.$('bioSocials'); + if (sc && w.socials && typeof w.socials === 'object') { + const links = Object.keys(SOC).filter(k => w.socials[k]).map(k => + '' + SOC[k] + ''); + sc.innerHTML = links.join(''); + } IAP.$('wallCtaHead').textContent = 'Join ' + w.name + '’s line'; IAP.$('wallJoin').href = w.joinUrl; // per-wall viewed-position memory (survives revisits; anonymous-friendly) diff --git a/public/contract.html b/public/contract.html index 6a95288..af86125 100644 --- a/public/contract.html +++ b/public/contract.html @@ -4,8 +4,19 @@ The contract | InstantAdPay + + + + + + + + + + + - +
@@ -129,8 +140,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 a85f40a..03b226c 100644 --- a/public/index.html +++ b/public/index.html @@ -3,9 +3,23 @@ InstantAdPay: advertise and earn, locked in code - + + + + + + + + + + + + + + + - + @@ -423,9 +437,9 @@ - - - - + + + + diff --git a/public/ledger.html b/public/ledger.html index dfd1c74..5b501bd 100644 --- a/public/ledger.html +++ b/public/ledger.html @@ -4,8 +4,19 @@ Live ledger | InstantAdPay + + + + + + + + + + + - +
@@ -25,8 +36,8 @@
InstantAdPay Β· how it works Β· contract source β†—
- - - + + + diff --git a/public/my.html b/public/my.html index 9f0442a..f35f97a 100644 --- a/public/my.html +++ b/public/my.html @@ -4,7 +4,7 @@ Member area | InstantAdPay - + @@ -534,7 +534,18 @@

-

+

Social links (https URLs β€” shown on your public page):

+
+ + + + + + + + +
+

@@ -584,9 +595,9 @@
- - - - + + + + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..d399d23 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Allow: / +Disallow: /my +Disallow: /api/ +Sitemap: https://instantadpay.com/sitemap.xml diff --git a/public/shorts.html b/public/shorts.html index e7917cc..a5d274c 100644 --- a/public/shorts.html +++ b/public/shorts.html @@ -4,7 +4,7 @@ Shorts | InstantAdPay - +