mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
migrated alerts and community
This commit is contained in:
1 parent
607f8f9bc1
commit
c42fd733f0
16 files changed
+274
-58
No files matched your search
@@ -2,12 +2,18 @@ import type { ReactNode } from 'react';
|
|||||||
import { ThemeProvider } from '../contexts/ThemeContext';
|
import { ThemeProvider } from '../contexts/ThemeContext';
|
||||||
import { AuthProvider } from '../contexts/AuthContext';
|
import { AuthProvider } from '../contexts/AuthContext';
|
||||||
import { NotificationProvider } from '../contexts/NotificationContext';
|
import { NotificationProvider } from '../contexts/NotificationContext';
|
||||||
|
import { ToastProvider } from '../contexts/ToastContext';
|
||||||
|
import { ConfirmProvider } from '../contexts/ConfirmContext';
|
||||||
import '../i18n';
|
import '../i18n';
|
||||||
|
|
||||||
export const AppProviders = ({ children }: { children: ReactNode }) => (
|
export const AppProviders = ({ children }: { children: ReactNode }) => (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<NotificationProvider>{children}</NotificationProvider>
|
<NotificationProvider>
|
||||||
|
<ToastProvider>
|
||||||
|
<ConfirmProvider>{children}</ConfirmProvider>
|
||||||
|
</ToastProvider>
|
||||||
|
</NotificationProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { createContext, useCallback, useRef, useState, type ReactNode } from 'react';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
|
export interface ConfirmOptions {
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
confirmLabel?: string;
|
||||||
|
cancelLabel?: string;
|
||||||
|
danger?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmContextValue {
|
||||||
|
confirm: (opts: ConfirmOptions | string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfirmContext = createContext<ConfirmContextValue | null>(null);
|
||||||
|
|
||||||
|
export const ConfirmProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [opts, setOpts] = useState<ConfirmOptions | null>(null);
|
||||||
|
const resolverRef = useRef<((value: boolean) => void) | null>(null);
|
||||||
|
|
||||||
|
const confirm = useCallback((input: ConfirmOptions | string) => {
|
||||||
|
const normalized: ConfirmOptions =
|
||||||
|
typeof input === 'string' ? { message: input } : input;
|
||||||
|
setOpts(normalized);
|
||||||
|
setOpen(true);
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
resolverRef.current = resolve;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handle = (result: boolean) => {
|
||||||
|
setOpen(false);
|
||||||
|
resolverRef.current?.(result);
|
||||||
|
resolverRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmContext.Provider value={{ confirm }}>
|
||||||
|
{children}
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={() => handle(false)}
|
||||||
|
maxWidth="xs"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{ sx: { borderRadius: 3 } }}
|
||||||
|
>
|
||||||
|
{opts?.title && <DialogTitle sx={{ fontWeight: 'bold' }}>{opts.title}</DialogTitle>}
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>{opts?.message}</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||||
|
<Button onClick={() => handle(false)} variant="outlined">
|
||||||
|
{opts?.cancelLabel ?? 'Cancel'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handle(true)}
|
||||||
|
variant="contained"
|
||||||
|
color={opts?.danger ? 'error' : 'primary'}
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
{opts?.confirmLabel ?? 'Confirm'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</ConfirmContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { createContext, useCallback, useState, type ReactNode } from 'react';
|
||||||
|
import Snackbar from '@mui/material/Snackbar';
|
||||||
|
import Alert, { type AlertColor } from '@mui/material/Alert';
|
||||||
|
|
||||||
|
export interface ToastOptions {
|
||||||
|
message: string;
|
||||||
|
severity?: AlertColor;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToastContextValue {
|
||||||
|
notify: (opts: ToastOptions | string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToastContext = createContext<ToastContextValue | null>(null);
|
||||||
|
|
||||||
|
interface ToastState extends ToastOptions {
|
||||||
|
key: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToastProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [toast, setToast] = useState<ToastState | null>(null);
|
||||||
|
|
||||||
|
const notify = useCallback((opts: ToastOptions | string) => {
|
||||||
|
const normalized: ToastOptions =
|
||||||
|
typeof opts === 'string' ? { message: opts, severity: 'info' } : opts;
|
||||||
|
setToast({
|
||||||
|
message: normalized.message,
|
||||||
|
severity: normalized.severity ?? 'info',
|
||||||
|
duration: normalized.duration ?? 4000,
|
||||||
|
key: Date.now(),
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = (_: unknown, reason?: string) => {
|
||||||
|
if (reason === 'clickaway') return;
|
||||||
|
setToast(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={{ notify }}>
|
||||||
|
{children}
|
||||||
|
<Snackbar
|
||||||
|
key={toast?.key}
|
||||||
|
open={!!toast}
|
||||||
|
autoHideDuration={toast?.duration}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||||
|
>
|
||||||
|
{toast ? (
|
||||||
|
<Alert
|
||||||
|
onClose={() => setToast(null)}
|
||||||
|
severity={toast.severity}
|
||||||
|
variant="filled"
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{toast.message}
|
||||||
|
</Alert>
|
||||||
|
) : undefined}
|
||||||
|
</Snackbar>
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { ConfirmContext } from '../contexts/ConfirmContext';
|
||||||
|
|
||||||
|
export const useConfirm = () => {
|
||||||
|
const ctx = useContext(ConfirmContext);
|
||||||
|
if (!ctx) throw new Error('useConfirm must be used within ConfirmProvider');
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { ToastContext } from '../contexts/ToastContext';
|
||||||
|
|
||||||
|
export const useToast = () => {
|
||||||
|
const ctx = useContext(ToastContext);
|
||||||
|
if (!ctx) throw new Error('useToast must be used within ToastProvider');
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { PageShell, GlassCard, GlassPaper, EmptyState } from '../components/ui';
|
import { PageShell, GlassCard, GlassPaper, EmptyState } from '../components/ui';
|
||||||
|
import { useToast } from '../hooks/useToast';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import EmailIcon from '@mui/icons-material/Email';
|
import EmailIcon from '@mui/icons-material/Email';
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
@@ -134,6 +135,7 @@ const AdminDashboard: React.FC = () => {
|
|||||||
const [currentTab, setCurrentTab] = useState(0);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
const [selectedBanner, setSelectedBanner] = useState<Banner | null>(null);
|
const [selectedBanner, setSelectedBanner] = useState<Banner | null>(null);
|
||||||
const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false);
|
const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false);
|
||||||
|
const { notify } = useToast();
|
||||||
|
|
||||||
const fetchData = useCallback(async (silent = false) => {
|
const fetchData = useCallback(async (silent = false) => {
|
||||||
if (!silent) setLoading(true);
|
if (!silent) setLoading(true);
|
||||||
@@ -177,7 +179,7 @@ const AdminDashboard: React.FC = () => {
|
|||||||
const res = await api.patch('admin-custom/config/', { [key]: value });
|
const res = await api.patch('admin-custom/config/', { [key]: value });
|
||||||
setConfig(res.data);
|
setConfig(res.data);
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to update configuration');
|
notify({ message: 'Failed to update configuration', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,7 +188,7 @@ const AdminDashboard: React.FC = () => {
|
|||||||
await api.post(`admin-custom/${id}/trigger_job/`);
|
await api.post(`admin-custom/${id}/trigger_job/`);
|
||||||
fetchData(true);
|
fetchData(true);
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to trigger job');
|
notify({ message: 'Failed to trigger job', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -197,7 +199,7 @@ const AdminDashboard: React.FC = () => {
|
|||||||
await api.patch(`admin-custom/${id}/update_job_schedule/`, data);
|
await api.patch(`admin-custom/${id}/update_job_schedule/`, data);
|
||||||
fetchData(true);
|
fetchData(true);
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to update schedule');
|
notify({ message: 'Failed to update schedule', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,7 +218,10 @@ const AdminDashboard: React.FC = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as { response?: { data?: { error?: string } } };
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
alert(error.response?.data?.error || 'Failed to toggle visibility');
|
notify({
|
||||||
|
message: error.response?.data?.error || 'Failed to toggle visibility',
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -227,10 +232,13 @@ const AdminDashboard: React.FC = () => {
|
|||||||
const handleSendTestEmail = async () => {
|
const handleSendTestEmail = async () => {
|
||||||
try {
|
try {
|
||||||
await api.post('admin-custom/send_test_email/');
|
await api.post('admin-custom/send_test_email/');
|
||||||
alert(t('admin.testEmailSuccess'));
|
notify({ message: t('admin.testEmailSuccess'), severity: 'success' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as { response?: { data?: { error?: string } } };
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
alert(`Error: ${error.response?.data?.error || 'Failed to send'}`);
|
notify({
|
||||||
|
message: `Error: ${error.response?.data?.error || 'Failed to send'}`,
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import api from '../lib/api';
|
|||||||
import { useTitle } from '../hooks/useTitle';
|
import { useTitle } from '../hooks/useTitle';
|
||||||
import MentionTextField from '../components/MentionTextField';
|
import MentionTextField from '../components/MentionTextField';
|
||||||
import { PageShell, SectionHeader, FormCard } from '../components/ui';
|
import { PageShell, SectionHeader, FormCard } from '../components/ui';
|
||||||
|
import { useToast } from '../hooks/useToast';
|
||||||
|
import { useConfirm } from '../hooks/useConfirm';
|
||||||
|
|
||||||
interface AdminRating {
|
interface AdminRating {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -25,6 +27,8 @@ const AdminRatingDetail: React.FC = () => {
|
|||||||
const [comment, setComment] = useState('');
|
const [comment, setComment] = useState('');
|
||||||
const [rating, setRating] = useState<AdminRating | null>(null);
|
const [rating, setRating] = useState<AdminRating | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const { notify } = useToast();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
|
||||||
const fetchRating = useCallback(async () => {
|
const fetchRating = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -51,19 +55,19 @@ const AdminRatingDetail: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await api.patch(`ratings/${id}/`, { score, comment });
|
await api.patch(`ratings/${id}/`, { score, comment });
|
||||||
fetchRating();
|
fetchRating();
|
||||||
alert('Rating updated!');
|
notify({ message: 'Rating updated!', severity: 'success' });
|
||||||
} catch {
|
} catch {
|
||||||
alert('Update failed');
|
notify({ message: 'Update failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!window.confirm('Delete this rating and all replies?')) return;
|
if (!(await confirm({ message: 'Delete this rating and all replies?', danger: true }))) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`ratings/${id}/`);
|
await api.delete(`ratings/${id}/`);
|
||||||
navigate('/admin-panel');
|
navigate('/admin-panel');
|
||||||
} catch {
|
} catch {
|
||||||
alert('Delete failed');
|
notify({ message: 'Delete failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import api from '../lib/api';
|
|||||||
import { useTitle } from '../hooks/useTitle';
|
import { useTitle } from '../hooks/useTitle';
|
||||||
import MentionTextField from '../components/MentionTextField';
|
import MentionTextField from '../components/MentionTextField';
|
||||||
import { PageShell, SectionHeader, FormCard } from '../components/ui';
|
import { PageShell, SectionHeader, FormCard } from '../components/ui';
|
||||||
|
import { useToast } from '../hooks/useToast';
|
||||||
|
import { useConfirm } from '../hooks/useConfirm';
|
||||||
|
|
||||||
interface AdminReply {
|
interface AdminReply {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -22,6 +24,8 @@ const AdminReplyDetail: React.FC = () => {
|
|||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
const [reply, setReply] = useState<AdminReply | null>(null);
|
const [reply, setReply] = useState<AdminReply | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const { notify } = useToast();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
|
||||||
const fetchReply = useCallback(async () => {
|
const fetchReply = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -47,19 +51,19 @@ const AdminReplyDetail: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await api.patch(`replies/${id}/`, { text });
|
await api.patch(`replies/${id}/`, { text });
|
||||||
fetchReply();
|
fetchReply();
|
||||||
alert('Reply updated!');
|
notify({ message: 'Reply updated!', severity: 'success' });
|
||||||
} catch {
|
} catch {
|
||||||
alert('Update failed');
|
notify({ message: 'Update failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!window.confirm('Delete this reply?')) return;
|
if (!(await confirm({ message: 'Delete this reply?', danger: true }))) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`replies/${id}/`);
|
await api.delete(`replies/${id}/`);
|
||||||
navigate('/admin-panel');
|
navigate('/admin-panel');
|
||||||
} catch {
|
} catch {
|
||||||
alert('Delete failed');
|
notify({ message: 'Delete failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import {
|
|||||||
FormCard,
|
FormCard,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
} from '../components/ui';
|
} from '../components/ui';
|
||||||
|
import { useToast } from '../hooks/useToast';
|
||||||
|
import { useConfirm } from '../hooks/useConfirm';
|
||||||
|
|
||||||
interface AdminReply {
|
interface AdminReply {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -81,6 +83,9 @@ const AdminUserDetail: React.FC = () => {
|
|||||||
const [editingReplyId, setEditingReplyId] = useState<number | null>(null);
|
const [editingReplyId, setEditingReplyId] = useState<number | null>(null);
|
||||||
const [editReplyText, setEditReplyText] = useState('');
|
const [editReplyText, setEditReplyText] = useState('');
|
||||||
|
|
||||||
|
const { notify } = useToast();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
|
||||||
const fetchUser = useCallback(async () => {
|
const fetchUser = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(`admin-custom/${id}/user_detail/`);
|
const res = await api.get(`admin-custom/${id}/user_detail/`);
|
||||||
@@ -105,7 +110,7 @@ const AdminUserDetail: React.FC = () => {
|
|||||||
await api.patch(`admin-custom/${id}/user_detail/`, { is_active: !user.is_active });
|
await api.patch(`admin-custom/${id}/user_detail/`, { is_active: !user.is_active });
|
||||||
fetchUser();
|
fetchUser();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Action failed');
|
notify({ message: 'Action failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,17 +120,17 @@ const AdminUserDetail: React.FC = () => {
|
|||||||
setEditingRatingId(null);
|
setEditingRatingId(null);
|
||||||
fetchUser();
|
fetchUser();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Update failed');
|
notify({ message: 'Update failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteRating = async (ratingId: number) => {
|
const handleDeleteRating = async (ratingId: number) => {
|
||||||
if (!confirm('Delete this rating?')) return;
|
if (!(await confirm({ message: 'Delete this rating?', danger: true }))) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`ratings/${ratingId}/`);
|
await api.delete(`ratings/${ratingId}/`);
|
||||||
fetchUser();
|
fetchUser();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Delete failed');
|
notify({ message: 'Delete failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,28 +140,33 @@ const AdminUserDetail: React.FC = () => {
|
|||||||
setEditingReplyId(null);
|
setEditingReplyId(null);
|
||||||
fetchUser();
|
fetchUser();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Update failed');
|
notify({ message: 'Update failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteReply = async (replyId: number) => {
|
const handleDeleteReply = async (replyId: number) => {
|
||||||
if (!confirm('Delete this reply?')) return;
|
if (!(await confirm({ message: 'Delete this reply?', danger: true }))) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`replies/${replyId}/`);
|
await api.delete(`replies/${replyId}/`);
|
||||||
fetchUser();
|
fetchUser();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Delete failed');
|
notify({ message: 'Delete failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!window.confirm('PERMANENTLY DELETE THIS USER AND ALL DATA? This cannot be undone.'))
|
const ok = await confirm({
|
||||||
return;
|
title: 'Delete user',
|
||||||
|
message: 'PERMANENTLY DELETE THIS USER AND ALL DATA? This cannot be undone.',
|
||||||
|
confirmLabel: 'Delete forever',
|
||||||
|
danger: true,
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`admin-custom/${id}/user_detail/`);
|
await api.delete(`admin-custom/${id}/user_detail/`);
|
||||||
navigate('/admin-panel');
|
navigate('/admin-panel');
|
||||||
} catch {
|
} catch {
|
||||||
alert('Delete failed');
|
notify({ message: 'Delete failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
GlassCard,
|
GlassCard,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
} from '../components/ui';
|
} from '../components/ui';
|
||||||
|
import { useToast } from '../hooks/useToast';
|
||||||
|
|
||||||
interface Reply {
|
interface Reply {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -129,6 +130,7 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
useTitle(t('community.title'));
|
useTitle(t('community.title'));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { notify } = useToast();
|
||||||
const [ratings, setRatings] = useState<FeedRating[]>([]);
|
const [ratings, setRatings] = useState<FeedRating[]>([]);
|
||||||
const [following, setFollowing] = useState<FollowedUser[]>([]);
|
const [following, setFollowing] = useState<FollowedUser[]>([]);
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
@@ -213,7 +215,7 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
|
|||||||
);
|
);
|
||||||
setReplyInputs((prev) => ({ ...prev, [ratingId]: '' }));
|
setReplyInputs((prev) => ({ ...prev, [ratingId]: '' }));
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to send reply');
|
notify({ message: 'Failed to send reply', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import MentionTextField from '../components/MentionTextField';
|
|||||||
import RichText from '../components/RichText';
|
import RichText from '../components/RichText';
|
||||||
import { formatDate } from '../utils/date';
|
import { formatDate } from '../utils/date';
|
||||||
import { PageShell, GlassCard, GlassSurface, EmptyState } from '../components/ui';
|
import { PageShell, GlassCard, GlassSurface, EmptyState } from '../components/ui';
|
||||||
|
import { useToast } from '../hooks/useToast';
|
||||||
|
import { useConfirm } from '../hooks/useConfirm';
|
||||||
|
|
||||||
interface Reply {
|
interface Reply {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -108,12 +110,14 @@ const Dashboard: React.FC = () => {
|
|||||||
|
|
||||||
const [replyInputs, setReplyInputs] = useState<Record<number, string>>({});
|
const [replyInputs, setReplyInputs] = useState<Record<number, string>>({});
|
||||||
const [expandedReplies, setExpandedReplies] = useState<Record<number, boolean>>({});
|
const [expandedReplies, setExpandedReplies] = useState<Record<number, boolean>>({});
|
||||||
|
const { notify } = useToast();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
|
||||||
const shareUrl = data ? `${window.location.origin}/profile/${data.user.username}` : '';
|
const shareUrl = data ? `${window.location.origin}/profile/${data.user.username}` : '';
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = () => {
|
||||||
navigator.clipboard.writeText(shareUrl);
|
navigator.clipboard.writeText(shareUrl);
|
||||||
alert(t('dashboard.copySuccess'));
|
notify({ message: t('dashboard.copySuccess'), severity: 'success' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoBack = () => {
|
const handleGoBack = () => {
|
||||||
@@ -175,17 +179,17 @@ const Dashboard: React.FC = () => {
|
|||||||
setEditingRatingId(null);
|
setEditingRatingId(null);
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to update review');
|
notify({ message: 'Failed to update review', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteRating = async (ratingId: number) => {
|
const handleDeleteRating = async (ratingId: number) => {
|
||||||
if (!confirm('Delete this review?')) return;
|
if (!(await confirm({ message: 'Delete this review?', danger: true }))) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`ratings/${ratingId}/`);
|
await api.delete(`ratings/${ratingId}/`);
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to delete review');
|
notify({ message: 'Failed to delete review', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,17 +202,17 @@ const Dashboard: React.FC = () => {
|
|||||||
setExpandedReplies({ ...expandedReplies, [ratingId]: true });
|
setExpandedReplies({ ...expandedReplies, [ratingId]: true });
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to send reply');
|
notify({ message: 'Failed to send reply', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteReply = async (replyId: number) => {
|
const handleDeleteReply = async (replyId: number) => {
|
||||||
if (!confirm('Delete this reply?')) return;
|
if (!(await confirm({ message: 'Delete this reply?', danger: true }))) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`replies/${replyId}/`);
|
await api.delete(`replies/${replyId}/`);
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to delete reply');
|
notify({ message: 'Failed to delete reply', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import api from '../lib/api';
|
|||||||
import RatingBadge from '../components/RatingBadge';
|
import RatingBadge from '../components/RatingBadge';
|
||||||
import StatusBadge from '../components/StatusBadge';
|
import StatusBadge from '../components/StatusBadge';
|
||||||
import { PageShell, HeroBackdrop, GlassCard, SectionHeader } from '../components/ui';
|
import { PageShell, HeroBackdrop, GlassCard, SectionHeader } from '../components/ui';
|
||||||
|
import { useToast } from '../hooks/useToast';
|
||||||
|
|
||||||
interface Rating {
|
interface Rating {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -55,6 +56,7 @@ const FlavorList: React.FC = () => {
|
|||||||
const [flavors, setFlavors] = useState<Flavor[]>([]);
|
const [flavors, setFlavors] = useState<Flavor[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [ratingInput, setRatingInput] = useState<Record<number, RatingInput>>({});
|
const [ratingInput, setRatingInput] = useState<Record<number, RatingInput>>({});
|
||||||
|
const { notify } = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -82,9 +84,12 @@ const FlavorList: React.FC = () => {
|
|||||||
});
|
});
|
||||||
const res = await api.get('flavors/');
|
const res = await api.get('flavors/');
|
||||||
setFlavors(res.data);
|
setFlavors(res.data);
|
||||||
alert('Rating submitted!');
|
notify({ message: 'Rating submitted!', severity: 'success' });
|
||||||
} catch {
|
} catch {
|
||||||
alert("Failed to submit rating. Make sure you are logged in and haven't rated this yet.");
|
notify({
|
||||||
|
message: "Failed to submit rating. Make sure you are logged in and haven't rated this yet.",
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import RichText from '../components/RichText';
|
|||||||
import DynamicBanner from '../components/DynamicBanner';
|
import DynamicBanner from '../components/DynamicBanner';
|
||||||
import { formatDate } from '../utils/date';
|
import { formatDate } from '../utils/date';
|
||||||
import { PageShell, GlassCard, GlassSurface, FormCard, EmptyState } from '../components/ui';
|
import { PageShell, GlassCard, GlassSurface, FormCard, EmptyState } from '../components/ui';
|
||||||
|
import { useToast } from '../hooks/useToast';
|
||||||
|
import { useConfirm } from '../hooks/useConfirm';
|
||||||
|
|
||||||
interface Rating {
|
interface Rating {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -94,6 +96,8 @@ const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [palette, setPalette] = useState<string[]>([]);
|
const [palette, setPalette] = useState<string[]>([]);
|
||||||
const [newComment, setNewComment] = useState('');
|
const [newComment, setNewComment] = useState('');
|
||||||
|
const { notify } = useToast();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
|
||||||
const fetchProfile = useCallback(async () => {
|
const fetchProfile = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -158,7 +162,7 @@ const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
|
|||||||
setData({ ...data, is_following: true, followers_count: data.followers_count + 1 });
|
setData({ ...data, is_following: true, followers_count: data.followers_count + 1 });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to update follow status. Please login.');
|
notify({ message: 'Failed to update follow status. Please login.', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,7 +172,7 @@ const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
|
|||||||
else await api.post(`users/${user.id}/follow/`);
|
else await api.post(`users/${user.id}/follow/`);
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed');
|
notify({ message: 'Failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,17 +184,18 @@ const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
|
|||||||
setNewComment('');
|
setNewComment('');
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to add comment');
|
notify({ message: 'Failed to add comment', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteComment = async (commentId: number) => {
|
const handleDeleteComment = async (commentId: number) => {
|
||||||
if (!data || !confirm('Delete this comment?')) return;
|
if (!data) return;
|
||||||
|
if (!(await confirm({ message: 'Delete this comment?', danger: true }))) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`users/${data.id}/delete_comment/${commentId}/`);
|
await api.delete(`users/${data.id}/delete_comment/${commentId}/`);
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to delete comment');
|
notify({ message: 'Failed to delete comment', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useTitle } from '../hooks/useTitle';
|
import { useTitle } from '../hooks/useTitle';
|
||||||
import type { CatppuccinTheme } from '../theme';
|
import type { CatppuccinTheme } from '../theme';
|
||||||
import { PageShell, GlassCard, FormCard } from '../components/ui';
|
import { PageShell, GlassCard, FormCard } from '../components/ui';
|
||||||
|
import { useConfirm } from '../hooks/useConfirm';
|
||||||
|
|
||||||
interface SettingsProps {
|
interface SettingsProps {
|
||||||
themeName: CatppuccinTheme;
|
themeName: CatppuccinTheme;
|
||||||
@@ -39,6 +40,7 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
|||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
useTitle(t('nav.settings'));
|
useTitle(t('nav.settings'));
|
||||||
|
const { confirm } = useConfirm();
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [currentEmail, setCurrentEmail] = useState('');
|
const [currentEmail, setCurrentEmail] = useState('');
|
||||||
@@ -177,7 +179,13 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
|||||||
|
|
||||||
const handleConfirmDeletion = async (e: React.FormEvent) => {
|
const handleConfirmDeletion = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!window.confirm('Are you absolutely sure? This cannot be undone.')) return;
|
const ok = await confirm({
|
||||||
|
title: 'Delete account',
|
||||||
|
message: 'Are you absolutely sure? This cannot be undone.',
|
||||||
|
confirmLabel: 'Delete forever',
|
||||||
|
danger: true,
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await api.post('users/confirm_account_deletion/', { code: deletionCode });
|
await api.post('users/confirm_account_deletion/', { code: deletionCode });
|
||||||
localStorage.removeItem('access');
|
localStorage.removeItem('access');
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import api from '../lib/api';
|
|||||||
import { useTitle } from '../hooks/useTitle';
|
import { useTitle } from '../hooks/useTitle';
|
||||||
import { formatDate } from '../utils/date';
|
import { formatDate } from '../utils/date';
|
||||||
import { PageShell, SectionHeader, GlassCard, FormCard, EmptyState } from '../components/ui';
|
import { PageShell, SectionHeader, GlassCard, FormCard, EmptyState } from '../components/ui';
|
||||||
|
import { useToast } from '../hooks/useToast';
|
||||||
|
import { useConfirm } from '../hooks/useConfirm';
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -78,6 +80,8 @@ const Support: React.FC = () => {
|
|||||||
|
|
||||||
const [replyText, setReplyText] = useState<Record<number, string>>({});
|
const [replyText, setReplyText] = useState<Record<number, string>>({});
|
||||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
const [expandedId, setExpandedId] = useState<number | null>(null);
|
||||||
|
const { notify } = useToast();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
const [currentUser, setCurrentUser] = useState<CurrentUser | null>(null);
|
const [currentUser, setCurrentUser] = useState<CurrentUser | null>(null);
|
||||||
const [unreadTicketIds, setUnreadTicketIds] = useState<Set<number>>(new Set());
|
const [unreadTicketIds, setUnreadTicketIds] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
@@ -131,7 +135,7 @@ const Support: React.FC = () => {
|
|||||||
setShowCreate(false);
|
setShowCreate(false);
|
||||||
fetchTickets();
|
fetchTickets();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to create ticket');
|
notify({ message: 'Failed to create ticket', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,7 +147,7 @@ const Support: React.FC = () => {
|
|||||||
setReplyText({ ...replyText, [ticketId]: '' });
|
setReplyText({ ...replyText, [ticketId]: '' });
|
||||||
fetchTickets();
|
fetchTickets();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to send message');
|
notify({ message: 'Failed to send message', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,17 +156,17 @@ const Support: React.FC = () => {
|
|||||||
await api.post(`tickets/${ticketId}/update_status/`, { status });
|
await api.post(`tickets/${ticketId}/update_status/`, { status });
|
||||||
fetchTickets();
|
fetchTickets();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to update status');
|
notify({ message: 'Failed to update status', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteTicket = async (ticketId: number) => {
|
const handleDeleteTicket = async (ticketId: number) => {
|
||||||
if (!confirm('Delete this ticket?')) return;
|
if (!(await confirm({ message: 'Delete this ticket?', danger: true }))) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`tickets/${ticketId}/`);
|
await api.delete(`tickets/${ticketId}/`);
|
||||||
fetchTickets();
|
fetchTickets();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to delete ticket');
|
notify({ message: 'Failed to delete ticket', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import {
|
|||||||
SectionHeader,
|
SectionHeader,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
} from '../../components/ui';
|
} from '../../components/ui';
|
||||||
|
import { useToast } from '../../hooks/useToast';
|
||||||
|
import { useConfirm } from '../../hooks/useConfirm';
|
||||||
|
|
||||||
interface Reply {
|
interface Reply {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -83,6 +85,8 @@ const FlavorDetail: React.FC<FlavorDetailProps> = ({ adminMode }) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { notify } = useToast();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
const [flavor, setFlavor] = useState<Flavor | null>(null);
|
const [flavor, setFlavor] = useState<Flavor | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [replyInputs, setReplyInputs] = useState<{ [key: number]: string }>({});
|
const [replyInputs, setReplyInputs] = useState<{ [key: number]: string }>({});
|
||||||
@@ -152,7 +156,7 @@ const FlavorDetail: React.FC<FlavorDetailProps> = ({ adminMode }) => {
|
|||||||
setIsAdminEditing(false);
|
setIsAdminEditing(false);
|
||||||
fetchFlavor();
|
fetchFlavor();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Admin update failed');
|
notify({ message: 'Admin update failed', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -170,7 +174,7 @@ const FlavorDetail: React.FC<FlavorDetailProps> = ({ adminMode }) => {
|
|||||||
});
|
});
|
||||||
fetchFlavor();
|
fetchFlavor();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Image upload failed');
|
notify({ message: 'Image upload failed', severity: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setUploadingImage(false);
|
setUploadingImage(false);
|
||||||
}
|
}
|
||||||
@@ -179,7 +183,7 @@ const FlavorDetail: React.FC<FlavorDetailProps> = ({ adminMode }) => {
|
|||||||
const handleRatingSubmit = async (e: React.FormEvent) => {
|
const handleRatingSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!newScore) {
|
if (!newScore) {
|
||||||
alert('Please select a score');
|
notify({ message: 'Please select a score', severity: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -189,7 +193,7 @@ const FlavorDetail: React.FC<FlavorDetailProps> = ({ adminMode }) => {
|
|||||||
fetchFlavor();
|
fetchFlavor();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = (err as { response?: { data?: { error?: string } } }).response?.data?.error;
|
const msg = (err as { response?: { data?: { error?: string } } }).response?.data?.error;
|
||||||
alert(msg || 'Failed to submit rating');
|
notify({ message: msg || 'Failed to submit rating', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -199,11 +203,10 @@ const FlavorDetail: React.FC<FlavorDetailProps> = ({ adminMode }) => {
|
|||||||
try {
|
try {
|
||||||
await api.post(`ratings/${ratingId}/reply/`, { text });
|
await api.post(`ratings/${ratingId}/reply/`, { text });
|
||||||
setReplyInputs({ ...replyInputs, [ratingId]: '' });
|
setReplyInputs({ ...replyInputs, [ratingId]: '' });
|
||||||
// Auto-expand replies when submitting a new one
|
|
||||||
setExpandedReplies((prev) => ({ ...prev, [ratingId]: true }));
|
setExpandedReplies((prev) => ({ ...prev, [ratingId]: true }));
|
||||||
fetchFlavor();
|
fetchFlavor();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to submit reply');
|
notify({ message: 'Failed to submit reply', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,27 +217,27 @@ const FlavorDetail: React.FC<FlavorDetailProps> = ({ adminMode }) => {
|
|||||||
setEditingReplyId(null);
|
setEditingReplyId(null);
|
||||||
fetchFlavor();
|
fetchFlavor();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to update reply');
|
notify({ message: 'Failed to update reply', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteReply = async (replyId: number) => {
|
const handleDeleteReply = async (replyId: number) => {
|
||||||
if (!confirm('Delete this reply?')) return;
|
if (!(await confirm({ message: 'Delete this reply?', danger: true }))) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`replies/${replyId}/`);
|
await api.delete(`replies/${replyId}/`);
|
||||||
fetchFlavor();
|
fetchFlavor();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to delete reply');
|
notify({ message: 'Failed to delete reply', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteRating = async (ratingId: number) => {
|
const handleDeleteRating = async (ratingId: number) => {
|
||||||
if (!confirm('Are you sure?')) return;
|
if (!(await confirm({ message: 'Delete this review?', danger: true }))) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`ratings/${ratingId}/`);
|
await api.delete(`ratings/${ratingId}/`);
|
||||||
fetchFlavor();
|
fetchFlavor();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to delete review');
|
notify({ message: 'Failed to delete review', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -250,7 +253,7 @@ const FlavorDetail: React.FC<FlavorDetailProps> = ({ adminMode }) => {
|
|||||||
setEditMode(null);
|
setEditMode(null);
|
||||||
fetchFlavor();
|
fetchFlavor();
|
||||||
} catch {
|
} catch {
|
||||||
alert('Failed to update review');
|
notify({ message: 'Failed to update review', severity: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user