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%
8.1 KB · 247 lines tsx
Raw Blame History
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