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:
@@ -17,6 +17,7 @@ const accounts = require('./accounts');
|
||||
const ads = require('./ads');
|
||||
const mailer = require('./mailer');
|
||||
const messages = require('./messages');
|
||||
let QR = null; try { QR = require('qrcode'); } catch (e) { /* optional */ }
|
||||
const chatbot = require('./chatbot');
|
||||
|
||||
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.' });
|
||||
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
|
||||
if (p === '/api/my/linebanner' && req.method === 'POST') {
|
||||
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)
|
||||
.map(x => ({ name: x.username ? '@' + x.username : x.memberId ? 'member #' + x.memberId : 'a member',
|
||||
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),
|
||||
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
|
||||
if (p === '/api/my/videos' && req.method === 'GET') {
|
||||
|
||||
Reference in New Issue
Block a user