Download src/components/RichTextEditor.tsx from sitcod3/modul-s: direct link, hf CLI and curl.
- Browser
- Download file 5.71 kB
-
https://huggingface.co/spaces/sitcod3/modul-s/resolve/main/src/components/RichTextEditor.tsx
- Command line
-
hf download hf://spaces/sitcod3/modul-s/src/components/RichTextEditor.tsx
-
curl -L -o RichTextEditor.tsx https://huggingface.co/spaces/sitcod3/modul-s/resolve/main/src/components/RichTextEditor.tsx
5.71 kB
| import { useEditor, EditorContent } from "@tiptap/react"; | |
| import StarterKit from "@tiptap/starter-kit"; | |
| import { Bold, Italic, List, ListOrdered, Code, Undo, Redo, Sparkles } from "lucide-react"; | |
| import { useEffect } from "react"; | |
| import { cn } from "../utils/cn"; | |
| interface RichTextEditorProps { | |
| content: string; | |
| onChange: (html: string) => void; | |
| placeholder?: string; | |
| variables?: string[]; | |
| } | |
| export default function RichTextEditor({ | |
| content, | |
| onChange, | |
| variables = ["first_name", "last_name", "company_name", "website", "location"], | |
| }: RichTextEditorProps) { | |
| const editor = useEditor({ | |
| extensions: [StarterKit], | |
| content, | |
| editorProps: { | |
| attributes: { | |
| class: | |
| "prose prose-invert max-w-none focus:outline-none min-h-[160px] p-3 text-sm text-zinc-200 leading-relaxed", | |
| }, | |
| }, | |
| onUpdate: ({ editor }) => { | |
| onChange(editor.getHTML()); | |
| }, | |
| }); | |
| useEffect(() => { | |
| if (editor && content !== editor.getHTML()) { | |
| editor.commands.setContent(content); | |
| } | |
| }, [content, editor]); | |
| if (!editor) return null; | |
| const insertVariable = (variable: string) => { | |
| editor.commands.insertContent(`{{${variable}}}`); | |
| }; | |
| return ( | |
| <div className="border border-zinc-800 rounded-xl overflow-hidden bg-zinc-950/80 focus-within:border-indigo-500/80 transition-colors"> | |
| {/* Toolbar */} | |
| <div className="flex flex-wrap items-center justify-between gap-1 p-2 border-b border-zinc-800/80 bg-zinc-900/60"> | |
| <div className="flex items-center gap-1"> | |
| <button | |
| type="button" | |
| onClick={() => editor.chain().focus().toggleBold().run()} | |
| className={cn( | |
| "p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors", | |
| editor.isActive("bold") ? "bg-zinc-800 text-indigo-400 font-bold" : "" | |
| )} | |
| title="Bold" | |
| > | |
| <Bold className="w-4 h-4" /> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => editor.chain().focus().toggleItalic().run()} | |
| className={cn( | |
| "p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors", | |
| editor.isActive("italic") ? "bg-zinc-800 text-indigo-400" : "" | |
| )} | |
| title="Italic" | |
| > | |
| <Italic className="w-4 h-4" /> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => editor.chain().focus().toggleBulletList().run()} | |
| className={cn( | |
| "p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors", | |
| editor.isActive("bulletList") ? "bg-zinc-800 text-indigo-400" : "" | |
| )} | |
| title="Bullet List" | |
| > | |
| <List className="w-4 h-4" /> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => editor.chain().focus().toggleOrderedList().run()} | |
| className={cn( | |
| "p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors", | |
| editor.isActive("orderedList") ? "bg-zinc-800 text-indigo-400" : "" | |
| )} | |
| title="Numbered List" | |
| > | |
| <ListOrdered className="w-4 h-4" /> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => editor.chain().focus().toggleCodeBlock().run()} | |
| className={cn( | |
| "p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors", | |
| editor.isActive("codeBlock") ? "bg-zinc-800 text-indigo-400" : "" | |
| )} | |
| title="Code Block" | |
| > | |
| <Code className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| <div className="flex items-center gap-1"> | |
| <button | |
| type="button" | |
| onClick={() => editor.chain().focus().undo().run()} | |
| disabled={!editor.can().undo()} | |
| className="p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 disabled:opacity-30" | |
| > | |
| <Undo className="w-4 h-4" /> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => editor.chain().focus().redo().run()} | |
| disabled={!editor.can().redo()} | |
| className="p-1.5 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 disabled:opacity-30" | |
| > | |
| <Redo className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Variables Bar */} | |
| {variables.length > 0 && ( | |
| <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"> | |
| <span className="text-zinc-500 flex items-center gap-1 shrink-0"> | |
| <Sparkles className="w-3 h-3 text-indigo-400" /> Insert tag: | |
| </span> | |
| <div className="flex items-center gap-1.5"> | |
| {variables.map((v) => ( | |
| <button | |
| key={v} | |
| type="button" | |
| onClick={() => insertVariable(v)} | |
| 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" | |
| > | |
| {{{v}}} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Editor Surface */} | |
| <EditorContent editor={editor} /> | |
| </div> | |
| ); | |
| } | |