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%
5.5 KB · 120 lines tsx
Raw Blame History
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&apos;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