diff --git a/frontend/src/contexts/ToastContext.tsx b/frontend/src/contexts/ToastContext.tsx index 09409a0bd..2078a6017 100644 --- a/frontend/src/contexts/ToastContext.tsx +++ b/frontend/src/contexts/ToastContext.tsx @@ -1,16 +1,19 @@ import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'; -import { CheckCircle, XCircle, AlertCircle, Info, X } from 'lucide-react'; +import { CheckCircle, XCircle, AlertCircle, Info, X, Loader2 } from 'lucide-react'; -type ToastType = 'success' | 'error' | 'warning' | 'info'; +type ToastType = 'success' | 'error' | 'warning' | 'info' | 'loading'; interface Toast { id: string; message: string; type: ToastType; + persistent?: boolean; } interface ToastContextType { showToast: (message: string, type?: ToastType) => void; + showPersistentToast: (id: string, message: string, type?: ToastType) => void; + dismissToast: (id: string) => void; } const ToastContext = createContext(undefined); @@ -28,6 +31,7 @@ const icons = { error: , warning: , info: , + loading: , }; const bgColors = { @@ -35,6 +39,7 @@ const bgColors = { error: 'bg-red-500/10 border-red-500/30', warning: 'bg-yellow-500/10 border-yellow-500/30', info: 'bg-blue-500/10 border-blue-500/30', + loading: 'bg-bambu-green/10 border-bambu-green/30', }; export function ToastProvider({ children }: { children: ReactNode }) { @@ -50,12 +55,23 @@ export function ToastProvider({ children }: { children: ReactNode }) { }, 3000); }, []); + const showPersistentToast = useCallback((id: string, message: string, type: ToastType = 'info') => { + setToasts((prev) => { + // Update existing toast if same id, otherwise add new one + const exists = prev.find((t) => t.id === id); + if (exists) { + return prev.map((t) => (t.id === id ? { ...t, message, type, persistent: true } : t)); + } + return [...prev, { id, message, type, persistent: true }]; + }); + }, []); + const dismissToast = useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); return ( - + {children} {/* Toast Container */} diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index e418cb44b..d6346c082 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -24,7 +24,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; export function SettingsPage() { const queryClient = useQueryClient(); const { t, i18n } = useTranslation(); - const { showToast } = useToast(); + const { showToast, showPersistentToast, dismissToast } = useToast(); const [localSettings, setLocalSettings] = useState(null); const [showPlugModal, setShowPlugModal] = useState(false); const [editingPlug, setEditingPlug] = useState(null); @@ -1836,17 +1836,35 @@ export function SettingsPage() { onClose={() => setShowBackupModal(false)} onExport={async (categories) => { setShowBackupModal(false); + const toastId = 'backup-progress'; + const includesArchives = categories.archives; + + // Show persistent loading toast for archive backups (can be large) + if (includesArchives) { + showPersistentToast(toastId, t('backup.preparing', { defaultValue: 'Preparing backup...' }), 'loading'); + } + try { const { blob, filename } = await api.exportBackup(categories); + + // Dismiss loading toast before download starts + if (includesArchives) { + dismissToast(toastId); + } + const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); - showToast('Backup downloaded', 'success'); + showToast(t('backup.downloaded', { defaultValue: 'Backup downloaded' }), 'success'); } catch (err) { - showToast('Failed to create backup', 'error'); + // Dismiss loading toast on error + if (includesArchives) { + dismissToast(toastId); + } + showToast(t('backup.failed', { defaultValue: 'Failed to create backup' }), 'error'); } }} />