mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
Added a persistent loading toast with spinner for backup exports that include print archives:
Changes to ToastContext.tsx: - Added 'loading' toast type with a spinning Loader2 icon - Added showPersistentToast(id, message, type) function for toasts that don't auto-dismiss - Exposed dismissToast(id) function to allow programmatic dismissal - Added green-themed styling for loading toasts Changes to SettingsPage.tsx: - When exporting a backup that includes archives, shows a persistent "Preparing backup..." toast with spinner - The toast is automatically dismissed when the download starts or if an error occurs - For non-archive backups (which are fast), no loading toast is shown The user will now see clear feedback when creating a backup with print archives - a toast with a spinner appears immediately after clicking export and stays visible until the download dialog appears.
This commit is contained in:
@@ -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<ToastContextType | undefined>(undefined);
|
||||
@@ -28,6 +31,7 @@ const icons = {
|
||||
error: <XCircle className="w-5 h-5 text-red-400" />,
|
||||
warning: <AlertCircle className="w-5 h-5 text-yellow-400" />,
|
||||
info: <Info className="w-5 h-5 text-blue-400" />,
|
||||
loading: <Loader2 className="w-5 h-5 text-bambu-green animate-spin" />,
|
||||
};
|
||||
|
||||
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 (
|
||||
<ToastContext.Provider value={{ showToast }}>
|
||||
<ToastContext.Provider value={{ showToast, showPersistentToast, dismissToast }}>
|
||||
{children}
|
||||
|
||||
{/* Toast Container */}
|
||||
|
||||
@@ -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<AppSettings | null>(null);
|
||||
const [showPlugModal, setShowPlugModal] = useState(false);
|
||||
const [editingPlug, setEditingPlug] = useState<SmartPlug | null>(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');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user