diff --git a/admin-spa/src/components/EmailBuilder/BlockRenderer.tsx b/admin-spa/src/components/EmailBuilder/BlockRenderer.tsx new file mode 100644 index 0000000..6e74f32 --- /dev/null +++ b/admin-spa/src/components/EmailBuilder/BlockRenderer.tsx @@ -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 ( +
+

+

+ ); + + case 'text': + return ( +
+ ); + + 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 ( +
+
+
+ ); + + 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 ( +
+ + {block.text} + +
+ ); + + case 'divider': + return
; + + case 'spacer': + return
; + + default: + return null; + } + }; + + return ( +
+ {/* Block Content */} +
+ {renderBlockContent()} +
+ + {/* Hover Controls */} +
+ {!isFirst && ( + + )} + {!isLast && ( + + )} + + +
+
+ ); +} diff --git a/admin-spa/src/components/EmailBuilder/EmailBuilder.tsx b/admin-spa/src/components/EmailBuilder/EmailBuilder.tsx new file mode 100644 index 0000000..770b37a --- /dev/null +++ b/admin-spa/src/components/EmailBuilder/EmailBuilder.tsx @@ -0,0 +1,336 @@ +import React, { useState } from 'react'; +import { EmailBlock, CardType, ButtonStyle } from './types'; +import { BlockRenderer } from './BlockRenderer'; +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 { 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'; +import { __ } from '@/lib/i18n'; + +interface EmailBuilderProps { + blocks: EmailBlock[]; + onChange: (blocks: EmailBlock[]) => void; + variables?: string[]; +} + +export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderProps) { + const [editingBlockId, setEditingBlockId] = useState(null); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [editingContent, setEditingContent] = useState(''); + const [editingCardType, setEditingCardType] = useState('default'); + const [editingButtonText, setEditingButtonText] = useState(''); + const [editingButtonLink, setEditingButtonLink] = useState(''); + const [editingButtonStyle, setEditingButtonStyle] = useState('solid'); + + const addBlock = (type: EmailBlock['type']) => { + const newBlock: EmailBlock = (() => { + const id = `block-${Date.now()}`; + switch (type) { + case 'header': + return { id, type, content: '

Header Title

' }; + case 'text': + return { id, type, content: '

Your text content here...

' }; + case 'card': + return { id, type, cardType: 'default', content: '

Card Title

Card content...

' }; + case 'button': + return { id, type, text: 'Click Here', link: '{order_url}', style: 'solid' }; + case 'divider': + return { id, type }; + case 'spacer': + return { id, type, height: 32 }; + default: + throw new Error(`Unknown block type: ${type}`); + } + })(); + + onChange([...blocks, newBlock]); + }; + + const deleteBlock = (id: string) => { + onChange(blocks.filter(b => b.id !== id)); + }; + + const moveBlock = (id: string, direction: 'up' | 'down') => { + const index = blocks.findIndex(b => b.id === id); + if (index === -1) return; + + const newIndex = direction === 'up' ? index - 1 : index + 1; + if (newIndex < 0 || newIndex >= blocks.length) return; + + const newBlocks = [...blocks]; + [newBlocks[index], newBlocks[newIndex]] = [newBlocks[newIndex], newBlocks[index]]; + onChange(newBlocks); + }; + + const openEditDialog = (block: EmailBlock) => { + setEditingBlockId(block.id); + + if (block.type === 'header' || block.type === 'text') { + setEditingContent(block.content); + } else if (block.type === 'card') { + setEditingContent(block.content); + setEditingCardType(block.cardType); + } else if (block.type === 'button') { + setEditingButtonText(block.text); + setEditingButtonLink(block.link); + setEditingButtonStyle(block.style); + } + + setEditDialogOpen(true); + }; + + const saveEdit = () => { + if (!editingBlockId) return; + + 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') { + return { ...block, content: editingContent, cardType: editingCardType }; + } else if (block.type === 'button') { + return { ...block, text: editingButtonText, link: editingButtonLink, style: editingButtonStyle }; + } + + return block; + }); + + onChange(newBlocks); + setEditDialogOpen(false); + setEditingBlockId(null); + }; + + const editingBlock = blocks.find(b => b.id === editingBlockId); + + return ( +
+ {/* Add Block Toolbar */} +
+ + {__('Add Block:')} + + + + + +
+ + +
+ + {/* Email Canvas */} +
+
+ {blocks.length === 0 ? ( +
+

{__('No blocks yet. Add blocks using the toolbar above.')}

+
+ ) : ( + blocks.map((block, index) => ( + openEditDialog(block)} + onDelete={() => deleteBlock(block.id)} + onMoveUp={() => moveBlock(block.id, 'up')} + onMoveDown={() => moveBlock(block.id, 'down')} + isFirst={index === 0} + isLast={index === blocks.length - 1} + /> + )) + )} +
+
+ + {/* Edit Dialog */} + + + + + {editingBlock?.type === 'header' && __('Edit Header')} + {editingBlock?.type === 'text' && __('Edit Text')} + {editingBlock?.type === 'card' && __('Edit Card')} + {editingBlock?.type === 'button' && __('Edit Button')} + + + {__('Make changes to your block. You can use variables like {customer_name} or {order_number}.')} + + + +
+ {(editingBlock?.type === 'header' || editingBlock?.type === 'text') && ( +
+ +