JavaScript 56.7%
TypeScript 42.6%
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'é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'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