diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx
index b9afdff..8fb003d 100644
--- a/frontend/src/app/providers.tsx
+++ b/frontend/src/app/providers.tsx
@@ -2,12 +2,18 @@ import type { ReactNode } from 'react';
import { ThemeProvider } from '../contexts/ThemeContext';
import { AuthProvider } from '../contexts/AuthContext';
import { NotificationProvider } from '../contexts/NotificationContext';
+import { ToastProvider } from '../contexts/ToastContext';
+import { ConfirmProvider } from '../contexts/ConfirmContext';
import '../i18n';
export const AppProviders = ({ children }: { children: ReactNode }) => (
- {children}
+
+
+ {children}
+
+
);
diff --git a/frontend/src/contexts/ConfirmContext.tsx b/frontend/src/contexts/ConfirmContext.tsx
new file mode 100644
index 0000000..0709679
--- /dev/null
+++ b/frontend/src/contexts/ConfirmContext.tsx
@@ -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;
+}
+
+export const ConfirmContext = createContext(null);
+
+export const ConfirmProvider = ({ children }: { children: ReactNode }) => {
+ const [open, setOpen] = useState(false);
+ const [opts, setOpts] = useState(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((resolve) => {
+ resolverRef.current = resolve;
+ });
+ }, []);
+
+ const handle = (result: boolean) => {
+ setOpen(false);
+ resolverRef.current?.(result);
+ resolverRef.current = null;
+ };
+
+ return (
+
+ {children}
+
+
+ );
+};
diff --git a/frontend/src/contexts/ToastContext.tsx b/frontend/src/contexts/ToastContext.tsx
new file mode 100644
index 0000000..227e668
--- /dev/null
+++ b/frontend/src/contexts/ToastContext.tsx
@@ -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(null);
+
+interface ToastState extends ToastOptions {
+ key: number;
+}
+
+export const ToastProvider = ({ children }: { children: ReactNode }) => {
+ const [toast, setToast] = useState(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 (
+
+ {children}
+
+ {toast ? (
+ setToast(null)}
+ severity={toast.severity}
+ variant="filled"
+ sx={{ width: '100%' }}
+ >
+ {toast.message}
+
+ ) : undefined}
+
+
+ );
+};
diff --git a/frontend/src/hooks/useConfirm.ts b/frontend/src/hooks/useConfirm.ts
new file mode 100644
index 0000000..35fe949
--- /dev/null
+++ b/frontend/src/hooks/useConfirm.ts
@@ -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;
+};
diff --git a/frontend/src/hooks/useToast.ts b/frontend/src/hooks/useToast.ts
new file mode 100644
index 0000000..265fee9
--- /dev/null
+++ b/frontend/src/hooks/useToast.ts
@@ -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;
+};
diff --git a/frontend/src/pages/AdminDashboard.tsx b/frontend/src/pages/AdminDashboard.tsx
index 8aee25a..42508f6 100644
--- a/frontend/src/pages/AdminDashboard.tsx
+++ b/frontend/src/pages/AdminDashboard.tsx
@@ -31,6 +31,7 @@ import {
FormControl,
} from '@mui/material';
import { PageShell, GlassCard, GlassPaper, EmptyState } from '../components/ui';
+import { useToast } from '../hooks/useToast';
import EditIcon from '@mui/icons-material/Edit';
import EmailIcon from '@mui/icons-material/Email';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
@@ -134,6 +135,7 @@ const AdminDashboard: React.FC = () => {
const [currentTab, setCurrentTab] = useState(0);
const [selectedBanner, setSelectedBanner] = useState(null);
const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false);
+ const { notify } = useToast();
const fetchData = useCallback(async (silent = false) => {
if (!silent) setLoading(true);
@@ -177,7 +179,7 @@ const AdminDashboard: React.FC = () => {
const res = await api.patch('admin-custom/config/', { [key]: value });
setConfig(res.data);
} 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/`);
fetchData(true);
} 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);
fetchData(true);
} catch {
- alert('Failed to update schedule');
+ notify({ message: 'Failed to update schedule', severity: 'error' });
}
};
@@ -216,7 +218,10 @@ const AdminDashboard: React.FC = () => {
fetchData();
} catch (err) {
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 () => {
try {
await api.post('admin-custom/send_test_email/');
- alert(t('admin.testEmailSuccess'));
+ notify({ message: t('admin.testEmailSuccess'), severity: 'success' });
} catch (err) {
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',
+ });
}
};
diff --git a/frontend/src/pages/AdminRatingDetail.tsx b/frontend/src/pages/AdminRatingDetail.tsx
index fe67266..bf80581 100644
--- a/frontend/src/pages/AdminRatingDetail.tsx
+++ b/frontend/src/pages/AdminRatingDetail.tsx
@@ -8,6 +8,8 @@ import api from '../lib/api';
import { useTitle } from '../hooks/useTitle';
import MentionTextField from '../components/MentionTextField';
import { PageShell, SectionHeader, FormCard } from '../components/ui';
+import { useToast } from '../hooks/useToast';
+import { useConfirm } from '../hooks/useConfirm';
interface AdminRating {
id: number;
@@ -25,6 +27,8 @@ const AdminRatingDetail: React.FC = () => {
const [comment, setComment] = useState('');
const [rating, setRating] = useState(null);
const [loading, setLoading] = useState(true);
+ const { notify } = useToast();
+ const { confirm } = useConfirm();
const fetchRating = useCallback(async () => {
try {
@@ -51,19 +55,19 @@ const AdminRatingDetail: React.FC = () => {
try {
await api.patch(`ratings/${id}/`, { score, comment });
fetchRating();
- alert('Rating updated!');
+ notify({ message: 'Rating updated!', severity: 'success' });
} catch {
- alert('Update failed');
+ notify({ message: 'Update failed', severity: 'error' });
}
};
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 {
await api.delete(`ratings/${id}/`);
navigate('/admin-panel');
} catch {
- alert('Delete failed');
+ notify({ message: 'Delete failed', severity: 'error' });
}
};
diff --git a/frontend/src/pages/AdminReplyDetail.tsx b/frontend/src/pages/AdminReplyDetail.tsx
index 07daf89..3554356 100644
--- a/frontend/src/pages/AdminReplyDetail.tsx
+++ b/frontend/src/pages/AdminReplyDetail.tsx
@@ -8,6 +8,8 @@ import api from '../lib/api';
import { useTitle } from '../hooks/useTitle';
import MentionTextField from '../components/MentionTextField';
import { PageShell, SectionHeader, FormCard } from '../components/ui';
+import { useToast } from '../hooks/useToast';
+import { useConfirm } from '../hooks/useConfirm';
interface AdminReply {
id: number;
@@ -22,6 +24,8 @@ const AdminReplyDetail: React.FC = () => {
const [text, setText] = useState('');
const [reply, setReply] = useState(null);
const [loading, setLoading] = useState(true);
+ const { notify } = useToast();
+ const { confirm } = useConfirm();
const fetchReply = useCallback(async () => {
try {
@@ -47,19 +51,19 @@ const AdminReplyDetail: React.FC = () => {
try {
await api.patch(`replies/${id}/`, { text });
fetchReply();
- alert('Reply updated!');
+ notify({ message: 'Reply updated!', severity: 'success' });
} catch {
- alert('Update failed');
+ notify({ message: 'Update failed', severity: 'error' });
}
};
const handleDelete = async () => {
- if (!window.confirm('Delete this reply?')) return;
+ if (!(await confirm({ message: 'Delete this reply?', danger: true }))) return;
try {
await api.delete(`replies/${id}/`);
navigate('/admin-panel');
} catch {
- alert('Delete failed');
+ notify({ message: 'Delete failed', severity: 'error' });
}
};
diff --git a/frontend/src/pages/AdminUserDetail.tsx b/frontend/src/pages/AdminUserDetail.tsx
index cdbccd2..2cca614 100644
--- a/frontend/src/pages/AdminUserDetail.tsx
+++ b/frontend/src/pages/AdminUserDetail.tsx
@@ -37,6 +37,8 @@ import {
FormCard,
EmptyState,
} from '../components/ui';
+import { useToast } from '../hooks/useToast';
+import { useConfirm } from '../hooks/useConfirm';
interface AdminReply {
id: number;
@@ -81,6 +83,9 @@ const AdminUserDetail: React.FC = () => {
const [editingReplyId, setEditingReplyId] = useState(null);
const [editReplyText, setEditReplyText] = useState('');
+ const { notify } = useToast();
+ const { confirm } = useConfirm();
+
const fetchUser = useCallback(async () => {
try {
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 });
fetchUser();
} catch {
- alert('Action failed');
+ notify({ message: 'Action failed', severity: 'error' });
}
};
@@ -115,17 +120,17 @@ const AdminUserDetail: React.FC = () => {
setEditingRatingId(null);
fetchUser();
} catch {
- alert('Update failed');
+ notify({ message: 'Update failed', severity: 'error' });
}
};
const handleDeleteRating = async (ratingId: number) => {
- if (!confirm('Delete this rating?')) return;
+ if (!(await confirm({ message: 'Delete this rating?', danger: true }))) return;
try {
await api.delete(`ratings/${ratingId}/`);
fetchUser();
} catch {
- alert('Delete failed');
+ notify({ message: 'Delete failed', severity: 'error' });
}
};
@@ -135,28 +140,33 @@ const AdminUserDetail: React.FC = () => {
setEditingReplyId(null);
fetchUser();
} catch {
- alert('Update failed');
+ notify({ message: 'Update failed', severity: 'error' });
}
};
const handleDeleteReply = async (replyId: number) => {
- if (!confirm('Delete this reply?')) return;
+ if (!(await confirm({ message: 'Delete this reply?', danger: true }))) return;
try {
await api.delete(`replies/${replyId}/`);
fetchUser();
} catch {
- alert('Delete failed');
+ notify({ message: 'Delete failed', severity: 'error' });
}
};
const handleDelete = async () => {
- if (!window.confirm('PERMANENTLY DELETE THIS USER AND ALL DATA? This cannot be undone.'))
- return;
+ const ok = await confirm({
+ title: 'Delete user',
+ message: 'PERMANENTLY DELETE THIS USER AND ALL DATA? This cannot be undone.',
+ confirmLabel: 'Delete forever',
+ danger: true,
+ });
+ if (!ok) return;
try {
await api.delete(`admin-custom/${id}/user_detail/`);
navigate('/admin-panel');
} catch {
- alert('Delete failed');
+ notify({ message: 'Delete failed', severity: 'error' });
}
};
diff --git a/frontend/src/pages/CommunityFeed.tsx b/frontend/src/pages/CommunityFeed.tsx
index 29e9b4b..1de53f4 100644
--- a/frontend/src/pages/CommunityFeed.tsx
+++ b/frontend/src/pages/CommunityFeed.tsx
@@ -40,6 +40,7 @@ import {
GlassCard,
EmptyState,
} from '../components/ui';
+import { useToast } from '../hooks/useToast';
interface Reply {
id: number;
@@ -129,6 +130,7 @@ const CommunityFeed: React.FC = ({ adminMode }) => {
const { t } = useTranslation();
useTitle(t('community.title'));
const navigate = useNavigate();
+ const { notify } = useToast();
const [ratings, setRatings] = useState([]);
const [following, setFollowing] = useState([]);
const [notifications, setNotifications] = useState([]);
@@ -213,7 +215,7 @@ const CommunityFeed: React.FC = ({ adminMode }) => {
);
setReplyInputs((prev) => ({ ...prev, [ratingId]: '' }));
} catch {
- alert('Failed to send reply');
+ notify({ message: 'Failed to send reply', severity: 'error' });
}
};
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index 3dcd5de..f66b99f 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -40,6 +40,8 @@ import MentionTextField from '../components/MentionTextField';
import RichText from '../components/RichText';
import { formatDate } from '../utils/date';
import { PageShell, GlassCard, GlassSurface, EmptyState } from '../components/ui';
+import { useToast } from '../hooks/useToast';
+import { useConfirm } from '../hooks/useConfirm';
interface Reply {
id: number;
@@ -108,12 +110,14 @@ const Dashboard: React.FC = () => {
const [replyInputs, setReplyInputs] = useState>({});
const [expandedReplies, setExpandedReplies] = useState>({});
+ const { notify } = useToast();
+ const { confirm } = useConfirm();
const shareUrl = data ? `${window.location.origin}/profile/${data.user.username}` : '';
const handleCopyLink = () => {
navigator.clipboard.writeText(shareUrl);
- alert(t('dashboard.copySuccess'));
+ notify({ message: t('dashboard.copySuccess'), severity: 'success' });
};
const handleGoBack = () => {
@@ -175,17 +179,17 @@ const Dashboard: React.FC = () => {
setEditingRatingId(null);
fetchData();
} catch {
- alert('Failed to update review');
+ notify({ message: 'Failed to update review', severity: 'error' });
}
};
const handleDeleteRating = async (ratingId: number) => {
- if (!confirm('Delete this review?')) return;
+ if (!(await confirm({ message: 'Delete this review?', danger: true }))) return;
try {
await api.delete(`ratings/${ratingId}/`);
fetchData();
} 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 });
fetchData();
} catch {
- alert('Failed to send reply');
+ notify({ message: 'Failed to send reply', severity: 'error' });
}
};
const handleDeleteReply = async (replyId: number) => {
- if (!confirm('Delete this reply?')) return;
+ if (!(await confirm({ message: 'Delete this reply?', danger: true }))) return;
try {
await api.delete(`replies/${replyId}/`);
fetchData();
} catch {
- alert('Failed to delete reply');
+ notify({ message: 'Failed to delete reply', severity: 'error' });
}
};
diff --git a/frontend/src/pages/FlavorList.tsx b/frontend/src/pages/FlavorList.tsx
index d74bcdb..c29ebd3 100644
--- a/frontend/src/pages/FlavorList.tsx
+++ b/frontend/src/pages/FlavorList.tsx
@@ -16,6 +16,7 @@ import api from '../lib/api';
import RatingBadge from '../components/RatingBadge';
import StatusBadge from '../components/StatusBadge';
import { PageShell, HeroBackdrop, GlassCard, SectionHeader } from '../components/ui';
+import { useToast } from '../hooks/useToast';
interface Rating {
id: number;
@@ -55,6 +56,7 @@ const FlavorList: React.FC = () => {
const [flavors, setFlavors] = useState([]);
const [loading, setLoading] = useState(true);
const [ratingInput, setRatingInput] = useState>({});
+ const { notify } = useToast();
useEffect(() => {
const fetchData = async () => {
@@ -82,9 +84,12 @@ const FlavorList: React.FC = () => {
});
const res = await api.get('flavors/');
setFlavors(res.data);
- alert('Rating submitted!');
+ notify({ message: 'Rating submitted!', severity: 'success' });
} 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',
+ });
}
};
diff --git a/frontend/src/pages/PublicProfile.tsx b/frontend/src/pages/PublicProfile.tsx
index 9b32b9e..2585e5d 100644
--- a/frontend/src/pages/PublicProfile.tsx
+++ b/frontend/src/pages/PublicProfile.tsx
@@ -34,6 +34,8 @@ import RichText from '../components/RichText';
import DynamicBanner from '../components/DynamicBanner';
import { formatDate } from '../utils/date';
import { PageShell, GlassCard, GlassSurface, FormCard, EmptyState } from '../components/ui';
+import { useToast } from '../hooks/useToast';
+import { useConfirm } from '../hooks/useConfirm';
interface Rating {
id: number;
@@ -94,6 +96,8 @@ const PublicProfile: React.FC = ({ adminMode }) => {
} | null>(null);
const [palette, setPalette] = useState([]);
const [newComment, setNewComment] = useState('');
+ const { notify } = useToast();
+ const { confirm } = useConfirm();
const fetchProfile = useCallback(async () => {
try {
@@ -158,7 +162,7 @@ const PublicProfile: React.FC = ({ adminMode }) => {
setData({ ...data, is_following: true, followers_count: data.followers_count + 1 });
}
} 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 = ({ adminMode }) => {
else await api.post(`users/${user.id}/follow/`);
fetchProfile();
} catch {
- alert('Failed');
+ notify({ message: 'Failed', severity: 'error' });
}
};
@@ -180,17 +184,18 @@ const PublicProfile: React.FC = ({ adminMode }) => {
setNewComment('');
fetchProfile();
} catch {
- alert('Failed to add comment');
+ notify({ message: 'Failed to add comment', severity: 'error' });
}
};
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 {
await api.delete(`users/${data.id}/delete_comment/${commentId}/`);
fetchProfile();
} catch {
- alert('Failed to delete comment');
+ notify({ message: 'Failed to delete comment', severity: 'error' });
}
};
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx
index 22530a6..035126b 100644
--- a/frontend/src/pages/Settings.tsx
+++ b/frontend/src/pages/Settings.tsx
@@ -21,6 +21,7 @@ import { useTranslation } from 'react-i18next';
import { useTitle } from '../hooks/useTitle';
import type { CatppuccinTheme } from '../theme';
import { PageShell, GlassCard, FormCard } from '../components/ui';
+import { useConfirm } from '../hooks/useConfirm';
interface SettingsProps {
themeName: CatppuccinTheme;
@@ -39,6 +40,7 @@ const Settings: React.FC = ({ themeName, onThemeChange }) => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
useTitle(t('nav.settings'));
+ const { confirm } = useConfirm();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [currentEmail, setCurrentEmail] = useState('');
@@ -177,7 +179,13 @@ const Settings: React.FC = ({ themeName, onThemeChange }) => {
const handleConfirmDeletion = async (e: React.FormEvent) => {
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 {
await api.post('users/confirm_account_deletion/', { code: deletionCode });
localStorage.removeItem('access');
diff --git a/frontend/src/pages/Support.tsx b/frontend/src/pages/Support.tsx
index b2046b9..4b39159 100644
--- a/frontend/src/pages/Support.tsx
+++ b/frontend/src/pages/Support.tsx
@@ -25,6 +25,8 @@ import api from '../lib/api';
import { useTitle } from '../hooks/useTitle';
import { formatDate } from '../utils/date';
import { PageShell, SectionHeader, GlassCard, FormCard, EmptyState } from '../components/ui';
+import { useToast } from '../hooks/useToast';
+import { useConfirm } from '../hooks/useConfirm';
interface Message {
id: number;
@@ -78,6 +80,8 @@ const Support: React.FC = () => {
const [replyText, setReplyText] = useState>({});
const [expandedId, setExpandedId] = useState(null);
+ const { notify } = useToast();
+ const { confirm } = useConfirm();
const [currentUser, setCurrentUser] = useState(null);
const [unreadTicketIds, setUnreadTicketIds] = useState>(new Set());
@@ -131,7 +135,7 @@ const Support: React.FC = () => {
setShowCreate(false);
fetchTickets();
} 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]: '' });
fetchTickets();
} 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 });
fetchTickets();
} catch {
- alert('Failed to update status');
+ notify({ message: 'Failed to update status', severity: 'error' });
}
};
const handleDeleteTicket = async (ticketId: number) => {
- if (!confirm('Delete this ticket?')) return;
+ if (!(await confirm({ message: 'Delete this ticket?', danger: true }))) return;
try {
await api.delete(`tickets/${ticketId}/`);
fetchTickets();
} catch {
- alert('Failed to delete ticket');
+ notify({ message: 'Failed to delete ticket', severity: 'error' });
}
};
diff --git a/frontend/src/pages/flavors/FlavorDetail.tsx b/frontend/src/pages/flavors/FlavorDetail.tsx
index 1ec631e..b0a11b4 100644
--- a/frontend/src/pages/flavors/FlavorDetail.tsx
+++ b/frontend/src/pages/flavors/FlavorDetail.tsx
@@ -41,6 +41,8 @@ import {
SectionHeader,
EmptyState,
} from '../../components/ui';
+import { useToast } from '../../hooks/useToast';
+import { useConfirm } from '../../hooks/useConfirm';
interface Reply {
id: number;
@@ -83,6 +85,8 @@ const FlavorDetail: React.FC = ({ adminMode }) => {
const theme = useTheme();
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
+ const { notify } = useToast();
+ const { confirm } = useConfirm();
const [flavor, setFlavor] = useState(null);
const [loading, setLoading] = useState(true);
const [replyInputs, setReplyInputs] = useState<{ [key: number]: string }>({});
@@ -152,7 +156,7 @@ const FlavorDetail: React.FC = ({ adminMode }) => {
setIsAdminEditing(false);
fetchFlavor();
} catch {
- alert('Admin update failed');
+ notify({ message: 'Admin update failed', severity: 'error' });
}
};
@@ -170,7 +174,7 @@ const FlavorDetail: React.FC = ({ adminMode }) => {
});
fetchFlavor();
} catch {
- alert('Image upload failed');
+ notify({ message: 'Image upload failed', severity: 'error' });
} finally {
setUploadingImage(false);
}
@@ -179,7 +183,7 @@ const FlavorDetail: React.FC = ({ adminMode }) => {
const handleRatingSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!newScore) {
- alert('Please select a score');
+ notify({ message: 'Please select a score', severity: 'warning' });
return;
}
try {
@@ -189,7 +193,7 @@ const FlavorDetail: React.FC = ({ adminMode }) => {
fetchFlavor();
} catch (err) {
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 = ({ adminMode }) => {
try {
await api.post(`ratings/${ratingId}/reply/`, { text });
setReplyInputs({ ...replyInputs, [ratingId]: '' });
- // Auto-expand replies when submitting a new one
setExpandedReplies((prev) => ({ ...prev, [ratingId]: true }));
fetchFlavor();
} catch {
- alert('Failed to submit reply');
+ notify({ message: 'Failed to submit reply', severity: 'error' });
}
};
@@ -214,27 +217,27 @@ const FlavorDetail: React.FC = ({ adminMode }) => {
setEditingReplyId(null);
fetchFlavor();
} catch {
- alert('Failed to update reply');
+ notify({ message: 'Failed to update reply', severity: 'error' });
}
};
const handleDeleteReply = async (replyId: number) => {
- if (!confirm('Delete this reply?')) return;
+ if (!(await confirm({ message: 'Delete this reply?', danger: true }))) return;
try {
await api.delete(`replies/${replyId}/`);
fetchFlavor();
} catch {
- alert('Failed to delete reply');
+ notify({ message: 'Failed to delete reply', severity: 'error' });
}
};
const handleDeleteRating = async (ratingId: number) => {
- if (!confirm('Are you sure?')) return;
+ if (!(await confirm({ message: 'Delete this review?', danger: true }))) return;
try {
await api.delete(`ratings/${ratingId}/`);
fetchFlavor();
} catch {
- alert('Failed to delete review');
+ notify({ message: 'Failed to delete review', severity: 'error' });
}
};
@@ -250,7 +253,7 @@ const FlavorDetail: React.FC = ({ adminMode }) => {
setEditMode(null);
fetchFlavor();
} catch {
- alert('Failed to update review');
+ notify({ message: 'Failed to update review', severity: 'error' });
}
};