JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useState, useEffect, useCallback } from "react";4import { Clock, Download, RotateCcw, GitBranch, Camera } from "lucide-react";5import { formatFileSize, formatDate } from "@/lib/utils";6import type { FileVersionData } from "@/types/files";7import toast from "react-hot-toast";8import Modal from "../ui/Modal";9import Button from "../ui/Button";1011interface VersionHistoryProps {12 fileId: string;13 onRestore?: () => void;14}1516export default function VersionHistory({ fileId, onRestore }: VersionHistoryProps) {17 const [versions, setVersions] = useState<FileVersionData[]>([]);18 const [loading, setLoading] = useState(true);19 const [busy, setBusy] = useState(false);20 const [confirm, setConfirm] = useState<{ id: string; num: number } | null>(null);2122 const loadVersions = useCallback(async () => {23 try {24 const res = await fetch(`/api/files/${fileId}/versions`);25 if (res.ok) setVersions(await res.json());26 } catch {}27 setLoading(false);28 }, [fileId]);2930 useEffect(() => {31 setLoading(true);32 loadVersions();33 }, [loadVersions]);3435 const createSnapshot = async () => {36 setBusy(true);37 try {38 const res = await fetch(`/api/files/${fileId}/versions`, {39 method: "POST",40 headers: { "Content-Type": "application/json" },41 body: JSON.stringify({ changeNote: "Snapshot manuel" }),42 });43 if (res.ok) {44 toast.success("Snapshot créé");45 loadVersions();46 } else toast.error("Impossible de créer le snapshot");47 } catch {48 toast.error("Erreur");49 }50 setBusy(false);51 };5253 const restoreVersion = async () => {54 if (!confirm) return;55 setBusy(true);56 try {57 const res = await fetch(`/api/files/${fileId}/versions/${confirm.id}/restore`, { method: "POST" });58 if (res.ok) {59 toast.success(`Version ${confirm.num} restaurée`);60 loadVersions();61 onRestore?.();62 } else toast.error("Erreur de restauration");63 } catch {64 toast.error("Erreur de restauration");65 }66 setBusy(false);67 setConfirm(null);68 };6970 return (71 <section className="p-4 border-t border-line dark:border-white/[0.07]">72 <div className="flex items-center justify-between mb-3">73 <h4 className="text-[11px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500 flex items-center gap-1.5">74 <GitBranch className="w-3.5 h-3.5" />75 Versions {!loading && <span className="tabular-nums">({versions.length})</span>}76 </h4>77 <Button size="xs" variant="secondary" onClick={createSnapshot} loading={busy} title="Créer un snapshot de la version actuelle">78 <Camera className="w-3.5 h-3.5" /> Snapshot79 </Button>80 </div>8182 {loading ? (83 <div className="space-y-1.5">84 {[0, 1].map((i) => <div key={i} className="h-12 rounded-xl bg-gray-100 dark:bg-white/5 animate-pulse" />)}85 </div>86 ) : versions.length === 0 ? (87 <p className="text-[12px] text-gray-400">Aucune version précédente. Les modifications créent des versions automatiquement.</p>88 ) : (89 <ol className="relative space-y-1.5 max-h-60 overflow-auto">90 {versions.map((v) => (91 <li key={v.id} className="group flex items-center gap-2.5 p-2 rounded-xl bg-gray-50 dark:bg-white/[0.03] hover:bg-gray-100 dark:hover:bg-white/[0.06] border border-transparent hover:border-line dark:hover:border-white/10 transition-colors">92 <span className="w-8 h-8 rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-300 text-[11px] font-semibold flex items-center justify-center shrink-0 tabular-nums">v{v.versionNum}</span>93 <div className="flex-1 min-w-0">94 <p className="text-[12.5px] text-gray-800 dark:text-gray-100 truncate">{v.changeNote || "Sans note"}</p>95 <p className="text-[11px] text-gray-400 flex items-center gap-1 tabular-nums"><Clock className="w-3 h-3" />{formatDate(v.createdAt)} · {formatFileSize(v.size)}</p>96 </div>97 <div className="flex items-center gap-0.5 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">98 <button onClick={() => window.open(`/api/files/${fileId}/versions/${v.id}`, "_blank")} className="w-8 h-8 rounded-lg flex items-center justify-center text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200/60 dark:hover:bg-white/10 transition-colors" title="Télécharger cette version" aria-label="Télécharger">99 <Download className="w-3.5 h-3.5" />100 </button>101 <button onClick={() => setConfirm({ id: v.id, num: v.versionNum })} className="w-8 h-8 rounded-lg flex items-center justify-center text-gray-400 hover:text-amber-600 hover:bg-amber-500/10 transition-colors" title="Restaurer cette version" aria-label="Restaurer">102 <RotateCcw className="w-3.5 h-3.5" />103 </button>104 </div>105 </li>106 ))}107 </ol>108 )}109110 <Modal isOpen={!!confirm} onClose={() => setConfirm(null)} title={`Restaurer la version ${confirm?.num ?? ""}`}>111 <p className="text-sm text-gray-600 dark:text-gray-300 mb-5">La version actuelle sera d'abord sauvegardée comme nouvelle version, puis remplacée par la version {confirm?.num}.</p>112 <div className="flex gap-2 justify-end">113 <Button variant="ghost" onClick={() => setConfirm(null)}>Annuler</Button>114 <Button variant="accent" onClick={restoreVersion} loading={busy}><RotateCcw className="w-4 h-4" /> Restaurer</Button>115 </div>116 </Modal>117 </section>118 );119}120