feat: Replace TipTap with Visual Email Builder 🎨

## 🚀 MAJOR FEATURE: Visual Email Content Builder!

### What Changed:

**Before:**
- TipTap rich text editor
- Manual [card] syntax typing
- Hard to visualize final result
- Not beginner-friendly

**After:**
- Visual drag-and-drop builder
- Live preview as you build
- No code needed
- Professional UX

### New Components:

**1. EmailBuilder** (`/components/EmailBuilder/`)
- Main builder component
- Block-based editing
- Drag to reorder (via up/down buttons)
- Click to edit
- Live preview

**2. Block Types:**
- **Header** - Large title text
- **Text** - Paragraph content
- **Card** - Styled content box (5 types: default, success, info, warning, hero)
- **Button** - CTA with solid/outline styles
- **Divider** - Horizontal line
- **Spacer** - Vertical spacing

**3. Features:**
-  **Add Block Toolbar** - One-click block insertion
-  **Hover Controls** - Edit, Delete, Move Up/Down
-  **Edit Dialog** - Full editor for each block
-  **Variable Helper** - Click to insert variables
-  **Code Mode Toggle** - Switch between visual/code
-  **Auto-sync** - Converts blocks ↔ [card] syntax

### How It Works:

**Visual Mode:**
```
[Add Block: Header | Text | Card | Button | Divider | Spacer]

┌─────────────────────────────┐
│ Header Block          [↑ ↓ ✎ ×] │
│ New Order Received           │
└─────────────────────────────┘

┌─────────────────────────────┐
│ Card Block (Success)  [↑ ↓ ✎ ×] │
│  Order Confirmed!          │
└─────────────────────────────┘

┌─────────────────────────────┐
│ Button Block          [↑ ↓ ✎ ×] │
│    [View Order Details]      │
└─────────────────────────────┘
```

**Code Mode:**
```html
[card]
<h1>New Order Received</h1>
[/card]

[card type="success"]
<h2> Order Confirmed!</h2>
[/card]

[card]
<p style="text-align: center;">
  <a href="{order_url}" class="button">View Order Details</a>
</p>
[/card]
```

### Benefits:

1. **No Learning Curve**
   - Visual interface, no syntax to learn
   - Click, edit, done!

2. **Live Preview**
   - See exactly how email will look
   - WYSIWYG editing

3. **Flexible**
   - Switch to code mode anytime
   - Full HTML control when needed

4. **Professional**
   - Pre-designed block types
   - Consistent styling
   - Best practices built-in

5. **Variable Support**
   - Click to insert variables
   - Works in all block types
   - Helpful dropdown

### Technical Details:

**Converter Functions:**
- `blocksToHTML()` - Converts blocks to [card] syntax
- `htmlToBlocks()` - Parses [card] syntax to blocks
- Seamless sync between visual/code modes

**State Management:**
- Blocks stored as structured data
- Auto-converts to HTML on save
- Preserves all [card] attributes

### Next Steps:
- Install @radix-ui/react-radio-group for radio buttons
- Test email rendering end-to-end
- Polish and final review

This is a GAME CHANGER for email template editing! 🎉
This commit is contained in:
dwindown
2025-11-13 06:40:23 +07:00
parent 74e084caa6
commit 4ec0f3f890
7 changed files with 718 additions and 207 deletions

View File

@@ -0,0 +1,130 @@
import React from 'react';
import { EmailBlock } from './types';
import { __ } from '@/lib/i18n';
interface BlockRendererProps {
block: EmailBlock;
isEditing: boolean;
onEdit: () => void;
onDelete: () => void;
onMoveUp: () => void;
onMoveDown: () => void;
isFirst: boolean;
isLast: boolean;
}
export function BlockRenderer({
block,
isEditing,
onEdit,
onDelete,
onMoveUp,
onMoveDown,
isFirst,
isLast
}: BlockRendererProps) {
const renderBlockContent = () => {
switch (block.type) {
case 'header':
return (
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900" dangerouslySetInnerHTML={{ __html: block.content }} />
</div>
);
case 'text':
return (
<div className="prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: block.content }} />
);
case 'card':
const cardClasses = {
default: 'bg-white border border-gray-200',
success: 'bg-green-50 border border-green-200',
info: 'bg-blue-50 border border-blue-200',
warning: 'bg-orange-50 border border-orange-200',
hero: 'bg-gradient-to-r from-purple-500 to-indigo-600 text-white'
};
return (
<div className={`rounded-lg p-6 ${cardClasses[block.cardType]}`}>
<div
className={`prose prose-sm max-w-none ${block.cardType === 'hero' ? 'prose-invert' : ''}`}
dangerouslySetInnerHTML={{ __html: block.content }}
/>
</div>
);
case 'button':
const buttonClasses = block.style === 'solid'
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'border-2 border-purple-600 text-purple-600 hover:bg-purple-50';
return (
<div className="text-center">
<a
href={block.link}
className={`inline-block px-6 py-3 rounded-md font-semibold no-underline ${buttonClasses}`}
>
{block.text}
</a>
</div>
);
case 'divider':
return <hr className="border-t border-gray-300 my-4" />;
case 'spacer':
return <div style={{ height: `${block.height}px` }} />;
default:
return null;
}
};
return (
<div className="group relative">
{/* Block Content */}
<div className={`transition-all ${isEditing ? 'ring-2 ring-purple-500 ring-offset-2' : ''}`}>
{renderBlockContent()}
</div>
{/* Hover Controls */}
<div className="absolute -right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col gap-1 bg-white rounded-md shadow-lg border p-1">
{!isFirst && (
<button
onClick={onMoveUp}
className="p-1 hover:bg-gray-100 rounded text-gray-600 text-xs"
title={__('Move up')}
>
</button>
)}
{!isLast && (
<button
onClick={onMoveDown}
className="p-1 hover:bg-gray-100 rounded text-gray-600 text-xs"
title={__('Move down')}
>
</button>
)}
<button
onClick={onEdit}
className="p-1 hover:bg-gray-100 rounded text-blue-600 text-xs"
title={__('Edit')}
>
</button>
<button
onClick={onDelete}
className="p-1 hover:bg-gray-100 rounded text-red-600 text-xs"
title={__('Delete')}
>
×
</button>
</div>
</div>
);
}