mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
Q4
This commit is contained in:
1 parent
3d01bfb4d1
commit
cc916e472a
6 files changed
+31
-12
No files matched your search
@@ -74,6 +74,17 @@ Verify mutations `invalidateQueries`; consider optimistic insert so reviews appe
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Implementation status
|
## Implementation status
|
||||||
|
- [x] Q4 — dropped the `as object` cast. Exported `RetriableConfig` from
|
||||||
|
`lib/api.ts`; `AuthContext.refetchUser` now passes `{ skipAuthRedirect: true }
|
||||||
|
as RetriableConfig` (the field is part of that type) instead of the
|
||||||
|
`...({ ... } as object)` spread hack. `tsc` + ESLint (0 errors) clean.
|
||||||
|
- [x] U3 — mark-all-read no longer fails silently. `NotificationContext.markAllRead`
|
||||||
|
catch block now fires an error toast (`community.markAllReadFailed`, EN+DE) via
|
||||||
|
`useToast().notify` instead of swallowing. Root cause was also a provider-order
|
||||||
|
bug: `ToastProvider` sat *inside* `NotificationProvider`, so the context couldn't
|
||||||
|
reach `useToast` — hoisted `ToastProvider` above `AuthProvider`/`NotificationProvider`
|
||||||
|
in `app/providers.tsx` (it only needs the MUI theme, which stays outermost).
|
||||||
|
`tsc` + ESLint (0 errors) clean.
|
||||||
- [x] U2 — killed the hard `window.location.href='/'` reloads. `Login.tsx`
|
- [x] U2 — killed the hard `window.location.href='/'` reloads. `Login.tsx`
|
||||||
(post-login) now `await refetchUser()` + `navigate('/')`; `Settings.tsx`
|
(post-login) now `await refetchUser()` + `navigate('/')`; `Settings.tsx`
|
||||||
(post-account-deletion) `queryClient.clear()` + `await refetchUser()` +
|
(post-account-deletion) `queryClient.clear()` + `await refetchUser()` +
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import '../i18n';
|
|||||||
export const AppProviders = ({ children }: { children: ReactNode }) => (
|
export const AppProviders = ({ children }: { children: ReactNode }) => (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<ToastProvider>
|
||||||
<NotificationProvider>
|
<AuthProvider>
|
||||||
<ToastProvider>
|
<NotificationProvider>
|
||||||
<ConfirmProvider>{children}</ConfirmProvider>
|
<ConfirmProvider>{children}</ConfirmProvider>
|
||||||
</ToastProvider>
|
</NotificationProvider>
|
||||||
</NotificationProvider>
|
</AuthProvider>
|
||||||
</AuthProvider>
|
</ToastProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createContext, useCallback, useEffect, useState, type ReactNode } from 'react';
|
import { createContext, useCallback, useEffect, useState, type ReactNode } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import api from '../lib/api';
|
import api, { type RetriableConfig } from '../lib/api';
|
||||||
|
|
||||||
export interface AuthUser {
|
export interface AuthUser {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -51,8 +51,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
const refetchUser = useCallback(async () => {
|
const refetchUser = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get('users/me/', {
|
const res = await api.get('users/me/', {
|
||||||
...({ skipAuthRedirect: true } as object),
|
skipAuthRedirect: true,
|
||||||
});
|
} as RetriableConfig);
|
||||||
if (res.data?.username) {
|
if (res.data?.username) {
|
||||||
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);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createContext, useCallback, useEffect, type ReactNode } from 'react';
|
import { createContext, useCallback, useEffect, type ReactNode } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
|
import { useToast } from '../hooks/useToast';
|
||||||
import { queryKeys } from '../api/keys';
|
import { queryKeys } from '../api/keys';
|
||||||
import { useNotificationsQuery, type Notification } from '../api/queries/useNotifications';
|
import { useNotificationsQuery, type Notification } from '../api/queries/useNotifications';
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +24,8 @@ export const NotificationContext = createContext<NotificationContextValue | null
|
|||||||
export const NotificationProvider = ({ children }: { children: ReactNode }) => {
|
export const NotificationProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const { user, setUser, refetchUser } = useAuth();
|
const { user, setUser, refetchUser } = useAuth();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
const { notify } = useToast();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data: notifications = [], refetch } = useNotificationsQuery(!!user);
|
const { data: notifications = [], refetch } = useNotificationsQuery(!!user);
|
||||||
const markAllMutation = useMarkAllNotificationsRead();
|
const markAllMutation = useMarkAllNotificationsRead();
|
||||||
@@ -46,9 +50,11 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
await markAllMutation.mutateAsync();
|
await markAllMutation.mutateAsync();
|
||||||
if (user) setUser({ ...user, unread_notifications_count: 0 });
|
if (user) setUser({ ...user, unread_notifications_count: 0 });
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
// Surface the failure — otherwise the badge silently stays and the user
|
||||||
|
// thinks the click did nothing.
|
||||||
|
notify({ message: t('community.markAllReadFailed'), severity: 'error' });
|
||||||
}
|
}
|
||||||
}, [markAllMutation, setUser, user]);
|
}, [markAllMutation, setUser, user, notify, t]);
|
||||||
|
|
||||||
const markRead = useCallback(
|
const markRead = useCallback(
|
||||||
async (id: number) => {
|
async (id: number) => {
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ const resources = {
|
|||||||
notifTicketReplyAdmin: 'replied to a support ticket',
|
notifTicketReplyAdmin: 'replied to a support ticket',
|
||||||
noNotifications: 'No notifications.',
|
noNotifications: 'No notifications.',
|
||||||
markAllRead: 'Mark all read',
|
markAllRead: 'Mark all read',
|
||||||
|
markAllReadFailed: 'Failed to mark notifications as read',
|
||||||
searchFriends: 'Search friends...',
|
searchFriends: 'Search friends...',
|
||||||
writeReply: 'Add a reply...',
|
writeReply: 'Add a reply...',
|
||||||
rated: 'rated',
|
rated: 'rated',
|
||||||
@@ -502,6 +503,7 @@ const resources = {
|
|||||||
notifTicketReplyAdmin: 'hat auf ein Support-Ticket geantwortet',
|
notifTicketReplyAdmin: 'hat auf ein Support-Ticket geantwortet',
|
||||||
noNotifications: 'Keine Benachrichtigungen.',
|
noNotifications: 'Keine Benachrichtigungen.',
|
||||||
markAllRead: 'Alle als gelesen markieren',
|
markAllRead: 'Alle als gelesen markieren',
|
||||||
|
markAllReadFailed: 'Benachrichtigungen konnten nicht als gelesen markiert werden',
|
||||||
searchFriends: 'Freunde suchen...',
|
searchFriends: 'Freunde suchen...',
|
||||||
writeReply: 'Eine Antwort hinzufügen...',
|
writeReply: 'Eine Antwort hinzufügen...',
|
||||||
rated: 'bewertete',
|
rated: 'bewertete',
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const api = axios.create({
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
type RetriableConfig = AxiosRequestConfig & {
|
export type RetriableConfig = AxiosRequestConfig & {
|
||||||
_retry?: boolean;
|
_retry?: boolean;
|
||||||
/** When true, a 401 that survives refresh will silently reject instead of
|
/** When true, a 401 that survives refresh will silently reject instead of
|
||||||
* navigating to /login. Use for background probes (e.g. /users/me/) so
|
* navigating to /login. Use for background probes (e.g. /users/me/) so
|
||||||
|
|||||||
Reference in new issue
Block a user