Profile build-out (avatar + bio) + shareable bio page with QR

- Profile pane: avatar upload + bio, with a link to your public page
- Wall becomes a bio page: avatar, bio, scannable join QR, line ladder, join CTA
- qrcode npm dep; /api/qr renders SVG QR server-side (CSP-clean img)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-06 08:49:20 -05:00
parent 9e05711f4e
commit 1aab52ca90
15 changed files with 601 additions and 33 deletions
+16
View File
@@ -35,6 +35,7 @@ function newCode(taken) {
const pub = a => a ? { email: a.email, sponsorRef: a.sponsorRef || '', code: a.code || null, const pub = a => a ? { email: a.email, sponsorRef: a.sponsorRef || '', code: a.code || null,
username: a.username || null, memberId: a.memberId || 0, username: a.username || null, memberId: a.memberId || 0,
lineBannerUrl: a.lineBannerUrl || null, lineTargetUrl: a.lineTargetUrl || null, lineBannerUrl: a.lineBannerUrl || null, lineTargetUrl: a.lineTargetUrl || null,
avatarUrl: a.avatarUrl || null, bio: a.bio || null,
address: a.address || null, created: a.created } : null; address: a.address || null, created: a.created } : null;
const USER_RE = /^[a-zA-Z0-9_]{3,20}$/; const USER_RE = /^[a-zA-Z0-9_]{3,20}$/;
const normUser = u => String(u || '').trim().toLowerCase(); const normUser = u => String(u || '').trim().toLowerCase();
@@ -107,6 +108,14 @@ const J = {
this.save(); this.save();
return { ok: true, account: pub(acct) }; return { ok: true, account: pub(acct) };
}, },
async setProfile(e, avatarUrl, bio) {
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;
this.save();
return { ok: true, account: pub(acct) };
},
async byMemberId(id) { async byMemberId(id) {
const a = Object.values(this.db.byEmail).find(x => x.memberId === Number(id)); const a = Object.values(this.db.byEmail).find(x => x.memberId === Number(id));
return a ? pub(a) : null; return a ? pub(a) : null;
@@ -146,6 +155,7 @@ const J = {
const rowPub = r => r ? pub({ email: r.email, sponsorRef: r.sponsor_ref, code: r.code, const rowPub = r => r ? pub({ email: r.email, sponsorRef: r.sponsor_ref, code: r.code,
username: r.username, memberId: r.member_id || 0, username: r.username, memberId: r.member_id || 0,
lineBannerUrl: r.line_banner_url, lineTargetUrl: r.line_target_url, lineBannerUrl: r.line_banner_url, lineTargetUrl: r.line_target_url,
avatarUrl: r.avatar_url, bio: r.bio,
address: r.address, created: Number(r.created) }) : null; address: r.address, created: Number(r.created) }) : null;
const D = { const D = {
async signup(e, password, ref) { async signup(e, password, ref) {
@@ -195,6 +205,11 @@ const D = {
await db.q('UPDATE accounts SET line_banner_url=?, line_target_url=? WHERE email=?', [bannerUrl || null, targetUrl || null, e]); 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) }; return { ok: true, account: await this.byEmail(e) };
}, },
async setProfile(e, avatarUrl, bio) {
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]);
return { ok: true, account: await this.byEmail(e) };
},
async byMemberId(id) { async byMemberId(id) {
const r = await db.q('SELECT * FROM accounts WHERE member_id=?', [Number(id)]); const r = await db.q('SELECT * FROM accounts WHERE member_id=?', [Number(id)]);
return rowPub(r[0]); return rowPub(r[0]);
@@ -295,4 +310,5 @@ async function count() { return impl().count(); }
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername, module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername,
setUsername, setMemberId, namesForMembers, listByReferrer, downline, linkWallet, count, setUsername, setMemberId, namesForMembers, listByReferrer, downline, linkWallet, count,
setLineBanner: (e, b, t) => impl().setLineBanner(String(e || '').toLowerCase(), b, t), setLineBanner: (e, b, t) => impl().setLineBanner(String(e || '').toLowerCase(), b, t),
setProfile: (e, a, bio) => impl().setProfile(String(e || '').toLowerCase(), a, bio),
byMemberId: id => impl().byMemberId(id) }; byMemberId: id => impl().byMemberId(id) };
+2
View File
@@ -81,6 +81,8 @@ async function bootstrap() {
await alterSafe('ALTER TABLE accounts ADD UNIQUE KEY uq_username (username)'); await alterSafe('ALTER TABLE accounts ADD UNIQUE KEY uq_username (username)');
await alterSafe('ALTER TABLE accounts ADD COLUMN member_id INT NULL'); await alterSafe('ALTER TABLE accounts ADD COLUMN member_id INT NULL');
await alterSafe('ALTER TABLE accounts ADD KEY idx_member (member_id)'); 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 line_banner_url VARCHAR(500) NULL'); 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 alterSafe('ALTER TABLE accounts ADD COLUMN line_target_url VARCHAR(500) NULL');
await q(`CREATE TABLE IF NOT EXISTS daily_views ( await q(`CREATE TABLE IF NOT EXISTS daily_views (
+456
View File
@@ -0,0 +1,456 @@
{
"name": "instantadpay-site",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "instantadpay-site",
"version": "0.1.0",
"dependencies": {
"mysql2": "^3.11.0",
"qrcode": "^1.5.4"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@types/node": {
"version": "26.4.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz",
"integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~8.3.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"license": "MIT",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/iconv-lite": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/lru.min": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz",
"integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/mysql2": {
"version": "3.24.3",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.3.tgz",
"integrity": "sha512-OKfWHkMAg9v06neq8FmSyhbxPQKABN9PAW5G9/bDTXzJBO5xXtkKL0V27vju7HQWk9UD4Od5BZsvCtBTB1CPEw==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.2",
"generate-function": "^2.3.1",
"iconv-lite": "^0.7.3",
"long": "^5.3.2",
"lru.min": "^1.1.4",
"named-placeholders": "^1.1.6",
"sql-escaper": "^1.5.1"
},
"engines": {
"node": ">= 8.0"
},
"peerDependencies": {
"@types/node": ">= 8"
}
},
"node_modules/named-placeholders": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
"license": "MIT",
"dependencies": {
"lru.min": "^1.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/sql-escaper": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.1.tgz",
"integrity": "sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=2.0.0",
"node": ">=12.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"license": "MIT",
"peer": true
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
}
}
}
+1 -1
View File
@@ -6,5 +6,5 @@
"main": "server.js", "main": "server.js",
"scripts": {"start": "node server.js", "dev": "node --watch server.js"}, "scripts": {"start": "node server.js", "dev": "node --watch server.js"},
"engines": {"node": ">=20"}, "engines": {"node": ">=20"},
"dependencies": {"mysql2": "^3.11.0"} "dependencies": {"mysql2": "^3.11.0", "qrcode": "^1.5.4"}
} }
+38 -1
View File
@@ -1057,8 +1057,45 @@
: 'Not set yet. Until you set one, your tour slot is skipped.'; : 'Not set yet. Until you set one, your tour slot is skipped.';
} }
async function loadLineBanner() { async function loadLineBanner() {
try { fillLineBanner(await (await fetch('/api/me')).json()); } catch (e) {} try {
const a = await (await fetch('/api/me')).json();
fillLineBanner(a);
fillProfileDetails(a);
} catch (e) {}
} }
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; }
if (a.username) {
const link = location.origin + '/wall/' + a.username;
$('pfBioLink').textContent = link;
$('pfViewBio').hidden = false;
$('pfViewBio').href = '/wall/' + a.username;
}
}
let pfAvatar; // pending avatar url
$('pfAvatarBtn').addEventListener('click', () => $('pfAvatarFile').click());
$('pfAvatarFile').addEventListener('change', async () => {
const f = $('pfAvatarFile').files[0];
if (!f) return;
$('pfAvatarInfo').textContent = 'Uploading…';
try {
const r = await (await fetch('/api/my/upload', { method: 'POST', headers: { 'Content-Type': f.type }, body: f })).json();
if (r.error) { $('pfAvatarInfo').textContent = r.error; $('pfAvatarFile').value = ''; return; }
pfAvatar = r.url;
$('pfAvatarInfo').textContent = 'Uploaded — save to apply.';
const p = $('pfAvatarPrev'); p.src = r.url; p.hidden = false;
} catch (e) { $('pfAvatarInfo').textContent = 'Upload failed.'; }
$('pfAvatarFile').value = '';
});
$('pfDetailsSave').addEventListener('click', busy2($('pfDetailsSave'), async () => {
const body = { bio: $('pfBio').value };
if (pfAvatar) body.avatarUrl = pfAvatar;
const r = await api('/api/my/profile-details', body);
IAP.status('Profile saved.', 'ok');
if (r.account) fillProfileDetails(r.account);
}));
$('lbUploadBtn').addEventListener('click', () => $('lbFile').click()); $('lbUploadBtn').addEventListener('click', () => $('lbFile').click());
$('lbFile').addEventListener('change', async () => { $('lbFile').addEventListener('change', async () => {
const f = $('lbFile').files[0]; const f = $('lbFile').files[0];
+6
View File
@@ -349,6 +349,12 @@ textarea{resize:vertical;font:inherit}
/* ── welcome tour (gauntlet): framed line sites + countdown ── */ /* ── welcome tour (gauntlet): framed line sites + countdown ── */
.lgate-frame{flex:1;border:0;width:100%;background:#fff;min-height:0} .lgate-frame{flex:1;border:0;width:100%;background:#fff;min-height:0}
.ggmeta{padding:8px 16px;background:var(--panel-solid);border-bottom:1px solid var(--line)} .ggmeta{padding:8px 16px;background:var(--panel-solid);border-bottom:1px solid var(--line)}
/* ── bio header (wall/profile page) ── */
.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-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 ── */ /* ── wall page ── */
.wall-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:16px;text-align:center} .wall-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:16px;text-align:center}
.wall-card img{max-width:100%;border-radius:10px;border:1px solid var(--line-strong)} .wall-card img{max-width:100%;border-radius:10px;border:1px solid var(--line-strong)}
+6 -1
View File
@@ -12,7 +12,12 @@
IAP.$('wallJoin').href = '/my'; IAP.$('wallJoin').href = '/my';
return; return;
} }
title.innerHTML = 'The <em>' + String(w.name).replace(/[&<>]/g, '') + '</em> line'; const safeName = String(w.name).replace(/[&<>]/g, '');
title.innerHTML = safeName;
IAP.$('bioHead').hidden = false;
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;
IAP.$('wallCtaHead').textContent = 'Join ' + w.name + '’s line'; IAP.$('wallCtaHead').textContent = 'Join ' + w.name + '’s line';
IAP.$('wallJoin').href = w.joinUrl; IAP.$('wallJoin').href = w.joinUrl;
const grid = IAP.$('wallGrid'); const grid = IAP.$('wallGrid');
+4 -4
View File
@@ -5,7 +5,7 @@
<title>The contract | InstantAdPay</title> <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."> <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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906a"> <link rel="stylesheet" href="/assets/site.css?v=20260906b">
</head> </head>
<body> <body>
<div class="wrap"> <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> <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> </footer>
</div> </div>
<script src="/assets/common.js?v=20260906a"></script> <script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/contract.js?v=20260906a"></script> <script src="/assets/contract.js?v=20260906b"></script>
<script src="/assets/chat.js?v=20260906a"></script> <script src="/assets/chat.js?v=20260906b"></script>
</body> </body>
</html> </html>
+5 -5
View File
@@ -5,7 +5,7 @@
<title>InstantAdPay: advertise and earn, locked in code</title> <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."> <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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906a"> <link rel="stylesheet" href="/assets/site.css?v=20260906b">
</head> </head>
<body> <body>
@@ -410,9 +410,9 @@
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260906a"></script> <script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/wallet.js?v=20260906a"></script> <script src="/assets/wallet.js?v=20260906b"></script>
<script src="/assets/home.js?v=20260906a"></script> <script src="/assets/home.js?v=20260906b"></script>
<script src="/assets/chat.js?v=20260906a"></script> <script src="/assets/chat.js?v=20260906b"></script>
</body> </body>
</html> </html>
+4 -4
View File
@@ -5,7 +5,7 @@
<title>Live ledger | InstantAdPay</title> <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."> <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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906a"> <link rel="stylesheet" href="/assets/site.css?v=20260906b">
</head> </head>
<body> <body>
<div class="wrap"> <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> <div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
</footer> </footer>
</div> </div>
<script src="/assets/common.js?v=20260906a"></script> <script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/ledger.js?v=20260906a"></script> <script src="/assets/ledger.js?v=20260906b"></script>
<script src="/assets/chat.js?v=20260906a"></script> <script src="/assets/chat.js?v=20260906b"></script>
</body> </body>
</html> </html>
+17 -5
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Member area | InstantAdPay</title> <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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906a"> <link rel="stylesheet" href="/assets/site.css?v=20260906b">
</head> </head>
<body class="bo-body"> <body class="bo-body">
@@ -486,6 +486,18 @@
<button class="btn" id="pfSaveBtn">Save username</button> <button class="btn" id="pfSaveBtn">Save username</button>
<p class="muted small" id="pfCurrent" style="margin-top:10px"></p> <p class="muted small" id="pfCurrent" style="margin-top:10px"></p>
</div> </div>
<div class="card">
<h3>Avatar &amp; bio</h3>
<p class="muted small">Your face and your story, shown on your public profile page
(<span class="mono" id="pfBioLink">set a username to get your link</span>).</p>
<p><img id="pfAvatarPrev" alt="avatar" style="width:84px;height:84px;border-radius:50%;object-fit:cover;border:2px solid var(--line-strong)" hidden></p>
<p><input type="file" id="pfAvatarFile" accept="image/png,image/jpeg,image/webp,image/gif" hidden>
<button class="btn small sec" id="pfAvatarBtn" type="button">Upload avatar</button>
<span class="small muted" id="pfAvatarInfo"></span></p>
<p><textarea id="pfBio" rows="3" maxlength="600" placeholder="A short bio for your profile page…" style="width:100%"></textarea></p>
<p><button class="btn" id="pfDetailsSave">Save profile</button>
<a class="btn small sec" id="pfViewBio" target="_blank" rel="noopener" hidden>View my page</a></p>
</div>
<div class="card"> <div class="card">
<h3>Your line banner</h3> <h3>Your line banner</h3>
<p class="muted small">Set a destination link (and a banner image for your wall), and every new <p class="muted small">Set a destination link (and a banner image for your wall), and every new
@@ -533,9 +545,9 @@
</div> </div>
</div> </div>
<script src="/assets/common.js?v=20260906a"></script> <script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/wallet.js?v=20260906a"></script> <script src="/assets/wallet.js?v=20260906b"></script>
<script src="/assets/my.js?v=20260906a"></script> <script src="/assets/my.js?v=20260906b"></script>
<script src="/assets/chat.js?v=20260906a"></script> <script src="/assets/chat.js?v=20260906b"></script>
</body> </body>
</html> </html>
+3 -3
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Transaction | InstantAdPay</title> <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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906a"> <link rel="stylesheet" href="/assets/site.css?v=20260906b">
</head> </head>
<body> <body>
<div id="nav"></div> <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> <p><a href="/ledger">← Back to the live ledger</a> · <a href="/contract">Read the contract review</a></p>
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260906a"></script> <script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/tx.js?v=20260906a"></script> <script src="/assets/tx.js?v=20260906b"></script>
</body> </body>
</html> </html>
+2 -2
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Viewing ad — InstantAdPay</title> <title>Viewing ad — InstantAdPay</title>
<link rel="stylesheet" href="/assets/site.css?v=20260906a"> <link rel="stylesheet" href="/assets/site.css?v=20260906b">
<style> <style>
html,body{height:100%;margin:0;overflow:hidden} 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)} .vw{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--bg,#04110c);color:var(--ink,#e8fff7)}
@@ -43,6 +43,6 @@
</div> </div>
<iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe> <iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe>
</div> </div>
<script src="/assets/view.js?v=20260906a"></script> <script src="/assets/view.js?v=20260906b"></script>
</body> </body>
</html> </html>
+14 -6
View File
@@ -4,15 +4,23 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Banner wall | InstantAdPay</title> <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="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260906a"> <link rel="stylesheet" href="/assets/site.css?v=20260906b">
</head> </head>
<body> <body>
<div id="nav"></div> <div id="nav"></div>
<section> <section>
<div class="wrap" style="max-width:920px"> <div class="wrap" style="max-width:920px">
<div class="sectionhead" style="text-align:left"> <div class="bio-head" id="bioHead" hidden>
<p class="eyebrow">Member banner wall</p> <img id="bioAvatar" class="bio-avatar" alt="" hidden>
<h2 id="wallTitle">Loading the wall…</h2> <div class="bio-meta">
<p class="eyebrow">Member profile</p>
<h2 id="wallTitle" style="margin:.1em 0">Loading…</h2>
<p id="bioText" class="lead" style="font-size:16px"></p>
</div>
<div class="bio-qr"><img id="bioQr" alt="Join QR code" width="150" height="150"><div class="small muted" style="text-align:center">scan to join</div></div>
</div>
<div class="sectionhead" style="text-align:left;margin-top:10px">
<h3 style="margin:0">The line, three levels deep</h3>
<p>Three positions, three levels — the exact levels the contract pays. Every banner here belongs <p>Three positions, three levels — the exact levels the contract pays. Every banner here belongs
to a real member of this line, and every payment between them settles on-chain, instantly.</p> to a real member of this line, and every payment between them settles on-chain, instantly.</p>
</div> </div>
@@ -26,7 +34,7 @@
</div> </div>
</div> </div>
</section> </section>
<script src="/assets/common.js?v=20260906a"></script> <script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/wall.js?v=20260906a"></script> <script src="/assets/wall.js?v=20260906b"></script>
</body> </body>
</html> </html>
+27 -1
View File
@@ -17,6 +17,7 @@ const accounts = require('./accounts');
const ads = require('./ads'); const ads = require('./ads');
const mailer = require('./mailer'); const mailer = require('./mailer');
const messages = require('./messages'); const messages = require('./messages');
let QR = null; try { QR = require('qrcode'); } catch (e) { /* optional */ }
const chatbot = require('./chatbot'); const chatbot = require('./chatbot');
const PORT = Number(process.env.PORT || 3000); const PORT = Number(process.env.PORT || 3000);
@@ -651,6 +652,29 @@ const server = http.createServer(async (req, res) => {
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' }); if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await messages.markRead(s.email, m[1])); return json(res, 200, await messages.markRead(s.email, m[1]));
} }
// -- QR code (SVG) for any InstantAdPay URL — used on bio/wall pages
if (p === '/api/qr' && req.method === 'GET') {
const data = String(u.searchParams.get('d') || '').slice(0, 300);
if (!QR || !data) { res.writeHead(404, baseHeaders()); return res.end(); }
try {
const svg = await QR.toString(data, { type: 'svg', margin: 1, width: 240,
color: { dark: '#0b1512', light: '#eef7f3' } });
res.writeHead(200, baseHeaders({ 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=3600' }));
return res.end(svg);
} catch (e) { res.writeHead(500, baseHeaders()); return res.end(); }
}
// -- profile: avatar + bio
if (p === '/api/my/profile-details' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const avatar = b.avatarUrl === undefined ? undefined : String(b.avatarUrl || '').trim();
if (avatar && !/^(\/uploads\/[a-z0-9]{24}\.(png|jpg|webp|gif)|https:\/\/[^\s]+)$/i.test(avatar))
return json(res, 400, { error: 'Avatar must be an uploaded image or an https image URL.' });
const bio = b.bio === undefined ? undefined : String(b.bio || '').trim().slice(0, 600);
const r = await accounts.setProfile(s.email, avatar, bio);
return json(res, r.error ? 400 : 200, r);
}
// -- line banner: the member's viral slot on welcome tours + their wall // -- line banner: the member's viral slot on welcome tours + their wall
if (p === '/api/my/linebanner' && req.method === 'POST') { if (p === '/api/my/linebanner' && req.method === 'POST') {
const s = await auth.fromRequest(req); const s = await auth.fromRequest(req);
@@ -700,8 +724,10 @@ const server = http.createServer(async (req, res) => {
const ladder = [a, ...await uplineSlides(a.email, 2)].slice(0, 3) const ladder = [a, ...await uplineSlides(a.email, 2)].slice(0, 3)
.map(x => ({ name: x.username ? '@' + x.username : x.memberId ? 'member #' + x.memberId : 'a member', .map(x => ({ name: x.username ? '@' + x.username : x.memberId ? 'member #' + x.memberId : 'a member',
bannerUrl: x.lineBannerUrl || null, targetUrl: x.lineTargetUrl || null })); bannerUrl: x.lineBannerUrl || null, targetUrl: x.lineTargetUrl || null }));
const joinPath = '/join/' + (a.username || a.code);
return json(res, 200, { name: a.username ? '@' + a.username : 'member #' + (a.memberId || 0), return json(res, 200, { name: a.username ? '@' + a.username : 'member #' + (a.memberId || 0),
joinUrl: '/join/' + (a.username || a.code), ladder }); avatarUrl: a.avatarUrl || null, bio: a.bio || null,
joinUrl: joinPath, qrUrl: '/api/qr?d=' + encodeURIComponent('https://instantadpay.com' + joinPath), ladder });
} }
// -- watch-to-earn video ads: serve one, then reward a server-clock-verified watch // -- watch-to-earn video ads: serve one, then reward a server-clock-verified watch
if (p === '/api/my/videos' && req.method === 'GET') { if (p === '/api/my/videos' && req.method === 'GET') {