From aa702ad8234da5f39d6b27edd1a60f148e9615d3 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Fri, 8 May 2026 23:11:39 +0200 Subject: [PATCH] drawer fix for desktop --- frontend/src/App.tsx | 1469 +++++++++------ frontend/src/api.ts | 44 +- .../src/components/BannerSettingsDialog.tsx | 311 ++-- frontend/src/components/CookieBanner.tsx | 223 ++- frontend/src/components/DynamicBanner.tsx | 188 +- frontend/src/components/FirefliesBanner.tsx | 494 ++--- frontend/src/components/Footer.tsx | 92 +- frontend/src/components/GenerativeBanner.tsx | 553 +++--- .../src/components/GenerativeFlowBanner.tsx | 433 +++-- frontend/src/components/HextechBanner.tsx | 539 +++--- .../components/HextechCorruptionBanner.tsx | 488 ++--- frontend/src/components/MentionTextField.tsx | 382 ++-- frontend/src/components/NebulaBanner.tsx | 585 +++--- frontend/src/components/RatingBadge.tsx | 112 +- frontend/src/components/RichText.tsx | 66 +- frontend/src/components/StatusBadge.tsx | 101 +- frontend/src/hooks/useTitle.ts | 2 +- frontend/src/i18n.ts | 979 +++++----- frontend/src/main.tsx | 14 +- frontend/src/pages/About.tsx | 185 +- frontend/src/pages/AdminDashboard.tsx | 1502 +++++++++------ frontend/src/pages/AdminRatingDetail.tsx | 209 ++- frontend/src/pages/AdminReplyDetail.tsx | 190 +- frontend/src/pages/AdminUserDetail.tsx | 884 ++++++--- frontend/src/pages/CommunityFeed.tsx | 1070 +++++++---- frontend/src/pages/Dashboard.tsx | 1617 +++++++++++------ frontend/src/pages/FlavorList.tsx | 343 ++-- frontend/src/pages/Login.tsx | 530 +++--- frontend/src/pages/MainPage.tsx | 1600 ++++++++++------ frontend/src/pages/Privacy.tsx | 252 ++- frontend/src/pages/PublicProfile.tsx | 1581 ++++++++++------ frontend/src/pages/Settings.tsx | 770 ++++---- frontend/src/pages/Support.tsx | 880 +++++---- frontend/src/pages/VerifyEmail.tsx | 199 +- .../src/pages/categories/CategoryFlavors.tsx | 229 ++- frontend/src/pages/flavors/FlavorDetail.tsx | 1409 +++++++++----- frontend/src/theme.ts | 530 +++--- frontend/src/utils/date.ts | 16 +- 38 files changed, 12938 insertions(+), 8133 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6d83f9a..a7684f7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,12 +1,18 @@ -import React, { useState, useEffect, useMemo, Suspense, lazy } from 'react'; -import { Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom'; -import { - ThemeProvider, - CssBaseline, - AppBar, - Toolbar, - Typography, - Button, +import React, { useState, useEffect, useMemo, Suspense, lazy } from "react"; +import { + Routes, + Route, + Link, + useNavigate, + useLocation, +} from "react-router-dom"; +import { + ThemeProvider, + CssBaseline, + AppBar, + Toolbar, + Typography, + Button, Box, IconButton, Avatar, @@ -27,254 +33,303 @@ import { ListSubheader, Badge, alpha, - useMediaQuery -} from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; -import SearchIcon from '@mui/icons-material/Search'; -import NotificationsIcon from '@mui/icons-material/Notifications'; -import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; -import ExpandLess from '@mui/icons-material/ExpandLess'; -import ExpandMore from '@mui/icons-material/ExpandMore'; -import { getTheme, type CatppuccinTheme } from './theme'; -import api from './api'; -import './i18n'; -import { useTranslation } from 'react-i18next'; -import Footer from './components/Footer'; -import { formatDate } from './utils/date'; -import CookieBanner from './components/CookieBanner'; + useMediaQuery, +} from "@mui/material"; +import MenuIcon from "@mui/icons-material/Menu"; +import SearchIcon from "@mui/icons-material/Search"; +import NotificationsIcon from "@mui/icons-material/Notifications"; +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import ExpandLess from "@mui/icons-material/ExpandLess"; +import ExpandMore from "@mui/icons-material/ExpandMore"; +import { getTheme, type CatppuccinTheme } from "./theme"; +import api from "./api"; +import "./i18n"; +import { useTranslation } from "react-i18next"; +import Footer from "./components/Footer"; +import { formatDate } from "./utils/date"; +import CookieBanner from "./components/CookieBanner"; // Code splitting -const MainPage = lazy(() => import('./pages/MainPage')); -const CategoryFlavors = lazy(() => import('./pages/categories/CategoryFlavors')); -const FlavorDetail = lazy(() => import('./pages/flavors/FlavorDetail')); -const Dashboard = lazy(() => import('./pages/Dashboard')); -const PublicProfile = lazy(() => import('./pages/PublicProfile')); -const CommunityFeed = lazy(() => import('./pages/CommunityFeed')); -const About = lazy(() => import('./pages/About')); -const Privacy = lazy(() => import('./pages/Privacy')); -const Login = lazy(() => import('./pages/Login')); -const Settings = lazy(() => import('./pages/Settings')); -const Support = lazy(() => import('./pages/Support')); -const VerifyEmail = lazy(() => import('./pages/VerifyEmail')); -const AdminDashboard = lazy(() => import('./pages/AdminDashboard')); -const AdminUserDetail = lazy(() => import('./pages/AdminUserDetail')); -const AdminRatingDetail = lazy(() => import('./pages/AdminRatingDetail')); -const AdminReplyDetail = lazy(() => import('./pages/AdminReplyDetail')); +const MainPage = lazy(() => import("./pages/MainPage")); +const CategoryFlavors = lazy( + () => import("./pages/categories/CategoryFlavors"), +); +const FlavorDetail = lazy(() => import("./pages/flavors/FlavorDetail")); +const Dashboard = lazy(() => import("./pages/Dashboard")); +const PublicProfile = lazy(() => import("./pages/PublicProfile")); +const CommunityFeed = lazy(() => import("./pages/CommunityFeed")); +const About = lazy(() => import("./pages/About")); +const Privacy = lazy(() => import("./pages/Privacy")); +const Login = lazy(() => import("./pages/Login")); +const Settings = lazy(() => import("./pages/Settings")); +const Support = lazy(() => import("./pages/Support")); +const VerifyEmail = lazy(() => import("./pages/VerifyEmail")); +const AdminDashboard = lazy(() => import("./pages/AdminDashboard")); +const AdminUserDetail = lazy(() => import("./pages/AdminUserDetail")); +const AdminRatingDetail = lazy(() => import("./pages/AdminRatingDetail")); +const AdminReplyDetail = lazy(() => import("./pages/AdminReplyDetail")); interface SearchResult { - id: number; - name: string; - type: 'flavor' | 'category' | 'user'; - subtitle: string; - image_url: string | null; - slug: string | null; + id: number; + name: string; + type: "flavor" | "category" | "user"; + subtitle: string; + image_url: string | null; + slug: string | null; } interface Notification { - id: number; - actor_username: string; - actor_avatar: string | null; - notification_type: 'reply' | 'mention' | 'follow' | 'ticket_new' | 'ticket_reply' | 'profile_comment'; - rating: number | null; - reply: number | null; - is_read: boolean; - created_at: string; - flavor_name: string | null; - flavor_id: number | null; - profile_owner_username: string | null; + id: number; + actor_username: string; + actor_avatar: string | null; + notification_type: + | "reply" + | "mention" + | "follow" + | "ticket_new" + | "ticket_reply" + | "profile_comment"; + rating: number | null; + reply: number | null; + is_read: boolean; + created_at: string; + flavor_name: string | null; + flavor_id: number | null; + profile_owner_username: string | null; } const GlobalSearch = () => { - const { t } = useTranslation(); - const [query, setQuery] = useState(''); - const [options, setOptions] = useState([]); - const [loading, setLoading] = useState(false); - const navigate = useNavigate(); - const location = useLocation(); - const isMobile = useMediaQuery('(max-width:1150px)'); + const { t } = useTranslation(); + const [query, setQuery] = useState(""); + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const location = useLocation(); + const isMobile = useMediaQuery("(max-width:1150px)"); - // Debounced search - useEffect(() => { - if (!query.trim()) { - setOptions([]); - return; + // Debounced search + useEffect(() => { + if (!query.trim()) { + setOptions([]); + return; + } + + const timer = setTimeout(async () => { + setLoading(true); + try { + const res = await api.get( + `flavors/search/?q=${encodeURIComponent(query)}`, + ); + const results = Array.isArray(res.data) + ? res.data + : res.data.results || []; + setOptions(results); + } catch (err) { + console.error("Search failed"); + } finally { + setLoading(false); + } + }, 300); + + return () => clearTimeout(timer); + }, [query]); + + useEffect(() => { + const params = new URLSearchParams(location.search); + const q = params.get("q"); + if (q) setQuery(q); + }, [location.search]); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + if (query.trim()) { + navigate(`/?q=${encodeURIComponent(query.trim())}`); + } else { + navigate("/"); + } + }; + + return ( + + x} + getOptionLabel={(option) => { + if (typeof option === "string") return option; + return option.name || ""; + }} + inputValue={query} + onInputChange={(_, newValue) => + setQuery(typeof newValue === "string" ? newValue : "") } - - const timer = setTimeout(async () => { - setLoading(true); - try { - const res = await api.get(`flavors/search/?q=${encodeURIComponent(query)}`); - const results = Array.isArray(res.data) ? res.data : (res.data.results || []); - setOptions(results); - } catch (err) { - console.error('Search failed'); - } finally { - setLoading(false); - } - }, 300); - - return () => clearTimeout(timer); - }, [query]); - - useEffect(() => { - const params = new URLSearchParams(location.search); - const q = params.get('q'); - if (q) setQuery(q); - }, [location.search]); - - const handleSearch = (e: React.FormEvent) => { - e.preventDefault(); - if (query.trim()) { - navigate(`/?q=${encodeURIComponent(query.trim())}`); - } else { - navigate('/'); - } - }; - - return ( - - x} - getOptionLabel={(option) => { - if (typeof option === 'string') return option; - return option.name || ''; + onChange={(_, newValue) => { + if (newValue && typeof newValue !== "string") { + if (newValue.type === "flavor") navigate(`/flavor/${newValue.id}`); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleSearch(e as any); + } + }} + renderInput={(params) => ( + + + + ), + endAdornment: ( + + {loading ? ( + + ) : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + renderOption={(props, option) => { + const { key, ...optionProps } = props as any; + return ( + + setQuery(typeof newValue === 'string' ? newValue : '')} - onChange={(_, newValue) => { - if (newValue && typeof newValue !== 'string') { - if (newValue.type === 'flavor') navigate(`/flavor/${newValue.id}`); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleSearch(e as any); - } - }} - renderInput={(params) => ( - - - - ), - endAdornment: ( - - {loading ? : null} - {params.InputProps.endAdornment} - - ), - }} - /> - )} - renderOption={(props, option) => { - const { key, ...optionProps } = props as any; - return ( - - - - {option.name} - {option.subtitle} - - - ); - }} - /> - - ); + /> + + + {option.name} + + + {option.subtitle} + + + + ); + }} + /> + + ); }; const App: React.FC = () => { const { i18n, t } = useTranslation(); const navigate = useNavigate(); - const [themeName, setThemeName] = useState((localStorage.getItem('theme') as CatppuccinTheme) || 'holy_light'); + const [themeName, setThemeName] = useState( + (localStorage.getItem("theme") as CatppuccinTheme) || "holy_light", + ); const [anchorEl, setAnchorEl] = useState(null); const [notifAnchorEl, setNotifAnchorEl] = useState(null); const [catAnchorEl, setCatAnchorEl] = useState(null); - const [categories, setCategories] = useState<{name: string, slug: string}[]>([]); - const [user, setUser] = useState<{username: string, avatar: string | null, unread_notifications_count: number, is_superuser: boolean} | null>(null); + const [categories, setCategories] = useState< + { name: string; slug: string }[] + >([]); + const [user, setUser] = useState<{ + username: string; + avatar: string | null; + unread_notifications_count: number; + is_superuser: boolean; + } | null>(null); const [notifications, setNotifications] = useState([]); - const [following, setFollowing] = useState<{id: number, username: string, avatar: string | null}[]>([]); + const [following, setFollowing] = useState< + { id: number; username: string; avatar: string | null }[] + >([]); const [loadingUser, setLoadingUser] = useState(true); const [drawerOpen, setDrawerOpen] = useState(false); const [catOpen, setCatOpen] = useState(false); - const [adminMode, setAdminMode] = useState(localStorage.getItem('admin-mode') === 'true'); - const isMobile = useMediaQuery('(max-width:1150px)'); + const [adminMode, setAdminMode] = useState( + localStorage.getItem("admin-mode") === "true", + ); + const isMobile = useMediaQuery("(max-width:1150px)"); const toggleAdminMode = () => { - const newVal = !adminMode; - setAdminMode(newVal); - localStorage.setItem('admin-mode', String(newVal)); + const newVal = !adminMode; + setAdminMode(newVal); + localStorage.setItem("admin-mode", String(newVal)); }; const fetchNotifications = async () => { try { - const res = await api.get('notifications/'); - const data = Array.isArray(res.data) ? res.data : (res.data.results || []); - setNotifications(data); + const res = await api.get("notifications/"); + const data = Array.isArray(res.data) ? res.data : res.data.results || []; + setNotifications(data); } catch (err) { - console.error('Failed to fetch notifications'); + console.error("Failed to fetch notifications"); } }; useEffect(() => { const fetchData = async () => { try { - const token = localStorage.getItem('token'); + const token = localStorage.getItem("token"); const [userRes, catRes] = await Promise.all([ - token ? api.get('users/me/') : Promise.resolve({ data: null }), - api.get('categories/') + token ? api.get("users/me/") : Promise.resolve({ data: null }), + api.get("categories/"), ]); - + if (userRes.data && userRes.data.username) { - setUser(userRes.data); - if (userRes.data.theme) { - setThemeName(userRes.data.theme); - localStorage.setItem('theme', userRes.data.theme); - } - if (userRes.data.language) i18n.changeLanguage(userRes.data.language); - - try { - const followRes = await api.get('users/following_list/'); - const followData = Array.isArray(followRes.data) ? followRes.data : (followRes.data.results || []); - setFollowing(followData); - fetchNotifications(); - } catch (err) { - console.error('Failed to fetch following list'); - } + setUser(userRes.data); + if (userRes.data.theme) { + setThemeName(userRes.data.theme); + localStorage.setItem("theme", userRes.data.theme); + } + if (userRes.data.language) i18n.changeLanguage(userRes.data.language); + + try { + const followRes = await api.get("users/following_list/"); + const followData = Array.isArray(followRes.data) + ? followRes.data + : followRes.data.results || []; + setFollowing(followData); + fetchNotifications(); + } catch (err) { + console.error("Failed to fetch following list"); + } } else { - setUser(null); + setUser(null); } - - const catData = Array.isArray(catRes.data) ? catRes.data : (catRes.data.results || []); + + const catData = Array.isArray(catRes.data) + ? catRes.data + : catRes.data.results || []; setCategories(catData); } catch (err) { setUser(null); @@ -285,10 +340,13 @@ const App: React.FC = () => { fetchData(); const interval = setInterval(() => { - if (localStorage.getItem('token')) { - fetchNotifications(); - api.get('users/me/').then(res => setUser(res.data)).catch(() => {}); - } + if (localStorage.getItem("token")) { + fetchNotifications(); + api + .get("users/me/") + .then((res) => setUser(res.data)) + .catch(() => {}); + } }, 60000); return () => clearInterval(interval); }, []); @@ -303,182 +361,277 @@ const App: React.FC = () => { const handleMarkAllRead = async () => { try { - await api.post('notifications/mark_all_read/'); - setNotifications(notifications.map(n => ({ ...n, is_read: true }))); - if (user) setUser({ ...user, unread_notifications_count: 0 }); + await api.post("notifications/mark_all_read/"); + setNotifications(notifications.map((n) => ({ ...n, is_read: true }))); + if (user) setUser({ ...user, unread_notifications_count: 0 }); } catch (err) { - console.error('Failed to mark all as read'); + console.error("Failed to mark all as read"); } }; const handleNotificationClick = async (notif: Notification) => { if (!notif.is_read) { - try { - await api.post(`notifications/${notif.id}/mark_read/`); - setNotifications(notifications.map(n => n.id === notif.id ? { ...n, is_read: true } : n)); - if (user) setUser({ ...user, unread_notifications_count: Math.max(0, user.unread_notifications_count - 1) }); - } catch (err) { /* ignore */ } + try { + await api.post(`notifications/${notif.id}/mark_read/`); + setNotifications( + notifications.map((n) => + n.id === notif.id ? { ...n, is_read: true } : n, + ), + ); + if (user) + setUser({ + ...user, + unread_notifications_count: Math.max( + 0, + user.unread_notifications_count - 1, + ), + }); + } catch (err) { + /* ignore */ + } } setNotifAnchorEl(null); if (notif.flavor_id) { - window.location.href = `/flavor/${notif.flavor_id}`; + window.location.href = `/flavor/${notif.flavor_id}`; } }; const handleThemeChange = async (newTheme: CatppuccinTheme) => { setThemeName(newTheme); - localStorage.setItem('theme', newTheme); + localStorage.setItem("theme", newTheme); if (user) { - try { - await api.patch('users/update_preferences/', { theme: newTheme }); - } catch (err) { - console.error('Failed to update theme on server'); - } + try { + await api.patch("users/update_preferences/", { theme: newTheme }); + } catch (err) { + console.error("Failed to update theme on server"); + } } setAnchorEl(null); }; const handleLogout = () => { - localStorage.removeItem('token'); + localStorage.removeItem("token"); setUser(null); setFollowing([]); - i18n.changeLanguage(navigator.language.split('-')[0] || 'en'); - window.location.href = '/'; + i18n.changeLanguage(navigator.language.split("-")[0] || "en"); + window.location.href = "/"; }; const drawer = ( - - - - setDrawerOpen(false)} style={{ color: 'inherit', textDecoration: 'none' }}> - Holy Flavors Archive - - + + + + setDrawerOpen(false)} + style={{ color: "inherit", textDecoration: "none" }} + > + Holy Flavors Archive + + - setDrawerOpen(false)}> - - + setDrawerOpen(false)} + > + + {user && ( - - setDrawerOpen(false)}> - - - + + setDrawerOpen(false)} + > + + + )} setCatOpen(!catOpen)}> - - {catOpen ? : } + + {catOpen ? : } - - {categories.map(cat => ( - setDrawerOpen(false)} - sx={{ pl: 4 }} - > - - - ))} - + + {categories.map((cat) => ( + setDrawerOpen(false)} + sx={{ pl: 4 }} + > + + + ))} + - + {user ? ( - <> - - setDrawerOpen(false)}> - - - {user.username.charAt(0).toUpperCase()} - - - {user.username} - - - } - /> - - - - setDrawerOpen(false)}> - - - - - setDrawerOpen(false)}> - - - - - setDrawerOpen(false)}> - - - - - {user.is_superuser && ( - - - - - - )} - - {user.is_superuser && ( - - setDrawerOpen(false)}> - - - - )} - - {following.length > 0 && ( - <> - - {t('nav.following')} - {following.map(f => ( - - setDrawerOpen(false)}> - - - {!f.avatar && f.username.charAt(0).toUpperCase()} - - - - - - ))} - - )} - - - - - - - - ) : ( + <> - setDrawerOpen(false)}> - - + setDrawerOpen(false)} + > + + + {user.username.charAt(0).toUpperCase()} + + + {user.username} + + + } + /> + + + setDrawerOpen(false)} + > + + + + + setDrawerOpen(false)} + > + + + + + setDrawerOpen(false)} + > + + + + + {user.is_superuser && ( + + + + + + )} + + {user.is_superuser && ( + + setDrawerOpen(false)} + > + + + + )} + + {following.length > 0 && ( + <> + + + {t("nav.following")} + + {following.map((f) => ( + + setDrawerOpen(false)} + > + + + {!f.avatar && f.username.charAt(0).toUpperCase()} + + + + + + ))} + + )} + + + + + + + + ) : ( + + setDrawerOpen(false)} + > + + + )} @@ -486,240 +639,426 @@ const App: React.FC = () => { const notificationsMenu = ( <> - setNotifAnchorEl(e.currentTarget)} sx={{ ml: 1 }}> - - - - - setNotifAnchorEl(null)} - elevation={3} - sx={{ mt: 1 }} - PaperProps={{ sx: { width: 320, maxHeight: 400, borderRadius: 1 } }} + setNotifAnchorEl(e.currentTarget)} + sx={{ ml: 1 }} + > + - - Notifications - {(user?.unread_notifications_count || 0) > 0 && ( - - )} - - {notifications.length === 0 ? ( - - No notifications yet - - ) : ( - notifications.map(n => { - const handleClick = () => { - if (n.notification_type.startsWith('ticket')) { - handleNotificationClick(n); - navigate('/support'); - } else if (n.notification_type === 'profile_comment') { - handleNotificationClick(n); - navigate(`/profile/${user?.username}`); - } else if (n.notification_type === 'follow') { - handleNotificationClick(n); - navigate(`/profile/${n.actor_username}`); - } else { - handleNotificationClick(n); - } - }; + + + + setNotifAnchorEl(null)} + elevation={3} + sx={{ mt: 1 }} + PaperProps={{ sx: { width: 320, maxHeight: 400, borderRadius: 1 } }} + > + + + Notifications + + {(user?.unread_notifications_count || 0) > 0 && ( + + )} + + {notifications.length === 0 ? ( + + + No notifications yet + + + ) : ( + notifications.map((n) => { + const handleClick = () => { + if (n.notification_type.startsWith("ticket")) { + handleNotificationClick(n); + navigate("/support"); + } else if (n.notification_type === "profile_comment") { + handleNotificationClick(n); + navigate(`/profile/${user?.username}`); + } else if (n.notification_type === "follow") { + handleNotificationClick(n); + navigate(`/profile/${n.actor_username}`); + } else { + handleNotificationClick(n); + } + }; - return ( - - - {n.actor_username.charAt(0).toUpperCase()} - - - - {n.actor_username} { - n.notification_type === 'reply' ? `replied to your review on ${n.flavor_name}` : - n.notification_type === 'mention' ? `mentioned you on ${n.flavor_name}` : - n.notification_type === 'follow' ? t('community.notifFollow') : - n.notification_type === 'profile_comment' ? `left a message on your guestbook` : - n.notification_type === 'ticket_new' ? t('community.notifTicketNew') : - user?.is_superuser ? t('community.notifTicketReplyAdmin') : t('community.notifTicketReply') - } - - - {formatDate(n.created_at)} - - - - ); - }) - )} - + return ( + + + {n.actor_username.charAt(0).toUpperCase()} + + + + {n.actor_username}{" "} + {n.notification_type === "reply" + ? `replied to your review on ${n.flavor_name}` + : n.notification_type === "mention" + ? `mentioned you on ${n.flavor_name}` + : n.notification_type === "follow" + ? t("community.notifFollow") + : n.notification_type === "profile_comment" + ? `left a message on your guestbook` + : n.notification_type === "ticket_new" + ? t("community.notifTicketNew") + : user?.is_superuser + ? t("community.notifTicketReplyAdmin") + : t("community.notifTicketReply")} + + + {formatDate(n.created_at)} + + + + ); + }) + )} + ); return ( - - + alpha(theme.palette.background.paper, 0.8), - backdropFilter: 'blur(12px)', - color: 'text.primary' + backdropFilter: "blur(12px)", + color: "text.primary", }} > - - - - Holy Flavors Archive - HFA - - + + + + + Holy Flavors Archive + + + HFA + + + - - {user && ( - + )} + + setCatAnchorEl(null)} + elevation={3} + sx={{ mt: 1 }} + > + {categories.map((cat) => ( + setCatAnchorEl(null)} + > + {t(`categories.${cat.slug}`, { defaultValue: cat.name })} + + ))} + + + + + + + {!loadingUser && ( + <> + {user && notificationsMenu} + + {user ? ( + + setAnchorEl(e.currentTarget)} + > + - {t('nav.community')} - - )} - - setCatAnchorEl(null)} + {user.username.charAt(0).toUpperCase()} + + + setAnchorEl(null)} elevation={3} sx={{ mt: 1 }} - > - {categories.map(cat => ( - setCatAnchorEl(null)} - > - {t(`categories.${cat.slug}`, { defaultValue: cat.name })} - - ))} - - - - - - - {!loadingUser && ( - <> - {user && notificationsMenu} - - {user ? ( - - setAnchorEl(e.currentTarget)}> - - {user.username.charAt(0).toUpperCase()} - - - setAnchorEl(null)} - elevation={3} - sx={{ mt: 1 }} - > - setAnchorEl(null)}>{t('nav.profile')} - setAnchorEl(null)}>{t('nav.dashboard')} - setAnchorEl(null)}>{t('nav.settings')} - setAnchorEl(null)}>{t('support.title')} - {user.is_superuser && ( - - {t('admin.adminMode')}: {adminMode ? "ON" : "OFF"} - - )} - {user.is_superuser && ( - setAnchorEl(null)} sx={{ fontWeight: 'bold', color: 'primary.main' }}>Admin Panel - )} - {t('nav.logout')} - - ) : ( - - - - )} - - )} - - {isMobile && ( - setDrawerOpen(true)} - sx={{ ml: 1 }} > - - + setAnchorEl(null)} + > + {t("nav.profile")} + + setAnchorEl(null)} + > + {t("nav.dashboard")} + + setAnchorEl(null)} + > + {t("nav.settings")} + + setAnchorEl(null)} + > + {t("support.title")} + + {user.is_superuser && ( + + {t("admin.adminMode")}: {adminMode ? "ON" : "OFF"} + + )} + {user.is_superuser && ( + setAnchorEl(null)} + sx={{ fontWeight: "bold", color: "primary.main" }} + > + Admin Panel + + )}{" "} + + + {t("nav.logout")} + {" "} + + + ) : ( + + + )} - - - + + )} - setDrawerOpen(false)} + {isMobile && ( + setDrawerOpen(true)} + sx={{ ml: 1 }} + > + + + )} + + + + + setDrawerOpen(false)} + slotProps={{ + paper: { + sx: { + height: "100vh", + maxHeight: "100vh", + top: 0, + bottom: 0, + }, + }, + }} + > + {drawer} + + + + + + + } > - {drawer} - - - - }> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - 404 - Page Not Found} /> - - - -