refactor(frontend): lint, one session guard, one request helper, one user context

This commit is contained in:
lobo committed 2026-09-29 15:40:55 +02:00
1 parent 111ab1e200
commit d0ea9e900d
78 files changed
+2138 -624

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
import {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import {request} from '../utils/api.js';
import {SESSION_KEY, hasSession} from '../utils/auth.js';
import {ME_URL, REQUEST_TIMEOUT_MS} from '../utils/settings.js';
const UserContext = createContext(null);
// Apart from the user, so a component that only reloads or clears it does not render again on each change.
const ActionsContext = createContext(null);
/**
* GET /api/users/me for the whole app. `user` is undefined until the first answer and null without a session.
* `reload()` loads it again and resolves to the `request()` result (not ok for a 200 without a body).
* `updateUser(fn)` changes it locally after a save, and skips it when the user is gone by then; `setUser(null)`
* drops it. A save that replaces settings still loads /me itself (see saveSettings), as it must be fresh.
*/
export function CurrentUserProvider({children}) {
const [user, setUser] = useState(() => (hasSession() ? undefined : null));
const latest = useRef(0);
const reload = useCallback(async () => {
const call = ++latest.current;
// After a logout, "no user" becomes "loading" (e.g. the import waits for the favorite platform).
setUser((current) => current ?? undefined);
const response = await request(ME_URL(), {signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)});
const result = {...response, ok: response.ok && response.data != null};
if (call === latest.current) {
// Only a 401 means "no user"; another failure keeps the user shown so far, or ends "loading".
if (result.ok) setUser(result.data);
else if (result.status === 401) setUser(null);
else setUser((current) => current ?? null);
}
return result;
}, []);
useEffect(() => {
if (hasSession()) reload();
// A login, logout or "view as" in another tab.
const onStorage = (event) => {
if (event.key !== null && event.key !== SESSION_KEY) return;
if (hasSession()) reload();
else setUser(null);
};
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}, [reload]);
const updateUser = useCallback((change) => setUser((current) => current && change(current)), []);
const actions = useMemo(() => ({reload, setUser, updateUser}), [reload, updateUser]);
return (
<ActionsContext.Provider value={actions}>
<UserContext.Provider value={user}>{children}</UserContext.Provider>
</ActionsContext.Provider>
);
}
const NO_ACTIONS = {reload: async () => ({ok: false, status: 0, data: null}), setUser: () => {}, updateUser: () => {}};
/**
* `{reload, setUser, updateUser}`, without the user. Outside a provider (e.g. a test of one component) they do
* nothing.
*/
export function useCurrentUserActions() {
return useContext(ActionsContext) ?? NO_ACTIONS;
}
/**
* `{user, reload, setUser, updateUser}`; outside a provider there is no user.
*/
export function useCurrentUserContext() {
const user = useContext(UserContext);
const actions = useContext(ActionsContext);
return {user: actions ? user : null, ...(actions ?? NO_ACTIONS)};
}