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
+38 -1
View File
@@ -1057,8 +1057,45 @@
: 'Not set yet. Until you set one, your tour slot is skipped.';
}
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());
$('lbFile').addEventListener('change', async () => {
const f = $('lbFile').files[0];
+6
View File
@@ -349,6 +349,12 @@ textarea{resize:vertical;font:inherit}
/* ── welcome tour (gauntlet): framed line sites + countdown ── */
.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)}
/* ── 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-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)}
+6 -1
View File
@@ -12,7 +12,12 @@
IAP.$('wallJoin').href = '/my';
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.$('wallJoin').href = w.joinUrl;
const grid = IAP.$('wallGrid');
+4 -4
View File
@@ -5,7 +5,7 @@
<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.">
<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>
<body>
<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>
</footer>
</div>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/contract.js?v=20260906a"></script>
<script src="/assets/chat.js?v=20260906a"></script>
<script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/contract.js?v=20260906b"></script>
<script src="/assets/chat.js?v=20260906b"></script>
</body>
</html>
+5 -5
View File
@@ -5,7 +5,7 @@
<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.">
<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>
<body>
@@ -410,9 +410,9 @@
</div>
</section>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/wallet.js?v=20260906a"></script>
<script src="/assets/home.js?v=20260906a"></script>
<script src="/assets/chat.js?v=20260906a"></script>
<script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/wallet.js?v=20260906b"></script>
<script src="/assets/home.js?v=20260906b"></script>
<script src="/assets/chat.js?v=20260906b"></script>
</body>
</html>
+4 -4
View File
@@ -5,7 +5,7 @@
<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.">
<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>
<body>
<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>
</footer>
</div>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/ledger.js?v=20260906a"></script>
<script src="/assets/chat.js?v=20260906a"></script>
<script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/ledger.js?v=20260906b"></script>
<script src="/assets/chat.js?v=20260906b"></script>
</body>
</html>
+17 -5
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<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="/assets/site.css?v=20260906a">
<link rel="stylesheet" href="/assets/site.css?v=20260906b">
</head>
<body class="bo-body">
@@ -486,6 +486,18 @@
<button class="btn" id="pfSaveBtn">Save username</button>
<p class="muted small" id="pfCurrent" style="margin-top:10px"></p>
</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">
<h3>Your line banner</h3>
<p class="muted small">Set a destination link (and a banner image for your wall), and every new
@@ -533,9 +545,9 @@
</div>
</div>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/wallet.js?v=20260906a"></script>
<script src="/assets/my.js?v=20260906a"></script>
<script src="/assets/chat.js?v=20260906a"></script>
<script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/wallet.js?v=20260906b"></script>
<script src="/assets/my.js?v=20260906b"></script>
<script src="/assets/chat.js?v=20260906b"></script>
</body>
</html>
+3 -3
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<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="/assets/site.css?v=20260906a">
<link rel="stylesheet" href="/assets/site.css?v=20260906b">
</head>
<body>
<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>
</div>
</section>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/tx.js?v=20260906a"></script>
<script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/tx.js?v=20260906b"></script>
</body>
</html>
+2 -2
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Viewing ad — InstantAdPay</title>
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
<link rel="stylesheet" href="/assets/site.css?v=20260906b">
<style>
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)}
@@ -43,6 +43,6 @@
</div>
<iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe>
</div>
<script src="/assets/view.js?v=20260906a"></script>
<script src="/assets/view.js?v=20260906b"></script>
</body>
</html>
+14 -6
View File
@@ -4,15 +4,23 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<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="/assets/site.css?v=20260906a">
<link rel="stylesheet" href="/assets/site.css?v=20260906b">
</head>
<body>
<div id="nav"></div>
<section>
<div class="wrap" style="max-width:920px">
<div class="sectionhead" style="text-align:left">
<p class="eyebrow">Member banner wall</p>
<h2 id="wallTitle">Loading the wall…</h2>
<div class="bio-head" id="bioHead" hidden>
<img id="bioAvatar" class="bio-avatar" alt="" hidden>
<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
to a real member of this line, and every payment between them settles on-chain, instantly.</p>
</div>
@@ -26,7 +34,7 @@
</div>
</div>
</section>
<script src="/assets/common.js?v=20260906a"></script>
<script src="/assets/wall.js?v=20260906a"></script>
<script src="/assets/common.js?v=20260906b"></script>
<script src="/assets/wall.js?v=20260906b"></script>
</body>
</html>