JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useState, useEffect } from "react";4import { motion } from "framer-motion";5import {6 Clock,7 Camera,8 RotateCcw,9 Trash2,10 CheckCircle,11 XCircle,12 Loader2,13 AlertTriangle,14 CalendarDays,15} from "lucide-react";16import Button from "../ui/Button";17import Modal from "../ui/Modal";18import { EmptyState } from "../ui/PageHeader";19import { formatFileSize } from "@/lib/utils";20import toast from "react-hot-toast";2122interface SnapshotData {23 id: string;24 label: string;25 snapshotDate: string;26 path: string;27 size: number;28 status: string;29 error: string | null;30 createdAt: string;31}3233function StatusIcon({ status }: { status: string }) {34 if (status === "completed") return <CheckCircle className="w-4 h-4 text-emerald-500 flex-shrink-0" />;35 if (status === "failed") return <XCircle className="w-4 h-4 text-rose-500 flex-shrink-0" />;36 return <Loader2 className="w-4 h-4 text-blue-400 animate-spin flex-shrink-0" />;37}3839function isToday(dateStr: string): boolean {40 return new Date().toISOString().split("T")[0] === dateStr;41}4243export default function SnapshotPanel() {44 const [snapshots, setSnapshots] = useState<SnapshotData[]>([]);45 const [loading, setLoading] = useState(true);46 const [creating, setCreating] = useState(false);47 const [restoringId, setRestoringId] = useState<string | null>(null);48 const [deletingId, setDeletingId] = useState<string | null>(null);49 const [confirmRestore, setConfirmRestore] = useState<SnapshotData | null>(null);5051 const fetchSnapshots = async () => {52 const res = await fetch("/api/snapshots");53 if (res.ok) setSnapshots(await res.json());54 setLoading(false);55 };5657 useEffect(() => {58 fetchSnapshots();59 }, []);6061 const handleCreate = async () => {62 setCreating(true);63 try {64 const res = await fetch("/api/snapshots", { method: "POST" });65 if (res.ok) {66 toast.success("Snapshot créé");67 await fetchSnapshots();68 } else {69 const data = await res.json();70 toast.error(data.error || "Échec du snapshot");71 }72 } catch {73 toast.error("Erreur réseau");74 }75 setCreating(false);76 };7778 const handleRestore = async (snapshot: SnapshotData) => {79 setConfirmRestore(null);80 setRestoringId(snapshot.id);81 try {82 const res = await fetch(`/api/snapshots/${snapshot.id}/restore`, { method: "POST" });83 if (res.ok) {84 toast.success(`Restauré au ${snapshot.label}`);85 await fetchSnapshots();86 } else {87 const data = await res.json();88 toast.error(data.error || "Échec de la restauration");89 }90 } catch {91 toast.error("Erreur réseau");92 }93 setRestoringId(null);94 };9596 const handleDelete = async (id: string) => {97 setDeletingId(id);98 try {99 const res = await fetch(`/api/snapshots/${id}`, { method: "DELETE" });100 if (res.ok) {101 toast.success("Snapshot supprimé");102 setSnapshots((prev) => prev.filter((s) => s.id !== id));103 } else {104 toast.error("Échec de la suppression");105 }106 } catch {107 toast.error("Erreur réseau");108 }109 setDeletingId(null);110 };111112 const todayHasSnapshot = snapshots.some(113 (s) => isToday(s.snapshotDate) && s.status === "completed"114 );115116 return (117 <div className="space-y-5">118 {/* Header */}119 <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">120 <div>121 <h2 className="text-[15px] font-semibold text-gray-900 dark:text-white flex items-center gap-2">122 <Clock className="w-[18px] h-[18px] text-blue-500" />123 Snapshots quotidiens124 </h2>125 <p className="text-[13px] text-gray-500 dark:text-gray-400 mt-0.5">126 {snapshots.length} instantané{snapshots.length !== 1 ? "s" : ""} · 7 jours conservés · restauration en un clic127 </p>128 </div>129 <Button130 size="sm"131 variant="accent"132 onClick={handleCreate}133 loading={creating}134 >135 <Camera className="w-4 h-4" />136 {todayHasSnapshot ? "Re-snapshot maintenant" : "Snapshot maintenant"}137 </Button>138 </div>139140 {/* Auto info banner */}141 <div className="flex items-start gap-2.5 p-3 rounded-2xl bg-blue-500/[0.07] text-[13px] text-blue-800 dark:text-blue-200">142 <CalendarDays className="w-4 h-4 mt-0.5 flex-shrink-0" />143 <span>144 Un snapshot automatique est créé chaque jour au premier démarrage du serveur.145 Les plus anciens sont supprimés automatiquement.146 </span>147 </div>148149 {/* Snapshot list */}150 {loading ? (151 <div className="space-y-2">152 {[0, 1, 2].map((i) => (153 <div key={i} className="rounded-2xl border border-line dark:border-white/[0.07] p-3 pl-12 flex items-center gap-3 animate-pulse">154 <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>155 </div>156 ))}157 </div>158 ) : snapshots.length === 0 ? (159 <EmptyState icon={Camera} title="Aucun snapshot" description="Créez votre premier snapshot manuellement ou attendez le démarrage automatique quotidien." />160 ) : (161 <div className="relative space-y-2">162 {/* Vertical timeline line */}163 <div className="absolute left-[22px] top-4 bottom-4 w-px bg-gray-200 dark:bg-white/10 pointer-events-none" />164165 {snapshots.map((snapshot, i) => {166 const today = isToday(snapshot.snapshotDate);167 return (168 <motion.div169 key={snapshot.id}170 initial={{ opacity: 0, x: -8 }}171 animate={{ opacity: 1, x: 0 }}172 transition={{ delay: i * 0.04 }}173 className={`relative flex flex-col sm:flex-row sm:items-center gap-3 p-3 pl-12 rounded-2xl border transition-colors ${174 today175 ? "bg-blue-500/[0.07] border-blue-500/25"176 : "bg-surface dark:bg-white/[0.03] border-line dark:border-white/[0.07] hover:bg-gray-50 dark:hover:bg-white/[0.05]"177 }`}178 >179 {/* Timeline dot */}180 <div181 className={`absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 rounded-full border-2 flex items-center justify-center ${182 snapshot.status === "completed"183 ? today184 ? "bg-blue-500 border-blue-300 shadow-glow"185 : "bg-emerald-500 border-emerald-300"186 : snapshot.status === "failed"187 ? "bg-rose-500 border-rose-300"188 : "bg-gray-400 border-gray-300"189 }`}190 />191192 {/* Info */}193 <div className="flex-1 min-w-0">194 <div className="flex items-center gap-2 flex-wrap">195 <p className="text-sm font-medium text-gray-900 dark:text-white capitalize truncate">196 {snapshot.label}197 </p>198 {today && (199 <span className="text-[10px] font-semibold px-1.5 py-0.5 bg-blue-500/15 text-blue-700 dark:text-blue-300 rounded-full uppercase tracking-wide">200 Aujourd'hui201 </span>202 )}203 <StatusIcon status={snapshot.status} />204 </div>205 <p className="text-xs text-gray-500 mt-0.5">206 {snapshot.status === "completed"207 ? formatFileSize(snapshot.size)208 : snapshot.status === "failed"209 ? snapshot.error || "Erreur inconnue"210 : "En cours…"}211 {snapshot.status === "completed" && (212 <> · {new Date(snapshot.createdAt).toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}</>213 )}214 </p>215 </div>216217 {/* Actions */}218 {snapshot.status === "completed" && (219 <div className="flex gap-1 pl-0 sm:pl-0">220 <button221 onClick={() => setConfirmRestore(snapshot)}222 disabled={restoringId === snapshot.id}223 title="Restaurer"224 className="w-10 h-10 flex items-center justify-center text-gray-400 hover:text-blue-600 dark:hover:text-blue-300 rounded-xl hover:bg-blue-500/10 transition-colors disabled:opacity-50"225 >226 {restoringId === snapshot.id ? (227 <Loader2 className="w-4 h-4 animate-spin" />228 ) : (229 <RotateCcw className="w-4 h-4" />230 )}231 </button>232 <button233 onClick={() => handleDelete(snapshot.id)}234 disabled={deletingId === snapshot.id}235 title="Supprimer"236 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 disabled:opacity-50"237 >238 {deletingId === snapshot.id ? (239 <Loader2 className="w-4 h-4 animate-spin" />240 ) : (241 <Trash2 className="w-4 h-4" />242 )}243 </button>244 </div>245 )}246 </motion.div>247 );248 })}249 </div>250 )}251252 {/* Restore confirmation modal */}253 <Modal254 isOpen={!!confirmRestore}255 onClose={() => setConfirmRestore(null)}256 title="Restaurer ce snapshot ?"257 description={confirmRestore?.label}258 footer={259 <>260 <Button variant="ghost" onClick={() => setConfirmRestore(null)}>Annuler</Button>261 <Button onClick={() => confirmRestore && handleRestore(confirmRestore)} className="!bg-amber-500 hover:!bg-amber-600 !text-white dark:!bg-amber-500 dark:!text-white">262 <RotateCcw className="w-4 h-4" /> Oui, restaurer263 </Button>264 </>265 }266 >267 <div className="flex items-start gap-3 p-3 rounded-2xl bg-amber-500/[0.08] text-[13.5px] text-amber-800 dark:text-amber-300">268 <AlertTriangle className="w-5 h-5 shrink-0 mt-0.5" />269 <p>Tous vos fichiers et la base de données seront remplacés par ceux du snapshot sélectionné. <strong>Cette action est irréversible.</strong></p>270 </div>271 </Modal>272 </div>273 );274}275