JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useState, useEffect } from "react";4import { motion } from "framer-motion";5import { HardDrive, Download, Trash2, RefreshCw, CheckCircle, XCircle, Loader2, Layers } from "lucide-react";6import Button from "../ui/Button";7import Modal from "../ui/Modal";8import { EmptyState } from "../ui/PageHeader";9import { formatFileSize, formatDate } from "@/lib/utils";10import toast from "react-hot-toast";1112interface BackupData {13 id: string;14 status: string;15 type: string;16 size: number;17 path: string | null;18 startedAt: string;19 completedAt: string | null;20}2122export default function BackupPanel() {23 const [backups, setBackups] = useState<BackupData[]>([]);24 const [loading, setLoading] = useState(true);25 const [creating, setCreating] = useState<"full" | "incremental" | null>(null);26 const [toDelete, setToDelete] = useState<BackupData | null>(null);2728 const fetchBackups = async () => {29 const res = await fetch("/api/backup");30 if (res.ok) setBackups(await res.json());31 setLoading(false);32 };3334 useEffect(() => { fetchBackups(); }, []);3536 const createBackup = async (type: "full" | "incremental") => {37 setCreating(type);38 try {39 const res = await fetch("/api/backup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type }) });40 if (res.ok) {41 toast.success(`Backup ${type === "full" ? "complet" : "incrémental"} créé`);42 fetchBackups();43 } else toast.error("Échec du backup");44 } catch {45 toast.error("Erreur réseau");46 }47 setCreating(null);48 };4950 const deleteBackup = async () => {51 if (!toDelete) return;52 const res = await fetch("/api/backup", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: toDelete.id }) });53 setToDelete(null);54 if (res.ok) {55 toast.success("Backup supprimé");56 fetchBackups();57 } else toast.error("Suppression impossible");58 };5960 const statusIcon = (status: string) => {61 if (status === "completed") return <CheckCircle className="w-4 h-4 text-emerald-500 shrink-0" />;62 if (status === "failed") return <XCircle className="w-4 h-4 text-rose-500 shrink-0" />;63 return <Loader2 className="w-4 h-4 text-blue-500 animate-spin shrink-0" />;64 };6566 const totalSize = backups.reduce((s, b) => s + (b.size || 0), 0);6768 return (69 <div className="space-y-5">70 <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">71 <div>72 <h2 className="text-[15px] font-semibold text-gray-900 dark:text-white flex items-center gap-2"><HardDrive className="w-[18px] h-[18px] text-cyan-500" /> Sauvegardes manuelles</h2>73 <p className="text-[13px] text-gray-500 dark:text-gray-400 mt-0.5">{backups.length} backup{backups.length !== 1 ? "s" : ""} · {formatFileSize(totalSize)} au total</p>74 </div>75 <div className="flex gap-2">76 <Button size="sm" variant="secondary" onClick={() => createBackup("incremental")} loading={creating === "incremental"} disabled={!!creating}>77 <Layers className="w-4 h-4" /> Incrémental78 </Button>79 <Button size="sm" variant="accent" onClick={() => createBackup("full")} loading={creating === "full"} disabled={!!creating}>80 <Download className="w-4 h-4" /> Backup complet81 </Button>82 </div>83 </div>8485 {loading ? (86 <div className="space-y-2">87 {[0, 1, 2].map((i) => (88 <div key={i} className="rounded-2xl border border-line dark:border-white/[0.07] p-4 flex items-center gap-3 animate-pulse">89 <div className="w-4 h-4 rounded-full bg-gray-200 dark:bg-white/10" />90 <div className="flex-1 space-y-2"><div className="h-3 w-1/3 rounded bg-gray-200 dark:bg-white/10" /><div className="h-2 w-1/4 rounded bg-gray-200 dark:bg-white/10" /></div>91 </div>92 ))}93 </div>94 ) : backups.length === 0 ? (95 <EmptyState icon={HardDrive} title="Aucun backup" description="Lancez un backup complet pour créer une première archive de vos fichiers et de la base de données." />96 ) : (97 <div className="rounded-2xl border border-line dark:border-white/[0.07] overflow-hidden divide-y divide-line dark:divide-white/[0.06]">98 {backups.map((backup, i) => (99 <motion.div100 key={backup.id}101 initial={{ opacity: 0, y: 5 }}102 animate={{ opacity: 1, y: 0 }}103 transition={{ delay: Math.min(i, 10) * 0.03 }}104 className="flex flex-col sm:flex-row sm:items-center gap-3 px-4 py-3 hover:bg-gray-50 dark:hover:bg-white/[0.03] transition-colors"105 >106 <div className="flex items-center gap-3 flex-1 min-w-0">107 {statusIcon(backup.status)}108 <div className="flex-1 min-w-0">109 <p className="text-[14px] text-gray-900 dark:text-white flex items-center gap-2 flex-wrap">110 <span className="font-medium">{formatDate(backup.startedAt)}</span>111 <span className={`inline-flex px-2 py-0.5 rounded-full text-[11px] font-medium ring-1 ring-inset ${backup.type === "full" ? "bg-cyan-500/10 text-cyan-700 dark:text-cyan-300 ring-cyan-500/20" : "bg-gray-100 dark:bg-white/[0.06] text-gray-600 dark:text-gray-300 ring-gray-500/15"}`}>112 {backup.type === "full" ? "Complet" : "Incrémental"}113 </span>114 </p>115 <p className="text-[12px] text-gray-500 dark:text-gray-400 tabular-nums">116 {backup.size > 0 ? formatFileSize(backup.size) : "—"}117 {backup.completedAt && ` · terminé à ${new Date(backup.completedAt).toLocaleTimeString("fr-CA", { hour: "2-digit", minute: "2-digit" })}`}118 {backup.status === "failed" && " · échec"}119 </p>120 </div>121 </div>122 <div className="flex gap-1 pl-7 sm:pl-0">123 <button onClick={fetchBackups} className="w-10 h-10 flex items-center justify-center text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-xl hover:bg-gray-100 dark:hover:bg-white/[0.08] transition-colors" title="Actualiser">124 <RefreshCw className="w-4 h-4" />125 </button>126 <button onClick={() => setToDelete(backup)} className="w-10 h-10 flex items-center justify-center text-gray-400 hover:text-rose-500 rounded-xl hover:bg-rose-500/10 transition-colors" title="Supprimer">127 <Trash2 className="w-4 h-4" />128 </button>129 </div>130 </motion.div>131 ))}132 </div>133 )}134135 <Modal136 isOpen={!!toDelete}137 onClose={() => setToDelete(null)}138 title="Supprimer ce backup ?"139 description={toDelete ? `${formatDate(toDelete.startedAt)} · ${formatFileSize(toDelete.size || 0)}` : undefined}140 footer={<><Button variant="ghost" onClick={() => setToDelete(null)}>Annuler</Button><Button variant="danger" onClick={deleteBackup}><Trash2 className="w-4 h-4" /> Supprimer</Button></>}141 >142 <p className="text-sm text-gray-600 dark:text-gray-300">L'archive sera effacée du disque. Cette action est irréversible.</p>143 </Modal>144 </div>145 );146}147