From 556bae35569489a39e58ccc2000d58a8b973e596 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 27 Apr 2026 09:23:22 +0200 Subject: [PATCH] updated admin panel + side bar --- .../api/migrations/0033_user_drawer_anchor.py | 18 + backend/api/models/user.py | 7 + backend/api/serializers/user.py | 1 + backend/api/views/user.py | 5 + frontend/src/App.tsx | 591 ++++-------------- .../src/components/auth/RequireSuperuser.tsx | 33 + frontend/src/components/layout/NavSidebar.tsx | 337 ++++++++++ frontend/src/contexts/AuthContext.tsx | 1 + frontend/src/hooks/useDrawerAnchor.ts | 49 ++ frontend/src/pages/CommunityFeed.tsx | 10 +- frontend/src/pages/Forbidden.tsx | 38 ++ frontend/src/pages/MainPage.tsx | 6 +- frontend/src/pages/PublicProfile.tsx | 8 +- frontend/src/pages/Settings.tsx | 29 + frontend/src/pages/Support.tsx | 164 +---- frontend/src/pages/admin/AdminBanners.tsx | 188 ++++++ frontend/src/pages/admin/AdminConfig.tsx | 185 ++++++ frontend/src/pages/admin/AdminJobs.tsx | 248 ++++++++ frontend/src/pages/admin/AdminLayout.tsx | 116 ++++ frontend/src/pages/admin/AdminOverview.tsx | 197 ++++++ frontend/src/pages/admin/AdminTickets.tsx | 410 ++++++++++++ frontend/src/pages/admin/AdminUsers.tsx | 200 ++++++ frontend/src/pages/flavors/FlavorDetail.tsx | 13 +- 23 files changed, 2217 insertions(+), 637 deletions(-) create mode 100644 backend/api/migrations/0033_user_drawer_anchor.py create mode 100644 frontend/src/components/auth/RequireSuperuser.tsx create mode 100644 frontend/src/components/layout/NavSidebar.tsx create mode 100644 frontend/src/hooks/useDrawerAnchor.ts create mode 100644 frontend/src/pages/Forbidden.tsx create mode 100644 frontend/src/pages/admin/AdminBanners.tsx create mode 100644 frontend/src/pages/admin/AdminConfig.tsx create mode 100644 frontend/src/pages/admin/AdminJobs.tsx create mode 100644 frontend/src/pages/admin/AdminLayout.tsx create mode 100644 frontend/src/pages/admin/AdminOverview.tsx create mode 100644 frontend/src/pages/admin/AdminTickets.tsx create mode 100644 frontend/src/pages/admin/AdminUsers.tsx diff --git a/backend/api/migrations/0033_user_drawer_anchor.py b/backend/api/migrations/0033_user_drawer_anchor.py new file mode 100644 index 0000000..1a4d069 --- /dev/null +++ b/backend/api/migrations/0033_user_drawer_anchor.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.4 on 2026-04-27 01:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0032_flavor_local_image_paths_flavor_main_image_path'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='drawer_anchor', + field=models.CharField(choices=[('left', 'Left'), ('right', 'Right')], default='left', max_length=5), + ), + ] diff --git a/backend/api/models/user.py b/backend/api/models/user.py index 255e077..3a04548 100644 --- a/backend/api/models/user.py +++ b/backend/api/models/user.py @@ -26,8 +26,15 @@ class User(AbstractUser): ("en", "English"), ("de", "Deutsch"), ] + DRAWER_ANCHOR_CHOICES = [ + ("left", "Left"), + ("right", "Right"), + ] theme = models.CharField(max_length=20, choices=THEME_CHOICES, default="holy_light") language = models.CharField(max_length=2, choices=LANGUAGE_CHOICES, default="en") + drawer_anchor = models.CharField( + max_length=5, choices=DRAWER_ANCHOR_CHOICES, default="left" + ) avatar = models.ImageField(upload_to="avatars/", blank=True, null=True) pending_email = models.EmailField(max_length=254, blank=True, null=True) email_confirmation_code = models.CharField(max_length=6, blank=True, null=True) diff --git a/backend/api/serializers/user.py b/backend/api/serializers/user.py index 74cbcc6..02b8de3 100644 --- a/backend/api/serializers/user.py +++ b/backend/api/serializers/user.py @@ -24,6 +24,7 @@ class UserSerializer(serializers.ModelSerializer): "email", "theme", "language", + "drawer_anchor", "avatar", "following_count", "followers_count", diff --git a/backend/api/views/user.py b/backend/api/views/user.py index 14e554b..f66f272 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -271,6 +271,7 @@ class UserViewSet(viewsets.ModelViewSet): user = current_user(request) theme = request.data.get("theme") language = request.data.get("language") + drawer_anchor = request.data.get("drawer_anchor") banner_id = request.data.get("selected_banner") updated = False @@ -280,6 +281,9 @@ class UserViewSet(viewsets.ModelViewSet): if language in dict(User.LANGUAGE_CHOICES): user.language = language updated = True + if drawer_anchor in dict(User.DRAWER_ANCHOR_CHOICES): + user.drawer_anchor = drawer_anchor + updated = True if banner_id is not None: if banner_id == "": @@ -303,6 +307,7 @@ class UserViewSet(viewsets.ModelViewSet): "status": "preferences updated", "theme": user.theme, "language": user.language, + "drawer_anchor": user.drawer_anchor, "selected_banner": (user.selected_banner.id if user.selected_banner else None), } ) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4c33a78..36e0743 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import React, { Suspense, lazy, useEffect, useState } from 'react'; -import { Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom'; +import { Routes, Route, Link, useNavigate, Navigate } from 'react-router-dom'; import { Typography, Button, @@ -9,15 +9,7 @@ import { Menu, MenuItem, SwipeableDrawer, - Divider, - List, - ListItem, - ListItemButton, - ListItemIcon, - ListItemText, - Collapse, CircularProgress, - ListItemAvatar, Badge, alpha, useMediaQuery, @@ -26,21 +18,7 @@ import { import { GlassAppBar } from './components/ui'; import MenuIcon from '@mui/icons-material/Menu'; 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 HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'; -import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined'; -import CategoryOutlinedIcon from '@mui/icons-material/CategoryOutlined'; -import PersonOutlineOutlinedIcon from '@mui/icons-material/PersonOutlineOutlined'; -import CollectionsBookmarkOutlinedIcon from '@mui/icons-material/CollectionsBookmarkOutlined'; -import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; -import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; -import AdminPanelSettingsOutlinedIcon from '@mui/icons-material/AdminPanelSettingsOutlined'; -import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined'; import LogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined'; -import LoginOutlinedIcon from '@mui/icons-material/LoginOutlined'; -import PeopleOutlineOutlinedIcon from '@mui/icons-material/PeopleOutlineOutlined'; import { useTranslation } from 'react-i18next'; import api from './lib/api'; import Footer from './components/Footer'; @@ -50,6 +28,9 @@ import { GlobalSearch } from './app/GlobalSearch'; import { useAuth } from './hooks/useAuth'; import { useTheme } from './hooks/useTheme'; import { useNotifications } from './hooks/useNotifications'; +import { useDrawerAnchor } from './hooks/useDrawerAnchor'; +import { NavSidebar, NAV_SIDEBAR_WIDTH } from './components/layout/NavSidebar'; +import { RequireSuperuser } from './components/auth/RequireSuperuser'; import type { Notification } from './contexts/NotificationContext'; import type { CatppuccinTheme } from './theme'; @@ -65,7 +46,14 @@ 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 Forbidden = lazy(() => import('./pages/Forbidden')); +const AdminLayout = lazy(() => import('./pages/admin/AdminLayout')); +const AdminOverview = lazy(() => import('./pages/admin/AdminOverview')); +const AdminUsers = lazy(() => import('./pages/admin/AdminUsers')); +const AdminBanners = lazy(() => import('./pages/admin/AdminBanners')); +const AdminJobs = lazy(() => import('./pages/admin/AdminJobs')); +const AdminTickets = lazy(() => import('./pages/admin/AdminTickets')); +const AdminConfig = lazy(() => import('./pages/admin/AdminConfig')); const AdminUserDetail = lazy(() => import('./pages/AdminUserDetail')); const AdminRatingDetail = lazy(() => import('./pages/AdminRatingDetail')); const AdminReplyDetail = lazy(() => import('./pages/AdminReplyDetail')); @@ -73,20 +61,16 @@ const AdminReplyDetail = lazy(() => import('./pages/AdminReplyDetail')); const App: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); - const location = useLocation(); const muiTheme = useMuiTheme(); - const { user, following, loadingUser, logout } = useAuth(); + const { user, loadingUser, logout } = useAuth(); const { themeName, handleThemeChange } = useTheme(); const { notifications, markAllRead, markRead } = useNotifications(); + const { anchor: mobileAnchor } = useDrawerAnchor(); const [anchorEl, setAnchorEl] = useState(null); const [notifAnchorEl, setNotifAnchorEl] = useState(null); - const [catAnchorEl, setCatAnchorEl] = useState(null); const [categories, setCategories] = useState<{ name: string; slug: string }[]>([]); const [drawerOpen, setDrawerOpen] = useState(false); - const [catOpen, setCatOpen] = useState(false); - const [followingOpen, setFollowingOpen] = useState(false); - const [adminMode, setAdminMode] = useState(localStorage.getItem('admin-mode') === 'true'); const isMobile = useMediaQuery(muiTheme.breakpoints.down('md')); useEffect(() => { @@ -104,12 +88,6 @@ const App: React.FC = () => { document.body.style.color = muiTheme.palette.text.primary; }, [muiTheme]); - const toggleAdminMode = () => { - const newVal = !adminMode; - setAdminMode(newVal); - localStorage.setItem('admin-mode', String(newVal)); - }; - const onThemeChange = async (name: CatppuccinTheme) => { await handleThemeChange(name, !!user); setAnchorEl(null); @@ -119,7 +97,7 @@ const App: React.FC = () => { if (!notif.is_read) await markRead(notif.id); setNotifAnchorEl(null); if (notif.notification_type.startsWith('ticket')) { - navigate('/support'); + navigate(user?.is_superuser ? '/admin-panel/tickets' : '/support'); } else if (notif.notification_type === 'profile_comment') { navigate(`/profile/${user?.username}`); } else if (notif.notification_type === 'follow') { @@ -129,303 +107,15 @@ const App: React.FC = () => { } }; - const closeDrawer = () => setDrawerOpen(false); - const isActive = (path: string) => - path === '/' ? location.pathname === '/' : location.pathname.startsWith(path); - - const drawer = ( - setDrawerOpen(true)} + aria-label="open navigation" + sx={{ mx: 0.5 }} > - {user ? ( - - - {user.username.charAt(0).toUpperCase()} - - - - {user.username} - - - {t('nav.profile')} - - - - ) : ( - - - - Holy Flavors Archive - - - - )} - - - - - - - - - - - - {user && ( - - - - - - - - - )} - - setCatOpen(!catOpen)} - selected={location.pathname.startsWith('/category/')} - > - - - - - {catOpen ? : } - - - - {categories.map((cat) => ( - - - - ))} - - - - {user && ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {user.is_superuser && ( - <> - - - - - - - - - - - - - - - - - - - )} - - {following.length > 0 && ( - <> - - setFollowingOpen(!followingOpen)}> - - - - - {t('nav.following')} - - {following.length} - - - } - /> - {followingOpen ? : } - - - - {following.map((f) => ( - - - - - {!f.avatar && f.username.charAt(0).toUpperCase()} - - - - - - ))} - - - - )} - - )} - - {!user && ( - <> - - - - - - - - - - - )} - - - {user && ( - - { - closeDrawer(); - logout(); - }} - > - - - - - - - )} - + + ); const notificationsMenu = ( @@ -527,13 +217,14 @@ const App: React.FC = () => { }} > + {isMobile && mobileAnchor === 'left' && hamburger} + @@ -546,50 +237,6 @@ const App: React.FC = () => { - - {user && ( - - )} - - setCatAnchorEl(null)} - elevation={3} - sx={{ mt: 1 }} - > - {categories.map((cat) => ( - setCatAnchorEl(null)} - > - {t(`categories.${cat.slug}`, { defaultValue: cat.name })} - - ))} - - - @@ -599,7 +246,11 @@ const App: React.FC = () => { {user ? ( - setAnchorEl(e.currentTarget)}> + setAnchorEl(e.currentTarget)} + aria-label="account menu" + > { sx={{ mt: 1 }} > setAnchorEl(null)} + onClick={() => { + setAnchorEl(null); + logout(); + }} + sx={{ color: 'error.main', gap: 1.5 }} > - {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')} @@ -669,68 +292,118 @@ const App: React.FC = () => { )} - {isMobile && ( - setDrawerOpen(true)} sx={{ ml: 1 }}> - - - )} + {isMobile && mobileAnchor === 'right' && hamburger} setDrawerOpen(true)} onClose={() => setDrawerOpen(false)} disableBackdropTransition disableDiscovery > - {drawer} + setDrawerOpen(false)} /> - - - - - } + + {!isMobile && ( + + + + )} + + - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - 404 - Page Not Found - + + + } - /> - - + > + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + } /> + + 404 - Page Not Found + + + } + /> + + + +