Add public read-only mode toggle
This commit is contained in:
28
TODO.md
28
TODO.md
@@ -43,20 +43,20 @@ Concrete follow-up work for Jellomator, prioritized by implementation risk and u
|
|||||||
|
|
||||||
## P2 - UX and Product Improvements
|
## P2 - UX and Product Improvements
|
||||||
|
|
||||||
- Replace browser `alert()` with inline form errors/toasts.
|
- [x] Replace browser `alert()` with inline form errors/toasts.
|
||||||
- Show server errors near submit controls.
|
- [x] Show server errors near submit controls.
|
||||||
- Add success toasts for create/update/delete.
|
- [x] Add success toasts for create/update/delete.
|
||||||
- Remove forced reload in auth forms.
|
- [x] Remove forced reload in auth forms.
|
||||||
- Replace `location.reload()` with state refresh only.
|
- [x] Replace `location.reload()` with state refresh only.
|
||||||
- Keep SPA navigation predictable on setup/login/logout.
|
- [x] Keep SPA navigation predictable on setup/login/logout.
|
||||||
- Add drag-and-drop ordering in admin.
|
- [x] Add drag-and-drop ordering in admin.
|
||||||
- Persist `sort_order` updates.
|
- [x] Persist `sort_order` updates.
|
||||||
- Provide keyboard-accessible move controls as fallback.
|
- [x] Provide keyboard-accessible move controls as fallback.
|
||||||
- Add duplicate/cloning for links.
|
- [x] Add duplicate/cloning for links.
|
||||||
- Pre-fill form from an existing link.
|
- [x] Pre-fill form from an existing link.
|
||||||
- Save as new record with unique name validation.
|
- [x] Save as new record with unique name validation.
|
||||||
- Add public read-only mode toggle.
|
- [x] Add public read-only mode toggle.
|
||||||
- Hide admin entry points and editing affordances for non-admin view.
|
- [x] Hide admin entry points and editing affordances for non-admin view.
|
||||||
|
|
||||||
## P3 - Nice-to-Have
|
## P3 - Nice-to-Have
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default function App() {
|
|||||||
const [page, setPage] = useState<Page>('loading');
|
const [page, setPage] = useState<Page>('loading');
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [toast, setToast] = useState<{ message: string; tone: 'success' | 'error' } | null>(null);
|
const [toast, setToast] = useState<{ message: string; tone: 'success' | 'error' } | null>(null);
|
||||||
|
const [publicMode, setPublicMode] = useState<boolean>(() => window.localStorage.getItem('public_mode') === '1');
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
const current = await api.request<SetupState>('/api/me');
|
const current = await api.request<SetupState>('/api/me');
|
||||||
@@ -29,7 +30,7 @@ export default function App() {
|
|||||||
if (!current.current_user) {
|
if (!current.current_user) {
|
||||||
setPage(path.startsWith('/admin') ? 'login' : 'dashboard');
|
setPage(path.startsWith('/admin') ? 'login' : 'dashboard');
|
||||||
} else {
|
} else {
|
||||||
setPage(path.startsWith('/admin') ? 'admin' : 'dashboard');
|
setPage(path.startsWith('/admin') && !publicMode ? 'admin' : 'dashboard');
|
||||||
}
|
}
|
||||||
setLinks(await api.request<LinkItem[]>('/api/links'));
|
setLinks(await api.request<LinkItem[]>('/api/links'));
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ export default function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh().catch(() => setPage('setup'));
|
refresh().catch(() => setPage('setup'));
|
||||||
}, []);
|
}, [publicMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handlePopState = () => {
|
const handlePopState = () => {
|
||||||
@@ -107,6 +108,14 @@ export default function App() {
|
|||||||
query={query}
|
query={query}
|
||||||
setQuery={setQuery}
|
setQuery={setQuery}
|
||||||
canLogout={Boolean(state?.current_user)}
|
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')}
|
onAdmin={() => nav('/admin')}
|
||||||
onLogout={async () => {
|
onLogout={async () => {
|
||||||
await api.request('/api/logout', { method: 'POST' });
|
await api.request('/api/logout', { method: 'POST' });
|
||||||
@@ -155,6 +164,9 @@ function Dashboard({
|
|||||||
query,
|
query,
|
||||||
setQuery,
|
setQuery,
|
||||||
canLogout,
|
canLogout,
|
||||||
|
canAdmin,
|
||||||
|
publicMode,
|
||||||
|
onTogglePublicMode,
|
||||||
onAdmin,
|
onAdmin,
|
||||||
onLogout,
|
onLogout,
|
||||||
}: {
|
}: {
|
||||||
@@ -162,6 +174,9 @@ function Dashboard({
|
|||||||
query: string;
|
query: string;
|
||||||
setQuery: (value: string) => void;
|
setQuery: (value: string) => void;
|
||||||
canLogout: boolean;
|
canLogout: boolean;
|
||||||
|
canAdmin: boolean;
|
||||||
|
publicMode: boolean;
|
||||||
|
onTogglePublicMode: () => void;
|
||||||
onAdmin: () => void;
|
onAdmin: () => void;
|
||||||
onLogout: () => Promise<void>;
|
onLogout: () => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
@@ -191,8 +206,11 @@ function Dashboard({
|
|||||||
<header className="row-between">
|
<header className="row-between">
|
||||||
<h1 className="title-sm">Jellomator</h1>
|
<h1 className="title-sm">Jellomator</h1>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<button type="button" className="btn-subtle" onClick={onTogglePublicMode}>
|
||||||
|
{publicMode ? 'Exit public mode' : 'Public mode'}
|
||||||
|
</button>
|
||||||
{canLogout ? <button type="button" className="btn-subtle" onClick={onLogout}>Logout</button> : null}
|
{canLogout ? <button type="button" className="btn-subtle" onClick={onLogout}>Logout</button> : null}
|
||||||
<button type="button" className="btn-subtle" onClick={onAdmin}>Admin</button>
|
{canAdmin ? <button type="button" className="btn-subtle" onClick={onAdmin}>Admin</button> : null}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user