JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useEffect, useRef, useState, useCallback } from "react";4import { Copy, Check, WrapText } from "lucide-react";5import type { PreviewPluginProps } from "./types";6import { getLanguageFromMime, getLanguageFromFilename } from "@/lib/utils";78export default function CodePreview({ file, isEditing, onSave }: PreviewPluginProps) {9 const editorRef = useRef<HTMLDivElement>(null);10 // eslint-disable-next-line @typescript-eslint/no-explicit-any11 const viewRef = useRef<any>(null);12 const [content, setContent] = useState<string>("");13 const [loading, setLoading] = useState(true);14 const [copied, setCopied] = useState(false);15 const [wordWrap, setWordWrap] = useState(false);16 const [modified, setModified] = useState(false);17 const saveTimeout = useRef<ReturnType<typeof setTimeout>>();1819 // Load content20 useEffect(() => {21 setLoading(true);22 fetch(`/api/files/${file.id}/preview`)23 .then((res) => res.text())24 .then((text) => {25 setContent(text);26 setLoading(false);27 })28 .catch(() => setLoading(false));29 }, [file.id]);3031 // Initialize CodeMirror32 useEffect(() => {33 if (loading || !editorRef.current) return;3435 let destroyed = false;3637 const initEditor = async () => {38 const { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, highlightActiveLine } = await import("@codemirror/view");39 const { EditorState } = await import("@codemirror/state");40 const { defaultKeymap, history, historyKeymap, indentWithTab } = await import("@codemirror/commands");41 const { syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldGutter, foldKeymap, indentOnInput } = await import("@codemirror/language");42 const { searchKeymap, highlightSelectionMatches } = await import("@codemirror/search");43 const { oneDark } = await import("@codemirror/theme-one-dark");4445 if (destroyed || !editorRef.current) return;4647 // Get language extension48 const lang = getLanguageFromMime(file.mimeType) || getLanguageFromFilename(file.name);49 // eslint-disable-next-line @typescript-eslint/no-explicit-any50 let langExtension: any = null;5152 try {53 switch (lang) {54 case "javascript": case "typescript": {55 const { javascript } = await import("@codemirror/lang-javascript");56 langExtension = javascript({ typescript: lang === "typescript", jsx: true });57 break;58 }59 case "html": {60 const { html } = await import("@codemirror/lang-html");61 langExtension = html();62 break;63 }64 case "css": {65 const { css } = await import("@codemirror/lang-css");66 langExtension = css();67 break;68 }69 case "json": {70 const { json } = await import("@codemirror/lang-json");71 langExtension = json();72 break;73 }74 case "markdown": {75 const { markdown } = await import("@codemirror/lang-markdown");76 langExtension = markdown();77 break;78 }79 case "python": {80 const { python } = await import("@codemirror/lang-python");81 langExtension = python();82 break;83 }84 case "xml": {85 const { xml } = await import("@codemirror/lang-xml");86 langExtension = xml();87 break;88 }89 case "sql": {90 const { sql } = await import("@codemirror/lang-sql");91 langExtension = sql();92 break;93 }94 case "java": {95 const { java } = await import("@codemirror/lang-java");96 langExtension = java();97 break;98 }99 case "c": case "cpp": {100 const { cpp } = await import("@codemirror/lang-cpp");101 langExtension = cpp();102 break;103 }104 case "rust": {105 const { rust } = await import("@codemirror/lang-rust");106 langExtension = rust();107 break;108 }109 case "go": {110 const { go } = await import("@codemirror/lang-go");111 langExtension = go();112 break;113 }114 case "php": {115 const { php } = await import("@codemirror/lang-php");116 langExtension = php();117 break;118 }119 }120 } catch {121 // Language not available, continue without122 }123124 const extensions = [125 lineNumbers(),126 highlightActiveLineGutter(),127 highlightSpecialChars(),128 drawSelection(),129 highlightActiveLine(),130 bracketMatching(),131 foldGutter(),132 indentOnInput(),133 syntaxHighlighting(defaultHighlightStyle),134 highlightSelectionMatches(),135 history(),136 oneDark,137 keymap.of([138 ...defaultKeymap,139 ...historyKeymap,140 ...foldKeymap,141 ...searchKeymap,142 indentWithTab,143 ]),144 EditorView.editable.of(isEditing),145 EditorState.readOnly.of(!isEditing),146 ];147148 if (langExtension) extensions.push(langExtension);149150 if (wordWrap) {151 extensions.push(EditorView.lineWrapping);152 }153154 if (isEditing && onSave) {155 extensions.push(156 EditorView.updateListener.of((update) => {157 if (update.docChanged) {158 setModified(true);159 if (saveTimeout.current) clearTimeout(saveTimeout.current);160 saveTimeout.current = setTimeout(() => {161 const text = update.state.doc.toString();162 onSave(text, false).then(() => setModified(false));163 }, 2000);164 }165 }),166 keymap.of([{167 key: "Mod-s",168 run: (view) => {169 if (saveTimeout.current) clearTimeout(saveTimeout.current);170 const text = view.state.doc.toString();171 onSave(text, false).then(() => setModified(false));172 return true;173 },174 }]),175 );176 }177178 // Clean up previous editor179 if (viewRef.current) viewRef.current.destroy();180181 editorRef.current.innerHTML = "";182 const view = new EditorView({183 state: EditorState.create({ doc: content, extensions }),184 parent: editorRef.current,185 });186187 viewRef.current = view;188 };189190 initEditor();191192 return () => {193 destroyed = true;194 if (viewRef.current) {195 viewRef.current.destroy();196 viewRef.current = null;197 }198 };199 }, [loading, content, isEditing, wordWrap, file.mimeType, file.name]);200201 const copyContent = useCallback(() => {202 navigator.clipboard.writeText(content);203 setCopied(true);204 setTimeout(() => setCopied(false), 2000);205 }, [content]);206207 if (loading) {208 return (209 <div className="flex items-center justify-center h-full">210 <div className="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin" />211 </div>212 );213 }214215 return (216 <div className="flex flex-col h-full">217 {/* Toolbar */}218 <div className="flex items-center gap-1 px-3 py-2 border-b border-white/10 bg-white/5">219 <span className="text-xs text-gray-500 mr-2">220 {getLanguageFromMime(file.mimeType) || getLanguageFromFilename(file.name)}221 </span>222 {isEditing && modified && (223 <span className="text-xs text-yellow-400 mr-2">modifié</span>224 )}225 <div className="flex-1" />226 <button227 onClick={() => setWordWrap(!wordWrap)}228 className={`p-1.5 transition-colors rounded hover:bg-white/10 ${wordWrap ? "text-white" : "text-gray-400"}`}229 title="Retour à la ligne"230 >231 <WrapText className="w-4 h-4" />232 </button>233 <button234 onClick={copyContent}235 className="p-1.5 text-gray-400 hover:text-white transition-colors rounded hover:bg-white/10"236 title="Copier"237 >238 {copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}239 </button>240 </div>241242 {/* Editor */}243 <div ref={editorRef} className="flex-1 overflow-auto [&_.cm-editor]:h-full [&_.cm-scroller]:!overflow-auto" />244 </div>245 );246}247