feat: Major Email Builder Improvements! 🚀

## 🎯 All User Feedback Implemented:

### 1.  Header & Button Outside Cards
**Problem:**
- Header and Button were wrapped in [card] tags
- Not honest rendering
- Doesn't make sense to wrap single elements

**Solution:**
- Removed Header and Text as separate block types
- Only Card contains rich content now
- Button, Divider, Spacer render outside cards
- Honest, semantic HTML structure

**Before:**
```
[card]<h1>Header</h1>[/card]
[card]<button>Click</button>[/card]
```

**After:**
```
[card]<h1>Header</h1><p>Content...</p>[/card]
<button>Click</button>
```

### 2.  Rich Content in Cards
**Problem:**
- Cards had plain textarea
- No formatting options
- Hard to create mixed content

**Solution:**
- Cards now use RichTextEditor
- Full WYSIWYG editing
- Headers, text, lists, links, images, alignment
- All in one card!

**Card Dialog:**
```
Edit Card
─────────────────────
Card Type: [Default ▼]

Content:
┌──────────────────────────────┐
│ [B][I][List][Link][←][↔][→][📷]│
│                              │
│ <h2>Customer Details</h2>    │
│ <p>Name: {customer_name}</p> │
│                              │
└──────────────────────────────┘
```

### 3.  Text Alignment & Image Support
**Added to RichTextEditor:**
- ← Align Left
- ↔ Align Center
- → Align Right
- 📷 Insert Image

**Extensions:**
- `@tiptap/extension-text-align`
- `@tiptap/extension-image`

### 4.  CodeMirror for Code Mode
**Problem:**
- Plain textarea for code
- No syntax highlighting
- Hard to read/edit

**Solution:**
- CodeMirror editor
- HTML syntax highlighting
- One Dark theme
- Auto-completion
- Professional code editing

**Features:**
- Syntax highlighting
- Line numbers
- Bracket matching
- Auto-indent
- Search & replace

## 📦 Block Structure:

**Simplified to 4 types:**
1. **Card** - Rich content container (headers, text, images, etc.)
2. **Button** - Standalone CTA (outside card)
3. **Divider** - Horizontal line (outside card)
4. **Spacer** - Vertical spacing (outside card)

## 🔄 Converter Updates:

**blocksToHTML():**
- Cards → `[card]...[/card]`
- Buttons → `<a class="button">...</a>` (no card wrapper)
- Dividers → `<hr />` (no card wrapper)
- Spacers → `<div style="height:...">` (no card wrapper)

**htmlToBlocks():**
- Parses cards AND standalone elements
- Correctly identifies buttons outside cards
- Maintains structure integrity

## 📋 Required Dependencies:

**TipTap Extensions:**
```bash
npm install @tiptap/extension-text-align @tiptap/extension-image
```

**CodeMirror:**
```bash
npm install codemirror @codemirror/lang-html @codemirror/theme-one-dark
```

**Radix UI:**
```bash
npm install @radix-ui/react-radio-group
```

## 🎨 User Experience:

**For Non-Technical Users:**
- Visual builder with rich text editing
- No HTML knowledge needed
- Click, type, format, done!

**For Tech-Savvy Users:**
- Code mode with CodeMirror
- Full HTML control
- Syntax highlighting
- Professional editing

**Best of Both Worlds!** 🎉

## Summary:

 Honest rendering (no unnecessary card wrappers)
 Rich content in cards (WYSIWYG editing)
 Text alignment & images
 Professional code editor
 Perfect for all skill levels

This is PRODUCTION-READY! 🚀
This commit is contained in:
dwindown
2025-11-13 07:52:16 +07:00
parent db6ddf67bd
commit fde198c09f
7 changed files with 180 additions and 154 deletions

View File

@@ -30,12 +30,8 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP
const newBlock: EmailBlock = (() => {
const id = `block-${Date.now()}`;
switch (type) {
case 'header':
return { id, type, content: '<h1>Header Title</h1>' };
case 'text':
return { id, type, content: '<p>Your text content here...</p>' };
case 'card':
return { id, type, cardType: 'default', content: '<h2>Card Title</h2><p>Card content...</p>' };
return { id, type, cardType: 'default', content: '<h2>Card Title</h2><p>Your content here...</p>' };
case 'button':
return { id, type, text: 'Click Here', link: '{order_url}', style: 'solid' };
case 'divider':
@@ -69,9 +65,7 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP
const openEditDialog = (block: EmailBlock) => {
setEditingBlockId(block.id);
if (block.type === 'header' || block.type === 'text') {
setEditingContent(block.content);
} else if (block.type === 'card') {
if (block.type === 'card') {
setEditingContent(block.content);
setEditingCardType(block.cardType);
} else if (block.type === 'button') {
@@ -89,11 +83,7 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP
const newBlocks = blocks.map(block => {
if (block.id !== editingBlockId) return block;
if (block.type === 'header') {
return { ...block, content: editingContent };
} else if (block.type === 'text') {
return { ...block, content: editingContent };
} else if (block.type === 'card') {
if (block.type === 'card') {
return { ...block, content: editingContent, cardType: editingCardType };
} else if (block.type === 'button') {
return { ...block, text: editingButtonText, link: editingButtonLink, style: editingButtonStyle };
@@ -116,26 +106,6 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP
<span className="text-xs font-medium text-muted-foreground flex items-center">
{__('Add Block:')}
</span>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => addBlock('header')}
className="h-7 text-xs gap-1"
>
<Type className="h-3 w-3" />
{__('Header')}
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => addBlock('text')}
className="h-7 text-xs gap-1"
>
<Type className="h-3 w-3" />
{__('Text')}
</Button>
<Button
type="button"
variant="outline"
@@ -209,8 +179,6 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>
{editingBlock?.type === 'header' && __('Edit Header')}
{editingBlock?.type === 'text' && __('Edit Text')}
{editingBlock?.type === 'card' && __('Edit Card')}
{editingBlock?.type === 'button' && __('Edit Button')}
</DialogTitle>
@@ -220,21 +188,6 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP
</DialogHeader>
<div className="space-y-4 py-4">
{(editingBlock?.type === 'header' || editingBlock?.type === 'text') && (
<div className="space-y-2">
<Label htmlFor="content">{__('Content')}</Label>
<RichTextEditor
content={editingContent}
onChange={setEditingContent}
placeholder={__('Enter your content...')}
variables={variables}
/>
<p className="text-xs text-muted-foreground">
{__('Use the toolbar to format text. HTML will be generated automatically.')}
</p>
</div>
)}
{editingBlock?.type === 'card' && (
<>
<div className="space-y-2">
@@ -300,30 +253,6 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP
</div>
</>
)}
{/* Variable Helper */}
{variables.length > 0 && (
<div className="pt-2 border-t">
<Label className="text-xs text-muted-foreground">{__('Available Variables:')}</Label>
<div className="flex flex-wrap gap-1 mt-2">
{variables.map(variable => (
<code
key={variable}
className="text-xs bg-muted px-2 py-1 rounded cursor-pointer hover:bg-muted/80"
onClick={() => {
if (editingBlock?.type === 'button') {
setEditingButtonLink(editingButtonLink + `{${variable}}`);
} else {
setEditingContent(editingContent + `{${variable}}`);
}
}}
>
{`{${variable}}`}
</code>
))}
</div>
</div>
)}
</div>
<DialogFooter>