SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
2 h agolast push
JavaScript 56.7% TypeScript 42.6%
7.2 KB · 147 lines tsx
Raw Blame History
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&apos;archive sera effacée du disque. Cette action est irréversible.</p>143      </Modal>144    </div>145  );146}147