From 0f106e35449b5c45ab911a0a65eac3258de6c6fe Mon Sep 17 00:00:00 2001 From: Luna Date: Thu, 21 May 2026 19:47:28 +0000 Subject: [PATCH] Add public read-only mode toggle --- TODO.md | 28 ++++++++++++++-------------- frontend/src/app.tsx | 24 +++++++++++++++++++++--- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/TODO.md b/TODO.md index 2007301..af04370 100644 --- a/TODO.md +++ b/TODO.md @@ -43,20 +43,20 @@ Concrete follow-up work for Jellomator, prioritized by implementation risk and u ## P2 - UX and Product Improvements -- Replace browser `alert()` with inline form errors/toasts. - - Show server errors near submit controls. - - Add success toasts for create/update/delete. -- Remove forced reload in auth forms. - - Replace `location.reload()` with state refresh only. - - Keep SPA navigation predictable on setup/login/logout. -- Add drag-and-drop ordering in admin. - - Persist `sort_order` updates. - - Provide keyboard-accessible move controls as fallback. -- Add duplicate/cloning for links. - - Pre-fill form from an existing link. - - Save as new record with unique name validation. -- Add public read-only mode toggle. - - Hide admin entry points and editing affordances for non-admin view. +- [x] Replace browser `alert()` with inline form errors/toasts. + - [x] Show server errors near submit controls. + - [x] Add success toasts for create/update/delete. +- [x] Remove forced reload in auth forms. + - [x] Replace `location.reload()` with state refresh only. + - [x] Keep SPA navigation predictable on setup/login/logout. +- [x] Add drag-and-drop ordering in admin. + - [x] Persist `sort_order` updates. + - [x] Provide keyboard-accessible move controls as fallback. +- [x] Add duplicate/cloning for links. + - [x] Pre-fill form from an existing link. + - [x] Save as new record with unique name validation. +- [x] Add public read-only mode toggle. + - [x] Hide admin entry points and editing affordances for non-admin view. ## P3 - Nice-to-Have diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index c27d30c..677781d 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -17,6 +17,7 @@ export default function App() { const [page, setPage] = useState('loading'); const [query, setQuery] = useState(''); const [toast, setToast] = useState<{ message: string; tone: 'success' | 'error' } | null>(null); + const [publicMode, setPublicMode] = useState(() => window.localStorage.getItem('public_mode') === '1'); async function refresh() { const current = await api.request('/api/me'); @@ -29,7 +30,7 @@ export default function App() { if (!current.current_user) { setPage(path.startsWith('/admin') ? 'login' : 'dashboard'); } else { - setPage(path.startsWith('/admin') ? 'admin' : 'dashboard'); + setPage(path.startsWith('/admin') && !publicMode ? 'admin' : 'dashboard'); } setLinks(await api.request('/api/links')); } @@ -43,7 +44,7 @@ export default function App() { useEffect(() => { refresh().catch(() => setPage('setup')); - }, []); + }, [publicMode]); useEffect(() => { const handlePopState = () => { @@ -107,6 +108,14 @@ export default function App() { query={query} setQuery={setQuery} canLogout={Boolean(state?.current_user)} + canAdmin={Boolean(state?.current_user) && !publicMode} + publicMode={publicMode} + onTogglePublicMode={() => { + const next = !publicMode; + setPublicMode(next); + window.localStorage.setItem('public_mode', next ? '1' : '0'); + if (next && window.location.pathname.startsWith('/admin')) nav('/'); + }} onAdmin={() => nav('/admin')} onLogout={async () => { await api.request('/api/logout', { method: 'POST' }); @@ -155,6 +164,9 @@ function Dashboard({ query, setQuery, canLogout, + canAdmin, + publicMode, + onTogglePublicMode, onAdmin, onLogout, }: { @@ -162,6 +174,9 @@ function Dashboard({ query: string; setQuery: (value: string) => void; canLogout: boolean; + canAdmin: boolean; + publicMode: boolean; + onTogglePublicMode: () => void; onAdmin: () => void; onLogout: () => Promise; }) { @@ -191,8 +206,11 @@ function Dashboard({

Jellomator

+ {canLogout ? : null} - + {canAdmin ? : null}