diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index c476494..a58cf83 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -57,6 +57,8 @@ const Settings = lazy(() => import('./pages/Settings'));
const Support = lazy(() => import('./pages/Support'));
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;
@@ -215,8 +217,15 @@ const App: React.FC = () => {
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:900px)');
+ const toggleAdminMode = () => {
+ const newVal = !adminMode;
+ setAdminMode(newVal);
+ localStorage.setItem('admin-mode', String(newVal));
+ };
+
const fetchNotifications = async () => {
try {
const res = await api.get('notifications/');
@@ -403,7 +412,19 @@ const App: React.FC = () => {
{user.is_superuser && (
- setDrawerOpen(false)}>
+
+
+
+
+ )}
+
+ {user.is_superuser && (
+
+ setDrawerOpen(false)}>
@@ -592,11 +613,15 @@ const App: React.FC = () => {
-
- {user.is_superuser && (
-
- )}
-
+
+ {user.is_superuser && (
+
+ )}
+ {user.is_superuser && (
+
+ )}
) : (
@@ -631,18 +656,20 @@ const App: React.FC = () => {
}>
- } />
+ } />
} />
- } />
- } />
- } />
+ } />
+ } />
+ } />
} />
} />
} />
} />
} />
- } />
- } />
+ } />
+ } />
+ } />
+ } />
} />
404 - Page Not Found} />
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
index ac64f52..9c6b9ed 100644
--- a/frontend/src/i18n.ts
+++ b/frontend/src/i18n.ts
@@ -156,6 +156,9 @@ const resources = {
"testEmailSuccess": "Test email sent successfully!",
"ipHistory": "Login IP History",
"manageUser": "Manage User",
+ "manageRating": "Manage Rating",
+ "manageReply": "Manage Reply",
+ "adminMode": "Admin Mode",
"deactivate": "Deactivate Account",
"activate": "Activate Account",
"dangerZone": "Danger Zone",
@@ -342,8 +345,11 @@ const resources = {
"sendTestEmail": "Test-E-Mail senden",
"testEmailSuccess": "Test-E-Mail erfolgreich gesendet!",
"ipHistory": "Login-IP-Verlauf",
- "manageUser": "Benutzer verwalten",
- "deactivate": "Konto deaktivieren",
+ "manageUser": "Manage User",
+ "manageRating": "Manage Rating",
+ "manageReply": "Manage Reply",
+ "adminMode": "Admin Mode",
+ "deactivate": "Deactivate Account",
"activate": "Konto aktivieren",
"dangerZone": "Gefahrenzone",
"deleteUser": "Benutzer dauerhaft löschen"
diff --git a/frontend/src/pages/AdminDashboard.tsx b/frontend/src/pages/AdminDashboard.tsx
index 8400b9b..af79e0a 100644
--- a/frontend/src/pages/AdminDashboard.tsx
+++ b/frontend/src/pages/AdminDashboard.tsx
@@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react';
import {
Container, Typography, Box, Grid, Card, CardContent, Button,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
- Paper, Stack, alpha, CircularProgress, Chip, TextField, IconButton
+ Paper, Stack, alpha, CircularProgress, Chip, TextField, IconButton,
+ useMediaQuery
} from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import EmailIcon from '@mui/icons-material/Email';
@@ -40,6 +41,7 @@ interface AdminUser {
const AdminDashboard: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
+ const isMobile = useMediaQuery('(max-width:900px)');
useTitle(t('admin.title'));
const [stats, setStats] = useState(null);
const [users, setUsers] = useState([]);
@@ -84,38 +86,38 @@ const AdminDashboard: React.FC = () => {
return (
- {t('admin.title')}
+ {t('admin.title')}
-
-
+
+
alpha(theme.palette.primary.main, 0.1) }}>
-
- {t('admin.totalUsers')}
- {stats?.total_users}
+
+ {t('admin.totalUsers')}
+ {stats?.total_users}
-
+
alpha(theme.palette.error.main, 0.1) }}>
-
- {t('admin.openTickets')}
- {stats?.open_tickets}
+
+ {t('admin.openTickets')}
+ {stats?.open_tickets}
-
-
+
+
{t('admin.config')}: SMTP
-
+
{stats?.email_config.host}:{stats?.email_config.port} •
TLS: {String(stats?.email_config.use_tls)} •
Insecure: {String(stats?.email_config.skip_verify)}
- } onClick={handleSendTestEmail}>
+ } onClick={handleSendTestEmail} size="small">
{t('admin.sendTestEmail')}
@@ -125,62 +127,86 @@ const AdminDashboard: React.FC = () => {
-
+
{t('admin.users')}
setSearch(e.target.value)}
- sx={{ width: 300 }}
+ sx={{ width: isMobile ? '100%' : 300 }}
/>
-
-
-
-
- ID
- User
- Email
- Status
- Joined
- IP History
- Actions
-
-
-
- {filteredUsers.map((user) => (
-
- {user.id}
-
- {user.username}
- {user.is_superuser && }
-
- {user.email}
-
-
-
- {formatDate(user.date_joined)}
-
-
- {user.ips.slice(0, 3).map(ip => (
-
- ))}
- {user.ips.length > 3 && +{user.ips.length - 3}}
-
-
-
- navigate(`/admin/user/${user.id}`)}>
+
+ {isMobile ? (
+
+ {filteredUsers.map(user => (
+
+
+
+ {user.username}
+ navigate(`/admin-panel/user/${user.id}`)}>
-
+
+ {user.email}
+
+
+ {user.is_superuser && }
+
+
+
+ ))}
+
+ ) : (
+
+
+
+
+ ID
+ User
+ Email
+ Status
+ Joined
+ IP History
+ Actions
- ))}
-
-
-
+
+
+ {filteredUsers.map((user) => (
+
+ {user.id}
+
+ {user.username}
+ {user.is_superuser && }
+
+ {user.email}
+
+
+
+ {formatDate(user.date_joined)}
+
+
+ {user.ips.slice(0, 3).map(ip => (
+
+ ))}
+ {user.ips.length > 3 && +{user.ips.length - 3}}
+
+
+
+ navigate(`/admin-panel/user/${user.id}`)}>
+
+
+
+
+ ))}
+
+
+
+ )}
);
diff --git a/frontend/src/pages/AdminRatingDetail.tsx b/frontend/src/pages/AdminRatingDetail.tsx
new file mode 100644
index 0000000..a7ee2a6
--- /dev/null
+++ b/frontend/src/pages/AdminRatingDetail.tsx
@@ -0,0 +1,98 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Container, Typography, Box, Button,
+ Stack, CircularProgress, TextField, Paper
+} from '@mui/material';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import DeleteIcon from '@mui/icons-material/Delete';
+import { useTranslation } from 'react-i18next';
+import { useNavigate, useParams } from 'react-router-dom';
+import api from '../api';
+import { useTitle } from '../hooks/useTitle';
+import MentionTextField from '../components/MentionTextField';
+
+const AdminRatingDetail: React.FC = () => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const { id } = useParams<{ id: string }>();
+ const [score, setScore] = useState(0);
+ const [comment, setComment] = useState('');
+ const [rating, setRating] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const fetchRating = async () => {
+ try {
+ const res = await api.get(`ratings/${id}/`);
+ setRating(res.data);
+ setScore(res.data.score);
+ setComment(res.data.comment || '');
+ } catch (err) {
+ console.error(err);
+ navigate('/admin-panel');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchRating();
+ }, [id]);
+
+ useTitle(rating ? `${t('admin.manageRating')}: ${rating.flavor_name}` : 'Rating Management');
+
+ const handleUpdate = async () => {
+ try {
+ await api.patch(`ratings/${id}/`, { score, comment });
+ fetchRating();
+ alert('Rating updated!');
+ } catch (err) { alert('Update failed'); }
+ };
+
+ const handleDelete = async () => {
+ if (!window.confirm('Delete this rating and all replies?')) return;
+ try {
+ await api.delete(`ratings/${id}/`);
+ navigate('/admin-panel');
+ } catch (err) { alert('Delete failed'); }
+ };
+
+ if (loading) return ;
+
+ return (
+
+ }
+ onClick={() => navigate(-1)}
+ sx={{ mb: 4, borderRadius: 2 }}
+ >
+ {t('common.back')}
+
+
+ {t('admin.manageRating')}
+
+
+
+ {rating.flavor_name} (User: {rating.user})
+
+
+ setScore(Number(e.target.value))}
+ />
+
+
+
+ } onClick={handleDelete}>{t('common.delete')}
+
+
+
+
+ );
+};
+
+export default AdminRatingDetail;
diff --git a/frontend/src/pages/AdminReplyDetail.tsx b/frontend/src/pages/AdminReplyDetail.tsx
new file mode 100644
index 0000000..4854a5b
--- /dev/null
+++ b/frontend/src/pages/AdminReplyDetail.tsx
@@ -0,0 +1,92 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Container, Typography, Box, Button,
+ Stack, CircularProgress, Paper
+} from '@mui/material';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import DeleteIcon from '@mui/icons-material/Delete';
+import { useTranslation } from 'react-i18next';
+import { useNavigate, useParams } from 'react-router-dom';
+import api from '../api';
+import { useTitle } from '../hooks/useTitle';
+import MentionTextField from '../components/MentionTextField';
+
+const AdminReplyDetail: React.FC = () => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const { id } = useParams<{ id: string }>();
+ const [text, setText] = useState('');
+ const [reply, setReply] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const fetchReply = async () => {
+ try {
+ // We use the replies list/detail endpoint
+ const res = await api.get(`replies/${id}/`);
+ setReply(res.data);
+ setText(res.data.text);
+ } catch (err) {
+ console.error(err);
+ navigate('/admin-panel');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchReply();
+ }, [id]);
+
+ useTitle(reply ? `${t('admin.manageReply')}` : 'Reply Management');
+
+ const handleUpdate = async () => {
+ try {
+ await api.patch(`replies/${id}/`, { text });
+ fetchReply();
+ alert('Reply updated!');
+ } catch (err) { alert('Update failed'); }
+ };
+
+ const handleDelete = async () => {
+ if (!window.confirm('Delete this reply?')) return;
+ try {
+ await api.delete(`replies/${id}/`);
+ navigate('/admin-panel');
+ } catch (err) { alert('Delete failed'); }
+ };
+
+ if (loading) return ;
+
+ return (
+
+ }
+ onClick={() => navigate(-1)}
+ sx={{ mb: 4, borderRadius: 2 }}
+ >
+ {t('common.back')}
+
+
+ {t('admin.manageReply')}
+
+
+
+ Reply by {reply.user}
+
+
+
+
+
+ } onClick={handleDelete}>{t('common.delete')}
+
+
+
+
+ );
+};
+
+export default AdminReplyDetail;
diff --git a/frontend/src/pages/CommunityFeed.tsx b/frontend/src/pages/CommunityFeed.tsx
index 5452d5a..bc7c4e0 100644
--- a/frontend/src/pages/CommunityFeed.tsx
+++ b/frontend/src/pages/CommunityFeed.tsx
@@ -79,7 +79,11 @@ interface Notification {
flavor_id: number | null;
}
-const CommunityFeed: React.FC = () => {
+interface CommunityFeedProps {
+ adminMode?: boolean;
+}
+
+const CommunityFeed: React.FC = ({ adminMode }) => {
const { t } = useTranslation();
useTitle(t('community.title'));
const navigate = useNavigate();
@@ -274,6 +278,16 @@ const CommunityFeed: React.FC = () => {
+ {adminMode && (
+
+ )}
+
{/* Flavor and Comment */}
@@ -315,7 +329,17 @@ const CommunityFeed: React.FC = () => {
{reply.user}
{formatTimestamp(reply.created_at)}
-
+
+
+ {adminMode && (
+
+ )}
+
))}
diff --git a/frontend/src/pages/MainPage.tsx b/frontend/src/pages/MainPage.tsx
index 736ae9e..a3e9291 100644
--- a/frontend/src/pages/MainPage.tsx
+++ b/frontend/src/pages/MainPage.tsx
@@ -22,6 +22,7 @@ import api from '../api';
import { useTitle } from '../hooks/useTitle';
import { formatDate } from '../utils/date';
import RatingBadge from '../components/RatingBadge';
+import { useNavigate } from 'react-router-dom';
interface Flavor {
id: number;
@@ -47,8 +48,13 @@ interface Review {
flavor: number;
}
-const MainPage: React.FC = () => {
+interface MainPageProps {
+ adminMode?: boolean;
+}
+
+const MainPage: React.FC = ({ adminMode }) => {
const { t } = useTranslation();
+ const navigate = useNavigate();
const [topFlavors, setTopFlavors] = useState([]);
const [newestFlavors, setNewestFlavors] = useState([]);
const [recentReviews, setRecentReviews] = useState([]);
@@ -282,6 +288,16 @@ const MainPage: React.FC = () => {
{t('home.rated')} {formatDate(rating.created_at)}
+
+ {adminMode && (
+
+ )}
{
+ {adminMode && (
+
+ )}
{formatDate(review.created_at)} • on {review.flavor_name}
diff --git a/frontend/src/pages/PublicProfile.tsx b/frontend/src/pages/PublicProfile.tsx
index 6e7a76b..4774dbb 100644
--- a/frontend/src/pages/PublicProfile.tsx
+++ b/frontend/src/pages/PublicProfile.tsx
@@ -45,7 +45,11 @@ interface ProfileData {
ratings: Rating[];
}
-const PublicProfile: React.FC = () => {
+interface PublicProfileProps {
+ adminMode?: boolean;
+}
+
+const PublicProfile: React.FC = ({ adminMode }) => {
const { t } = useTranslation();
const { username } = useParams<{ username: string }>();
const navigate = useNavigate();
@@ -265,6 +269,15 @@ const PublicProfile: React.FC = () => {
}}>
{rating.flavor_name}
+ {adminMode && (
+
+ )}
diff --git a/frontend/src/pages/flavors/FlavorDetail.tsx b/frontend/src/pages/flavors/FlavorDetail.tsx
index 03042a5..85f5f7c 100644
--- a/frontend/src/pages/flavors/FlavorDetail.tsx
+++ b/frontend/src/pages/flavors/FlavorDetail.tsx
@@ -61,7 +61,11 @@ interface Flavor {
shop_url: string | null;
}
-const FlavorDetail: React.FC = () => {
+interface FlavorDetailProps {
+ adminMode?: boolean;
+}
+
+const FlavorDetail: React.FC = ({ adminMode }) => {
const { t } = useTranslation();
const theme = useTheme();
const { id } = useParams<{ id: string }>();
@@ -403,7 +407,18 @@ const FlavorDetail: React.FC = () => {
-
+
+
+ {adminMode && (
+
+ )}
+
{rating.comment ? (
@@ -457,6 +472,15 @@ const FlavorDetail: React.FC = () => {
)}
+ {adminMode && (
+
+ )}
{editingReplyId === reply.id ? (