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.
-
-
-
+
+
+