feat: Rich text editor and email system integration

##  Step 4-5: Rich Text Editor & Integration

### RichTextEditor Component (TipTap)
-  Modern WYSIWYG editor for React
-  Toolbar: Bold, Italic, Lists, Links, Undo/Redo
-  Variable insertion with buttons
-  Placeholder support
-  Clean, minimal UI

### TemplateEditor Updated
-  Replaced Textarea with RichTextEditor
-  Variables shown as clickable buttons
-  Better UX for content editing
-  HTML output for email templates

### Bootstrap Integration
-  EmailManager initialized on plugin load
-  Hooks into WooCommerce events automatically
-  Disables WC emails to prevent duplicates

### Plugin Constants
-  WOONOOW_PATH for template paths
-  WOONOOW_URL for assets
-  WOONOOW_VERSION for versioning

### Dependencies
-  @tiptap/react
-  @tiptap/starter-kit
-  @tiptap/extension-placeholder
-  @tiptap/extension-link

---

**Status:** Core email system complete!
**Next:** Test and create content templates 🚀
This commit is contained in:
dwindown
2025-11-12 18:53:20 +07:00
parent 30384464a1
commit a1a5dc90c6
6 changed files with 960 additions and 38 deletions

View File

@@ -0,0 +1,175 @@
import React from 'react';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Placeholder from '@tiptap/extension-placeholder';
import Link from '@tiptap/extension-link';
import {
Bold,
Italic,
List,
ListOrdered,
Link as LinkIcon,
Undo,
Redo,
} from 'lucide-react';
import { Button } from './button';
import { __ } from '@/lib/i18n';
interface RichTextEditorProps {
content: string;
onChange: (content: string) => void;
placeholder?: string;
variables?: string[];
onVariableInsert?: (variable: string) => void;
}
export function RichTextEditor({
content,
onChange,
placeholder = __('Start typing...'),
variables = [],
onVariableInsert,
}: RichTextEditorProps) {
const editor = useEditor({
extensions: [
StarterKit,
Placeholder.configure({
placeholder,
}),
Link.configure({
openOnClick: false,
HTMLAttributes: {
class: 'text-primary underline',
},
}),
],
content,
onUpdate: ({ editor }) => {
onChange(editor.getHTML());
},
editorProps: {
attributes: {
class:
'prose prose-sm max-w-none focus:outline-none min-h-[200px] px-4 py-3',
},
},
});
if (!editor) {
return null;
}
const insertVariable = (variable: string) => {
editor.chain().focus().insertContent(`{${variable}}`).run();
if (onVariableInsert) {
onVariableInsert(variable);
}
};
const setLink = () => {
const url = window.prompt(__('Enter URL:'));
if (url) {
editor.chain().focus().setLink({ href: url }).run();
}
};
return (
<div className="border rounded-lg overflow-hidden">
{/* Toolbar */}
<div className="border-b bg-muted/30 p-2 flex flex-wrap gap-1">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? 'bg-accent' : ''}
>
<Bold className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive('italic') ? 'bg-accent' : ''}
>
<Italic className="h-4 w-4" />
</Button>
<div className="w-px h-6 bg-border mx-1" />
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive('bulletList') ? 'bg-accent' : ''}
>
<List className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={editor.isActive('orderedList') ? 'bg-accent' : ''}
>
<ListOrdered className="h-4 w-4" />
</Button>
<div className="w-px h-6 bg-border mx-1" />
<Button
type="button"
variant="ghost"
size="sm"
onClick={setLink}
className={editor.isActive('link') ? 'bg-accent' : ''}
>
<LinkIcon className="h-4 w-4" />
</Button>
<div className="w-px h-6 bg-border mx-1" />
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().undo()}
>
<Undo className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().redo()}
>
<Redo className="h-4 w-4" />
</Button>
</div>
{/* Editor */}
<EditorContent editor={editor} />
{/* Variables */}
{variables.length > 0 && (
<div className="border-t bg-muted/30 p-3">
<div className="text-xs text-muted-foreground mb-2">
{__('Available Variables:')}
</div>
<div className="flex flex-wrap gap-1">
{variables.map((variable) => (
<Button
key={variable}
type="button"
variant="outline"
size="sm"
onClick={() => insertVariable(variable)}
className="text-xs h-7"
>
{`{${variable}}`}
</Button>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { RichTextEditor } from '@/components/ui/rich-text-editor';
import { Label } from '@/components/ui/label';
import {
Dialog,
@@ -90,24 +90,8 @@ export default function TemplateEditor({
},
});
const insertVariable = (variable: string) => {
const textarea = document.querySelector('textarea[name="body"]') as HTMLTextAreaElement;
if (textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = body;
const before = text.substring(0, start);
const after = text.substring(end);
const newText = before + `{${variable}}` + after;
setBody(newText);
// Set cursor position after inserted variable
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(start + variable.length + 2, start + variable.length + 2);
}, 0);
}
};
// Get variable keys for the rich text editor
const variableKeys = Object.keys(variables);
return (
<Dialog open={open} onOpenChange={onClose}>
@@ -141,30 +125,26 @@ export default function TemplateEditor({
{/* Body */}
<div className="space-y-2">
<Label htmlFor="body">{__('Message Body')}</Label>
<Textarea
id="body"
name="body"
value={body}
onChange={(e) => setBody(e.target.value)}
<RichTextEditor
content={body}
onChange={setBody}
placeholder={__('Enter notification message')}
rows={10}
className="font-mono text-sm"
variables={variableKeys}
/>
<p className="text-xs text-muted-foreground">
{__('Use variables from the list below to personalize your message')}
{__('Click variables below to insert them into your message')}
</p>
</div>
{/* Variables */}
{/* Variable Reference */}
<div className="space-y-3">
<Label>{__('Available Variables')}</Label>
<Label>{__('Variable Reference')}</Label>
<div className="flex flex-wrap gap-2">
{Object.entries(variables).map(([key, label]) => (
<Badge
key={key}
variant="secondary"
className="cursor-pointer hover:bg-primary hover:text-primary-foreground transition-colors"
onClick={() => insertVariable(key)}
className="cursor-default"
>
<Plus className="h-3 w-3 mr-1" />
{`{${key}}`}