From f6e80064ff8c4869a6ba2cef908cc54326948463 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 30 Aug 2026 23:57:57 +0200 Subject: refactor(ui): extract Settings, Profile and Admin pages from app.js Admin page is lazy-loaded so non-admin users never fetch it. Co-Authored-By: Claude Opus 4.6 --- .../src/meshbay_hub/static/profile-page.js | 200 +++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/profile-page.js (limited to 'packages/meshbay-hub/src/meshbay_hub/static/profile-page.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js new file mode 100644 index 0000000..3d3e578 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js @@ -0,0 +1,200 @@ +import { + html, useState, useEffect, useCallback, +} from './vendor/htm-preact.js'; +import { t } from './i18n.js'; +import { Icon } from './icon.js'; +import { hubFetch } from './hub-client.js'; + +export function ProfilePage({ user, onLogout }) { + const [nodeKey, setNodeKey] = useState(''); + const [currentNodeKey, setCurrentNodeKey] = useState(null); + const [nodeKeyStatus, setNodeKeyStatus] = useState(''); + const [nodeKeyLoading, setNodeKeyLoading] = useState(false); + const [email, setEmail] = useState(''); + const [emailDraft, setEmailDraft] = useState(''); + const [emailEditing, setEmailEditing] = useState(false); + const [emailSaving, setEmailSaving] = useState(false); + const [emailStatus, setEmailStatus] = useState(''); + const [pinCount, setPinCount] = useState( + () => (window.MeshBayTransport?.pinnedNodeCount?.() ?? 0)); + const [delOpen, setDelOpen] = useState(false); + const [delPass, setDelPass] = useState(''); + const [delError, setDelError] = useState(''); + const [deleting, setDeleting] = useState(false); + + const deleteAccount = useCallback(async (e) => { + e.preventDefault(); + setDeleting(true); + setDelError(''); + try { + const authKey = await window.MeshBayKeys.deriveAuthKey(delPass, user.username); + await hubFetch('/v1/users/me', { + method: 'DELETE', token: user.token, body: { auth_key: authKey }, + }); + onLogout(); + } catch (err) { + setDelError(err.message); + } finally { + setDeleting(false); + } + }, [delPass, user]); + + const clearPins = useCallback(() => { + window.MeshBayTransport?.clearNodePin?.(); + setPinCount(window.MeshBayTransport?.pinnedNodeCount?.() ?? 0); + }, []); + + useEffect(() => { + hubFetch('/v1/users/me', { token: user.token }) + .then(data => { + if (data.email) { setEmail(data.email); setEmailDraft(data.email); } + }) + .catch(() => {}); + hubFetch(`/v1/users/${user.username}/pubkeys`, { token: user.token }) + .then(data => { + if (data.pk_node_ed25519) setCurrentNodeKey(data.pk_node_ed25519); + }) + .catch(() => {}); + }, [user.username, user.token]); + + const saveEmail = useCallback(async () => { + const val = emailDraft.trim(); + if (!val || val === email) { setEmailEditing(false); return; } + setEmailSaving(true); + setEmailStatus(''); + try { + await hubFetch('/v1/users/me', { + method: 'PATCH', token: user.token, body: { email: val }, + }); + setEmail(val); + setEmailEditing(false); + setEmailStatus(t('settings.email_saved')); + setTimeout(() => setEmailStatus(''), 3000); + } catch (e) { + setEmailStatus(e.message); + } finally { + setEmailSaving(false); + } + }, [emailDraft, email, user.token]); + + const submitNodeKey = useCallback(async () => { + const key = nodeKey.trim(); + if (!key) return; + setNodeKeyLoading(true); + setNodeKeyStatus(''); + try { + await hubFetch('/v1/users/me/node_key', { + method: 'PUT', token: user.token, + body: { pk_node_ed25519: key }, + }); + setCurrentNodeKey(key); + setNodeKey(''); + setNodeKeyStatus(t('settings.node_key_success')); + } catch (e) { + setNodeKeyStatus(e.message); + } finally { + setNodeKeyLoading(false); + } + }, [nodeKey, user.token]); + + return html` +
+

${t('profile.title')}

+ +
+

${t('settings.profile')}

+
+ ${t('settings.username')} + ${user.username} +
+
+ ${t('settings.email')} + ${emailEditing + ? html` + setEmailDraft(e.target.value)} + onKeyDown=${e => e.key === 'Enter' && saveEmail()} + style="font-size:0.9em;padding:4px 8px;border:1px solid var(--border);border-radius:4px" /> + + + ` + : html` + ${email || '—'} + + ` + } +
+ ${emailStatus && html`

${emailStatus}

`} +
+ +
+

${t('settings.node_key')}

+

${t('settings.node_key_desc')}

+ ${currentNodeKey && html` +
+ ${t('settings.node_key_current')} + ${currentNodeKey} +
+ `} +
+ setNodeKey(e.target.value)} + onKeyDown=${e => e.key === 'Enter' && submitNodeKey()} /> + +
+ ${nodeKeyStatus && html` +

+ ${nodeKeyStatus} +

+ `} +
+ +
+

${t('settings.node_pins')}

+

${t('settings.node_pins_hint')}

+
+ ${t('settings.node_pins_count', { n: pinCount })} + +
+
+ +
+

${t('settings.danger')}

+

${t('settings.delete_hint')}

+ ${delError && html`

${delError}

`} + ${!delOpen + ? html`` + : html` +
+

${t('settings.delete_confirm')}

+
+ setDelPass(e.target.value)} required /> + + +
+
+ `} +
+ +
+ `; +} -- cgit v1.2.3