mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
redesigned admin for website
This commit is contained in:
1 parent
4e7c11c38b
commit
e681b74533
9 files changed
+368
-33
No files matched your search
+36
-9
@@ -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 && (
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to="/admin" onClick={() => setDrawerOpen(false)}>
|
||||
<ListItemButton onClick={toggleAdminMode}>
|
||||
<ListItemText
|
||||
primary={t('admin.adminMode')}
|
||||
secondary={adminMode ? "ON" : "OFF"}
|
||||
sx={{ color: adminMode ? 'primary.main' : 'inherit' }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
{user.is_superuser && (
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to="/admin-panel" onClick={() => setDrawerOpen(false)}>
|
||||
<ListItemText primary="Admin Panel" sx={{ color: 'primary.main', fontWeight: 'bold' }} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
@@ -594,9 +615,13 @@ const App: React.FC = () => {
|
||||
<MenuItem component={Link} to="/settings" onClick={() => setAnchorEl(null)}>{t('nav.settings')}</MenuItem>
|
||||
<MenuItem component={Link} to="/support" onClick={() => setAnchorEl(null)}>{t('support.title')}</MenuItem>
|
||||
{user.is_superuser && (
|
||||
<MenuItem component={Link} to="/admin" onClick={() => setAnchorEl(null)} sx={{ fontWeight: 'bold', color: 'primary.main' }}>Admin Panel</MenuItem>
|
||||
<MenuItem onClick={toggleAdminMode} sx={{ color: adminMode ? 'primary.main' : 'inherit' }}>
|
||||
{t('admin.adminMode')}: {adminMode ? "ON" : "OFF"}
|
||||
</MenuItem>
|
||||
)}
|
||||
<Divider />
|
||||
{user.is_superuser && (
|
||||
<MenuItem component={Link} to="/admin-panel" onClick={() => setAnchorEl(null)} sx={{ fontWeight: 'bold', color: 'primary.main' }}>Admin Panel</MenuItem>
|
||||
)} <Divider />
|
||||
<MenuItem onClick={handleLogout} sx={{ color: 'error.main' }}>{t('nav.logout')}</MenuItem> </Menu>
|
||||
</Box>
|
||||
) : (
|
||||
@@ -631,18 +656,20 @@ const App: React.FC = () => {
|
||||
<Box sx={{ flexGrow: 1, width: '100%' }}>
|
||||
<Suspense fallback={<Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>}>
|
||||
<Routes>
|
||||
<Route path="/" element={<MainPage />} />
|
||||
<Route path="/" element={<MainPage adminMode={adminMode} />} />
|
||||
<Route path="/category/:slug" element={<CategoryFlavors />} />
|
||||
<Route path="/flavor/:id" element={<FlavorDetail />} />
|
||||
<Route path="/profile/:username" element={<PublicProfile />} />
|
||||
<Route path="/community" element={<CommunityFeed />} />
|
||||
<Route path="/flavor/:id" element={<FlavorDetail adminMode={adminMode} />} />
|
||||
<Route path="/profile/:username" element={<PublicProfile adminMode={adminMode} />} />
|
||||
<Route path="/community" element={<CommunityFeed adminMode={adminMode} />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/privacy" element={<Privacy />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/settings" element={<Settings themeName={themeName} onThemeChange={handleThemeChange} />} />
|
||||
<Route path="/support" element={<Support />} />
|
||||
<Route path="/admin" element={<AdminDashboard />} />
|
||||
<Route path="/admin/user/:id" element={<AdminUserDetail />} />
|
||||
<Route path="/admin-panel" element={<AdminDashboard />} />
|
||||
<Route path="/admin-panel/user/:id" element={<AdminUserDetail />} />
|
||||
<Route path="/admin-panel/rating/:id" element={<AdminRatingDetail />} />
|
||||
<Route path="/admin-panel/reply/:id" element={<AdminReplyDetail />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="*" element={<Box sx={{ p: 4, textAlign: 'center' }}><Typography variant="h5">404 - Page Not Found</Typography><Button component={Link} to="/" sx={{ mt: 2 }}>Back to Home</Button></Box>} />
|
||||
</Routes>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<Stats | null>(null);
|
||||
const [users, setUsers] = useState<AdminUser[]>([]);
|
||||
@@ -84,38 +86,38 @@ const AdminDashboard: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Container maxWidth="xl" sx={{ py: 4 }}>
|
||||
<Typography variant="h3" sx={{ fontWeight: 'bold', mb: 4 }}>{t('admin.title')}</Typography>
|
||||
<Typography variant={isMobile ? "h4" : "h3"} sx={{ fontWeight: 'bold', mb: 4 }}>{t('admin.title')}</Typography>
|
||||
|
||||
<Grid container spacing={3} sx={{ mb: 6 }}>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<Grid container spacing={isMobile ? 2 : 3} sx={{ mb: 6 }}>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<Card sx={{ bgcolor: (theme) => alpha(theme.palette.primary.main, 0.1) }}>
|
||||
<CardContent>
|
||||
<Typography variant="overline">{t('admin.totalUsers')}</Typography>
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold' }}>{stats?.total_users}</Typography>
|
||||
<CardContent sx={{ p: isMobile ? 2 : 3 }}>
|
||||
<Typography variant="overline" sx={{ fontSize: '0.6rem' }}>{t('admin.totalUsers')}</Typography>
|
||||
<Typography variant={isMobile ? "h5" : "h4"} sx={{ fontWeight: 'bold' }}>{stats?.total_users}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<Card sx={{ bgcolor: (theme) => alpha(theme.palette.error.main, 0.1) }}>
|
||||
<CardContent>
|
||||
<Typography variant="overline">{t('admin.openTickets')}</Typography>
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold', color: 'error.main' }}>{stats?.open_tickets}</Typography>
|
||||
<CardContent sx={{ p: isMobile ? 2 : 3 }}>
|
||||
<Typography variant="overline" sx={{ fontSize: '0.6rem' }}>{t('admin.openTickets')}</Typography>
|
||||
<Typography variant={isMobile ? "h5" : "h4"} sx={{ fontWeight: 'bold', color: 'error.main' }}>{stats?.open_tickets}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<CardContent sx={{ p: isMobile ? 2 : 3 }}>
|
||||
<Stack direction={isMobile ? "column" : "row"} justifyContent="space-between" alignItems={isMobile ? "flex-start" : "center"} spacing={2}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold' }}>{t('admin.config')}: SMTP</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
{stats?.email_config.host}:{stats?.email_config.port} •
|
||||
TLS: {String(stats?.email_config.use_tls)} •
|
||||
Insecure: {String(stats?.email_config.skip_verify)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button variant="outlined" startIcon={<EmailIcon />} onClick={handleSendTestEmail}>
|
||||
<Button variant="outlined" fullWidth={isMobile} startIcon={<EmailIcon />} onClick={handleSendTestEmail} size="small">
|
||||
{t('admin.sendTestEmail')}
|
||||
</Button>
|
||||
</Stack>
|
||||
@@ -125,14 +127,37 @@ const AdminDashboard: React.FC = () => {
|
||||
</Grid>
|
||||
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
|
||||
<Stack direction={isMobile ? "column" : "row"} justifyContent="space-between" alignItems={isMobile ? "flex-start" : "center"} sx={{ mb: 2 }} spacing={2}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 'bold' }}>{t('admin.users')}</Typography>
|
||||
<TextField
|
||||
size="small" placeholder="Search..."
|
||||
fullWidth={isMobile}
|
||||
value={search} onChange={(e) => setSearch(e.target.value)}
|
||||
sx={{ width: 300 }}
|
||||
sx={{ width: isMobile ? '100%' : 300 }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{isMobile ? (
|
||||
<Stack spacing={2}>
|
||||
{filteredUsers.map(user => (
|
||||
<Card key={user.id} variant="outlined" sx={{ borderRadius: 3 }}>
|
||||
<CardContent sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold' }}>{user.username}</Typography>
|
||||
<IconButton size="small" onClick={() => navigate(`/admin-panel/user/${user.id}`)}>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">{user.email}</Typography>
|
||||
<Stack direction="row" spacing={1} sx={{ mt: 1 }}>
|
||||
<Chip label={user.is_active ? 'Active' : 'Inactive'} color={user.is_active ? 'success' : 'default'} size="small" sx={{ height: 20, fontSize: '0.65rem' }} />
|
||||
{user.is_superuser && <Chip label="Admin" color="secondary" size="small" sx={{ height: 20, fontSize: '0.65rem' }} />}
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<TableContainer component={Paper} sx={{ borderRadius: 3, border: '1px solid', borderColor: 'divider' }}>
|
||||
<Table>
|
||||
<TableHead sx={{ bgcolor: 'action.hover' }}>
|
||||
@@ -172,7 +197,7 @@ const AdminDashboard: React.FC = () => {
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton size="small" onClick={() => navigate(`/admin/user/${user.id}`)}>
|
||||
<IconButton size="small" onClick={() => navigate(`/admin-panel/user/${user.id}`)}>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
@@ -181,6 +206,7 @@ const AdminDashboard: React.FC = () => {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -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<any>(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 <Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>;
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ py: 4 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{ mb: 4, borderRadius: 2 }}
|
||||
>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold', mb: 4 }}>{t('admin.manageRating')}</Typography>
|
||||
|
||||
<Paper sx={{ p: 3, borderRadius: 4 }}>
|
||||
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||
{rating.flavor_name} (User: {rating.user})
|
||||
</Typography>
|
||||
<Stack spacing={3} sx={{ mt: 3 }}>
|
||||
<TextField
|
||||
label="Score" type="number"
|
||||
slotProps={{ input: { inputProps: { min: 1, max: 10 } } }}
|
||||
value={score} onChange={(e) => setScore(Number(e.target.value))}
|
||||
/>
|
||||
<MentionTextField
|
||||
multiline rows={4}
|
||||
value={comment} onChange={setComment}
|
||||
/>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button variant="contained" fullWidth onClick={handleUpdate}>{t('common.save')}</Button>
|
||||
<Button variant="outlined" color="error" fullWidth startIcon={<DeleteIcon />} onClick={handleDelete}>{t('common.delete')}</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminRatingDetail;
|
||||
@@ -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<any>(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 <Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>;
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ py: 4 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{ mb: 4, borderRadius: 2 }}
|
||||
>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold', mb: 4 }}>{t('admin.manageReply')}</Typography>
|
||||
|
||||
<Paper sx={{ p: 3, borderRadius: 4 }}>
|
||||
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||
Reply by {reply.user}
|
||||
</Typography>
|
||||
<Stack spacing={3} sx={{ mt: 3 }}>
|
||||
<MentionTextField
|
||||
multiline rows={4}
|
||||
value={text} onChange={setText}
|
||||
/>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button variant="contained" fullWidth onClick={handleUpdate}>{t('common.save')}</Button>
|
||||
<Button variant="outlined" color="error" fullWidth startIcon={<DeleteIcon />} onClick={handleDelete}>{t('common.delete')}</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminReplyDetail;
|
||||
@@ -79,7 +79,11 @@ interface Notification {
|
||||
flavor_id: number | null;
|
||||
}
|
||||
|
||||
const CommunityFeed: React.FC = () => {
|
||||
interface CommunityFeedProps {
|
||||
adminMode?: boolean;
|
||||
}
|
||||
|
||||
const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
|
||||
const { t } = useTranslation();
|
||||
useTitle(t('community.title'));
|
||||
const navigate = useNavigate();
|
||||
@@ -274,6 +278,16 @@ const CommunityFeed: React.FC = () => {
|
||||
<RatingBadge score={rating.score} />
|
||||
</Box>
|
||||
|
||||
{adminMode && (
|
||||
<Button
|
||||
size="small" variant="outlined" color="secondary"
|
||||
onClick={() => navigate(`/admin-panel/rating/${rating.id}`)}
|
||||
sx={{ mb: 2, borderRadius: 2, textTransform: 'none', fontWeight: 'bold' }}
|
||||
>
|
||||
{t('admin.manageRating')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Flavor and Comment */}
|
||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
@@ -315,7 +329,17 @@ const CommunityFeed: React.FC = () => {
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', fontSize: '0.8rem' }}>{reply.user}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{formatTimestamp(reply.created_at)}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2"><RichText text={reply.text} /></Typography>
|
||||
{adminMode && (
|
||||
<Button
|
||||
size="small" sx={{ minWidth: 0, py: 0, fontSize: '0.7rem' }}
|
||||
onClick={() => navigate(`/admin-panel/reply/${reply.id}`)}
|
||||
>
|
||||
{t('admin.manageReply')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
|
||||
|
||||
@@ -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<MainPageProps> = ({ adminMode }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [topFlavors, setTopFlavors] = useState<Flavor[]>([]);
|
||||
const [newestFlavors, setNewestFlavors] = useState<Flavor[]>([]);
|
||||
const [recentReviews, setRecentReviews] = useState<Review[]>([]);
|
||||
@@ -283,6 +289,16 @@ const MainPage: React.FC = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{adminMode && (
|
||||
<Button
|
||||
size="small" variant="outlined" color="secondary"
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); navigate(`/admin-panel/rating/${rating.id}`); }}
|
||||
sx={{ mb: 1, borderRadius: 1, fontSize: '0.6rem', py: 0, textTransform: 'none' }}
|
||||
>
|
||||
{t('admin.manageRating')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
<Box
|
||||
component="img"
|
||||
@@ -496,6 +512,15 @@ const MainPage: React.FC = () => {
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<RatingBadge score={review.score} size="small" />
|
||||
</Box>
|
||||
{adminMode && (
|
||||
<Button
|
||||
size="small" variant="text" color="secondary"
|
||||
onClick={() => navigate(`/admin-panel/rating/${review.id}`)}
|
||||
sx={{ mb: 1, fontSize: '0.65rem', py: 0, textTransform: 'none', fontWeight: 'bold' }}
|
||||
>
|
||||
{t('admin.manageRating')}
|
||||
</Button>
|
||||
)}
|
||||
<Typography variant="caption" color="text.secondary" display="block" sx={{ mb: 1 }}>
|
||||
{formatDate(review.created_at)} • on <Link to={`/flavor/${review.flavor}`} style={{ color: 'inherit', fontWeight: 'bold' }}>{review.flavor_name}</Link>
|
||||
</Typography>
|
||||
|
||||
@@ -45,7 +45,11 @@ interface ProfileData {
|
||||
ratings: Rating[];
|
||||
}
|
||||
|
||||
const PublicProfile: React.FC = () => {
|
||||
interface PublicProfileProps {
|
||||
adminMode?: boolean;
|
||||
}
|
||||
|
||||
const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
|
||||
const { t } = useTranslation();
|
||||
const { username } = useParams<{ username: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -265,6 +269,15 @@ const PublicProfile: React.FC = () => {
|
||||
}}>
|
||||
{rating.flavor_name}
|
||||
</Typography>
|
||||
{adminMode && (
|
||||
<Button
|
||||
size="small" fullWidth variant="text" color="secondary"
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); navigate(`/admin-panel/rating/${rating.id}`); }}
|
||||
sx={{ fontSize: '0.6rem', py: 0, textTransform: 'none' }}
|
||||
>
|
||||
Manage
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
@@ -61,7 +61,11 @@ interface Flavor {
|
||||
shop_url: string | null;
|
||||
}
|
||||
|
||||
const FlavorDetail: React.FC = () => {
|
||||
interface FlavorDetailProps {
|
||||
adminMode?: boolean;
|
||||
}
|
||||
|
||||
const FlavorDetail: React.FC<FlavorDetailProps> = ({ adminMode }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -403,7 +407,18 @@ const FlavorDetail: React.FC = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<RatingBadge score={rating.score} />
|
||||
{adminMode && (
|
||||
<Button
|
||||
size="small" variant="outlined" color="secondary"
|
||||
onClick={() => navigate(`/admin-panel/rating/${rating.id}`)}
|
||||
sx={{ borderRadius: 2, textTransform: 'none', fontWeight: 'bold' }}
|
||||
>
|
||||
{t('admin.manageRating')}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{rating.comment ? (
|
||||
@@ -457,6 +472,15 @@ const FlavorDetail: React.FC = () => {
|
||||
<Button size="small" color="error" sx={{ minWidth: 0, py: 0, fontSize: '0.7rem', fontWeight: 'bold' }} onClick={() => handleDeleteReply(reply.id)}>{t('common.delete')}</Button>
|
||||
</Box>
|
||||
)}
|
||||
{adminMode && (
|
||||
<Button
|
||||
size="small" variant="text" color="secondary"
|
||||
onClick={() => navigate(`/admin-panel/reply/${reply.id}`)}
|
||||
sx={{ minWidth: 0, py: 0, fontSize: '0.7rem', fontWeight: 'bold' }}
|
||||
>
|
||||
{t('admin.manageReply')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
{editingReplyId === reply.id ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
|
||||
|
||||
Reference in new issue
Block a user