From cc916e472a015132a1ed1c28d3db63354487a6b7 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 15 Jun 2026 20:18:51 +0200 Subject: [PATCH] Q4 --- audit.md | 11 +++++++++++ frontend/src/app/providers.tsx | 12 ++++++------ frontend/src/contexts/AuthContext.tsx | 6 +++--- frontend/src/contexts/NotificationContext.tsx | 10 ++++++++-- frontend/src/i18n.ts | 2 ++ frontend/src/lib/api.ts | 2 +- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/audit.md b/audit.md index afaa0eb..58376a6 100644 --- a/audit.md +++ b/audit.md @@ -74,6 +74,17 @@ Verify mutations `invalidateQueries`; consider optimistic insert so reviews appe --- ## 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` (post-login) now `await refetchUser()` + `navigate('/')`; `Settings.tsx` (post-account-deletion) `queryClient.clear()` + `await refetchUser()` + diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx index d777edf..1975d17 100644 --- a/frontend/src/app/providers.tsx +++ b/frontend/src/app/providers.tsx @@ -12,13 +12,13 @@ import '../i18n'; export const AppProviders = ({ children }: { children: ReactNode }) => ( - - - + + + {children} - - - + + + {import.meta.env.DEV && } diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 0c971b7..31fc186 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -1,6 +1,6 @@ import { createContext, useCallback, useEffect, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; -import api from '../lib/api'; +import api, { type RetriableConfig } from '../lib/api'; export interface AuthUser { username: string; @@ -51,8 +51,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const refetchUser = useCallback(async () => { try { const res = await api.get('users/me/', { - ...({ skipAuthRedirect: true } as object), - }); + skipAuthRedirect: true, + } as RetriableConfig); if (res.data?.username) { setUser(res.data); if (res.data.theme) localStorage.setItem('theme', res.data.theme); diff --git a/frontend/src/contexts/NotificationContext.tsx b/frontend/src/contexts/NotificationContext.tsx index eac31f3..83d8049 100644 --- a/frontend/src/contexts/NotificationContext.tsx +++ b/frontend/src/contexts/NotificationContext.tsx @@ -1,6 +1,8 @@ import { createContext, useCallback, useEffect, type ReactNode } from 'react'; import { useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; import { useAuth } from '../hooks/useAuth'; +import { useToast } from '../hooks/useToast'; import { queryKeys } from '../api/keys'; import { useNotificationsQuery, type Notification } from '../api/queries/useNotifications'; import { @@ -22,6 +24,8 @@ export const NotificationContext = createContext { const { user, setUser, refetchUser } = useAuth(); const qc = useQueryClient(); + const { notify } = useToast(); + const { t } = useTranslation(); const { data: notifications = [], refetch } = useNotificationsQuery(!!user); const markAllMutation = useMarkAllNotificationsRead(); @@ -46,9 +50,11 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => { await markAllMutation.mutateAsync(); if (user) setUser({ ...user, unread_notifications_count: 0 }); } 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( async (id: number) => { diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 1ba5529..63f98b2 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -192,6 +192,7 @@ const resources = { notifTicketReplyAdmin: 'replied to a support ticket', noNotifications: 'No notifications.', markAllRead: 'Mark all read', + markAllReadFailed: 'Failed to mark notifications as read', searchFriends: 'Search friends...', writeReply: 'Add a reply...', rated: 'rated', @@ -502,6 +503,7 @@ const resources = { notifTicketReplyAdmin: 'hat auf ein Support-Ticket geantwortet', noNotifications: 'Keine Benachrichtigungen.', markAllRead: 'Alle als gelesen markieren', + markAllReadFailed: 'Benachrichtigungen konnten nicht als gelesen markiert werden', searchFriends: 'Freunde suchen...', writeReply: 'Eine Antwort hinzufügen...', rated: 'bewertete', diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 3ca0833..a36f8f4 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -18,7 +18,7 @@ const api = axios.create({ withCredentials: true, }); -type RetriableConfig = AxiosRequestConfig & { +export type RetriableConfig = AxiosRequestConfig & { _retry?: boolean; /** When true, a 401 that survives refresh will silently reject instead of * navigating to /login. Use for background probes (e.g. /users/me/) so