Wall page: mint QR, achievement badge, admin-ad backfill, join gated on views

- QR is mint-green (still high-contrast for reliable scanning).
- The wall shows the owner's achievement badge next to their avatar.
- Positions 2 & 3 fill with upline line-banners, then ADMIN ADS when there's no
  upline (configurable via data/admin-wall-ads.json; sensible default) so the
  wall is never sparse.
- "Join free through this wall" is disabled until the visitor has viewed every
  ad on the wall, with a "X/Y viewed" prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-07 08:35:44 -05:00
parent a4119d652d
commit 0dbd99f69e
11 changed files with 59 additions and 21 deletions
+27 -4
View File
@@ -56,6 +56,12 @@ async function uplineSlides(email, depth = 3) {
}
return out;
}
// admin fallback ads for wall positions 2 & 3 when a member has no upline.
// Configurable by dropping data/admin-wall-ads.json ([{name,targetUrl,bannerUrl}]).
function getAdminWallAds() {
try { const j = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'admin-wall-ads.json'), 'utf8')); if (Array.isArray(j) && j.length) return j; } catch (e) {}
return [{ name: 'InstantAdPay', targetUrl: 'https://instantadpay.com/', bannerUrl: null }];
}
const chatHits = new Map();
function chatLimited(ip) {
const now = Date.now(), rec = chatHits.get(ip);
@@ -870,7 +876,7 @@ const server = http.createServer(async (req, res) => {
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' } });
color: { dark: '#0e7d5f', light: '#f2fbf8' } }); // mint-green modules, still high-contrast for reliable scanning
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(); }
@@ -944,14 +950,31 @@ const server = http.createServer(async (req, res) => {
let a = await accounts.byUsername(tok);
if (!a) a = await accounts.byCode(tok);
if (!a) return json(res, 404, { error: 'No wall under that name.' });
const ladder = [a, ...await uplineSlides(a.email, 2)].slice(0, 3)
const ladder = [a, ...await uplineSlides(a.email, 2)]
.map(x => ({ name: x.username ? '@' + x.username : x.memberId ? 'member #' + x.memberId : 'a member',
bannerUrl: x.lineBannerUrl || null, targetUrl: x.lineTargetUrl || null }));
// fill positions 2 & 3 with admin ads when there isn't enough upline, so the wall is never sparse
const adAds = getAdminWallAds();
let ai = 0;
while (ladder.length < 3 && adAds.length) {
const ad = adAds[ai++ % adAds.length];
ladder.push({ name: ad.name || 'InstantAdPay', bannerUrl: ad.bannerUrl || null, targetUrl: ad.targetUrl || 'https://instantadpay.com/', admin: true });
}
const finalLadder = ladder.slice(0, 3);
// the wall owner's achievement badge (their highest reached tier)
let badge = null;
if (a.memberId) {
try {
const bc = (await chain.member(a.memberId)).buyerCount || 0;
const t = bc >= 5 ? ['nexus', 'Nexus'] : bc >= 2 ? ['circuit', 'Circuit'] : bc >= 1 ? ['surge', 'Surge'] : ['spark', 'Spark'];
badge = { img: '/badges/badge-' + t[0] + '.jpg', label: t[1] };
} catch (e) {}
}
const joinPath = '/join/' + (a.username || a.code);
let socials = null; try { socials = a.socials ? JSON.parse(a.socials) : null; } catch (e) {}
return json(res, 200, { name: a.username ? '@' + a.username : 'member #' + (a.memberId || 0),
avatarUrl: a.avatarUrl || null, bio: a.bio || null, socials,
joinUrl: joinPath, qrUrl: '/api/qr?d=' + encodeURIComponent('https://instantadpay.com' + joinPath), ladder });
avatarUrl: a.avatarUrl || null, bio: a.bio || null, socials, badge,
joinUrl: joinPath, qrUrl: '/api/qr?d=' + encodeURIComponent('https://instantadpay.com' + joinPath), ladder: finalLadder });
}
// -- watch-to-earn video ads: serve one, then reward a server-clock-verified watch
if (p === '/api/my/videos' && req.method === 'GET') {