2026-02-01 22:31:00 -06:00
|
|
|
import React, { useEffect } from "react";
|
2026-01-27 18:07:00 -06:00
|
|
|
import { useEditor, EditorContent } from "@tiptap/react";
|
|
|
|
|
import StarterKit from "@tiptap/starter-kit";
|
|
|
|
|
import Placeholder from "@tiptap/extension-placeholder";
|
|
|
|
|
import {
|
|
|
|
|
Bold,
|
|
|
|
|
Italic,
|
|
|
|
|
List,
|
|
|
|
|
ListOrdered,
|
|
|
|
|
Heading2,
|
|
|
|
|
Quote,
|
|
|
|
|
Undo,
|
|
|
|
|
Redo,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
import { Button } from "./ui/button";
|
|
|
|
|
|
|
|
|
|
const MenuBar = ({ editor }) => {
|
|
|
|
|
if (!editor) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="border-b border-border p-2 flex flex-wrap gap-1">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant={editor.isActive("bold") ? "default" : "outline"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
|
|
|
>
|
|
|
|
|
<Bold className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant={editor.isActive("italic") ? "default" : "outline"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
|
|
|
>
|
|
|
|
|
<Italic className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant={
|
|
|
|
|
editor.isActive("heading", { level: 2 }) ? "default" : "outline"
|
|
|
|
|
}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
|
|
|
>
|
|
|
|
|
<Heading2 className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant={editor.isActive("bulletList") ? "default" : "outline"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
|
|
|
>
|
|
|
|
|
<List className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant={editor.isActive("orderedList") ? "default" : "outline"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
|
|
|
>
|
|
|
|
|
<ListOrdered className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant={editor.isActive("blockquote") ? "default" : "outline"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
|
|
|
|
>
|
|
|
|
|
<Quote className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<div className="w-px h-6 bg-border mx-1" />
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => editor.chain().focus().undo().run()}
|
|
|
|
|
disabled={!editor.can().undo()}
|
|
|
|
|
>
|
|
|
|
|
<Undo className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => editor.chain().focus().redo().run()}
|
|
|
|
|
disabled={!editor.can().redo()}
|
|
|
|
|
>
|
|
|
|
|
<Redo className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const RichTextEditor = ({
|
2026-02-01 22:31:00 -06:00
|
|
|
value,
|
2026-01-27 18:07:00 -06:00
|
|
|
content,
|
|
|
|
|
onChange,
|
|
|
|
|
placeholder = "Enter description...",
|
|
|
|
|
}) => {
|
2026-02-01 22:31:00 -06:00
|
|
|
// Support both 'value' and 'content' props for flexibility
|
|
|
|
|
const initialContent = value || content || "";
|
|
|
|
|
|
2026-01-27 18:07:00 -06:00
|
|
|
const editor = useEditor({
|
|
|
|
|
extensions: [
|
|
|
|
|
StarterKit,
|
|
|
|
|
Placeholder.configure({
|
|
|
|
|
placeholder,
|
|
|
|
|
}),
|
|
|
|
|
],
|
2026-02-01 22:31:00 -06:00
|
|
|
content: initialContent,
|
2026-01-27 18:07:00 -06:00
|
|
|
onUpdate: ({ editor }) => {
|
|
|
|
|
onChange(editor.getHTML());
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-01 22:31:00 -06:00
|
|
|
// Update editor content when value/content prop changes externally
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (editor && initialContent !== undefined) {
|
|
|
|
|
const currentContent = editor.getHTML();
|
|
|
|
|
// Only update if the content is actually different (prevents cursor jump)
|
|
|
|
|
if (currentContent !== initialContent && initialContent !== "<p></p>") {
|
|
|
|
|
editor.commands.setContent(initialContent);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [editor, initialContent]);
|
|
|
|
|
|
2026-01-27 18:07:00 -06:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="border border-border rounded-md overflow-hidden resize-y min-h-[240px] max-h-[600px]"
|
|
|
|
|
style={{ overflow: "auto" }}
|
|
|
|
|
>
|
|
|
|
|
<MenuBar editor={editor} />
|
|
|
|
|
<div className="overflow-y-auto custom-scrollbar h-full">
|
|
|
|
|
<EditorContent
|
|
|
|
|
editor={editor}
|
|
|
|
|
className="prose prose-sm max-w-none p-4 min-h-[180px] focus:outline-none"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default RichTextEditor;
|