"use client"; import { IconCalendarRepeat, IconCloudDownload, IconCloudUpload, IconDatabaseExport, IconPlus, IconTrash, } from "@tabler/icons-react"; import { format, formatDistanceToNow } from "date-fns"; import { AnimatePresence, motion } from "motion/react"; import { useRef, useState } from "react"; import { toast } from "sonner"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { CardContent, CardDescription, CardTitle } from "@/components/ui/card"; import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import type { BackupInfo } from "@/lib/services/backup"; import { createBackupAction, deleteBackupAction, setMaxBackupsAction, setScheduledBackupAction, } from "./backup-actions"; function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function formatBackupDate(dateStr: string): string { return format(new Date(dateStr), "MMM d, h:mm a"); } export function BackupSection({ initialBackups, initialScheduledEnabled, initialMaxRetention, }: { initialBackups: BackupInfo[]; initialScheduledEnabled: boolean; initialMaxRetention: number; }) { const [backups, setBackups] = useState(initialBackups); const [creating, setCreating] = useState(false); const [deleting, setDeleting] = useState(null); const [restoring, setRestoring] = useState(false); const [scheduledEnabled, setScheduledEnabled] = useState( initialScheduledEnabled, ); const [maxRetention, setMaxRetention] = useState(initialMaxRetention); const [togglingSchedule, setTogglingSchedule] = useState(false); const [restoreDialogOpen, setRestoreDialogOpen] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const fileInputRef = useRef(null); async function handleCreateBackup() { setCreating(true); try { const backup = await createBackupAction(); setBackups((prev) => [backup, ...prev]); toast.success("Backup created"); } catch { toast.error("Failed to create backup"); } finally { setCreating(false); } } async function handleDelete(filename: string) { const previous = backups; setDeleting(filename); setBackups((prev) => prev.filter((b) => b.filename !== filename)); try { await deleteBackupAction(filename); toast.success("Backup deleted"); } catch { setBackups(previous); toast.error("Failed to delete backup"); } finally { setDeleting(null); } } async function handleRestore(file: File) { setRestoring(true); try { const formData = new FormData(); formData.append("file", file); const res = await fetch("/api/backup/restore", { method: "POST", body: formData, }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Restore failed"); } toast.success("Database restored. Reloading..."); setTimeout(() => window.location.reload(), 1500); } catch (err) { const message = err instanceof Error ? err.message : "Restore failed"; toast.error(message); } finally { setRestoring(false); if (fileInputRef.current) fileInputRef.current.value = ""; } } async function handleToggleScheduled(checked: boolean) { const previous = scheduledEnabled; setScheduledEnabled(checked); setTogglingSchedule(true); try { await setScheduledBackupAction(checked); toast.success( checked ? "Scheduled backups enabled" : "Scheduled backups disabled", ); } catch { setScheduledEnabled(previous); toast.error("Failed to update scheduled backup setting"); } finally { setTogglingSchedule(false); } } async function handleMaxRetentionChange(value: number) { const previous = maxRetention; setMaxRetention(value); try { await setMaxBackupsAction(value); } catch { setMaxRetention(previous); toast.error("Failed to update retention setting"); } } return ( <> {/* Create backup header */}
Database snapshots {backups.length > 0 ? `${backups.length} backup${backups.length !== 1 ? "s" : ""} stored` : "No backups yet"}
{/* Backup list */} {backups.length > 0 && (
{backups.map((backup) => (
{formatBackupDate(backup.createdAt)} {formatBytes(backup.sizeBytes)} {formatDistanceToNow(new Date(backup.createdAt), { addSuffix: true, })}
{backup.filename.startsWith("pre-restore") && ( Pre-restore safety backup )}
} /> } /> Download } /> } > {deleting === backup.filename ? ( ) : ( )} Delete Delete backup? This will permanently delete the backup from{" "} {formatBackupDate(backup.createdAt)} . This cannot be undone. Cancel handleDelete(backup.filename)} > Delete
))}
)}
{/* Restore */}
Restore Upload a .db file to replace the current database. A safety backup is created first.
{ const file = e.target.files?.[0]; if (file) { setSelectedFile(file); setRestoreDialogOpen(true); } }} /> { if (selectedFile) handleRestore(selectedFile); setRestoreDialogOpen(false); setSelectedFile(null); }} onCancel={() => { setRestoreDialogOpen(false); setSelectedFile(null); if (fileInputRef.current) fileInputRef.current.value = ""; }} />
{/* Scheduled backups */}
Scheduled backups Daily at 2:00 AM {scheduledEnabled && ( {" "} · keeping last{" "} )}
); } function RestoreDialog({ open, onOpenChange, onConfirm, onCancel, }: { open: boolean; onOpenChange: (open: boolean) => void; onConfirm: () => void; onCancel: () => void; }) { return ( Restore database? This will replace your entire database with the uploaded file. A safety backup of your current data will be created first. Active sessions may need to refresh after restore. Cancel Restore ); }