feat: Polish Email Builder - Perfect UX!

## 🎨 4 Major Improvements Based on Feedback:

### 1.  Move Tabs to Message Body Section
**Before:**
- Tabs at top level
- Subject shown in preview (unnecessary)

**After:**
- Tabs inside Message Body section
- Subject removed from preview (it's just a string)
- Cleaner, more focused UI

**Layout:**
```
Subject / Title
[input field]

Message Body
                    [Editor | Preview]  [Code Mode]
┌─────────────────────────────────────────────────┐
│ Email Builder / Preview                         │
└─────────────────────────────────────────────────┘
```

### 2.  Darker Canvas Background
**Before:**
- Light gray canvas (bg-gray-50)
- White email wrapper
- Cards hard to see

**After:**
- Darker canvas (bg-gray-100)
- Light gray email wrapper (bg-gray-50)
- Cards stand out clearly
- Better visual hierarchy

### 3.  Editor Matches Preview Exactly
**Problem:**
- Editor used Tailwind classes
- Preview used inline styles
- Different rendering!

**Solution:**
- Editor now uses inline styles
- Matches email rendering exactly
- WYSIWYG is truly WYSIWYG

**Card Styles (Inline):**
```tsx
default: {
  background: "#ffffff",
  borderRadius: "8px",
  padding: "32px 40px"
}

success: {
  background: "#e8f5e9",
  border: "1px solid #4caf50",
  ...
}
```

**Button Styles (Inline):**
```tsx
solid: {
  background: "#7f54b3",
  color: "#fff",
  padding: "14px 28px",
  ...
}

outline: {
  border: "2px solid #7f54b3",
  color: "#7f54b3",
  ...
}
```

**Result:**
- What you see in editor = What you get in email
- No surprises!
- Honest rendering

### 4.  RichTextEditor in Edit Dialog
**Before:**
- Plain textarea for content
- Manual HTML typing
- No formatting toolbar

**After:**
- Full RichTextEditor with toolbar
- Bold, Italic, Lists, Links
- Variable insertion
- HTML generated automatically

**Benefits:**
-  Easy to use
-  No HTML knowledge needed
-  Professional formatting
-  Variable support
-  Much better UX

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

Content:
┌─────────────────────────────┐
│ [B] [I] [List] [Link] [Undo]│
│                             │
│ Your content here...        │
│                             │
└─────────────────────────────┘

Available Variables:
{customer_name} {order_number} ...

[Cancel] [Save Changes]
```

## Summary:

All 4 improvements implemented:
1.  Tabs moved to Message Body
2.  Darker canvas for better contrast
3.  Editor matches preview exactly
4.  RichTextEditor for easy editing

**The Email Builder is now PERFECT for non-technical users!** 🎉
This commit is contained in:
dwindown
2025-11-13 06:55:20 +07:00
parent 4ec0f3f890
commit db6ddf67bd
3 changed files with 120 additions and 100 deletions

View File

@@ -39,33 +39,80 @@ export function BlockRenderer({
);
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'
const cardStyles: { [key: string]: React.CSSProperties } = {
default: {
background: '#ffffff',
borderRadius: '8px',
padding: '32px 40px',
marginBottom: '24px'
},
success: {
background: '#e8f5e9',
border: '1px solid #4caf50',
borderRadius: '8px',
padding: '32px 40px',
marginBottom: '24px'
},
info: {
background: '#f0f7ff',
border: '1px solid #0071e3',
borderRadius: '8px',
padding: '32px 40px',
marginBottom: '24px'
},
warning: {
background: '#fff8e1',
border: '1px solid #ff9800',
borderRadius: '8px',
padding: '32px 40px',
marginBottom: '24px'
},
hero: {
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: '#fff',
borderRadius: '8px',
padding: '32px 40px',
marginBottom: '24px'
}
};
return (
<div className={`rounded-lg p-6 ${cardClasses[block.cardType]}`}>
<div style={cardStyles[block.cardType]}>
<div
className={`prose prose-sm max-w-none ${block.cardType === 'hero' ? 'prose-invert' : ''}`}
className="prose prose-sm max-w-none"
style={block.cardType === 'hero' ? { color: '#fff' } : {}}
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';
const buttonStyle: React.CSSProperties = block.style === 'solid'
? {
display: 'inline-block',
background: '#7f54b3',
color: '#fff',
padding: '14px 28px',
borderRadius: '6px',
textDecoration: 'none',
fontWeight: 600
}
: {
display: 'inline-block',
background: 'transparent',
color: '#7f54b3',
padding: '12px 26px',
border: '2px solid #7f54b3',
borderRadius: '6px',
textDecoration: 'none',
fontWeight: 600
};
return (
<div className="text-center">
<div style={{ textAlign: 'center' }}>
<a
href={block.link}
className={`inline-block px-6 py-3 rounded-md font-semibold no-underline ${buttonClasses}`}
style={buttonStyle}
>
{block.text}
</a>

View File

@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { RichTextEditor } from '@/components/ui/rich-text-editor';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Plus, Type, Square, MousePointer, Minus, Space } from 'lucide-react';
@@ -179,8 +180,8 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP
</div>
{/* Email Canvas */}
<div className="bg-gray-50 rounded-lg p-6 min-h-[400px]">
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-sm p-8 space-y-6">
<div className="bg-gray-100 rounded-lg p-6 min-h-[400px]">
<div className="max-w-2xl mx-auto bg-gray-50 rounded-lg shadow-sm p-8 space-y-6">
{blocks.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<p>{__('No blocks yet. Add blocks using the toolbar above.')}</p>
@@ -221,14 +222,16 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP
<div className="space-y-4 py-4">
{(editingBlock?.type === 'header' || editingBlock?.type === 'text') && (
<div className="space-y-2">
<Label htmlFor="content">{__('Content (HTML)')}</Label>
<Textarea
id="content"
value={editingContent}
onChange={(e) => setEditingContent(e.target.value)}
rows={6}
className="font-mono text-sm"
<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>
)}
@@ -250,14 +253,16 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="card-content">{__('Content (HTML)')}</Label>
<Textarea
id="card-content"
value={editingContent}
onChange={(e) => setEditingContent(e.target.value)}
rows={8}
className="font-mono text-sm"
<Label htmlFor="card-content">{__('Content')}</Label>
<RichTextEditor
content={editingContent}
onChange={setEditingContent}
placeholder={__('Enter card content...')}
variables={variables}
/>
<p className="text-xs text-muted-foreground">
{__('Use the toolbar to format text. HTML will be generated automatically.')}
</p>
</div>
</>
)}

View File

@@ -317,25 +317,7 @@ export default function EditTemplate() {
</div>
}
>
{/* Tabs - Sticky in header area */}
<div className="-mt-6 mb-6 sticky top-0 z-10 bg-background pb-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full max-w-md grid-cols-2">
<TabsTrigger value="editor" className="flex items-center gap-2">
<Edit className="h-4 w-4" />
{__('Editor')}
</TabsTrigger>
<TabsTrigger value="preview" className="flex items-center gap-2">
<Eye className="h-4 w-4" />
{__('Preview')}
</TabsTrigger>
</TabsList>
</Tabs>
</div>
{/* Editor Tab */}
{activeTab === 'editor' && (
<Card>
<Card>
<CardContent className="pt-6 space-y-6">
{/* Subject */}
<div className="space-y-2">
@@ -354,20 +336,37 @@ export default function EditTemplate() {
</div>
{/* Body */}
<div className="space-y-2">
<div className="space-y-4">
{/* Tabs for Editor/Preview */}
<div className="flex items-center justify-between">
<Label htmlFor="body">{__('Message Body')}</Label>
<Button
variant="ghost"
size="sm"
onClick={handleCodeModeToggle}
className="h-8 text-xs"
>
{codeMode ? __('Visual Builder') : __('Code Mode')}
</Button>
<Label>{__('Message Body')}</Label>
<div className="flex items-center gap-2">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-auto">
<TabsList className="grid grid-cols-2">
<TabsTrigger value="editor" className="flex items-center gap-1 text-xs">
<Edit className="h-3 w-3" />
{__('Editor')}
</TabsTrigger>
<TabsTrigger value="preview" className="flex items-center gap-1 text-xs">
<Eye className="h-3 w-3" />
{__('Preview')}
</TabsTrigger>
</TabsList>
</Tabs>
{activeTab === 'editor' && (
<Button
variant="ghost"
size="sm"
onClick={handleCodeModeToggle}
className="h-8 text-xs"
>
{codeMode ? __('Visual Builder') : __('Code Mode')}
</Button>
)}
</div>
</div>
{codeMode ? (
{activeTab === 'editor' && codeMode ? (
<div className="space-y-2">
<textarea
value={body}
@@ -379,7 +378,7 @@ export default function EditTemplate() {
{__('Edit raw HTML code with [card] syntax. Switch to Visual Builder for drag-and-drop editing.')}
</p>
</div>
) : (
) : activeTab === 'editor' ? (
<div>
<EmailBuilder
blocks={blocks}
@@ -390,49 +389,18 @@ export default function EditTemplate() {
{__('Build your email visually. Add blocks, edit content, and see live preview.')}
</p>
</div>
)}
) : activeTab === 'preview' ? (
<div className="border rounded-md overflow-hidden">
<iframe
srcDoc={generatePreviewHTML()}
className="w-full h-[600px] bg-white"
title={__('Email Preview')}
/>
</div>
) : null}
</div>
</CardContent>
</Card>
)}
{/* Preview Tab */}
{activeTab === 'preview' && (
<Card>
<CardContent className="pt-6 space-y-4">
{/* Subject Preview */}
<div>
<Label className="text-sm text-muted-foreground">{__('Subject')}</Label>
<div className="mt-2 p-3 bg-muted/50 rounded-md font-medium">
{subject || <span className="text-muted-foreground italic">{__('(No subject)')}</span>}
</div>
</div>
{/* Email Preview */}
<div>
<Label className="text-sm text-muted-foreground">{__('Email Preview')}</Label>
<div className="mt-2 rounded-lg border bg-white overflow-hidden">
<iframe
srcDoc={generatePreviewHTML()}
className="w-full border-0"
style={{ minHeight: '400px', height: 'auto' }}
title="Email Preview"
onLoad={(e) => {
const iframe = e.target as HTMLIFrameElement;
if (iframe.contentWindow) {
const height = iframe.contentWindow.document.body.scrollHeight;
iframe.style.height = height + 40 + 'px';
}
}}
/>
</div>
<p className="text-xs text-muted-foreground mt-2">
{__('This is how your email will look. Dynamic variables are highlighted in yellow.')}
</p>
</div>
</CardContent>
</Card>
)}
</SettingsLayout>
);
}