improvements

This commit is contained in:
nvrl committed 2026-06-15 20:14:34 +02:00
1 parent 1620b24ff3
commit 3d01bfb4d1
7 files changed
+65 -25

No files matched your search

+14
View File
@@ -74,6 +74,20 @@ Verify mutations `invalidateQueries`; consider optimistic insert so reviews appe
--- ---
## Implementation status ## Implementation status
- [x] U2 — killed the hard `window.location.href='/'` reloads. `Login.tsx`
(post-login) now `await refetchUser()` + `navigate('/')`; `Settings.tsx`
(post-account-deletion) `queryClient.clear()` + `await refetchUser()` +
`navigate('/')` — deletion wipes the whole react-query cache so no stale user
data survives the now-gone account. Both keep the SPA mounted (warm cache, no
white flash). `tsc` + ESLint (0 errors) clean.
- [x] R4 — split `refetchUser` / `refetchFollowing` (`AuthContext.tsx`). The 60s
badge poll (`NotificationContext.tsx:34`) now only hits `users/me/`; the
following list is fetched once on login (effect keyed on `user?.username`) and
cleared on logout. Added `refetchFollowing` to the context value and call it
after follow/unfollow in `PublicProfile.tsx` (both the main + mini toggles) so
the sidebar list updates immediately instead of waiting for the next poll.
Drops one `following_list` GET per minute per logged-in client. `tsc` + ESLint
(0 errors) clean.
- [x] Q2 — `useCategories()` query hook (`api/queries/useCategories.ts`, key - [x] Q2 — `useCategories()` query hook (`api/queries/useCategories.ts`, key
`queryKeys.categories()`, 30 min `staleTime`). Replaced raw `queryKeys.categories()`, 30 min `staleTime`). Replaced raw
`api.get('categories/')` + `useState`/`useEffect` in `App.tsx` with the hook; `api.get('categories/')` + `useState`/`useEffect` in `App.tsx` with the hook;
+3 -2
View File
@@ -41,8 +41,9 @@ const fetchAllFlavors = async (): Promise<FlavorListItem[]> => {
const all: FlavorListItem[] = []; const all: FlavorListItem[] = [];
let url: string | null = 'flavors/'; let url: string | null = 'flavors/';
while (url) { while (url) {
const res: { data: FlavorListItem[] | Paginated<FlavorListItem> } = const res: { data: FlavorListItem[] | Paginated<FlavorListItem> } = await api.get<
await api.get<FlavorListItem[] | Paginated<FlavorListItem>>(url); FlavorListItem[] | Paginated<FlavorListItem>
>(url);
all.push(...unwrap(res.data)); all.push(...unwrap(res.data));
const next = Array.isArray(res.data) ? null : (res.data.next ?? null); const next = Array.isArray(res.data) ? null : (res.data.next ?? null);
// `next` is an absolute URL; api baseURL already covers the prefix, so use the relative tail. // `next` is an absolute URL; api baseURL already covers the prefix, so use the relative tail.
@@ -1,13 +1,4 @@
import { import { Avatar, Box, Button, Menu, MenuItem, Typography, alpha, useTheme } from '@mui/material';
Avatar,
Box,
Button,
Menu,
MenuItem,
Typography,
alpha,
useTheme,
} from '@mui/material';
import type { MenuProps, SxProps, Theme } from '@mui/material'; import type { MenuProps, SxProps, Theme } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
+26 -10
View File
@@ -24,6 +24,7 @@ export interface AuthContextValue {
following: FollowingEntry[]; following: FollowingEntry[];
loadingUser: boolean; loadingUser: boolean;
refetchUser: () => Promise<void>; refetchUser: () => Promise<void>;
refetchFollowing: () => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
} }
@@ -35,6 +36,18 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [following, setFollowing] = useState<FollowingEntry[]>([]); const [following, setFollowing] = useState<FollowingEntry[]>([]);
const [loadingUser, setLoadingUser] = useState(true); const [loadingUser, setLoadingUser] = useState(true);
// Following list rarely changes: fetched on login and after a follow/unfollow
// mutation — NOT on the 60s badge poll (that only needs `users/me/`).
const refetchFollowing = useCallback(async () => {
try {
const followRes = await api.get('users/following_list/');
const data = Array.isArray(followRes.data) ? followRes.data : followRes.data.results || [];
setFollowing(data);
} catch {
/* ignore */
}
}, []);
const refetchUser = useCallback(async () => { const refetchUser = useCallback(async () => {
try { try {
const res = await api.get('users/me/', { const res = await api.get('users/me/', {
@@ -44,15 +57,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
setUser(res.data); setUser(res.data);
if (res.data.theme) localStorage.setItem('theme', res.data.theme); if (res.data.theme) localStorage.setItem('theme', res.data.theme);
if (res.data.language) i18n.changeLanguage(res.data.language); if (res.data.language) i18n.changeLanguage(res.data.language);
try {
const followRes = await api.get('users/following_list/');
const data = Array.isArray(followRes.data)
? followRes.data
: followRes.data.results || [];
setFollowing(data);
} catch {
/* ignore */
}
} else { } else {
setUser(null); setUser(null);
} }
@@ -67,6 +71,16 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
refetchUser(); refetchUser();
}, [refetchUser]); }, [refetchUser]);
// Sync following list with login state: load once on login, clear on logout.
// Keyed on username so it doesn't re-run on every badge-count refresh.
useEffect(() => {
if (user?.username) {
refetchFollowing();
} else {
setFollowing([]);
}
}, [user?.username, refetchFollowing]);
const logout = useCallback(async () => { const logout = useCallback(async () => {
try { try {
await api.post('auth/logout/'); await api.post('auth/logout/');
@@ -80,7 +94,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
}, [i18n]); }, [i18n]);
return ( return (
<AuthContext.Provider value={{ user, setUser, following, loadingUser, refetchUser, logout }}> <AuthContext.Provider
value={{ user, setUser, following, loadingUser, refetchUser, refetchFollowing, logout }}
>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );
+8 -2
View File
@@ -1,9 +1,10 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Typography, TextField, Button, Tab, Tabs, Alert, Stack, Box } from '@mui/material'; import { Typography, TextField, Button, Tab, Tabs, Alert, Stack, Box } from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import api from '../lib/api'; import api from '../lib/api';
import { useTitle } from '../hooks/useTitle'; import { useTitle } from '../hooks/useTitle';
import { useAuth } from '../hooks/useAuth';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PageShell, GlassCard, FormCard } from '../components/ui'; import { PageShell, GlassCard, FormCard } from '../components/ui';
@@ -15,6 +16,8 @@ const readError = (err: unknown): ApiError => err as ApiError;
const Login: React.FC = () => { const Login: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const { refetchUser } = useAuth();
useTitle('Login'); useTitle('Login');
const [tab, setTab] = useState(0); const [tab, setTab] = useState(0);
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@@ -32,7 +35,10 @@ const Login: React.FC = () => {
setMessage(null); setMessage(null);
try { try {
await api.post('auth/token/', { username, password }); await api.post('auth/token/', { username, password });
window.location.href = '/'; // Pull the new session into AuthContext, then SPA-navigate — keeps the
// warm react-query cache instead of a full-page reload.
await refetchUser();
navigate('/');
} catch (err) { } catch (err) {
const data = readError(err).response?.data; const data = readError(err).response?.data;
const errorMsg = data?.non_field_errors?.[0] || t('auth.loginFailed'); const errorMsg = data?.non_field_errors?.[0] || t('auth.loginFailed');
+4
View File
@@ -32,6 +32,7 @@ import {
type PublicProfileRating, type PublicProfileRating,
} from '../api/queries/usePublicProfile'; } from '../api/queries/usePublicProfile';
import { useCurrentUserLite } from '../api/queries/useCurrentUserLite'; import { useCurrentUserLite } from '../api/queries/useCurrentUserLite';
import { useAuth } from '../hooks/useAuth';
import { import {
useAddProfileComment, useAddProfileComment,
useDeleteProfileComment, useDeleteProfileComment,
@@ -54,6 +55,7 @@ const PublicProfile: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { data, isLoading: loading } = usePublicProfile(username); const { data, isLoading: loading } = usePublicProfile(username);
const { data: currentUser } = useCurrentUserLite(); const { data: currentUser } = useCurrentUserLite();
const { refetchFollowing } = useAuth();
const followToggle = useFollowToggle(); const followToggle = useFollowToggle();
const addComment = useAddProfileComment(); const addComment = useAddProfileComment();
const deleteCommentMutation = useDeleteProfileComment(); const deleteCommentMutation = useDeleteProfileComment();
@@ -106,6 +108,7 @@ const PublicProfile: React.FC = () => {
userId: data.id, userId: data.id,
currentlyFollowing: data.is_following, currentlyFollowing: data.is_following,
}); });
refetchFollowing();
} catch { } catch {
notify({ message: 'Failed to update follow status. Please login.', severity: 'error' }); notify({ message: 'Failed to update follow status. Please login.', severity: 'error' });
} }
@@ -117,6 +120,7 @@ const PublicProfile: React.FC = () => {
userId: user.id, userId: user.id,
currentlyFollowing: !!user.is_following, currentlyFollowing: !!user.is_following,
}); });
refetchFollowing();
} catch { } catch {
notify({ message: 'Failed', severity: 'error' }); notify({ message: 'Failed', severity: 'error' });
} }
+9 -1
View File
@@ -29,7 +29,9 @@ import {
} from '@mui/material'; } from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAuth } from '../hooks/useAuth';
import { useTitle } from '../hooks/useTitle'; import { useTitle } from '../hooks/useTitle';
import type { CatppuccinTheme } from '../theme'; import type { CatppuccinTheme } from '../theme';
import { PageShell, GlassCard, FormCard } from '../components/ui'; import { PageShell, GlassCard, FormCard } from '../components/ui';
@@ -47,6 +49,8 @@ const readErr = (err: unknown): string | undefined =>
const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => { const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient();
const { refetchUser } = useAuth();
useTitle(t('nav.settings')); useTitle(t('nav.settings'));
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const { anchor: drawerAnchor, setAnchor: setDrawerAnchor } = useDrawerAnchor(); const { anchor: drawerAnchor, setAnchor: setDrawerAnchor } = useDrawerAnchor();
@@ -182,7 +186,11 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
if (!ok) return; if (!ok) return;
try { try {
await confirmDeletion.mutateAsync(deletionCode); await confirmDeletion.mutateAsync(deletionCode);
window.location.href = '/'; // Account gone: clear AuthContext + wipe every cached query (stale user
// data must not survive), then SPA-navigate home.
queryClient.clear();
await refetchUser();
navigate('/');
} catch { } catch {
setMessage({ type: 'error', text: 'Invalid code. Deletion failed.' }); setMessage({ type: 'error', text: 'Invalid code. Deletion failed.' });
} }