Skip to content

Commit e5bb175

Browse files
committed
chore: adjust importing file text-editor
1 parent 86f863e commit e5bb175

4 files changed

Lines changed: 187 additions & 184 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"use client";
2+
3+
import { useEditor, EditorContent } from "@tiptap/react";
4+
import StarterKit from "@tiptap/starter-kit";
5+
import Image from "@tiptap/extension-image";
6+
import Underline from "@tiptap/extension-underline";
7+
import Typography from "@tiptap/extension-typography";
8+
import Link from "@tiptap/extension-link";
9+
import Placeholder from "@tiptap/extension-placeholder";
10+
import { useState, useCallback, useMemo } from "react";
11+
import TurndownService from "turndown";
12+
13+
import { Button } from "@/components/ui/Button";
14+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
15+
import { Toolbar } from "@/components/common/TextEditor";
16+
17+
const TextEditor = () => {
18+
const [markdownContent, setMarkdownContent] = useState("");
19+
20+
const turndownService = useMemo(() => {
21+
const service = new TurndownService({
22+
headingStyle: "atx",
23+
codeBlockStyle: "fenced",
24+
});
25+
26+
// Custom rule for underline tags
27+
service.addRule("underline", {
28+
filter: "u",
29+
replacement: (content) => `<u>${content}</u>`,
30+
});
31+
32+
return service;
33+
}, []);
34+
35+
const editor = useEditor({
36+
extensions: [
37+
StarterKit,
38+
Underline,
39+
Typography,
40+
Placeholder.configure({
41+
placeholder: "Start typing your content here... Use the toolbar above to format your text.",
42+
}),
43+
Link.configure({
44+
openOnClick: false,
45+
HTMLAttributes: {
46+
class: "text-primary underline cursor-pointer",
47+
},
48+
}),
49+
Image.configure({
50+
inline: false,
51+
allowBase64: true,
52+
}),
53+
],
54+
content: "",
55+
immediatelyRender: false,
56+
onCreate: ({ editor }) => {
57+
const html = editor.getHTML();
58+
setMarkdownContent(turndownService.turndown(html));
59+
},
60+
onUpdate: ({ editor }) => {
61+
const html = editor.getHTML();
62+
setMarkdownContent(turndownService.turndown(html));
63+
},
64+
editorProps: {
65+
attributes: {
66+
class:
67+
"prose dark:prose-invert max-w-none mx-auto focus:outline-none min-h-[300px] p-3 prose-blockquote:border-primary prose-blockquote:bg-muted/50 prose-blockquote:pl-4 prose-blockquote:py-1 prose-blockquote:before:content-none prose-blockquote:not-italic prose-code:bg-muted prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-pre:bg-muted prose-pre:border prose-pre:text-foreground",
68+
},
69+
},
70+
});
71+
72+
const addImage = useCallback(() => {
73+
const url = window.prompt("Enter image URL:");
74+
75+
if (url && editor) {
76+
editor.chain().focus().setImage({ src: url }).run();
77+
}
78+
}, [editor]);
79+
80+
const addImageFromFile = useCallback(() => {
81+
const input = document.createElement("input");
82+
input.type = "file";
83+
input.accept = "image/*";
84+
85+
input.onchange = (e) => {
86+
const file = (e.target as HTMLInputElement).files?.[0];
87+
88+
if (file && editor) {
89+
const reader = new FileReader();
90+
reader.onload = (e) => {
91+
const src = e.target?.result as string;
92+
editor.chain().focus().setImage({ src }).run();
93+
};
94+
reader.readAsDataURL(file);
95+
}
96+
};
97+
98+
input.click();
99+
}, [editor]);
100+
101+
const downloadMarkdown = useCallback(() => {
102+
const blob = new Blob([markdownContent], { type: "text/markdown" });
103+
const url = URL.createObjectURL(blob);
104+
const a = document.createElement("a");
105+
a.href = url;
106+
a.download = "document.md";
107+
document.body.appendChild(a);
108+
a.click();
109+
document.body.removeChild(a);
110+
URL.revokeObjectURL(url);
111+
}, [markdownContent]);
112+
113+
const addLink = useCallback(() => {
114+
if (!editor) return;
115+
116+
const previousUrl = editor.getAttributes("link").href;
117+
const url = window.prompt("Enter URL", previousUrl || "");
118+
119+
if (url === null) {
120+
return;
121+
}
122+
123+
if (url === "") {
124+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
125+
return;
126+
}
127+
128+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
129+
}, [editor]);
130+
131+
if (!editor) {
132+
return (
133+
<div className="flex h-64 items-center justify-center">
134+
<div className="border-primary h-8 w-8 animate-spin rounded-full border-b-2"></div>
135+
</div>
136+
);
137+
}
138+
139+
return (
140+
<div className="mx-auto max-w-6xl space-y-6 p-6">
141+
<Card>
142+
<CardHeader className="p-1">
143+
<Toolbar
144+
editor={editor}
145+
onAddImage={addImage}
146+
onAddImageFromFile={addImageFromFile}
147+
onAddLink={addLink}
148+
onDownloadMarkdown={downloadMarkdown}
149+
isDownloadDisabled={!markdownContent}
150+
/>
151+
</CardHeader>
152+
153+
<CardContent className="p-0">
154+
<div className="max-h-[500px] overflow-y-auto border-t">
155+
<EditorContent editor={editor} className="min-h-[200px] focus-within:outline-none" />
156+
</div>
157+
</CardContent>
158+
</Card>
159+
<Card>
160+
<CardHeader>
161+
<CardTitle className="text-xl">Markdown Output</CardTitle>
162+
</CardHeader>
163+
<CardContent>
164+
<div className="relative">
165+
<pre className="bg-muted max-h-64 overflow-x-auto overflow-y-auto rounded-lg border p-4 text-sm">
166+
<code className="text-muted-foreground">{markdownContent || "No content yet..."}</code>
167+
</pre>
168+
<Button
169+
onClick={() => navigator.clipboard.writeText(markdownContent)}
170+
variant="outline"
171+
size="sm"
172+
className="absolute top-2 right-2"
173+
>
174+
Copy
175+
</Button>
176+
</div>
177+
</CardContent>
178+
</Card>
179+
</div>
180+
);
181+
};
182+
183+
export default TextEditor;

src/components/common/TextEditor/Toolbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
ExternalLink,
2020
} from "lucide-react";
2121
import { Separator } from "@/components/ui/Separator";
22-
import ToolbarButton from "./ToolbarButton";
22+
import { ToolbarButton } from "@/components/common/TextEditor";
2323

2424
interface ToolbarProps {
2525
editor: Editor;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default } from "./TextEditor";
2+
export { default as Toolbar } from "./Toolbar";
3+
export { default as ToolbarButton } from "./ToolbarButton";

src/components/common/TextEditor/index.tsx

Lines changed: 0 additions & 183 deletions
This file was deleted.

0 commit comments

Comments
 (0)