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];