diff --git a/src/components/RichTextEditor.tsx b/src/components/RichTextEditor.tsx
index 3449a6d..eb746f8 100644
--- a/src/components/RichTextEditor.tsx
+++ b/src/components/RichTextEditor.tsx
@@ -3,11 +3,16 @@ import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
import Image from '@tiptap/extension-image';
import Placeholder from '@tiptap/extension-placeholder';
+import { Node } from '@tiptap/core';
+import Table from '@tiptap/extension-table';
+import TableRow from '@tiptap/extension-table-row';
+import TableCell from '@tiptap/extension-table-cell';
+import TableHeader from '@tiptap/extension-table-header';
import { Button } from '@/components/ui/button';
-import {
- Bold, Italic, List, ListOrdered, Quote, Link as LinkIcon,
+import {
+ Bold, Italic, List, ListOrdered, Quote, Link as LinkIcon,
Image as ImageIcon, Heading1, Heading2, Undo, Redo,
- Maximize2, Minimize2
+ Maximize2, Minimize2, MousePointer, Square, Table as TableIcon
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useCallback, useEffect, useState } from 'react';
@@ -49,6 +54,247 @@ const ResizableImage = Image.extend({
},
});
+// Custom Button extension for email templates
+const EmailButton = Node.create({
+ name: 'emailButton',
+
+ group: 'block',
+
+ addAttributes() {
+ return {
+ url: {
+ default: '#',
+ parseHTML: element => element.getAttribute('data-url') || '#',
+ renderHTML: attributes => ({
+ 'data-url': attributes.url,
+ }),
+ },
+ text: {
+ default: 'Button',
+ parseHTML: element => element.textContent || 'Button',
+ renderHTML: attributes => ({}),
+ },
+ fullWidth: {
+ default: false,
+ parseHTML: element => element.classList.contains('btn-full'),
+ renderHTML: attributes => ({
+ class: attributes.fullWidth ? 'btn btn-full' : 'btn',
+ }),
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: 'div[data-email-button]',
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes, node }) {
+ const { url, text, fullWidth } = node.attrs;
+ return [
+ 'p',
+ { style: 'margin-top: 20px; text-align: center;' },
+ [
+ 'a',
+ {
+ href: url,
+ class: fullWidth ? 'btn btn-full' : 'btn',
+ 'data-email-button': '',
+ style: `
+ display: inline-block;
+ background-color: #000;
+ color: #FFF !important;
+ padding: 14px 28px;
+ font-weight: 700;
+ text-transform: uppercase;
+ text-decoration: none !important;
+ font-size: 16px;
+ border: 2px solid #000;
+ box-shadow: 4px 4px 0px 0px #000000;
+ margin: 10px 0;
+ transition: all 0.1s;
+ text-align: center;
+ ${fullWidth ? 'width: 100%; box-sizing: border-box;' : ''}
+ `,
+ },
+ text || 'Button',
+ ],
+ ];
+ },
+
+ addNodeView() {
+ return ({ node, editor }) => {
+ const dom = document.createElement('div');
+ dom.style.cssText = 'margin: 10px 0; border: 2px dashed #007acc; padding: 8px; border-radius: 4px; background: #f0f9ff;';
+
+ const button = document.createElement('a');
+ button.href = node.attrs.url;
+ button.textContent = node.attrs.text;
+ button.style.cssText = `
+ display: inline-block;
+ background-color: #000;
+ color: #FFF;
+ padding: 14px 28px;
+ font-weight: 700;
+ text-transform: uppercase;
+ text-decoration: none;
+ font-size: 16px;
+ border: 2px solid #000;
+ box-shadow: 4px 4px 0px 0px #000000;
+ cursor: pointer;
+ ${node.attrs.fullWidth ? 'width: 100%; text-align: center; box-sizing: border-box;' : ''}
+ `;
+
+ dom.appendChild(button);
+
+ return {
+ dom,
+ destroy: () => {
+ dom.remove();
+ },
+ };
+ };
+ },
+});
+
+// Custom OTP Box extension
+const OTPBox = Node.create({
+ name: 'otpBox',
+
+ group: 'block',
+
+ addAttributes() {
+ return {
+ code: {
+ default: '123-456',
+ parseHTML: element => element.getAttribute('data-code') || '123-456',
+ renderHTML: attributes => ({
+ 'data-code': attributes.code,
+ }),
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: 'div[data-otp-box]',
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes, node }) {
+ const { code } = node.attrs;
+ return [
+ 'div',
+ {
+ 'data-otp-box': '',
+ style: `
+ background-color: #F4F4F5;
+ border: 2px dashed #000;
+ padding: 20px;
+ text-align: center;
+ margin: 20px 0;
+ letter-spacing: 5px;
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 32px;
+ font-weight: 700;
+ color: #000;
+ `,
+ },
+ code,
+ ];
+ },
+
+ addNodeView() {
+ return ({ node, editor }) => {
+ const dom = document.createElement('div');
+ dom.style.cssText = 'margin: 10px 0; border: 2px dashed #007acc; padding: 8px; border-radius: 4px; background: #f0f9ff;';
+ dom.innerHTML = `
+
OTP Box: ${node.attrs.code}
+ ${node.attrs.code}
+ `;
+
+ return {
+ dom,
+ destroy: () => {
+ dom.remove();
+ },
+ };
+ };
+ },
+});
+
+// Custom Email Table extension
+const EmailTable = Table.extend({
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+ style: {
+ default: 'width: 100%; border: 2px solid #000; margin-bottom: 25px; border-collapse: collapse;',
+ renderHTML: attributes => {
+ return { style: attributes.style };
+ },
+ },
+ };
+ },
+});
+
+const EmailTableRow = TableRow.extend({
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+ style: {
+ default: '',
+ renderHTML: attributes => {
+ return { style: attributes.style };
+ },
+ },
+ };
+ },
+});
+
+const EmailTableCell = TableCell.extend({
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+ style: {
+ default: 'padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;',
+ renderHTML: attributes => {
+ return { style: attributes.style };
+ },
+ },
+ };
+ },
+});
+
+const EmailTableHeader = TableHeader.extend({
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+ style: {
+ default: 'background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;',
+ renderHTML: attributes => {
+ return { style: attributes.style };
+ },
+ },
+ };
+ },
+});
+
export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten...', className }: RichTextEditorProps) {
const [uploading, setUploading] = useState(false);
const [selectedImage, setSelectedImage] = useState<{ src: string; width?: number; height?: number } | null>(null);
@@ -57,7 +303,12 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
const editor = useEditor({
extensions: [
- StarterKit,
+ StarterKit.configure({
+ table: false,
+ tableRow: false,
+ tableCell: false,
+ tableHeader: false,
+ }),
Link.configure({
openOnClick: false,
HTMLAttributes: {
@@ -69,6 +320,17 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
class: 'max-w-full h-auto rounded-md cursor-pointer',
},
}),
+ EmailButton,
+ OTPBox,
+ EmailTable.configure({
+ resizable: true,
+ HTMLAttributes: {
+ class: 'email-table',
+ },
+ }),
+ EmailTableRow,
+ EmailTableCell,
+ EmailTableHeader,
Placeholder.configure({
placeholder,
}),
@@ -110,6 +372,63 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
}
}, [editor]);
+ const addButton = useCallback(() => {
+ if (!editor) return;
+ const text = window.prompt('Teks Button:') || 'Button';
+ const url = window.prompt('URL Button:') || '#';
+ const fullWidth = window.confirm('Gunakan lebar penuh?');
+
+ editor.chain().focus().insertContent({
+ type: 'emailButton',
+ attrs: {
+ text,
+ url,
+ fullWidth,
+ },
+ }).run();
+ }, [editor]);
+
+ const addOTPBox = useCallback(() => {
+ if (!editor) return;
+ const code = window.prompt('Kode OTP (contoh: 123-456):') || '123-456';
+
+ editor.chain().focus().insertContent({
+ type: 'otpBox',
+ attrs: {
+ code,
+ },
+ }).run();
+ }, [editor]);
+
+ const addTable = useCallback(() => {
+ if (!editor) return;
+ const rows = parseInt(window.prompt('Jumlah baris:') || '3');
+ const cols = parseInt(window.prompt('Jumlah kolom:') || '2');
+ const hasHeader = window.confirm('Apakah table memiliki header?');
+
+ let tableHTML = '';
+
+ if (hasHeader) {
+ tableHTML += '';
+ for (let i = 0; i < cols; i++) {
+ tableHTML += `| Kolom ${i + 1} | `;
+ }
+ tableHTML += '
';
+ }
+
+ tableHTML += '';
+ for (let i = 0; i < (hasHeader ? rows - 1 : rows); i++) {
+ tableHTML += '';
+ for (let j = 0; j < cols; j++) {
+ tableHTML += `| Isi sel | `;
+ }
+ tableHTML += '
';
+ }
+ tableHTML += '
';
+
+ editor.chain().focus().insertContent(tableHTML).run();
+ }, [editor]);
+
const uploadImageToStorage = async (file: File): Promise => {
try {
const fileExt = file.name.split('.').pop();
@@ -299,6 +618,42 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
>
+
+ {/* Email Components Separator */}
+
+
+ {/* Email Component Buttons */}
+
+
+
+
+ {/* Image Upload Separator */}
+
+