From 78e763c3777ccb352d0344bc96528b863ffb384c Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:17:45 +0200 Subject: [PATCH] =?UTF-8?q?feat(settings):=20frontend=20=E2=80=94=20Settin?= =?UTF-8?q?gsView=20component,=20settings=20nav,=20App=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.jsx | 4 + frontend/src/components/SettingsView.jsx | 271 +++++++++++++++++++++++ frontend/src/components/Topbar.jsx | 5 +- 3 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/SettingsView.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f8f01ab..0061113 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -10,6 +10,7 @@ import RunsView from './components/RunsView' import AdminView from './components/AdminView' import TagsView from './components/TagsView' import CollectionsView from './components/CollectionsView' +import SettingsView from './components/SettingsView' import ContextRail from './components/ContextRail' export const AuthContext = createContext(null); @@ -217,6 +218,9 @@ export default function App() { {view === 'collections' && ( )} + {view === 'settings' && ( + + )} +

Settings

+
+ {['profile', 'tokens', ...(isAdmin ? ['instance'] : [])].map(t => ( + + ))} +
+ + {tab === 'profile' && } + {tab === 'tokens' && } + {tab === 'instance' && isAdmin && } + + ) +} + +function ProfileTab({ currentUser, setCurrentUser }) { + const [displayName, setDisplayName] = useState(currentUser?.display_name ?? '') + const [saving, setSaving] = useState(false) + const [saveMsg, setSaveMsg] = useState(null) + + const [curPw, setCurPw] = useState('') + const [newPw, setNewPw] = useState('') + const [confirmPw, setConfirmPw] = useState('') + const [pwSaving, setPwSaving] = useState(false) + const [pwMsg, setPwMsg] = useState(null) + + async function handleSaveProfile(e) { + e.preventDefault() + setSaving(true) + setSaveMsg(null) + try { + await updateProfile(displayName) + setCurrentUser(u => ({ ...u, display_name: displayName || null })) + setSaveMsg({ ok: true, text: 'Saved.' }) + } catch (err) { + setSaveMsg({ ok: false, text: err.message }) + } finally { + setSaving(false) + } + } + + async function handleChangePassword(e) { + e.preventDefault() + if (newPw !== confirmPw) { setPwMsg({ ok: false, text: 'Passwords do not match.' }); return } + setPwSaving(true) + setPwMsg(null) + try { + await changePassword(curPw, newPw) + setCurPw(''); setNewPw(''); setConfirmPw('') + setPwMsg({ ok: true, text: 'Password changed.' }) + } catch (err) { + setPwMsg({ ok: false, text: err.message }) + } finally { + setPwSaving(false) + } + } + + return ( +
+
+

Display Name

+
+ setDisplayName(e.target.value)} /> + {saveMsg && ( +
{saveMsg.text}
+ )} + +
+
+ +
+

Change Password

+
+ setCurPw(e.target.value)} required /> + setNewPw(e.target.value)} required /> + setConfirmPw(e.target.value)} required /> + {pwMsg && ( +
{pwMsg.text}
+ )} + +
+
+
+ ) +} + +function TokensTab() { + const [tokens, setTokens] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [newName, setNewName] = useState('') + const [creating, setCreating] = useState(false) + const [newToken, setNewToken] = useState(null) // { raw_token, name } + + const refresh = useCallback(async () => { + setLoading(true); setError(null) + try { setTokens(await listTokens()) } + catch (e) { setError(e.message) } + finally { setLoading(false) } + }, []) + + useEffect(() => { refresh() }, [refresh]) + + async function handleCreate(e) { + e.preventDefault() + if (!newName.trim()) return + setCreating(true) + try { + const tok = await createToken(newName.trim()) + setNewToken(tok) + setNewName('') + refresh() + } catch (err) { + setError(err.message) + } finally { + setCreating(false) + } + } + + async function handleDelete(tokenUid) { + try { + await deleteToken(tokenUid) + setTokens(ts => ts.filter(t => t.token_uid !== tokenUid)) + } catch (err) { + setError(err.message) + } + } + + return ( +
+

API Tokens

+ {newToken && ( +
+ Token created. Copy it now \u2014 it will not be shown again.
+ + {newToken.raw_token} + + +
+ )} +
+ setNewName(e.target.value)} style={{ flex: 1 }} required /> + +
+ {error &&
{error}
} + {loading ?
Loading\u2026
: ( +
+ {tokens.length === 0 &&
No tokens yet.
} + {tokens.map(tok => ( +
+
+ {tok.name} +
+ Created {tok.created_at.slice(0, 10)} + {tok.last_used_at && ` \u00b7 Last used ${tok.last_used_at.slice(0, 10)}`} +
+
+ +
+ ))} +
+ )} +
+ ) +} + +function InstanceTab() { + const [settings, setSettings] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [saving, setSaving] = useState(false) + const [saveMsg, setSaveMsg] = useState(null) + + useEffect(() => { + (async () => { + try { setSettings(await getInstanceSettings()) } + catch (e) { setError(e.message) } + finally { setLoading(false) } + })() + }, []) + + async function handleSave(e) { + e.preventDefault() + setSaving(true); setSaveMsg(null) + try { + await updateInstanceSettings(settings) + setSaveMsg({ ok: true, text: 'Saved.' }) + } catch (err) { + setSaveMsg({ ok: false, text: err.message }) + } finally { + setSaving(false) + } + } + + if (loading) return
Loading\u2026
+ if (error) return
{error}
+ if (!settings) return null + + return ( +
+

Instance Settings

+
+
+ {[ + ['public_index_enabled', 'Public index (unauthenticated browsing)'], + ['public_entry_content_enabled', 'Public entry content'], + ['open_registration_enabled', 'Open registration'], + ].map(([key, label]) => ( + + ))} +
+ + +
+
+ {saveMsg &&
{saveMsg.text}
} + +
+
+ ) +} diff --git a/frontend/src/components/Topbar.jsx b/frontend/src/components/Topbar.jsx index bf6ced4..e188cec 100644 --- a/frontend/src/components/Topbar.jsx +++ b/frontend/src/components/Topbar.jsx @@ -21,7 +21,7 @@ export default function Topbar({ archives, archiveId, onArchiveChange, view, onV {archives.map(a => )}