tetrons 2.3.22 → 2.3.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/page.d.ts +2 -0
- package/dist/app/page.jsx +51 -0
- package/dist/components/UI/Button.tsx +0 -0
- package/dist/components/UI/Dropdown.tsx +0 -0
- package/dist/components/components/tetrons/EditorContent.tsx +60 -62
- package/dist/components/tetrons/EditorContent.d.ts +6 -0
- package/dist/components/tetrons/EditorContent.tsx +280 -0
- package/dist/components/tetrons/ResizableImage.ts +39 -0
- package/dist/components/tetrons/ResizableImageComponent.tsx +77 -0
- package/dist/components/tetrons/ResizableVideo.ts +66 -0
- package/dist/components/tetrons/ResizableVideoComponent.tsx +56 -0
- package/dist/components/tetrons/extensions/Spellcheck.ts +50 -0
- package/dist/components/tetrons/helpers.ts +0 -0
- package/dist/components/tetrons/toolbar/ActionGroup.tsx +218 -0
- package/dist/components/tetrons/toolbar/ClipboardGroup.tsx +58 -0
- package/dist/components/tetrons/toolbar/FileGroup.tsx +66 -0
- package/dist/components/tetrons/toolbar/FontStyleGroup.tsx +194 -0
- package/dist/components/tetrons/toolbar/InsertGroup.tsx +267 -0
- package/dist/components/tetrons/toolbar/ListAlignGroup.tsx +69 -0
- package/dist/components/tetrons/toolbar/MiscGroup.d.ts +7 -0
- package/dist/components/tetrons/toolbar/MiscGroup.jsx +55 -0
- package/dist/components/tetrons/toolbar/MiscGroup.tsx +104 -0
- package/dist/components/tetrons/toolbar/TableContextMenu.tsx +91 -0
- package/dist/components/tetrons/toolbar/TetronsToolbar.d.ts +6 -0
- package/dist/components/tetrons/toolbar/TetronsToolbar.tsx +71 -0
- package/dist/components/tetrons/toolbar/ToolbarButton.tsx +36 -0
- package/dist/components/tetrons/toolbar/extensions/Comment.ts +72 -0
- package/dist/components/tetrons/toolbar/extensions/Embed.ts +113 -0
- package/dist/components/tetrons/toolbar/extensions/FontFamily.ts +43 -0
- package/dist/components/tetrons/toolbar/extensions/FontSize.ts +43 -0
- package/dist/components/tetrons/toolbar/extensions/ResizableTable.ts +16 -0
- package/dist/components/tetrons/toolbar/marks/Subscript.ts +45 -0
- package/dist/components/tetrons/toolbar/marks/Superscript.ts +45 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17366 -22
- package/dist/index.mjs +4705 -4592
- package/dist/styles/styles/tetrons.css +1 -1
- package/dist/styles/tetrons.css +1 -1
- package/dist/utils/checkGrammar.d.ts +25 -0
- package/dist/utils/checkGrammar.js +17 -0
- package/package.json +9 -9
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import EditorContent from "../components/tetrons/EditorContent";
|
|
4
|
+
import "../styles/tetrons.css";
|
|
5
|
+
export default function Home() {
|
|
6
|
+
const [apiKey, setApiKey] = useState(null);
|
|
7
|
+
const [loading, setLoading] = useState(true);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const fetchOrGenerateApiKey = async () => {
|
|
10
|
+
let key = localStorage.getItem("tetrons-key");
|
|
11
|
+
if (!key) {
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch("/api/register", {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: { "Content-Type": "application/json" },
|
|
16
|
+
body: JSON.stringify({
|
|
17
|
+
email: "developer@finapsys.co.in",
|
|
18
|
+
organization: "FCSPL",
|
|
19
|
+
version: "platinum",
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
const data = await res.json();
|
|
23
|
+
if (!res.ok || !data.apiKey) {
|
|
24
|
+
throw new Error(data.error || "Failed to register for API key");
|
|
25
|
+
}
|
|
26
|
+
key = data.apiKey;
|
|
27
|
+
if (key) {
|
|
28
|
+
localStorage.setItem("tetrons-key", key);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error("❌ Failed to fetch or register API key:", err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
setApiKey(key);
|
|
36
|
+
setLoading(false);
|
|
37
|
+
};
|
|
38
|
+
fetchOrGenerateApiKey();
|
|
39
|
+
}, []);
|
|
40
|
+
if (loading) {
|
|
41
|
+
return <div className="text-center p-4">⏳ Loading Editor...</div>;
|
|
42
|
+
}
|
|
43
|
+
if (!apiKey) {
|
|
44
|
+
return <div className="text-red-600 text-center">❌ API key not found</div>;
|
|
45
|
+
}
|
|
46
|
+
return (<main className="flex flex-col h-screen overflow-hidden">
|
|
47
|
+
<div className="flex-1 overflow-auto flex flex-col">
|
|
48
|
+
<EditorContent apiKey={apiKey}/>
|
|
49
|
+
</div>
|
|
50
|
+
</main>);
|
|
51
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React from "react";
|
|
4
|
-
import { Comment } from "./toolbar/extensions/Comment";
|
|
5
|
-
import { useEffect, useRef } from "react";
|
|
3
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
6
4
|
import {
|
|
7
5
|
useEditor,
|
|
8
6
|
EditorContent as TiptapEditorContent,
|
|
9
|
-
Editor,
|
|
10
7
|
} from "@tiptap/react";
|
|
11
8
|
|
|
12
9
|
import Document from "@tiptap/extension-document";
|
|
@@ -22,41 +19,40 @@ import Blockquote from "@tiptap/extension-blockquote";
|
|
|
22
19
|
import HardBreak from "@tiptap/extension-hard-break";
|
|
23
20
|
import Heading from "@tiptap/extension-heading";
|
|
24
21
|
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
|
25
|
-
|
|
26
22
|
import TextAlign from "@tiptap/extension-text-align";
|
|
27
23
|
import Color from "@tiptap/extension-color";
|
|
28
24
|
import Highlight from "@tiptap/extension-highlight";
|
|
29
25
|
import Image from "@tiptap/extension-image";
|
|
30
26
|
import Link from "@tiptap/extension-link";
|
|
31
27
|
import TextStyle from "@tiptap/extension-text-style";
|
|
32
|
-
|
|
33
28
|
import ListItem from "@tiptap/extension-list-item";
|
|
34
29
|
import BulletList from "@tiptap/extension-bullet-list";
|
|
35
30
|
import OrderedList from "@tiptap/extension-ordered-list";
|
|
36
|
-
import { Subscript } from "./toolbar/marks/Subscript";
|
|
37
|
-
import { Superscript } from "./toolbar/marks/Superscript";
|
|
38
|
-
|
|
39
|
-
import { ResizableTable } from "./toolbar/extensions/ResizableTable";
|
|
40
|
-
import { Embed } from "./toolbar/extensions/Embed";
|
|
41
31
|
import TableRow from "@tiptap/extension-table-row";
|
|
42
32
|
import TableCell from "@tiptap/extension-table-cell";
|
|
43
33
|
import TableHeader from "@tiptap/extension-table-header";
|
|
34
|
+
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
|
35
|
+
|
|
36
|
+
import js from "highlight.js/lib/languages/javascript";
|
|
37
|
+
import ts from "highlight.js/lib/languages/typescript";
|
|
38
|
+
import { createLowlight } from "lowlight";
|
|
39
|
+
|
|
40
|
+
import { useTypo } from "../../utils/useTypo";
|
|
41
|
+
import { Spellcheck } from "./extensions/Spellcheck";
|
|
44
42
|
|
|
43
|
+
import { Comment } from "./toolbar/extensions/Comment";
|
|
44
|
+
import { Subscript } from "./toolbar/marks/Subscript";
|
|
45
|
+
import { Superscript } from "./toolbar/marks/Superscript";
|
|
46
|
+
import { ResizableTable } from "./toolbar/extensions/ResizableTable";
|
|
47
|
+
import { Embed } from "./toolbar/extensions/Embed";
|
|
45
48
|
import { FontFamily } from "./toolbar/extensions/FontFamily";
|
|
46
49
|
import { FontSize } from "./toolbar/extensions/FontSize";
|
|
47
|
-
import TetronsToolbar from "./toolbar/TetronsToolbar";
|
|
48
|
-
|
|
49
50
|
import { ResizableImage } from "./ResizableImage";
|
|
50
51
|
import { ResizableVideo } from "./ResizableVideo";
|
|
51
52
|
import TableContextMenu from "./toolbar/TableContextMenu";
|
|
52
|
-
import
|
|
53
|
-
import { createLowlight } from "lowlight";
|
|
54
|
-
|
|
55
|
-
import js from "highlight.js/lib/languages/javascript";
|
|
56
|
-
import ts from "highlight.js/lib/languages/typescript";
|
|
53
|
+
import TetronsToolbar from "./toolbar/TetronsToolbar";
|
|
57
54
|
|
|
58
55
|
const lowlight = createLowlight();
|
|
59
|
-
|
|
60
56
|
lowlight.register("js", js);
|
|
61
57
|
lowlight.register("ts", ts);
|
|
62
58
|
|
|
@@ -65,16 +61,19 @@ type EditorContentProps = {
|
|
|
65
61
|
};
|
|
66
62
|
|
|
67
63
|
export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
const [
|
|
71
|
-
const [
|
|
64
|
+
const typo = useTypo();
|
|
65
|
+
|
|
66
|
+
const [isValid, setIsValid] = useState<boolean | null>(null);
|
|
67
|
+
const [error, setError] = useState<string | null>(null);
|
|
68
|
+
const [versions, setVersions] = useState<string[]>([]);
|
|
69
|
+
const [userVersion, setUserVersion] = useState<
|
|
72
70
|
"free" | "pro" | "premium" | "platinum" | null
|
|
73
71
|
>(null);
|
|
72
|
+
const [currentVersionIndex, setCurrentVersionIndex] = useState<number | null>(
|
|
73
|
+
null
|
|
74
|
+
);
|
|
74
75
|
|
|
75
|
-
const
|
|
76
|
-
number | null
|
|
77
|
-
>(null);
|
|
76
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
78
77
|
|
|
79
78
|
function getApiBaseUrl(): string {
|
|
80
79
|
if (
|
|
@@ -92,7 +91,7 @@ export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
|
92
91
|
return "https://staging.tetrons.com";
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
const API_BASE_URL = getApiBaseUrl();
|
|
94
|
+
const API_BASE_URL = getApiBaseUrl();
|
|
96
95
|
|
|
97
96
|
useEffect(() => {
|
|
98
97
|
const validateKey = async () => {
|
|
@@ -103,7 +102,7 @@ export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
|
103
102
|
"Content-Type": "application/json",
|
|
104
103
|
},
|
|
105
104
|
body: JSON.stringify({ apiKey }),
|
|
106
|
-
});
|
|
105
|
+
});
|
|
107
106
|
|
|
108
107
|
const data = await res.json();
|
|
109
108
|
if (!res.ok) throw new Error(data.error || "Invalid API Key");
|
|
@@ -111,19 +110,15 @@ export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
|
111
110
|
setIsValid(true);
|
|
112
111
|
setUserVersion(data.version);
|
|
113
112
|
} catch (err: unknown) {
|
|
114
|
-
|
|
115
|
-
setError(err.message || "Invalid API Key");
|
|
116
|
-
} else {
|
|
117
|
-
setError("Invalid API Key");
|
|
118
|
-
}
|
|
113
|
+
setError(err instanceof Error ? err.message : "Invalid API Key");
|
|
119
114
|
setIsValid(false);
|
|
120
115
|
}
|
|
121
116
|
};
|
|
122
117
|
|
|
123
118
|
validateKey();
|
|
124
|
-
}, [apiKey]);
|
|
119
|
+
}, [apiKey, API_BASE_URL]);
|
|
125
120
|
|
|
126
|
-
const editor
|
|
121
|
+
const editor = useEditor({
|
|
127
122
|
extensions: [
|
|
128
123
|
Document,
|
|
129
124
|
Paragraph,
|
|
@@ -138,35 +133,28 @@ export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
|
138
133
|
HardBreak,
|
|
139
134
|
Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }),
|
|
140
135
|
HorizontalRule,
|
|
141
|
-
|
|
142
136
|
TextStyle,
|
|
143
137
|
Color,
|
|
144
138
|
Highlight.configure({ multicolor: true }),
|
|
145
139
|
FontFamily,
|
|
146
140
|
FontSize,
|
|
147
141
|
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
|
148
|
-
|
|
149
142
|
ListItem,
|
|
150
143
|
BulletList,
|
|
151
144
|
OrderedList,
|
|
152
145
|
Subscript,
|
|
153
146
|
Superscript,
|
|
154
|
-
|
|
155
147
|
Image,
|
|
156
148
|
Link.configure({
|
|
157
149
|
openOnClick: false,
|
|
158
150
|
autolink: true,
|
|
159
151
|
linkOnPaste: true,
|
|
160
152
|
}),
|
|
161
|
-
|
|
162
|
-
ResizableTable.configure({
|
|
163
|
-
resizable: true,
|
|
164
|
-
}),
|
|
153
|
+
ResizableTable.configure({ resizable: true }),
|
|
165
154
|
TableRow,
|
|
166
155
|
TableCell,
|
|
167
156
|
TableHeader,
|
|
168
157
|
Embed,
|
|
169
|
-
|
|
170
158
|
ResizableImage,
|
|
171
159
|
ResizableVideo,
|
|
172
160
|
Comment,
|
|
@@ -176,6 +164,13 @@ export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
|
176
164
|
class: "bg-gray-100 p-2 rounded font-mono text-sm overflow-auto",
|
|
177
165
|
},
|
|
178
166
|
}),
|
|
167
|
+
...(typo
|
|
168
|
+
? [
|
|
169
|
+
Spellcheck.configure({
|
|
170
|
+
spellcheckFn: (word: string) => typo.check(word),
|
|
171
|
+
}),
|
|
172
|
+
]
|
|
173
|
+
: []),
|
|
179
174
|
],
|
|
180
175
|
content: "",
|
|
181
176
|
editorProps: {
|
|
@@ -186,9 +181,7 @@ export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
|
186
181
|
},
|
|
187
182
|
immediatelyRender: false,
|
|
188
183
|
});
|
|
189
|
-
|
|
190
|
-
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
191
|
-
|
|
184
|
+
|
|
192
185
|
useEffect(() => {
|
|
193
186
|
return () => {
|
|
194
187
|
editor?.destroy();
|
|
@@ -215,7 +208,7 @@ export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
|
215
208
|
editor.commands.setContent(JSON.parse(versionContent));
|
|
216
209
|
setCurrentVersionIndex(index);
|
|
217
210
|
}
|
|
218
|
-
};
|
|
211
|
+
};
|
|
219
212
|
|
|
220
213
|
if (isValid === false) {
|
|
221
214
|
return <div className="editor-error">⚠️ {error}</div>;
|
|
@@ -225,6 +218,10 @@ export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
|
225
218
|
return <div className="editor-loading">🔍 Validating license...</div>;
|
|
226
219
|
}
|
|
227
220
|
|
|
221
|
+
if (!typo) {
|
|
222
|
+
return <div className="editor-loading">📖 Loading dictionary...</div>;
|
|
223
|
+
}
|
|
224
|
+
|
|
228
225
|
return (
|
|
229
226
|
<div className="editor-container">
|
|
230
227
|
{userVersion !== "free" && (
|
|
@@ -239,22 +236,23 @@ export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
|
239
236
|
</button>
|
|
240
237
|
|
|
241
238
|
<div className="editor-versions-wrapper">
|
|
242
|
-
{versions.length === 0
|
|
239
|
+
{versions.length === 0 ? (
|
|
243
240
|
<span className="editor-no-versions">No saved versions</span>
|
|
241
|
+
) : (
|
|
242
|
+
versions.map((_, idx) => (
|
|
243
|
+
<button
|
|
244
|
+
key={idx}
|
|
245
|
+
type="button"
|
|
246
|
+
onClick={() => restoreVersion(idx)}
|
|
247
|
+
className={`editor-version-btn ${
|
|
248
|
+
idx === currentVersionIndex ? "active" : ""
|
|
249
|
+
}`}
|
|
250
|
+
title={`Restore Version ${idx + 1}`}
|
|
251
|
+
>
|
|
252
|
+
{`V${idx + 1}`}
|
|
253
|
+
</button>
|
|
254
|
+
))
|
|
244
255
|
)}
|
|
245
|
-
{versions.map((_, idx) => (
|
|
246
|
-
<button
|
|
247
|
-
type="button"
|
|
248
|
-
key={idx}
|
|
249
|
-
onClick={() => restoreVersion(idx)}
|
|
250
|
-
className={`editor-version-btn ${
|
|
251
|
-
idx === currentVersionIndex ? "active" : ""
|
|
252
|
-
}`}
|
|
253
|
-
title={`Restore Version ${idx + 1}`}
|
|
254
|
-
>
|
|
255
|
-
{`V${idx + 1}`}
|
|
256
|
-
</button>
|
|
257
|
-
))}
|
|
258
256
|
</div>
|
|
259
257
|
</div>
|
|
260
258
|
)}
|
|
@@ -271,7 +269,7 @@ export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
|
271
269
|
{editor ? (
|
|
272
270
|
<>
|
|
273
271
|
<TiptapEditorContent editor={editor} />
|
|
274
|
-
|
|
272
|
+
<TableContextMenu editor={editor} />
|
|
275
273
|
</>
|
|
276
274
|
) : (
|
|
277
275
|
<div className="editor-loading">Loading editor...</div>
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
useEditor,
|
|
6
|
+
EditorContent as TiptapEditorContent,
|
|
7
|
+
} from "@tiptap/react";
|
|
8
|
+
|
|
9
|
+
import Document from "@tiptap/extension-document";
|
|
10
|
+
import Paragraph from "@tiptap/extension-paragraph";
|
|
11
|
+
import Text from "@tiptap/extension-text";
|
|
12
|
+
import History from "@tiptap/extension-history";
|
|
13
|
+
import Bold from "@tiptap/extension-bold";
|
|
14
|
+
import Italic from "@tiptap/extension-italic";
|
|
15
|
+
import Underline from "@tiptap/extension-underline";
|
|
16
|
+
import Strike from "@tiptap/extension-strike";
|
|
17
|
+
import Code from "@tiptap/extension-code";
|
|
18
|
+
import Blockquote from "@tiptap/extension-blockquote";
|
|
19
|
+
import HardBreak from "@tiptap/extension-hard-break";
|
|
20
|
+
import Heading from "@tiptap/extension-heading";
|
|
21
|
+
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
|
22
|
+
import TextAlign from "@tiptap/extension-text-align";
|
|
23
|
+
import Color from "@tiptap/extension-color";
|
|
24
|
+
import Highlight from "@tiptap/extension-highlight";
|
|
25
|
+
import Image from "@tiptap/extension-image";
|
|
26
|
+
import Link from "@tiptap/extension-link";
|
|
27
|
+
import TextStyle from "@tiptap/extension-text-style";
|
|
28
|
+
import ListItem from "@tiptap/extension-list-item";
|
|
29
|
+
import BulletList from "@tiptap/extension-bullet-list";
|
|
30
|
+
import OrderedList from "@tiptap/extension-ordered-list";
|
|
31
|
+
import TableRow from "@tiptap/extension-table-row";
|
|
32
|
+
import TableCell from "@tiptap/extension-table-cell";
|
|
33
|
+
import TableHeader from "@tiptap/extension-table-header";
|
|
34
|
+
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
|
35
|
+
|
|
36
|
+
import js from "highlight.js/lib/languages/javascript";
|
|
37
|
+
import ts from "highlight.js/lib/languages/typescript";
|
|
38
|
+
import { createLowlight } from "lowlight";
|
|
39
|
+
|
|
40
|
+
import { useTypo } from "../../utils/useTypo";
|
|
41
|
+
import { Spellcheck } from "./extensions/Spellcheck";
|
|
42
|
+
|
|
43
|
+
import { Comment } from "./toolbar/extensions/Comment";
|
|
44
|
+
import { Subscript } from "./toolbar/marks/Subscript";
|
|
45
|
+
import { Superscript } from "./toolbar/marks/Superscript";
|
|
46
|
+
import { ResizableTable } from "./toolbar/extensions/ResizableTable";
|
|
47
|
+
import { Embed } from "./toolbar/extensions/Embed";
|
|
48
|
+
import { FontFamily } from "./toolbar/extensions/FontFamily";
|
|
49
|
+
import { FontSize } from "./toolbar/extensions/FontSize";
|
|
50
|
+
import { ResizableImage } from "./ResizableImage";
|
|
51
|
+
import { ResizableVideo } from "./ResizableVideo";
|
|
52
|
+
import TableContextMenu from "./toolbar/TableContextMenu";
|
|
53
|
+
import TetronsToolbar from "./toolbar/TetronsToolbar";
|
|
54
|
+
|
|
55
|
+
const lowlight = createLowlight();
|
|
56
|
+
lowlight.register("js", js);
|
|
57
|
+
lowlight.register("ts", ts);
|
|
58
|
+
|
|
59
|
+
type EditorContentProps = {
|
|
60
|
+
apiKey: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default function EditorContent({ apiKey }: EditorContentProps) {
|
|
64
|
+
const typo = useTypo();
|
|
65
|
+
|
|
66
|
+
const [isValid, setIsValid] = useState<boolean | null>(null);
|
|
67
|
+
const [error, setError] = useState<string | null>(null);
|
|
68
|
+
const [versions, setVersions] = useState<string[]>([]);
|
|
69
|
+
const [userVersion, setUserVersion] = useState<
|
|
70
|
+
"free" | "pro" | "premium" | "platinum" | null
|
|
71
|
+
>(null);
|
|
72
|
+
const [currentVersionIndex, setCurrentVersionIndex] = useState<number | null>(
|
|
73
|
+
null
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
77
|
+
|
|
78
|
+
function getApiBaseUrl(): string {
|
|
79
|
+
if (
|
|
80
|
+
typeof import.meta !== "undefined" &&
|
|
81
|
+
import.meta.env?.VITE_TETRONS_API_URL
|
|
82
|
+
) {
|
|
83
|
+
return import.meta.env.VITE_TETRONS_API_URL;
|
|
84
|
+
}
|
|
85
|
+
if (
|
|
86
|
+
typeof process !== "undefined" &&
|
|
87
|
+
process.env?.NEXT_PUBLIC_TETRONS_API_URL
|
|
88
|
+
) {
|
|
89
|
+
return process.env.NEXT_PUBLIC_TETRONS_API_URL;
|
|
90
|
+
}
|
|
91
|
+
return "https://staging.tetrons.com";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const API_BASE_URL = getApiBaseUrl();
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const validateKey = async () => {
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(`${API_BASE_URL}/api/validate`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({ apiKey }),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const data = await res.json();
|
|
108
|
+
if (!res.ok) throw new Error(data.error || "Invalid API Key");
|
|
109
|
+
|
|
110
|
+
setIsValid(true);
|
|
111
|
+
setUserVersion(data.version);
|
|
112
|
+
} catch (err: unknown) {
|
|
113
|
+
setError(err instanceof Error ? err.message : "Invalid API Key");
|
|
114
|
+
setIsValid(false);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
validateKey();
|
|
119
|
+
}, [apiKey, API_BASE_URL]);
|
|
120
|
+
|
|
121
|
+
const editor = useEditor({
|
|
122
|
+
extensions: [
|
|
123
|
+
Document,
|
|
124
|
+
Paragraph,
|
|
125
|
+
Text,
|
|
126
|
+
History,
|
|
127
|
+
Bold,
|
|
128
|
+
Italic,
|
|
129
|
+
Underline,
|
|
130
|
+
Strike,
|
|
131
|
+
Code,
|
|
132
|
+
Blockquote,
|
|
133
|
+
HardBreak,
|
|
134
|
+
Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }),
|
|
135
|
+
HorizontalRule,
|
|
136
|
+
TextStyle,
|
|
137
|
+
Color,
|
|
138
|
+
Highlight.configure({ multicolor: true }),
|
|
139
|
+
FontFamily,
|
|
140
|
+
FontSize,
|
|
141
|
+
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
|
142
|
+
ListItem,
|
|
143
|
+
BulletList,
|
|
144
|
+
OrderedList,
|
|
145
|
+
Subscript,
|
|
146
|
+
Superscript,
|
|
147
|
+
Image,
|
|
148
|
+
Link.configure({
|
|
149
|
+
openOnClick: false,
|
|
150
|
+
autolink: true,
|
|
151
|
+
linkOnPaste: true,
|
|
152
|
+
}),
|
|
153
|
+
ResizableTable.configure({ resizable: true }),
|
|
154
|
+
TableRow,
|
|
155
|
+
TableCell,
|
|
156
|
+
TableHeader,
|
|
157
|
+
Embed,
|
|
158
|
+
ResizableImage,
|
|
159
|
+
ResizableVideo,
|
|
160
|
+
Comment,
|
|
161
|
+
CodeBlockLowlight.configure({
|
|
162
|
+
lowlight,
|
|
163
|
+
HTMLAttributes: {
|
|
164
|
+
class: "bg-gray-100 p-2 rounded font-mono text-sm overflow-auto",
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
...(typo
|
|
168
|
+
? [
|
|
169
|
+
Spellcheck.configure({
|
|
170
|
+
spellcheckFn: (word: string) => typo.check(word),
|
|
171
|
+
}),
|
|
172
|
+
]
|
|
173
|
+
: []),
|
|
174
|
+
],
|
|
175
|
+
content: "",
|
|
176
|
+
editorProps: {
|
|
177
|
+
attributes: {
|
|
178
|
+
class: "min-h-full focus:outline-none p-0",
|
|
179
|
+
"data-placeholder": "Start typing here...",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
immediatelyRender: false,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
return () => {
|
|
187
|
+
editor?.destroy();
|
|
188
|
+
};
|
|
189
|
+
}, [editor]);
|
|
190
|
+
|
|
191
|
+
const handleEditorClick = () => {
|
|
192
|
+
if (editor && !editor.isFocused) {
|
|
193
|
+
editor.commands.focus();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const saveVersion = () => {
|
|
198
|
+
if (!editor) return;
|
|
199
|
+
const content = editor.getJSON();
|
|
200
|
+
setVersions((prev) => [...prev, JSON.stringify(content)]);
|
|
201
|
+
setCurrentVersionIndex(versions.length);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const restoreVersion = (index: number) => {
|
|
205
|
+
if (!editor) return;
|
|
206
|
+
const versionContent = versions[index];
|
|
207
|
+
if (versionContent) {
|
|
208
|
+
editor.commands.setContent(JSON.parse(versionContent));
|
|
209
|
+
setCurrentVersionIndex(index);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
if (isValid === false) {
|
|
214
|
+
return <div className="editor-error">⚠️ {error}</div>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (isValid === null) {
|
|
218
|
+
return <div className="editor-loading">🔍 Validating license...</div>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!typo) {
|
|
222
|
+
return <div className="editor-loading">📖 Loading dictionary...</div>;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div className="editor-container">
|
|
227
|
+
{userVersion !== "free" && (
|
|
228
|
+
<div className="editor-toolbar">
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onClick={saveVersion}
|
|
232
|
+
disabled={!editor}
|
|
233
|
+
className="editor-save-btn"
|
|
234
|
+
>
|
|
235
|
+
Save Version
|
|
236
|
+
</button>
|
|
237
|
+
|
|
238
|
+
<div className="editor-versions-wrapper">
|
|
239
|
+
{versions.length === 0 ? (
|
|
240
|
+
<span className="editor-no-versions">No saved versions</span>
|
|
241
|
+
) : (
|
|
242
|
+
versions.map((_, idx) => (
|
|
243
|
+
<button
|
|
244
|
+
key={idx}
|
|
245
|
+
type="button"
|
|
246
|
+
onClick={() => restoreVersion(idx)}
|
|
247
|
+
className={`editor-version-btn ${
|
|
248
|
+
idx === currentVersionIndex ? "active" : ""
|
|
249
|
+
}`}
|
|
250
|
+
title={`Restore Version ${idx + 1}`}
|
|
251
|
+
>
|
|
252
|
+
{`V${idx + 1}`}
|
|
253
|
+
</button>
|
|
254
|
+
))
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
{editor && userVersion && (
|
|
261
|
+
<TetronsToolbar editor={editor} version={userVersion} />
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
<div
|
|
265
|
+
ref={wrapperRef}
|
|
266
|
+
className="editor-content-wrapper"
|
|
267
|
+
onClick={handleEditorClick}
|
|
268
|
+
>
|
|
269
|
+
{editor ? (
|
|
270
|
+
<>
|
|
271
|
+
<TiptapEditorContent editor={editor} />
|
|
272
|
+
<TableContextMenu editor={editor} />
|
|
273
|
+
</>
|
|
274
|
+
) : (
|
|
275
|
+
<div className="editor-loading">Loading editor...</div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Image from "@tiptap/extension-image";
|
|
2
|
+
import { ReactNodeViewRenderer } from "@tiptap/react";
|
|
3
|
+
import ResizableImageComponent from "./ResizableImageComponent";
|
|
4
|
+
|
|
5
|
+
export const ResizableImage = Image.extend({
|
|
6
|
+
name: "resizableImage",
|
|
7
|
+
|
|
8
|
+
addAttributes() {
|
|
9
|
+
return {
|
|
10
|
+
...this.parent?.(),
|
|
11
|
+
width: {
|
|
12
|
+
default: null,
|
|
13
|
+
},
|
|
14
|
+
height: {
|
|
15
|
+
default: null,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
renderHTML({ HTMLAttributes }) {
|
|
21
|
+
const { width, height, ...rest } = HTMLAttributes;
|
|
22
|
+
const style = [];
|
|
23
|
+
|
|
24
|
+
if (width) style.push(`width: ${width}px`);
|
|
25
|
+
if (height) style.push(`height: ${height}px`);
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
"img",
|
|
29
|
+
{
|
|
30
|
+
...rest,
|
|
31
|
+
style: style.join("; "),
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
addNodeView() {
|
|
37
|
+
return ReactNodeViewRenderer(ResizableImageComponent);
|
|
38
|
+
},
|
|
39
|
+
});
|