From c42fd733f0e697c642fd06f6efa51d491aece3c7 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Fri, 24 Apr 2026 12:20:54 +0200 Subject: [PATCH] migrated alerts and community --- frontend/src/app/providers.tsx | 8 ++- frontend/src/contexts/ConfirmContext.tsx | 74 +++++++++++++++++++++ frontend/src/contexts/ToastContext.tsx | 63 ++++++++++++++++++ frontend/src/hooks/useConfirm.ts | 8 +++ frontend/src/hooks/useToast.ts | 8 +++ frontend/src/pages/AdminDashboard.tsx | 20 ++++-- frontend/src/pages/AdminRatingDetail.tsx | 12 ++-- frontend/src/pages/AdminReplyDetail.tsx | 12 ++-- frontend/src/pages/AdminUserDetail.tsx | 30 ++++++--- frontend/src/pages/CommunityFeed.tsx | 4 +- frontend/src/pages/Dashboard.tsx | 18 +++-- frontend/src/pages/FlavorList.tsx | 9 ++- frontend/src/pages/PublicProfile.tsx | 15 +++-- frontend/src/pages/Settings.tsx | 10 ++- frontend/src/pages/Support.tsx | 14 ++-- frontend/src/pages/flavors/FlavorDetail.tsx | 27 ++++---- 16 files changed, 274 insertions(+), 58 deletions(-) create mode 100644 frontend/src/contexts/ConfirmContext.tsx create mode 100644 frontend/src/contexts/ToastContext.tsx create mode 100644 frontend/src/hooks/useConfirm.ts create mode 100644 frontend/src/hooks/useToast.ts 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} + handle(false)} + maxWidth="xs" + fullWidth + PaperProps={{ sx: { borderRadius: 3 } }} + > + {opts?.title && {opts.title}} + + {opts?.message} + + + + + + + + ); +}; 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' }); } };