mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
improvements
This commit is contained in:
1 parent
1620b24ff3
commit
3d01bfb4d1
7 files changed
+65
-25
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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.' });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user