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
- [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
`queryKeys.categories()`, 30 min `staleTime`). Replaced raw
`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[] = [];
let url: string | null = 'flavors/';
while (url) {
const res: { data: FlavorListItem[] | Paginated<FlavorListItem> } =
await api.get<FlavorListItem[] | Paginated<FlavorListItem>>(url);
const res: { data: FlavorListItem[] | Paginated<FlavorListItem> } = await api.get<
FlavorListItem[] | Paginated<FlavorListItem>
>(url);
all.push(...unwrap(res.data));
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.
@@ -1,13 +1,4 @@
import {
Avatar,
Box,
Button,
Menu,
MenuItem,
Typography,
alpha,
useTheme,
} from '@mui/material';
import { Avatar, Box, Button, Menu, MenuItem, Typography, alpha, useTheme } from '@mui/material';
import type { MenuProps, SxProps, Theme } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../hooks/useAuth';
+26 -10
View File
@@ -24,6 +24,7 @@ export interface AuthContextValue {
following: FollowingEntry[];
loadingUser: boolean;
refetchUser: () => Promise<void>;
refetchFollowing: () => Promise<void>;
logout: () => Promise<void>;
}
@@ -35,6 +36,18 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [following, setFollowing] = useState<FollowingEntry[]>([]);
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 () => {
try {
const res = await api.get('users/me/', {
@@ -44,15 +57,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
setUser(res.data);
if (res.data.theme) localStorage.setItem('theme', res.data.theme);
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 {
setUser(null);
}
@@ -67,6 +71,16 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
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 () => {
try {
await api.post('auth/logout/');
@@ -80,7 +94,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
}, [i18n]);
return (
<AuthContext.Provider value={{ user, setUser, following, loadingUser, refetchUser, logout }}>
<AuthContext.Provider
value={{ user, setUser, following, loadingUser, refetchUser, refetchFollowing, logout }}
>
{children}
</AuthContext.Provider>
);
+8 -2
View File
@@ -1,9 +1,10 @@
import React, { useState } from 'react';
import { Typography, TextField, Button, Tab, Tabs, Alert, Stack, Box } from '@mui/material';
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 { useTitle } from '../hooks/useTitle';
import { useAuth } from '../hooks/useAuth';
import { useTranslation } from 'react-i18next';
import { PageShell, GlassCard, FormCard } from '../components/ui';
@@ -15,6 +16,8 @@ const readError = (err: unknown): ApiError => err as ApiError;
const Login: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { refetchUser } = useAuth();
useTitle('Login');
const [tab, setTab] = useState(0);
const [username, setUsername] = useState('');
@@ -32,7 +35,10 @@ const Login: React.FC = () => {
setMessage(null);
try {
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) {
const data = readError(err).response?.data;
const errorMsg = data?.non_field_errors?.[0] || t('auth.loginFailed');
+4
View File
@@ -32,6 +32,7 @@ import {
type PublicProfileRating,
} from '../api/queries/usePublicProfile';
import { useCurrentUserLite } from '../api/queries/useCurrentUserLite';
import { useAuth } from '../hooks/useAuth';
import {
useAddProfileComment,
useDeleteProfileComment,
@@ -54,6 +55,7 @@ const PublicProfile: React.FC = () => {
const navigate = useNavigate();
const { data, isLoading: loading } = usePublicProfile(username);
const { data: currentUser } = useCurrentUserLite();
const { refetchFollowing } = useAuth();
const followToggle = useFollowToggle();
const addComment = useAddProfileComment();
const deleteCommentMutation = useDeleteProfileComment();
@@ -106,6 +108,7 @@ const PublicProfile: React.FC = () => {
userId: data.id,
currentlyFollowing: data.is_following,
});
refetchFollowing();
} catch {
notify({ message: 'Failed to update follow status. Please login.', severity: 'error' });
}
@@ -117,6 +120,7 @@ const PublicProfile: React.FC = () => {
userId: user.id,
currentlyFollowing: !!user.is_following,
});
refetchFollowing();
} catch {
notify({ message: 'Failed', severity: 'error' });
}
+9 -1
View File
@@ -29,7 +29,9 @@ import {
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../hooks/useAuth';
import { useTitle } from '../hooks/useTitle';
import type { CatppuccinTheme } from '../theme';
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 { t, i18n } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { refetchUser } = useAuth();
useTitle(t('nav.settings'));
const { confirm } = useConfirm();
const { anchor: drawerAnchor, setAnchor: setDrawerAnchor } = useDrawerAnchor();
@@ -182,7 +186,11 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
if (!ok) return;
try {
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 {
setMessage({ type: 'error', text: 'Invalid code. Deletion failed.' });
}