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
|
## 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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.' });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user