aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/app.js')
-rw-r--r--src/app.js61
1 files changed, 54 insertions, 7 deletions
diff --git a/src/app.js b/src/app.js
index 0138de2..0ff5ccb 100644
--- a/src/app.js
+++ b/src/app.js
@@ -3563,6 +3563,13 @@ const PUNK_PICKS = [7, 14, 21, 3, 33, 40, 47, 36, 61, 26, 12, 50];
// picker are 56px, so they load the small copies.
const punkImg = (n) => `punks/${n}.webp`;
const punkTile = (n) => assetUrl(`punks-sm/${n}.webp`);
+// Default covers (static/covers, Unsplash nature shots — see CREDITS.txt):
+// published full size (1500x500), previewed from the 750px copies. Like the
+// punk, a pubkey that never picks one still gets its own by default.
+const COVER_COUNT = 12;
+const coverImg = (n) => `covers/${n}.webp`;
+const coverPreview = (n) => assetUrl(`covers-sm/${n}.webp`);
+const defaultCover = (pk) => (pk ? parseInt(pk.slice(-6, -2), 16) % COVER_COUNT : 0) + 1;
async function onbUpload(file) {
const fd = new FormData();
@@ -3851,14 +3858,51 @@ function onboardScreen() {
// The big avatar IS the selection: it shows exactly what Continue will
// publish — a picked punk, an upload, or the pubkey-derived default.
const preview = sel || (me ? punkUrl(me) : null);
+ // ...and the cover behind it the same: a default (number) or an upload
+ // (URL). Untouched, it is this pubkey's default cover.
+ const cover = o.cover ?? defaultCover(me);
+ const coverN = typeof cover === 'number' ? cover : null;
+ const step = (d) => {
+ const from = coverN ?? (o.coverLast || defaultCover(me));
+ o.cover = ((from - 1 + d + COVER_COUNT) % COVER_COUNT) + 1;
+ o.coverLast = o.cover;
+ render();
+ };
+ // the neighbours load ahead, so cycling never shows a blank cover
+ if (coverN) for (const d of [1, -1]) { const im = new Image(); im.src = coverPreview(((coverN - 1 + d + COVER_COUNT) % COVER_COUNT) + 1); }
return page([
title(t('onbAvatarTitle')),
h('p', { class: 'muted', style: 'margin:0' }, t('onbAvatarBody')),
- preview ? h('img', {
- class: 'onb-avatar', src: /^(https?|data|blob):/i.test(preview) ? preview : assetUrl(preview), alt: '',
- style: ui.onbBusy ? 'opacity:.5' : '',
- onError: (e) => { e.target.style.visibility = 'hidden'; },
- }) : null,
+ h('div', { class: 'onb-look' },
+ h('div', {
+ class: 'onb-cover', role: 'group', 'aria-label': t('onbCover'),
+ style: `background-image:url(${JSON.stringify(coverN ? coverPreview(coverN) : cover)})` + (ui.onbBusy ? ';opacity:.5' : ''),
+ // on the wizard state, not this render's closure: a render mid-swipe
+ // swaps the handlers, and the release must still see the press
+ onPointerdown: (e) => { o.swipeX = e.clientX; },
+ onPointerup: (e) => {
+ if (o.swipeX == null) return;
+ const dx = e.clientX - o.swipeX; o.swipeX = null;
+ if (Math.abs(dx) > 40) step(dx < 0 ? 1 : -1);
+ },
+ },
+ h('button', { type: 'button', class: 'onb-cover-nav prev', 'aria-label': t('onbCoverPrev'), onClick: () => step(-1) }, '‹'),
+ h('button', { type: 'button', class: 'onb-cover-nav next', 'aria-label': t('onbCoverNext'), onClick: () => step(1) }, '›'),
+ h('span', { class: 'onb-cover-count' }, coverN ? `${coverN} / ${COVER_COUNT}` : t('onbCoverYours')),
+ h('button', { type: 'button', class: 'onb-cover-upload', disabled: ui.onbBusy, 'aria-label': t('onbCoverUpload'), title: t('onbCoverUpload'),
+ onClick: () => document.getElementById('onb-cover-file')?.click() }, '📷'),
+ h('input', { type: 'file', id: 'onb-cover-file', accept: 'image/*', style: 'display:none', onChange: async (e) => {
+ const f = e.target.files && e.target.files[0];
+ if (!f) return;
+ ui.onbBusy = true; ui.onbError = ''; render();
+ try { o.cover = await onbUpload(f); } catch (err) { ui.onbError = err.message; }
+ ui.onbBusy = false; render();
+ } })),
+ preview ? h('img', {
+ class: 'onb-avatar', src: /^(https?|data|blob):/i.test(preview) ? preview : assetUrl(preview), alt: '',
+ style: ui.onbBusy ? 'opacity:.5' : '',
+ onError: (e) => { e.target.style.visibility = 'hidden'; },
+ }) : null),
h('div', { class: 'onb-punks', 'data-keep-scroll': 'punks' },
...PUNK_PICKS.map((n) =>
h('img', {
@@ -3887,14 +3931,17 @@ function onboardScreen() {
// once replaced a real profile picture with a broken string).
const picked = o.avatar && !/^https?:/i.test(o.avatar) ? 'https://v3.coinos.io/' + o.avatar : o.avatar;
const picture = picked || (me ? new URL(punkUrl(me), 'https://v3.coinos.io/').href : undefined);
- const fields = { name: addr ? addr.split('@')[0] : undefined, picture };
+ const banner = typeof cover === 'number' ? 'https://v3.coinos.io/' + coverImg(cover) : cover;
+ const fields = { name: addr ? addr.split('@')[0] : undefined, picture, banner };
// Publishing the profile is a nicety — a signer that can't sign
// right now (a sleeping phone bunker) must not trap the wizard.
// And it's an OFFER: the address-derived name (and, unless they
// actually picked one, the default punk) only fills gaps — it
// never renames an identity that already has a profile.
if (fields.name || fields.picture) {
- try { await featureHook('publishProfile', fields, { fillOnly: o.avatar ? ['name'] : ['name', 'picture'] }); }
+ // a cover they actually chose replaces one they had; the default only fills a gap
+ const fillOnly = ['name', ...(o.avatar ? [] : ['picture']), ...(o.cover != null ? [] : ['banner'])];
+ try { await featureHook('publishProfile', fields, { fillOnly }); }
catch (e) { toast(t('onbProfileSkipped')); console.warn('onboarding: profile publish failed —', e.message); }
}
o.step = nextAfterAvatar();