'use client' import * as React from 'react' import { useState, useCallback } from 'react' import { Section, Block, ColumnLayout, GlobalStyles, MergeFieldDefinition, EditorSelection, DEFAULT_GLOBAL_STYLES, getColumnWidths, } from './types' import { useDragDrop } from './hooks/useDragDrop' import { useEmailEditorState } from './hooks/useEmailEditorState' import { BlockRenderer } from './BlockRenderer' import { BlockToolbar } from './block-toolbar' import { AddBlockMenu } from './add-block-menu' import { EditorSidebar } from './editor-sidebar' import { Button } from '../ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../ui/select' import { Undo, Redo, Plus, Settings, Columns2, Columns3, RectangleHorizontal, Trash2, } from 'lucide-react' import { cn } from '../../lib/utils' interface EmailEditorProps { sections: Section[] onChange: (sections: Section[]) => void globalStyles?: GlobalStyles onGlobalStylesChange?: (styles: GlobalStyles) => void mergeFields?: MergeFieldDefinition[] readOnly?: boolean /** Optional custom rich text editor for the text block. */ renderTextEditor?: (props: { content: string onChange: (html: string) => void placeholder?: string className?: string }) => React.ReactNode } // --- Main Editor --- export function EmailEditor({ sections: sectionsProp, onChange, globalStyles = DEFAULT_GLOBAL_STYLES, onGlobalStylesChange, mergeFields, readOnly = false, renderTextEditor, }: EmailEditorProps) { const { sections, selection, setSelection, updateBlock, addBlock, removeBlock, duplicateBlock, moveBlock, addSection, removeSection, addRow, changeRowLayout, removeRow, undo: handleUndo, redo: handleRedo, canUndo: canUndoVal, canRedo: canRedoVal, commitChange, } = useEmailEditorState({ initialSections: sectionsProp, onChange }) const [showSidebar, setShowSidebar] = useState(false) const { dragState, dragOverTarget, setDragOverTarget, handleDragStart, handleDragOver, handleDrop, handleDragEnd, } = useDragDrop({ sections, commitChange, setSelection }) // Get selected block const selectedBlock = selection ? sections[selection.sectionIndex]?.rows[selection.rowIndex]?.columns[selection.columnIndex]?.[ selection.blockIndex ] ?? null : null // --- Block update handler --- const handleBlockUpdate = useCallback( (sel: EditorSelection, updates: Partial) => { const current = sections[sel.sectionIndex]?.rows[sel.rowIndex]?.columns[sel.columnIndex]?.[sel.blockIndex] if (!current) return updateBlock(sel.sectionIndex, sel.rowIndex, sel.columnIndex, sel.blockIndex, { ...current, ...updates, } as Block) }, [sections, updateBlock] ) // --- Read-only mode --- if (readOnly) { return (
{sections.map((section, si) => (
{section.rows.map((row, ri) => { const widths = getColumnWidths(row.layout) return (
{row.columns.map((col, ci) => (
{col.map((block, bi) => (
handleBlockUpdate( { sectionIndex: si, rowIndex: ri, columnIndex: ci, blockIndex: bi }, updates ) } mergeFields={mergeFields ?? []} renderTextEditor={renderTextEditor} />
))}
))}
) })}
))}
) } // --- Editor mode --- return (
{/* Main canvas */}
{/* Toolbar */}
{/* Canvas area */}
setSelection(null)} >
{sections.map((section, si) => (
{/* Section wrapper */}
{/* Section controls */}
{sections.length > 1 && ( )}
{/* Rows */} {section.rows.map((row, ri) => { const widths = getColumnWidths(row.layout) return (
{/* Row layout controls */}
{/* Columns */}
{row.columns.map((col, ci) => (
{ e.preventDefault() if (col.length === 0) { setDragOverTarget({ sectionIndex: si, rowIndex: ri, colIndex: ci, blockIndex: 0, }) } }} onDrop={(e) => { if (col.length === 0) { handleDrop(e, si, ri, ci, 0) } }} > {col.length === 0 && (
addBlock(si, ri, ci, type)} variant="inline" />
)} {col.map((block, bi) => { const blockSel: EditorSelection = { sectionIndex: si, rowIndex: ri, columnIndex: ci, blockIndex: bi, } const isSelected = selection?.sectionIndex === si && selection?.rowIndex === ri && selection?.columnIndex === ci && selection?.blockIndex === bi const isDragOver = dragOverTarget?.sectionIndex === si && dragOverTarget?.rowIndex === ri && dragOverTarget?.colIndex === ci && dragOverTarget?.blockIndex === bi return (
{/* Drop zone indicator */} {isDragOver && dragState && (
)}
{ e.stopPropagation() setSelection(blockSel) }} draggable onDragStart={(e) => { e.stopPropagation() handleDragStart(si, ri, ci, bi) }} onDragOver={(e) => handleDragOver(e, si, ri, ci, bi) } onDrop={(e) => handleDrop(e, si, ri, ci, bi) } onDragEnd={handleDragEnd} > {/* Block toolbar (on hover when selected) */} {isSelected && ( removeBlock(si, ri, ci, bi)} onDuplicate={() => duplicateBlock(si, ri, ci, bi)} onMoveUp={() => moveBlock(si, ri, ci, bi, 'up') } onMoveDown={() => moveBlock(si, ri, ci, bi, 'down') } canMoveUp={bi > 0} canMoveDown={bi < col.length - 1} /> )} {/* Block content */}
handleBlockUpdate(blockSel, updates) } mergeFields={mergeFields ?? []} renderTextEditor={renderTextEditor} />
{/* Add block between items */} {isSelected && (
addBlock(si, ri, ci, type, bi) } variant="small" />
)}
) })} {/* Add block at end of column */} {col.length > 0 && (
addBlock( si, ri, ci, type, col.length - 1 ) } variant="small" />
)}
))}
{/* Add row button */} {ri === section.rows.length - 1 && (
)}
) })}
{/* Add section between */}
))}
{/* Sidebar */} {showSidebar && ( { if (selection) updateBlock(selection.sectionIndex, selection.rowIndex, selection.columnIndex, selection.blockIndex, block) }} globalStyles={globalStyles} onGlobalStylesChange={onGlobalStylesChange || (() => {})} onClose={() => setShowSidebar(false)} /> )}
) }