import React, { useState, useRef, useEffect } from 'react'; import { useToast } from '../Shared/ToastContext'; import { useTranslation } from 'react-i18next'; import ConfirmDialog from '../Shared/ConfirmDialog'; import { createBackup, listSavedBackups, downloadSavedBackup, restoreSavedBackup, deleteSavedBackup, importBackup, validateBackup, ValidationResult, SavedBackup, } from '../../utils/backupService'; import { ArrowDownTrayIcon, ArrowUpTrayIcon, DocumentCheckIcon, TrashIcon, ArrowPathIcon, } from '@heroicons/react/24/outline'; interface BackupRestoreProps { onImportSuccess?: () => void; } type TabType = 'export' | 'import'; interface ConfirmDialogState { isOpen: boolean; title: string; message: string; onConfirm: () => void; confirmButtonText?: string; } const BackupRestore: React.FC = ({ onImportSuccess }) => { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('export'); const [isExporting, setIsExporting] = useState(false); const [isImporting, setIsImporting] = useState(false); const [isValidating, setIsValidating] = useState(false); const [isLoadingBackups, setIsLoadingBackups] = useState(false); const [savedBackups, setSavedBackups] = useState([]); const [selectedFile, setSelectedFile] = useState(null); const [validationResult, setValidationResult] = useState(null); const [appVersion, setAppVersion] = useState(''); const fileInputRef = useRef(null); const [confirmDialog, setConfirmDialog] = useState({ isOpen: false, title: '', message: '', onConfirm: () => {}, confirmButtonText: undefined, }); const { showSuccessToast, showErrorToast } = useToast(); // Load saved backups and app version on mount useEffect(() => { loadBackups(); fetchAppVersion(); }, []); const fetchAppVersion = async () => { try { const response = await fetch('/api/version', { credentials: 'include', }); const data = await response.json(); setAppVersion(data.version); } catch (error) { console.error('Error fetching app version:', error); } }; const loadBackups = async () => { setIsLoadingBackups(true); try { const backups = await listSavedBackups(); setSavedBackups(backups); } catch (error) { console.error('Error loading backups:', error); } finally { setIsLoadingBackups(false); } }; const handleCreateBackup = async () => { setIsExporting(true); try { await createBackup(); showSuccessToast( t('backup.exportSuccess', 'Backup created successfully!') ); // Reload the backup list await loadBackups(); } catch (error) { console.error('Export error:', error); showErrorToast(t('backup.exportError', 'Failed to create backup')); } finally { setIsExporting(false); } }; const handleDownloadBackup = async (backupUid: string) => { try { await downloadSavedBackup(backupUid); showSuccessToast( t('backup.downloadSuccess', 'Backup downloaded successfully!') ); } catch (error) { console.error('Download error:', error); showErrorToast( t('backup.downloadError', 'Failed to download backup') ); } }; const handleRestoreBackup = (backupUid: string) => { setConfirmDialog({ isOpen: true, title: t('backup.confirmRestore', 'Restore Backup'), message: t( 'backup.confirmRestoreMessage', 'Are you sure you want to restore this backup? This will merge the backed up data with your current data.' ), confirmButtonText: t('backup.restoreButton', 'Restore'), onConfirm: async () => { setConfirmDialog({ ...confirmDialog, isOpen: false }); try { const result = await restoreSavedBackup(backupUid, true); showSuccessToast( t('backup.restoreSuccess', { tasks: result.stats.tasks.created, projects: result.stats.projects.created, notes: result.stats.notes.created, }) ); if (onImportSuccess) { onImportSuccess(); } } catch (error) { console.error('Restore error:', error); showErrorToast( t('backup.restoreError', 'Failed to restore backup') ); } }, }); }; const handleDeleteBackup = (backupUid: string) => { setConfirmDialog({ isOpen: true, title: t('backup.confirmDelete', 'Delete Backup'), message: t( 'backup.confirmDeleteMessage', 'Are you sure you want to delete this backup? This action cannot be undone.' ), onConfirm: async () => { setConfirmDialog({ ...confirmDialog, isOpen: false }); try { await deleteSavedBackup(backupUid); showSuccessToast( t( 'backup.deleteSuccess', 'Backup deleted successfully!' ) ); // Reload the backup list await loadBackups(); } catch (error) { console.error('Delete error:', error); showErrorToast( t('backup.deleteError', 'Failed to delete backup') ); } }, }); }; const handleFileSelect = async ( event: React.ChangeEvent ) => { const file = event.target.files?.[0]; if (!file) return; setSelectedFile(file); setValidationResult(null); // Auto-validate on file selection setIsValidating(true); try { const result = await validateBackup(file); setValidationResult(result); if (!result.valid) { showErrorToast( t( 'backup.validationError', 'The selected file is not a valid backup' ) ); } } catch (error) { console.error('Validation error:', error); showErrorToast( t('backup.validationError', 'Failed to validate backup file') ); } finally { setIsValidating(false); } }; const handleImport = async () => { if (!selectedFile || !validationResult?.valid) return; setIsImporting(true); try { const result = await importBackup(selectedFile, true); showSuccessToast( t('backup.importSuccess', { tasks: result.stats.tasks.created, projects: result.stats.projects.created, notes: result.stats.notes.created, }) ); // Reset file selection setSelectedFile(null); setValidationResult(null); if (fileInputRef.current) { fileInputRef.current.value = ''; } if (onImportSuccess) { onImportSuccess(); } } catch (error) { console.error('Import error:', error); showErrorToast(t('backup.importError', 'Failed to import backup')); } finally { setIsImporting(false); } }; const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleString(); }; const formatFileSize = (bytes: number) => { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'; return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; }; return ( <> {confirmDialog.isOpen && ( setConfirmDialog({ ...confirmDialog, isOpen: false }) } confirmButtonText={confirmDialog.confirmButtonText} /> )}
{/* Header */}

{t('backup.title', 'Backup & Restore')}

{t( 'backup.description', 'Create backups or restore from previous backups. Your last 5 backups are automatically saved.' )}

{/* Tabs */}
{/* Content */}
{activeTab === 'export' ? (

{t( 'backup.createNewBackup', 'Create New Backup' )}

{t( 'backup.createDescription', 'Create a new backup of all your data. Backups are saved on the server and you can restore them later.' )}

{/* Saved Backups Table */}

{t( 'backup.savedBackups', 'Saved Backups' )}

{isLoadingBackups ? (
{t('common.loading', 'Loading...')}
) : savedBackups.length === 0 ? (
{t( 'backup.noBackups', 'No backups found. Create your first backup above.' )}
) : (
{savedBackups.map( (backup) => ( ) )}
{t( 'backup.createdAt', 'Created' )} {t( 'backup.version', 'Version' )} {t( 'backup.size', 'Size' )} {t( 'backup.contents', 'Contents' )} {t( 'backup.actions', 'Actions' )}
{formatDate( backup.created_at )} { backup.version } {formatFileSize( backup.file_size )}
{ backup .item_counts .tasks }{' '} tasks { backup .item_counts .projects }{' '} projects { backup .item_counts .notes }{' '} notes
)}
) : (

{t( 'backup.importTitle', 'Import from File' )}

{t( 'backup.importDescription', 'Upload a backup file to restore your data. Your existing data will be preserved, and new items from the backup will be added.' )}

{t('backup.importNote', 'Important:')}

{t( 'backup.importNoteDescription', 'Import will merge data with your existing items. Duplicate items (same UID) will be skipped.' )}

{selectedFile && (

{selectedFile.name}

{( selectedFile.size / 1024 ).toFixed(2)}{' '} KB

{isValidating && ( )} {validationResult?.valid && ( )}
{validationResult?.valid && validationResult.summary && (

{t( 'backup.backupContents', 'Backup contents:' )}

{ validationResult .summary .tasks }{' '} tasks
{ validationResult .summary .projects }{' '} projects
{ validationResult .summary .notes }{' '} notes
{ validationResult .summary .tags }{' '} tags
{ validationResult .summary .areas }{' '} areas
{ validationResult .summary .views }{' '} views
)} {validationResult && !validationResult.valid && (
{validationResult.versionIncompatible ? ( <>

{t( 'backup.versionIncompatible', 'Version Incompatible' )}

{ validationResult.message }

{t( 'backup.backupVersion', 'Backup version' )} :{' '} { validationResult.backupVersion }

{t( 'backup.currentVersion', 'Current version' )} : {appVersion}

) : ( <>

{t( 'backup.validationErrors', 'Validation errors:' )}

    {validationResult.errors?.map( ( error, index ) => (
  • •{' '} { error }
  • ) )}
)}
)}
)} {selectedFile && validationResult?.valid && ( )}
)}
); }; const CheckIcon: React.FC<{ className?: string }> = ({ className }) => ( ); export default BackupRestore;