fix(build): add missing DiagramEditor and FullscreenAdBanner files to git tracking
This commit is contained in:
107
src/components/FullscreenAdBanner.js
Executable file
107
src/components/FullscreenAdBanner.js
Executable file
@@ -0,0 +1,107 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
const FullscreenAdBanner = () => {
|
||||||
|
const desktopIframeRef = useRef(null);
|
||||||
|
const mobileIframeRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize Desktop/Tablet Ad (728x90)
|
||||||
|
if (desktopIframeRef.current) {
|
||||||
|
const iframe = desktopIframeRef.current;
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
|
||||||
|
doc.open();
|
||||||
|
doc.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 0; display: block; background: transparent; overflow: hidden; height: 100vh; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="text/javascript">
|
||||||
|
atOptions = {
|
||||||
|
'key' : '5d1186bf7f51a6e8732651b00fefc51b',
|
||||||
|
'format' : 'iframe',
|
||||||
|
'height' : 90,
|
||||||
|
'width' : 728,
|
||||||
|
'params' : {}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript" src="https://downconvenientmagnetic.com/5d1186bf7f51a6e8732651b00fefc51b/invoke.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
doc.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Mobile Ad (320x50) using the existing mobile key
|
||||||
|
if (mobileIframeRef.current) {
|
||||||
|
const iframe = mobileIframeRef.current;
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
|
||||||
|
doc.open();
|
||||||
|
doc.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 0; display: block; background: transparent; overflow: hidden; height: 100vh; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="text/javascript">
|
||||||
|
atOptions = {
|
||||||
|
'key' : '2965bcf877388cafa84160592c550f5a',
|
||||||
|
'format' : 'iframe',
|
||||||
|
'height' : 50,
|
||||||
|
'width' : 320,
|
||||||
|
'params' : {}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript" src="https://downconvenientmagnetic.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
doc.close();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Desktop & Tablet View (>= 768px) */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 z-50 justify-center bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 py-2 shadow-lg hidden md:flex">
|
||||||
|
<iframe
|
||||||
|
ref={desktopIframeRef}
|
||||||
|
style={{
|
||||||
|
width: "728px",
|
||||||
|
height: "90px",
|
||||||
|
border: "none",
|
||||||
|
maxWidth: "100%",
|
||||||
|
}}
|
||||||
|
title="Fullscreen Advertisement Desktop"
|
||||||
|
sandbox="allow-scripts allow-same-origin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile View (< 768px) */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 z-50 flex justify-center items-end bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 shadow-lg md:hidden h-[51px]">
|
||||||
|
<iframe
|
||||||
|
ref={mobileIframeRef}
|
||||||
|
style={{
|
||||||
|
width: "320px",
|
||||||
|
height: "50px",
|
||||||
|
border: "none",
|
||||||
|
maxWidth: "100%",
|
||||||
|
display: "block",
|
||||||
|
}}
|
||||||
|
title="Fullscreen Advertisement Mobile"
|
||||||
|
sandbox="allow-scripts allow-same-origin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FullscreenAdBanner;
|
||||||
605
src/pages/DiagramEditor.js
Executable file
605
src/pages/DiagramEditor.js
Executable file
@@ -0,0 +1,605 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
|
||||||
|
import mermaid from "mermaid";
|
||||||
|
import {
|
||||||
|
GitGraph,
|
||||||
|
Plus,
|
||||||
|
Upload,
|
||||||
|
Download,
|
||||||
|
Globe,
|
||||||
|
Edit3,
|
||||||
|
Type,
|
||||||
|
Columns,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
|
Check,
|
||||||
|
Copy,
|
||||||
|
FileImage,
|
||||||
|
FileCode2,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
Maximize,
|
||||||
|
FileDown,
|
||||||
|
Eye,
|
||||||
|
AlertTriangle,
|
||||||
|
} from "lucide-react";
|
||||||
|
import ToolLayout from "../components/ToolLayout";
|
||||||
|
import CodeMirrorEditor from "../components/CodeMirrorEditor";
|
||||||
|
import AdvancedURLFetch from "../components/AdvancedURLFetch";
|
||||||
|
import SEO from "../components/SEO";
|
||||||
|
import RelatedTools from "../components/RelatedTools";
|
||||||
|
|
||||||
|
// Debounce helper
|
||||||
|
const useDebounce = (value, delay) => {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
}, [value, delay]);
|
||||||
|
return debouncedValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DIAGRAM_TEMPLATES = {
|
||||||
|
flowchart: `flowchart TD
|
||||||
|
A[Start] --> B{Is it?};
|
||||||
|
B -- Yes --> C[OK];
|
||||||
|
C --> D[Rethink];
|
||||||
|
D --> B;
|
||||||
|
B -- No ----> E[End];`,
|
||||||
|
sequence: `sequenceDiagram
|
||||||
|
participant Alice
|
||||||
|
participant Bob
|
||||||
|
Alice->>John: Hello John, how are you?
|
||||||
|
loop Healthcheck
|
||||||
|
John->>John: Fight against hypochondria
|
||||||
|
end
|
||||||
|
Note right of John: Rational thoughts <br/>prevail!
|
||||||
|
John-->>Alice: Great!
|
||||||
|
John->>Bob: How about you?
|
||||||
|
Bob-->>John: Jolly good!`,
|
||||||
|
class: `classDiagram
|
||||||
|
Animal <|-- Duck
|
||||||
|
Animal <|-- Fish
|
||||||
|
Animal <|-- Zebra
|
||||||
|
Animal : +int age
|
||||||
|
Animal : +String gender
|
||||||
|
Animal: +isMammal()
|
||||||
|
Animal: +mate()
|
||||||
|
class Duck{
|
||||||
|
+String beakColor
|
||||||
|
+swim()
|
||||||
|
+quack()
|
||||||
|
}
|
||||||
|
class Fish{
|
||||||
|
-int sizeInFeet
|
||||||
|
-canEat()
|
||||||
|
}
|
||||||
|
class Zebra{
|
||||||
|
+bool is_wild
|
||||||
|
+run()
|
||||||
|
}`,
|
||||||
|
state: `stateDiagram-v2
|
||||||
|
[*] --> Still
|
||||||
|
Still --> [*]
|
||||||
|
|
||||||
|
Still --> Moving
|
||||||
|
Moving --> Still
|
||||||
|
Moving --> Crash
|
||||||
|
Crash --> [*]`,
|
||||||
|
er: `erDiagram
|
||||||
|
CUSTOMER ||--o{ ORDER : places
|
||||||
|
ORDER ||--|{ LINE-ITEM : contains
|
||||||
|
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses`,
|
||||||
|
gantt: `gantt
|
||||||
|
title A Gantt Diagram
|
||||||
|
dateFormat YYYY-MM-DD
|
||||||
|
section Section
|
||||||
|
A task :a1, 2014-01-01, 30d
|
||||||
|
Another task :after a1 , 20d
|
||||||
|
section Another
|
||||||
|
Task in sec :2014-01-12 , 12d
|
||||||
|
another task : 24d`,
|
||||||
|
pie: `pie title Pets adopted by volunteers
|
||||||
|
"Dogs" : 386
|
||||||
|
"Cats" : 85
|
||||||
|
"Rats" : 15`,
|
||||||
|
git: `gitGraph
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
branch develop
|
||||||
|
checkout develop
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
checkout main
|
||||||
|
merge develop
|
||||||
|
commit
|
||||||
|
commit`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DiagramEditor = () => {
|
||||||
|
const [code, setCode] = useState(DIAGRAM_TEMPLATES.flowchart);
|
||||||
|
const debouncedCode = useDebounce(code, 500); // 500ms debounce
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState("create");
|
||||||
|
const [viewMode, setViewMode] = useState(() =>
|
||||||
|
window.innerWidth < 1024 ? "editor" : "split",
|
||||||
|
);
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [fetchUrl, setFetchUrl] = useState("");
|
||||||
|
const [fetching, setFetching] = useState(false);
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState("flowchart");
|
||||||
|
|
||||||
|
const [svgContent, setSvgContent] = useState("");
|
||||||
|
const svgContainerRef = useRef(null);
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
// Initialize Mermaid
|
||||||
|
useEffect(() => {
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: "default", // We can sync this with dark mode later
|
||||||
|
securityLevel: "loose",
|
||||||
|
fontFamily:
|
||||||
|
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Render diagram when debounced code changes
|
||||||
|
useEffect(() => {
|
||||||
|
const renderDiagram = async () => {
|
||||||
|
try {
|
||||||
|
setError(null); // Clear previous errors
|
||||||
|
if (!debouncedCode.trim()) {
|
||||||
|
setSvgContent("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate syntax first
|
||||||
|
const isValid = await mermaid.parse(debouncedCode, {
|
||||||
|
suppressErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
const id = `mermaid-${Date.now()}`;
|
||||||
|
const { svg } = await mermaid.render(id, debouncedCode);
|
||||||
|
setSvgContent(svg);
|
||||||
|
} else {
|
||||||
|
// If parse returns false but doesn't throw, it's still invalid
|
||||||
|
setError("Syntax error in diagram code");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Mermaid parsing error:", err);
|
||||||
|
// Extract the most readable part of the error
|
||||||
|
let errorMessage = "Syntax error";
|
||||||
|
if (err.message) {
|
||||||
|
errorMessage = err.message.split("\n")[0] || "Syntax error";
|
||||||
|
} else if (typeof err === "string") {
|
||||||
|
errorMessage = err.split("\n")[0];
|
||||||
|
}
|
||||||
|
setError(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderDiagram();
|
||||||
|
}, [debouncedCode]);
|
||||||
|
|
||||||
|
// Input Handlers
|
||||||
|
const handleTemplateChange = (e) => {
|
||||||
|
const template = e.target.value;
|
||||||
|
setSelectedTemplate(template);
|
||||||
|
if (DIAGRAM_TEMPLATES[template]) {
|
||||||
|
setCode(DIAGRAM_TEMPLATES[template]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
setCode(e.target.result);
|
||||||
|
setActiveTab("create");
|
||||||
|
};
|
||||||
|
reader.onerror = () => setError("Failed to read file");
|
||||||
|
reader.readAsText(file);
|
||||||
|
e.target.value = ""; // Reset input
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFetchFromURL = async () => {
|
||||||
|
if (!fetchUrl.trim()) return;
|
||||||
|
setFetching(true);
|
||||||
|
try {
|
||||||
|
let url = fetchUrl.trim();
|
||||||
|
if (
|
||||||
|
url.includes("github.com") &&
|
||||||
|
!url.includes("raw.githubusercontent.com")
|
||||||
|
) {
|
||||||
|
url = url
|
||||||
|
.replace("github.com", "raw.githubusercontent.com")
|
||||||
|
.replace("/blob/", "/");
|
||||||
|
}
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const text = await response.text();
|
||||||
|
setCode(text);
|
||||||
|
setActiveTab("create");
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Fetch failed: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setFetching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export Handlers
|
||||||
|
const downloadFile = (content, filename, mimeType) => {
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportSVG = () => {
|
||||||
|
if (!svgContent) return;
|
||||||
|
downloadFile(svgContent, "diagram.svg", "image/svg+xml");
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportPNG = () => {
|
||||||
|
if (!svgContent) return;
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
// Create blob URL from SVG
|
||||||
|
const svg = new Blob([svgContent], { type: "image/svg+xml;charset=utf-8" });
|
||||||
|
const DOMURL = window.URL || window.webkitURL || window;
|
||||||
|
const url = DOMURL.createObjectURL(svg);
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
canvas.width = img.width * 2; // High DPI
|
||||||
|
canvas.height = img.height * 2;
|
||||||
|
ctx.fillStyle = "white"; // Background
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const pngUrl = canvas.toDataURL("image/png");
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.download = "diagram.png";
|
||||||
|
a.href = pngUrl;
|
||||||
|
a.click();
|
||||||
|
DOMURL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
img.src = url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportCode = () => {
|
||||||
|
downloadFile(code, "diagram.mmd", "text/plain");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SEO
|
||||||
|
title="Mermaid Diagram Editor & Viewer"
|
||||||
|
description="Write Mermaid.js diagram code with live visual preview. Pan, zoom, and export your flowcharts, sequence diagrams, and architecture maps to PNG or SVG."
|
||||||
|
keywords="mermaid editor, diagram editor, diagram as code, mermaidjs, flowchart generator, sequence diagram, architecture diagram, export svg"
|
||||||
|
path="/diagram-editor"
|
||||||
|
toolId="diagram-editor"
|
||||||
|
/>
|
||||||
|
<ToolLayout
|
||||||
|
title="Diagram Tool"
|
||||||
|
description="Create diagrams as code using Mermaid.js with live preview and multi-format export."
|
||||||
|
icon={GitGraph}
|
||||||
|
>
|
||||||
|
{/* Input Tabs */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
|
||||||
|
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("create")}
|
||||||
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
|
||||||
|
activeTab === "create"
|
||||||
|
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
|
||||||
|
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Edit3 className="h-4 w-4" /> Templates
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("url")}
|
||||||
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
|
||||||
|
activeTab === "url"
|
||||||
|
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
|
||||||
|
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Globe className="h-4 w-4" /> URL Fetch
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("open")}
|
||||||
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
|
||||||
|
activeTab === "open"
|
||||||
|
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
|
||||||
|
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Upload className="h-4 w-4" /> Open File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
{activeTab === "create" && (
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Load Boilerplate Template
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedTemplate}
|
||||||
|
onChange={handleTemplateChange}
|
||||||
|
className="tool-input w-full max-w-xs"
|
||||||
|
>
|
||||||
|
<option value="flowchart">Flowchart</option>
|
||||||
|
<option value="sequence">Sequence Diagram</option>
|
||||||
|
<option value="class">Class Diagram</option>
|
||||||
|
<option value="state">State Diagram</option>
|
||||||
|
<option value="er">Entity Relationship (ER)</option>
|
||||||
|
<option value="gantt">Gantt Chart</option>
|
||||||
|
<option value="pie">Pie Chart</option>
|
||||||
|
<option value="git">Git Graph</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCode("")}
|
||||||
|
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
Clear Editor
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "url" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={fetchUrl}
|
||||||
|
onChange={(e) => setFetchUrl(e.target.value)}
|
||||||
|
onKeyPress={(e) =>
|
||||||
|
e.key === "Enter" && !fetching && handleFetchFromURL()
|
||||||
|
}
|
||||||
|
placeholder="https://raw.githubusercontent.com/.../diagram.mmd"
|
||||||
|
className="tool-input flex-1"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleFetchFromURL}
|
||||||
|
disabled={fetching || !fetchUrl.trim()}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{fetching ? "Fetching..." : "Fetch"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "open" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".mmd,.mermaid,.txt"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
className="tool-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Editor Section */}
|
||||||
|
<div
|
||||||
|
className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden min-w-0 w-full max-w-full ${isFullscreen ? "fixed inset-0 z-50 flex flex-col !mt-0" : "mb-6"}`}
|
||||||
|
>
|
||||||
|
{/* Header & Controls */}
|
||||||
|
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 sticky top-0 bg-white dark:bg-gray-800 z-10 flex flex-wrap justify-between items-center gap-3">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<GitGraph className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
Workspace
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode("editor")}
|
||||||
|
className={`flex items-center gap-2 px-3 py-1.5 text-sm font-medium transition-colors ${viewMode === "editor" ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300" : "text-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"}`}
|
||||||
|
>
|
||||||
|
<Type className="h-4 w-4" />{" "}
|
||||||
|
<span className="hidden sm:inline">Code</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode("split")}
|
||||||
|
className={`hidden lg:flex items-center gap-2 px-3 py-1.5 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${viewMode === "split" ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300" : "text-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"}`}
|
||||||
|
>
|
||||||
|
<Columns className="h-4 w-4" />{" "}
|
||||||
|
<span className="hidden sm:inline">Split</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode("preview")}
|
||||||
|
className={`flex items-center gap-2 px-3 py-1.5 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${viewMode === "preview" ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300" : "text-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"}`}
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />{" "}
|
||||||
|
<span className="hidden sm:inline">Preview</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsFullscreen(!isFullscreen)}
|
||||||
|
className="p-2 text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
title={isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
|
||||||
|
>
|
||||||
|
{isFullscreen ? (
|
||||||
|
<Minimize2 className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Maximize2 className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Split View Content */}
|
||||||
|
<div
|
||||||
|
className={`${viewMode === "split" ? "grid grid-cols-1 lg:grid-cols-2" : ""} overflow-hidden min-w-0 w-full flex-1`}
|
||||||
|
>
|
||||||
|
{/* Editor Pane */}
|
||||||
|
{(viewMode === "editor" || viewMode === "split") && (
|
||||||
|
<div
|
||||||
|
className={`${viewMode === "split" ? "border-r border-gray-200 dark:border-gray-700" : ""} h-[600px] w-full min-w-0 flex flex-col relative`}
|
||||||
|
>
|
||||||
|
<CodeMirrorEditor
|
||||||
|
value={code}
|
||||||
|
onChange={setCode}
|
||||||
|
language="markdown" // Mermaid syntax is closest to markdown structurally
|
||||||
|
placeholder="Write your mermaid code here..."
|
||||||
|
showToggle={false}
|
||||||
|
maxLines={999}
|
||||||
|
height="100%"
|
||||||
|
className="flex-1 h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Preview Pane */}
|
||||||
|
{(viewMode === "preview" || viewMode === "split") && (
|
||||||
|
<div className="h-[600px] w-full min-w-0 bg-slate-50 dark:bg-slate-900 flex flex-col relative">
|
||||||
|
{error ? (
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center p-6 text-center z-10 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm">
|
||||||
|
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-full mb-3">
|
||||||
|
<AlertTriangle className="h-8 w-8 text-red-500" />
|
||||||
|
</div>
|
||||||
|
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||||
|
Syntax Error
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-red-600 dark:text-red-400 max-w-md">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : !svgContent ? (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center text-gray-400">
|
||||||
|
<p>Start typing to render diagram...</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex-1 relative overflow-hidden"
|
||||||
|
ref={svgContainerRef}
|
||||||
|
>
|
||||||
|
<TransformWrapper
|
||||||
|
initialScale={1}
|
||||||
|
minScale={0.1}
|
||||||
|
maxScale={10}
|
||||||
|
centerOnInit={true}
|
||||||
|
wheel={{ step: 0.1 }}
|
||||||
|
>
|
||||||
|
{({ zoomIn, zoomOut, resetTransform }) => (
|
||||||
|
<>
|
||||||
|
<div className="absolute top-4 right-4 z-10 flex flex-col gap-2 bg-white dark:bg-gray-800 p-1.5 rounded-lg shadow-md border border-gray-200 dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
onClick={() => zoomIn()}
|
||||||
|
className="p-1.5 text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
||||||
|
title="Zoom In"
|
||||||
|
>
|
||||||
|
<ZoomIn className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => zoomOut()}
|
||||||
|
className="p-1.5 text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
||||||
|
title="Zoom Out"
|
||||||
|
>
|
||||||
|
<ZoomOut className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<div className="h-px bg-gray-200 dark:bg-gray-700 w-full" />
|
||||||
|
<button
|
||||||
|
onClick={() => resetTransform()}
|
||||||
|
className="p-1.5 text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
||||||
|
title="Fit to Screen"
|
||||||
|
>
|
||||||
|
<Maximize className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<TransformComponent
|
||||||
|
wrapperStyle={{ width: "100%", height: "100%" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full h-full flex items-center justify-center p-8"
|
||||||
|
dangerouslySetInnerHTML={{ __html: svgContent }}
|
||||||
|
/>
|
||||||
|
</TransformComponent>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TransformWrapper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Export Section */}
|
||||||
|
{code.trim() && (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
|
||||||
|
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<Download className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
Export Diagram
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 sm:p-6 grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<button
|
||||||
|
onClick={exportPNG}
|
||||||
|
className="flex flex-col items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all group"
|
||||||
|
>
|
||||||
|
<FileImage className="h-6 w-6 text-gray-500 group-hover:text-blue-500 mb-2" />
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
PNG Image
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
High-res rendering
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={exportSVG}
|
||||||
|
className="flex flex-col items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-green-500 hover:bg-green-50 dark:hover:bg-green-900/20 transition-all group"
|
||||||
|
>
|
||||||
|
<FileImage className="h-6 w-6 text-gray-500 group-hover:text-green-500 mb-2" />
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
SVG Vector
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">Scalable format</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={exportCode}
|
||||||
|
className="flex flex-col items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-purple-500 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-all group"
|
||||||
|
>
|
||||||
|
<FileCode2 className="h-6 w-6 text-gray-500 group-hover:text-purple-500 mb-2" />
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Raw Code
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">.mmd file format</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<RelatedTools toolId="diagram-editor" />
|
||||||
|
</ToolLayout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DiagramEditor;
|
||||||
Reference in New Issue
Block a user