Files
WooNooW/admin-spa/src/components/ui/rich-text-editor.tsx
dwindown c3ab31e14d fix: Template editor UX improvements
##  All 5 Issues Fixed!

### 1. Default Value in RichTextEditor 
- Added `useEffect` to sync content prop with editor
- Editor now properly displays default template content
- Fixed: `editor.commands.setContent(content)` when prop changes

### 2. Removed Duplicate Variable Section 
- Removed "Variable Reference" section (was redundant)
- Variables already available in rich text editor toolbar
- Kept small badge list under editor for quick reference

### 3. User-Friendly Preview 
- Preview now renders HTML (not raw code)
- Subject separated in dialog header
- Complete email template preview (header + content + footer)
- Variables highlighted in yellow for clarity
- Uses iframe with full base.html styling

### 4. Fixed Dialog Scrolling 
**New Structure:**
```
[Header] ← Fixed (title + subject input)
[Body]   ← Scrollable (tabs: editor/preview)
[Footer] ← Fixed (action buttons)
```
- No more annoying full-dialog scroll
- Each section scrolls independently
- Better UX with fixed header/footer

### 5. Editor/Preview Tabs 
**Tabs Implementation:**
- [Editor] tab: Rich text editor + variable badges
- [Preview] tab: Full email preview with styling
- Clean separation of editing vs previewing
- Preview shows complete email (not just content)
- 500px iframe height for comfortable viewing

---

**Benefits:**
-  Default content loads properly
- 🎨 Beautiful HTML preview
- 📱 Better scrolling UX
- 👁️ See exactly how email looks
- 🚀 Professional editing experience

**Next:** Email appearance settings + card insert buttons
2025-11-12 23:26:18 +07:00

183 lines
4.8 KiB
TypeScript

import React, { useEffect } 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',
},
},
});
// Update editor content when prop changes (fix for default value not showing)
useEffect(() => {
if (editor && content !== editor.getHTML()) {
editor.commands.setContent(content);
}
}, [content, editor]);
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>
);
}