Kuma/src/components/layout/RichTextEditor.tsx
2025-12-07 14:25:22 +01:00

203 lines
5.5 KiB
TypeScript

"use client";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import Underline from "@tiptap/extension-underline";
import { Label } from "../ui/label";
import {
Bold,
Italic,
Underline as UnderlineIcon,
List,
ListOrdered,
Heading1,
Heading2,
Heading3,
} from "lucide-react";
interface RichTextEditorProps {
label: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
error?: string;
}
const RichTextEditor: React.FC<RichTextEditorProps> = ({
label,
value,
onChange,
placeholder = "Text eingeben...",
error,
}) => {
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Placeholder.configure({
placeholder,
}),
],
content: value,
immediatelyRender: false,
onUpdate: ({ editor }) => {
onChange(editor.getHTML());
},
editorProps: {
attributes: {
class: "focus:outline-none min-h-[150px] p-4",
},
},
});
if (!editor) {
return null;
}
const MenuButton = ({
onClick,
active,
children,
}: {
onClick: () => void;
active: boolean;
children: React.ReactNode;
}) => (
<button
onClick={onClick}
type="button"
className={`p-2 rounded hover:bg-gray-100 transition-colors ${
active ? "bg-blue/10 text-blue" : "text-gray-600"
}`}
>
{children}
</button>
);
return (
<div className="w-full">
<Label className="p-1 mb-2 text-blue text-lg font-semibold">{label}</Label>
<style jsx global>{`
.editor-content .ProseMirror {
outline: none;
}
.editor-content h1 {
font-size: 2em;
font-weight: bold;
margin-top: 0.5em;
margin-bottom: 0.5em;
line-height: 1.2;
}
.editor-content h2 {
font-size: 1.5em;
font-weight: bold;
margin-top: 0.5em;
margin-bottom: 0.5em;
line-height: 1.3;
}
.editor-content h3 {
font-size: 1.25em;
font-weight: bold;
margin-top: 0.5em;
margin-bottom: 0.5em;
line-height: 1.4;
}
.editor-content ul {
list-style-type: disc;
padding-left: 1.5em;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.editor-content ol {
list-style-type: decimal;
padding-left: 1.5em;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.editor-content li {
margin-bottom: 0.25em;
}
.editor-content strong {
font-weight: bold;
}
.editor-content em {
font-style: italic;
}
.editor-content u {
text-decoration: underline;
}
.editor-content p {
margin-bottom: 0.5em;
}
.editor-content .ProseMirror p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
`}</style>
<div className="border border-blue rounded-xl overflow-hidden">
{/* Toolbar */}
<div className="flex gap-1 p-2 border-b border-gray-200 bg-gray-50 flex-wrap">
<MenuButton
onClick={() => editor.chain().focus().toggleBold().run()}
active={editor.isActive("bold")}
>
<Bold size={18} />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleItalic().run()}
active={editor.isActive("italic")}
>
<Italic size={18} />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleUnderline().run()}
active={editor.isActive("underline")}
>
<UnderlineIcon size={18} />
</MenuButton>
<div className="w-px h-6 bg-gray-300 mx-1 self-center" />
<MenuButton
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
active={editor.isActive("heading", { level: 1 })}
>
<Heading1 size={18} />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
active={editor.isActive("heading", { level: 2 })}
>
<Heading2 size={18} />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
active={editor.isActive("heading", { level: 3 })}
>
<Heading3 size={18} />
</MenuButton>
<div className="w-px h-6 bg-gray-300 mx-1 self-center" />
<MenuButton
onClick={() => editor.chain().focus().toggleBulletList().run()}
active={editor.isActive("bulletList")}
>
<List size={18} />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleOrderedList().run()}
active={editor.isActive("orderedList")}
>
<ListOrdered size={18} />
</MenuButton>
</div>
{/* Editor Content */}
<EditorContent editor={editor} className="bg-white editor-content" />
</div>
{error && <p className="text-red-500 text-sm mt-1">{error}</p>}
</div>
);
};
export default RichTextEditor;