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:
130
admin-spa/src/components/EmailBuilder/BlockRenderer.tsx
Normal file
130
admin-spa/src/components/EmailBuilder/BlockRenderer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user