diff --git a/audit.md b/audit.md index be5d494..21d31a9 100644 --- a/audit.md +++ b/audit.md @@ -74,6 +74,11 @@ Verify mutations `invalidateQueries`; consider optimistic insert so reviews appe --- ## Implementation status +- [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; + dropped the now-unused `api` import. Cached + shared across mounts. `tsc` + + ESLint (0 errors) clean. - [x] R1 + R2 + Q1 — notif menu dedup + i18n + SPA nav. Extracted shared `` (`components/layout/NotificationMenu.tsx`) + `useNotificationClick()` hook (`hooks/useNotificationClick.ts`); `App.tsx` and `NavSidebar.tsx` now both render diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 267d307..aa9fbef 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,7 +16,6 @@ import { GlassAppBar } from './components/ui'; import MenuIcon from '@mui/icons-material/Menu'; import NotificationsIcon from '@mui/icons-material/Notifications'; import { useTranslation } from 'react-i18next'; -import api from './lib/api'; import Footer from './components/Footer'; import CookieBanner from './components/CookieBanner'; import { GlobalSearch } from './app/GlobalSearch'; @@ -24,6 +23,7 @@ import { useAuth } from './hooks/useAuth'; import { useTheme } from './hooks/useTheme'; import { useDrawerAnchor } from './hooks/useDrawerAnchor'; import { NavSidebar, NAV_SIDEBAR_WIDTH } from './components/layout/NavSidebar'; +import { useCategories } from './api/queries/useCategories'; import { NotificationMenu } from './components/layout/NotificationMenu'; import { RequireSuperuser } from './components/auth/RequireSuperuser'; import type { CatppuccinTheme } from './theme'; @@ -61,20 +61,10 @@ const App: React.FC = () => { const { anchor: mobileAnchor } = useDrawerAnchor(); const [notifAnchorEl, setNotifAnchorEl] = useState(null); - const [categories, setCategories] = useState<{ name: string; slug: string }[]>([]); + const { data: categories = [] } = useCategories(); const isMobile = useMediaQuery(muiTheme.breakpoints.down('md')); const [drawerOpen, setDrawerOpen] = useState(false); - useEffect(() => { - api - .get('categories/') - .then((res) => { - const data = Array.isArray(res.data) ? res.data : res.data.results || []; - setCategories(data); - }) - .catch(() => setCategories([])); - }, []); - useEffect(() => { document.body.style.backgroundColor = muiTheme.palette.background.default; document.body.style.color = muiTheme.palette.text.primary; diff --git a/frontend/src/api/keys.ts b/frontend/src/api/keys.ts index c4b48af..28d7164 100644 --- a/frontend/src/api/keys.ts +++ b/frontend/src/api/keys.ts @@ -4,6 +4,7 @@ * params (URL-stable, not the full args object). */ export const queryKeys = { + categories: () => ['categories'] as const, dashboard: () => ['dashboard'] as const, publicProfile: (username: string) => ['publicProfile', username] as const, publicProfileAll: () => ['publicProfile'] as const, diff --git a/frontend/src/api/queries/useCategories.ts b/frontend/src/api/queries/useCategories.ts new file mode 100644 index 0000000..8bc0431 --- /dev/null +++ b/frontend/src/api/queries/useCategories.ts @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; +import api from '../../lib/api'; +import { queryKeys } from '../keys'; + +export interface Category { + name: string; + slug: string; +} + +/** + * Nav categories. Rarely change, so cached aggressively — one fetch shared by + * the AppBar drawer and desktop sidebar instead of a raw `useEffect` per mount. + */ +export const useCategories = () => + useQuery({ + queryKey: queryKeys.categories(), + queryFn: async () => { + const res = await api.get('categories/'); + return Array.isArray(res.data) ? res.data : (res.data.results ?? []); + }, + staleTime: 30 * 60_000, + });