SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
4 h agolast push
JavaScript 56.7% TypeScript 42.6%
6.3 KB · 159 lines tsx
Raw Blame History
1"use client";23import { useState, useEffect, useRef } from "react";4import { Loader2, Download, FileText } from "lucide-react";5import dynamic from "next/dynamic";6import type { PreviewPluginProps } from "./types";78// Load Tiptap editor only on demand (heavy bundle)9const WordTiptapEditor = dynamic(() => import("./WordTiptapEditor"), {10  ssr: false,11  loading: () => (12    <div className="flex flex-col items-center justify-center h-full gap-3 text-gray-400">13      <Loader2 className="w-8 h-8 animate-spin text-blue-400" />14      <p className="text-sm">Chargement de l&apos;éditeur…</p>15    </div>16  ),17});1819export default function WordPreview({ file, isEditing, onSave }: PreviewPluginProps) {20  const [html, setHtml] = useState<string | null>(null);21  const [error, setError] = useState(false);22  const [loading, setLoading] = useState(true);23  // Incremented when returning from edit mode to force re-fetch from server24  const [fetchKey, setFetchKey] = useState(0);25  const prevEditingRef = useRef(isEditing);2627  // When switching from edit → view, re-fetch to show saved content28  useEffect(() => {29    if (!isEditing && prevEditingRef.current) {30      setFetchKey((k) => k + 1);31    }32    prevEditingRef.current = isEditing;33  }, [isEditing]);3435  useEffect(() => {36    let cancelled = false;37    const convert = async () => {38      setLoading(true);39      setError(false);40      setHtml(null);41      try {42        const res = await fetch(`/api/files/${file.id}/download?t=${Date.now()}`);43        if (!res.ok) throw new Error("Download failed");44        const arrayBuffer = await res.arrayBuffer();45        const mammoth = await import("mammoth");46        const result = await mammoth.convertToHtml(47          { arrayBuffer },48          {49            styleMap: [50              "p[style-name='Heading 1'] => h1:fresh",51              "p[style-name='Heading 2'] => h2:fresh",52              "p[style-name='Heading 3'] => h3:fresh",53              "p[style-name='Titre 1'] => h1:fresh",54              "p[style-name='Titre 2'] => h2:fresh",55            ],56          }57        );58        if (!cancelled) setHtml(result.value || "<p>Document vide.</p>");59      } catch {60        if (!cancelled) setError(true);61      } finally {62        if (!cancelled) setLoading(false);63      }64    };65    convert();66    return () => { cancelled = true; };67  }, [file.id, fetchKey]);6869  // Edit mode — render Tiptap editor once html is ready70  if (isEditing) {71    if (loading) {72      return (73        <div className="flex flex-col items-center justify-center h-full gap-3 text-gray-400">74          <Loader2 className="w-8 h-8 animate-spin text-blue-400" />75          <p className="text-sm">Chargement du document…</p>76        </div>77      );78    }79    return (80      <WordTiptapEditor81        initialHtml={html ?? "<p>Document vide.</p>"}82        onSave={onSave ? async (blob, v) => onSave(blob, v) : undefined}83      />84    );85  }8687  // Read mode88  if (loading) {89    return (90      <div className="flex flex-col items-center justify-center h-full gap-3 text-gray-400">91        <Loader2 className="w-8 h-8 animate-spin text-blue-400" />92        <p className="text-sm">Chargement du document Word...</p>93      </div>94    );95  }9697  if (error) {98    return (99      <div className="flex flex-col items-center justify-center h-full gap-4 text-gray-400 p-8">100        <FileText className="w-16 h-16 text-blue-400 opacity-50" />101        <p className="text-sm text-center">Impossible d&apos;afficher ce document.</p>102        <button103          onClick={() => window.open(`/api/files/${file.id}/download`, "_blank")}104          className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-sm text-white transition-colors"105        >106          <Download className="w-4 h-4" />107          Télécharger le fichier108        </button>109      </div>110    );111  }112113  return (114    <div className="h-full overflow-auto bg-gray-100 dark:bg-canvas">115      <div className="max-w-3xl mx-auto my-6 px-4 sm:px-6">116        <div className="bg-white dark:bg-elevated shadow-md rounded-sm px-8 py-10 sm:px-14 sm:py-12 min-h-[500px]">117          <div className="flex items-center justify-between mb-6 pb-4 border-b border-gray-100 dark:border-white/10">118            <div className="flex items-center gap-2">119              <FileText className="w-4 h-4 text-blue-500" />120              <span className="text-sm font-medium text-gray-600 dark:text-gray-400 truncate max-w-xs">121                {file.name}122              </span>123            </div>124            <button125              onClick={() => window.open(`/api/files/${file.id}/download`, "_blank")}126              className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 border border-gray-200 dark:border-white/10 rounded-lg transition-colors"127            >128              <Download className="w-3.5 h-3.5" />129              Télécharger130            </button>131          </div>132          <div133            className="word-content prose prose-sm sm:prose dark:prose-invert max-w-none"134            dangerouslySetInnerHTML={{ __html: html ?? "" }}135          />136        </div>137      </div>138139      <style jsx global>{`140        .word-content h1 { font-size: 1.6rem; font-weight: 700; margin-bottom: 0.5rem; margin-top: 1.5rem; }141        .word-content h2 { font-size: 1.3rem; font-weight: 600; margin-bottom: 0.4rem; margin-top: 1.2rem; }142        .word-content h3 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.3rem; margin-top: 1rem; }143        .word-content p  { margin-bottom: 0.6rem; line-height: 1.7; }144        .word-content ul, .word-content ol { padding-left: 1.5rem; margin-bottom: 0.6rem; }145        .word-content li { margin-bottom: 0.2rem; }146        .word-content table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; }147        .word-content td, .word-content th { border: 1px solid #d1d5db; padding: 0.4rem 0.6rem; text-align: left; }148        .word-content th { background: #f3f4f6; font-weight: 600; }149        .dark .word-content td, .dark .word-content th { border-color: rgba(255,255,255,0.15); }150        .dark .word-content th { background: rgba(255,255,255,0.08); }151        .word-content strong { font-weight: 700; }152        .word-content em { font-style: italic; }153        .word-content a { color: #3b82f6; text-decoration: underline; }154        .word-content img { max-width: 100%; height: auto; margin: 0.5rem 0; }155      `}</style>156    </div>157  );158}159