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:
+38
-1
@@ -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];
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 & 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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user