sitcod3 commited on
Commit
f8ed3f8
·
verified ·
1 Parent(s): 1e394ce

Upload src/components/RichTextEditor.tsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/components/RichTextEditor.tsx +152 -0
src/components/RichTextEditor.tsx ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEditor, EditorContent } from "@tiptap/react";
2
+ import StarterKit from "@tiptap/starter-kit";
3
+ import { Bold, Italic, List, ListOrdered, Code, Undo, Redo, Sparkles } from "lucide-react";
4
+ import { useEffect } from "react";
5
+ import { cn } from "../utils/cn";
6
+
7
+ interface RichTextEditorProps {
8
+ content: string;
9
+ onChange: (html: string) => void;
10
+ placeholder?: string;
11
+ variables?: string[];
12
+ }
13
+
14
+ export default function RichTextEditor({
15
+ content,
16
+ onChange,
17
+ variables = ["first_name", "last_name", "company_name", "website", "location"],
18
+ }: RichTextEditorProps) {
19
+ const editor = useEditor({
20
+ extensions: [StarterKit],
21
+ content,
22
+ editorProps: {
23
+ attributes: {
24
+ class:
25
+ "prose prose-invert max-w-none focus:outline-none min-h-[160px] p-3 text-sm text-zinc-200 leading-relaxed",
26
+ },
27
+ },
28
+ onUpdate: ({ editor }) => {
29
+ onChange(editor.getHTML());
30
+ },
31
+ });
32
+
33
+ useEffect(() => {
34
+ if (editor && content !== editor.getHTML()) {
35
+ editor.commands.setContent(content);
36
+ }
37
+ }, [content, editor]);
38
+
39
+ if (!editor) return null;
40
+
41
+ const insertVariable = (variable: string) => {
42
+ editor.commands.insertContent(`{{${variable}}}`);
43
+ };
44
+
45
+ return (
46
+ <div className="border border-zinc-800 rounded-xl overflow-hidden bg-zinc-950/80 focus-within:border-indigo-500/80 transition-colors">
47
+ {/* Toolbar */}
48
+ <div className="flex flex-wrap items-center justify-between gap-1 p-2 border-b border-zinc-800/80 bg-zinc-900/60">
49
+ <div className="flex items-center gap-1">
50
+ <button
51
+ type="button"
52
+ onClick={() => editor.chain().focus().toggleBold().run()}
53
+ className={cn(
54
+ "p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors",
55
+ editor.isActive("bold") ? "bg-zinc-800 text-indigo-400 font-bold" : ""
56
+ )}
57
+ title="Bold"
58
+ >
59
+ <Bold className="w-4 h-4" />
60
+ </button>
61
+ <button
62
+ type="button"
63
+ onClick={() => editor.chain().focus().toggleItalic().run()}
64
+ className={cn(
65
+ "p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors",
66
+ editor.isActive("italic") ? "bg-zinc-800 text-indigo-400" : ""
67
+ )}
68
+ title="Italic"
69
+ >
70
+ <Italic className="w-4 h-4" />
71
+ </button>
72
+ <button
73
+ type="button"
74
+ onClick={() => editor.chain().focus().toggleBulletList().run()}
75
+ className={cn(
76
+ "p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors",
77
+ editor.isActive("bulletList") ? "bg-zinc-800 text-indigo-400" : ""
78
+ )}
79
+ title="Bullet List"
80
+ >
81
+ <List className="w-4 h-4" />
82
+ </button>
83
+ <button
84
+ type="button"
85
+ onClick={() => editor.chain().focus().toggleOrderedList().run()}
86
+ className={cn(
87
+ "p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors",
88
+ editor.isActive("orderedList") ? "bg-zinc-800 text-indigo-400" : ""
89
+ )}
90
+ title="Numbered List"
91
+ >
92
+ <ListOrdered className="w-4 h-4" />
93
+ </button>
94
+ <button
95
+ type="button"
96
+ onClick={() => editor.chain().focus().toggleCodeBlock().run()}
97
+ className={cn(
98
+ "p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors",
99
+ editor.isActive("codeBlock") ? "bg-zinc-800 text-indigo-400" : ""
100
+ )}
101
+ title="Code Block"
102
+ >
103
+ <Code className="w-4 h-4" />
104
+ </button>
105
+ </div>
106
+
107
+ <div className="flex items-center gap-1">
108
+ <button
109
+ type="button"
110
+ onClick={() => editor.chain().focus().undo().run()}
111
+ disabled={!editor.can().undo()}
112
+ className="p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 disabled:opacity-30"
113
+ >
114
+ <Undo className="w-4 h-4" />
115
+ </button>
116
+ <button
117
+ type="button"
118
+ onClick={() => editor.chain().focus().redo().run()}
119
+ disabled={!editor.can().redo()}
120
+ className="p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 disabled:opacity-30"
121
+ >
122
+ <Redo className="w-4 h-4" />
123
+ </button>
124
+ </div>
125
+ </div>
126
+
127
+ {/* Variables Bar */}
128
+ {variables.length > 0 && (
129
+ <div className="px-3 py-1.5 bg-zinc-900/40 border-b border-zinc-800/60 flex items-center gap-2 overflow-x-auto text-xs">
130
+ <span className="text-zinc-500 flex items-center gap-1 shrink-0">
131
+ <Sparkles className="w-3 h-3 text-indigo-400" /> Insert tag:
132
+ </span>
133
+ <div className="flex items-center gap-1.5">
134
+ {variables.map((v) => (
135
+ <button
136
+ key={v}
137
+ type="button"
138
+ onClick={() => insertVariable(v)}
139
+ className="px-2 py-0.5 bg-zinc-800/80 hover:bg-indigo-500/20 hover:text-indigo-300 text-zinc-400 rounded text-[11px] font-mono transition-colors border border-zinc-700/60 shrink-0"
140
+ >
141
+ &#123;&#123;{v}&#125;&#125;
142
+ </button>
143
+ ))}
144
+ </div>
145
+ </div>
146
+ )}
147
+
148
+ {/* Editor Surface */}
149
+ <EditorContent editor={editor} />
150
+ </div>
151
+ );
152
+ }