Files
WooNooW/admin-spa/src/components/EmailBuilder/BlockRenderer.tsx

229 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import { EmailBlock } from './types';
import { __ } from '@/lib/i18n';
import { parseMarkdownBasics } from '@/lib/markdown-utils';
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) {
// Prevent navigation in builder
const handleClick = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
if (
target.tagName === 'A' ||
target.tagName === 'BUTTON' ||
target.closest('a') ||
target.closest('button') ||
target.classList.contains('button') ||
target.classList.contains('button-outline') ||
target.closest('.button') ||
target.closest('.button-outline')
) {
e.preventDefault();
e.stopPropagation();
}
};
const renderBlockContent = () => {
switch (block.type) {
case 'card':
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, var(--wn-gradient-start, #667eea) 0%, var(--wn-gradient-end, #764ba2) 100%)',
color: '#fff',
borderRadius: '8px',
padding: '32px 40px',
marginBottom: '24px'
}
};
// Convert markdown to HTML for visual rendering
const htmlContent = parseMarkdownBasics(block.content);
return (
<div style={cardStyles[block.cardType]}>
<div
className="prose prose-sm max-w-none [&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mt-0 [&_h1]:mb-4 [&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mt-0 [&_h2]:mb-3 [&_h3]:text-xl [&_h3]:font-bold [&_h3]:mt-0 [&_h3]:mb-2 [&_h4]:text-lg [&_h4]:font-bold [&_h4]:mt-0 [&_h4]:mb-2 [&_.button]:inline-block [&_.button]:bg-purple-600 [&_.button]:text-white [&_.button]:px-7 [&_.button]:py-3.5 [&_.button]:rounded-md [&_.button]:no-underline [&_.button]:font-semibold [&_.button-outline]:inline-block [&_.button-outline]:bg-transparent [&_.button-outline]:text-purple-600 [&_.button-outline]:px-6 [&_.button-outline]:py-3 [&_.button-outline]:rounded-md [&_.button-outline]:no-underline [&_.button-outline]:font-semibold [&_.button-outline]:border-2 [&_.button-outline]:border-purple-600"
style={block.cardType === 'hero' ? { color: '#fff' } : {}}
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
</div>
);
case 'button': {
const buttonStyle: React.CSSProperties = block.style === 'solid'
? {
display: 'inline-block',
background: 'var(--wn-primary, #7f54b3)',
color: '#fff',
padding: '14px 28px',
borderRadius: '6px',
textDecoration: 'none',
fontWeight: 600,
}
: {
display: 'inline-block',
background: 'transparent',
color: 'var(--wn-secondary, #7f54b3)',
padding: '12px 26px',
border: '2px solid var(--wn-secondary, #7f54b3)',
borderRadius: '6px',
textDecoration: 'none',
fontWeight: 600,
};
const containerStyle: React.CSSProperties = {
textAlign: block.align || 'center',
};
if (block.widthMode === 'full') {
buttonStyle.display = 'block';
buttonStyle.width = '100%';
buttonStyle.textAlign = 'center';
} else if (block.widthMode === 'custom' && block.customMaxWidth) {
buttonStyle.maxWidth = `${block.customMaxWidth}px`;
buttonStyle.width = '100%';
}
return (
<div style={containerStyle}>
<a href={block.link} style={buttonStyle}>
{block.text}
</a>
</div>
);
}
case 'image': {
const containerStyle: React.CSSProperties = {
textAlign: block.align,
marginBottom: 24,
};
const imgStyle: React.CSSProperties = {
display: 'inline-block',
};
if (block.widthMode === 'full') {
imgStyle.display = 'block';
imgStyle.width = '100%';
imgStyle.height = 'auto';
} else if (block.widthMode === 'custom' && block.customMaxWidth) {
imgStyle.maxWidth = `${block.customMaxWidth}px`;
imgStyle.width = '100%';
imgStyle.height = 'auto';
}
return (
<div style={containerStyle}>
<img src={block.src} alt={block.alt || ''} style={imgStyle} />
</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" onClick={handleClick}>
{/* 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>
)}
{/* Only show edit button for card, button, and image blocks */}
{(block.type === 'card' || block.type === 'button' || block.type === 'image') && (
<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>
);
}