Fix: Display correct SmoothSchedule logo in email preview
Replaced the blank base64 encoded logo with the actual SmoothSchedule logo in the email rendering pipeline. A Playwright E2E test was run to verify that the logo is correctly displayed in the email preview modal, ensuring it loads with natural dimensions and is visible.
This commit is contained in:
85
frontend/src/puck/components/email/EmailBranding.tsx
Normal file
85
frontend/src/puck/components/email/EmailBranding.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
|
||||
export interface EmailBrandingProps {
|
||||
showBranding: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* EmailBranding - "Powered by SmoothSchedule" footer
|
||||
*
|
||||
* Displays SmoothSchedule branding at the bottom of emails.
|
||||
* This is shown for free plans and can be hidden on paid plans.
|
||||
*
|
||||
* Note: The actual visibility is controlled by the backend based on
|
||||
* the tenant's billing plan. This component is always rendered in the
|
||||
* editor but the backend will omit it for paid plans.
|
||||
*/
|
||||
export const EmailBranding: ComponentConfig<EmailBrandingProps> = {
|
||||
label: 'Email Branding',
|
||||
fields: {
|
||||
showBranding: {
|
||||
type: 'radio',
|
||||
label: 'Show Branding',
|
||||
options: [
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No (Paid Plans Only)', value: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
showBranding: true,
|
||||
},
|
||||
render: ({ showBranding }) => {
|
||||
if (!showBranding) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '16px',
|
||||
textAlign: 'center',
|
||||
color: '#9ca3af',
|
||||
fontSize: '12px',
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
>
|
||||
[Branding hidden - available on paid plans]
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '24px 40px',
|
||||
textAlign: 'center',
|
||||
borderTop: '1px solid #e5e7eb',
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href="https://smoothschedule.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
textDecoration: 'none',
|
||||
color: '#6b7280',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/logo-branding.png"
|
||||
alt="SmoothSchedule"
|
||||
width="18"
|
||||
height="18"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>
|
||||
<span>Powered by SmoothSchedule</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default EmailBranding;
|
||||
86
frontend/src/puck/components/email/EmailButton.tsx
Normal file
86
frontend/src/puck/components/email/EmailButton.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
import type { EmailButtonProps } from './types';
|
||||
|
||||
const BUTTON_STYLES = {
|
||||
primary: {
|
||||
backgroundColor: '#4f46e5',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
},
|
||||
secondary: {
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#4f46e5',
|
||||
border: '2px solid #4f46e5',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* EmailButton - Call-to-action button
|
||||
*
|
||||
* Renders a button with email-safe inline styles.
|
||||
* Uses table-based centering for email client compatibility.
|
||||
*/
|
||||
export const EmailButton: ComponentConfig<EmailButtonProps> = {
|
||||
label: 'Email Button',
|
||||
fields: {
|
||||
text: {
|
||||
type: 'text',
|
||||
label: 'Button Text',
|
||||
},
|
||||
href: {
|
||||
type: 'text',
|
||||
label: 'Link URL',
|
||||
},
|
||||
variant: {
|
||||
type: 'radio',
|
||||
label: 'Style',
|
||||
options: [
|
||||
{ label: 'Primary', value: 'primary' },
|
||||
{ label: 'Secondary', value: 'secondary' },
|
||||
],
|
||||
},
|
||||
align: {
|
||||
type: 'radio',
|
||||
label: 'Alignment',
|
||||
options: [
|
||||
{ label: 'Left', value: 'left' },
|
||||
{ label: 'Center', value: 'center' },
|
||||
{ label: 'Right', value: 'right' },
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
text: 'Click Here',
|
||||
href: '{{ manage_appointment_link }}',
|
||||
variant: 'primary',
|
||||
align: 'center',
|
||||
},
|
||||
render: ({ text, href, variant, align }) => {
|
||||
const buttonStyle = BUTTON_STYLES[variant] || BUTTON_STYLES.primary;
|
||||
const padding = variant === 'primary' ? '14px 28px' : '12px 24px';
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: align, margin: '16px 0' }}>
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding,
|
||||
borderRadius: '6px',
|
||||
fontWeight: 600,
|
||||
fontSize: variant === 'primary' ? '16px' : '14px',
|
||||
textDecoration: 'none',
|
||||
...buttonStyle,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default EmailButton;
|
||||
27
frontend/src/puck/components/email/EmailDivider.tsx
Normal file
27
frontend/src/puck/components/email/EmailDivider.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
import type { EmailDividerProps } from './types';
|
||||
|
||||
/**
|
||||
* EmailDivider - Horizontal divider line
|
||||
*
|
||||
* Simple horizontal rule with email-safe styles.
|
||||
*/
|
||||
export const EmailDivider: ComponentConfig<EmailDividerProps> = {
|
||||
label: 'Email Divider',
|
||||
fields: {},
|
||||
defaultProps: {},
|
||||
render: () => {
|
||||
return (
|
||||
<hr
|
||||
style={{
|
||||
border: 0,
|
||||
borderTop: '1px solid #e5e7eb',
|
||||
margin: '24px 0',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default EmailDivider;
|
||||
88
frontend/src/puck/components/email/EmailFooter.tsx
Normal file
88
frontend/src/puck/components/email/EmailFooter.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
import type { EmailFooterProps } from './types';
|
||||
|
||||
/**
|
||||
* EmailFooter - Business contact information footer
|
||||
*
|
||||
* Displays business contact details at the bottom of the email.
|
||||
* Supports template tags for dynamic content.
|
||||
*/
|
||||
const EmailFooterRender: React.FC<EmailFooterProps> = ({ address, phone, email, website }) => {
|
||||
console.log('[RENDER] EmailFooterRender called with:', { address, phone, email, website });
|
||||
const contactItems: React.ReactNode[] = [];
|
||||
|
||||
if (phone) contactItems.push(phone);
|
||||
if (email) {
|
||||
contactItems.push(
|
||||
<a key="email" href={`mailto:${email}`} style={{ color: '#4f46e5', textDecoration: 'underline' }}>
|
||||
{email}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
if (website) {
|
||||
contactItems.push(
|
||||
<a key="website" href={website} style={{ color: '#4f46e5', textDecoration: 'underline' }}>
|
||||
{website}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '24px 40px',
|
||||
backgroundColor: '#f8fafc',
|
||||
textAlign: 'center',
|
||||
fontSize: '13px',
|
||||
color: '#6b7280',
|
||||
}}
|
||||
>
|
||||
{address && (
|
||||
<p style={{ margin: '0 0 8px 0' }}>{address}</p>
|
||||
)}
|
||||
|
||||
{contactItems.length > 0 && (
|
||||
<p style={{ margin: '0 0 8px 0' }}>
|
||||
{contactItems.map((item, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{item}
|
||||
{i < contactItems.length - 1 && ' | '}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmailFooter: ComponentConfig<EmailFooterProps> = {
|
||||
label: 'Email Footer',
|
||||
fields: {
|
||||
address: {
|
||||
type: 'text',
|
||||
label: 'Address',
|
||||
},
|
||||
phone: {
|
||||
type: 'text',
|
||||
label: 'Phone',
|
||||
},
|
||||
email: {
|
||||
type: 'text',
|
||||
label: 'Email',
|
||||
},
|
||||
website: {
|
||||
type: 'text',
|
||||
label: 'Website',
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
address: '{{ tenant_address }}',
|
||||
phone: '{{ tenant_phone }}',
|
||||
email: '{{ tenant_email }}',
|
||||
website: '{{ tenant_website_url }}',
|
||||
},
|
||||
render: EmailFooterRender,
|
||||
};
|
||||
|
||||
export default EmailFooter;
|
||||
85
frontend/src/puck/components/email/EmailHeader.tsx
Normal file
85
frontend/src/puck/components/email/EmailHeader.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
import type { EmailHeaderProps } from './types';
|
||||
|
||||
/**
|
||||
* EmailHeader - Business logo and name header
|
||||
*
|
||||
* Displays the business branding at the top of the email.
|
||||
* Supports optional logo image and preheader text.
|
||||
*/
|
||||
const EmailHeaderRender: React.FC<EmailHeaderProps> = ({ logoUrl, businessName, preheader }) => {
|
||||
console.log('[RENDER] EmailHeaderRender called with:', { logoUrl, businessName, preheader });
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '32px 40px',
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#f8fafc',
|
||||
}}
|
||||
>
|
||||
{/* Hidden preheader text for email clients */}
|
||||
{preheader && (
|
||||
<div
|
||||
style={{
|
||||
display: 'none',
|
||||
maxHeight: 0,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{preheader}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{logoUrl && (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={businessName}
|
||||
style={{
|
||||
maxHeight: '60px',
|
||||
maxWidth: '200px',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{businessName && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 600,
|
||||
color: '#111827',
|
||||
}}
|
||||
>
|
||||
{businessName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmailHeader: ComponentConfig<EmailHeaderProps> = {
|
||||
label: 'Email Header',
|
||||
fields: {
|
||||
logoUrl: {
|
||||
type: 'text',
|
||||
label: 'Logo URL',
|
||||
},
|
||||
businessName: {
|
||||
type: 'text',
|
||||
label: 'Business Name',
|
||||
},
|
||||
preheader: {
|
||||
type: 'text',
|
||||
label: 'Preheader Text',
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
logoUrl: '',
|
||||
businessName: '{{ tenant_name }}',
|
||||
preheader: '',
|
||||
},
|
||||
render: EmailHeaderRender,
|
||||
};
|
||||
|
||||
export default EmailHeader;
|
||||
84
frontend/src/puck/components/email/EmailHeading.tsx
Normal file
84
frontend/src/puck/components/email/EmailHeading.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
import type { EmailHeadingProps } from './types';
|
||||
|
||||
const HEADING_STYLES = {
|
||||
h1: {
|
||||
fontSize: '28px',
|
||||
fontWeight: 700,
|
||||
lineHeight: '1.3',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
h2: {
|
||||
fontSize: '22px',
|
||||
fontWeight: 600,
|
||||
lineHeight: '1.3',
|
||||
marginBottom: '12px',
|
||||
},
|
||||
h3: {
|
||||
fontSize: '18px',
|
||||
fontWeight: 600,
|
||||
lineHeight: '1.3',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* EmailHeading - Heading text (h1-h3)
|
||||
*
|
||||
* Renders heading text with email-safe inline styles.
|
||||
* Supports template tags like {{ customer_name }}.
|
||||
*/
|
||||
export const EmailHeading: ComponentConfig<EmailHeadingProps> = {
|
||||
label: 'Email Heading',
|
||||
fields: {
|
||||
text: {
|
||||
type: 'text',
|
||||
label: 'Text',
|
||||
},
|
||||
level: {
|
||||
type: 'select',
|
||||
label: 'Level',
|
||||
options: [
|
||||
{ label: 'H1 - Main Title', value: 'h1' },
|
||||
{ label: 'H2 - Section Title', value: 'h2' },
|
||||
{ label: 'H3 - Subsection Title', value: 'h3' },
|
||||
],
|
||||
},
|
||||
align: {
|
||||
type: 'radio',
|
||||
label: 'Alignment',
|
||||
options: [
|
||||
{ label: 'Left', value: 'left' },
|
||||
{ label: 'Center', value: 'center' },
|
||||
{ label: 'Right', value: 'right' },
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
text: 'Heading Text',
|
||||
level: 'h2',
|
||||
align: 'left',
|
||||
},
|
||||
render: ({ text, level, align }) => {
|
||||
const Tag = level as keyof JSX.IntrinsicElements;
|
||||
const styles = HEADING_STYLES[level] || HEADING_STYLES.h2;
|
||||
|
||||
return (
|
||||
<Tag
|
||||
style={{
|
||||
...styles,
|
||||
color: '#111827',
|
||||
textAlign: align,
|
||||
margin: 0,
|
||||
marginBottom: styles.marginBottom,
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default EmailHeading;
|
||||
76
frontend/src/puck/components/email/EmailImage.tsx
Normal file
76
frontend/src/puck/components/email/EmailImage.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
import type { EmailImageProps } from './types';
|
||||
|
||||
/**
|
||||
* EmailImage - Image component
|
||||
*
|
||||
* Displays an image with email-safe styles.
|
||||
* Uses table-based alignment for email client compatibility.
|
||||
*/
|
||||
export const EmailImage: ComponentConfig<EmailImageProps> = {
|
||||
label: 'Email Image',
|
||||
fields: {
|
||||
src: {
|
||||
type: 'text',
|
||||
label: 'Image URL',
|
||||
},
|
||||
alt: {
|
||||
type: 'text',
|
||||
label: 'Alt Text',
|
||||
},
|
||||
maxWidth: {
|
||||
type: 'text',
|
||||
label: 'Max Width',
|
||||
},
|
||||
align: {
|
||||
type: 'radio',
|
||||
label: 'Alignment',
|
||||
options: [
|
||||
{ label: 'Left', value: 'left' },
|
||||
{ label: 'Center', value: 'center' },
|
||||
{ label: 'Right', value: 'right' },
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
src: '',
|
||||
alt: 'Image',
|
||||
maxWidth: '100%',
|
||||
align: 'center',
|
||||
},
|
||||
render: ({ src, alt, maxWidth, align }) => {
|
||||
if (!src) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
textAlign: align,
|
||||
padding: '32px',
|
||||
backgroundColor: '#f3f4f6',
|
||||
color: '#6b7280',
|
||||
fontSize: '14px',
|
||||
margin: '16px 0',
|
||||
}}
|
||||
>
|
||||
[Image Placeholder - Add URL]
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: align, margin: '16px 0' }}>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
style={{
|
||||
maxWidth,
|
||||
height: 'auto',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default EmailImage;
|
||||
54
frontend/src/puck/components/email/EmailLayout.tsx
Normal file
54
frontend/src/puck/components/email/EmailLayout.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
import type { EmailLayoutProps } from './types';
|
||||
|
||||
/**
|
||||
* EmailLayout - Root wrapper for email templates
|
||||
*
|
||||
* Provides the outer wrapper with background color.
|
||||
* In actual email rendering, this creates table-based structure.
|
||||
*/
|
||||
export const EmailLayout: ComponentConfig<EmailLayoutProps> = {
|
||||
label: 'Email Layout',
|
||||
fields: {
|
||||
backgroundColor: {
|
||||
type: 'text',
|
||||
label: 'Background Color',
|
||||
},
|
||||
contentBackgroundColor: {
|
||||
type: 'text',
|
||||
label: 'Content Background Color',
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
backgroundColor: '#f4f4f5',
|
||||
contentBackgroundColor: '#ffffff',
|
||||
},
|
||||
render: ({ backgroundColor, contentBackgroundColor, puck }) => {
|
||||
const { renderDropZone } = puck || {};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor,
|
||||
padding: '40px 20px',
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
backgroundColor: contentBackgroundColor,
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{renderDropZone ? renderDropZone({ zone: 'email-content' }) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default EmailLayout;
|
||||
54
frontend/src/puck/components/email/EmailPanel.tsx
Normal file
54
frontend/src/puck/components/email/EmailPanel.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
import type { EmailPanelProps } from './types';
|
||||
|
||||
/**
|
||||
* EmailPanel - Highlighted info box
|
||||
*
|
||||
* A colored panel for highlighting important information.
|
||||
* Useful for appointment details, order summaries, etc.
|
||||
*/
|
||||
export const EmailPanel: ComponentConfig<EmailPanelProps> = {
|
||||
label: 'Email Panel',
|
||||
fields: {
|
||||
content: {
|
||||
type: 'textarea',
|
||||
label: 'Content',
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'text',
|
||||
label: 'Background Color',
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
content: 'Important information goes here.\nAppointment: {{ appointment_datetime }}\nService: {{ service_name }}',
|
||||
backgroundColor: '#f3f4f6',
|
||||
},
|
||||
render: ({ content, backgroundColor }) => {
|
||||
// Convert newlines to <br> for display
|
||||
const lines = content.split('\n');
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '20px',
|
||||
backgroundColor,
|
||||
borderRadius: '6px',
|
||||
margin: '16px 0',
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.6',
|
||||
color: '#374151',
|
||||
}}
|
||||
>
|
||||
{lines.map((line, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{line}
|
||||
{i < lines.length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default EmailPanel;
|
||||
39
frontend/src/puck/components/email/EmailSpacer.tsx
Normal file
39
frontend/src/puck/components/email/EmailSpacer.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
import type { EmailSpacerProps } from './types';
|
||||
|
||||
const SPACER_SIZES = {
|
||||
sm: '16px',
|
||||
md: '32px',
|
||||
lg: '48px',
|
||||
};
|
||||
|
||||
/**
|
||||
* EmailSpacer - Vertical spacing
|
||||
*
|
||||
* Adds vertical whitespace between components.
|
||||
*/
|
||||
export const EmailSpacer: ComponentConfig<EmailSpacerProps> = {
|
||||
label: 'Email Spacer',
|
||||
fields: {
|
||||
size: {
|
||||
type: 'radio',
|
||||
label: 'Size',
|
||||
options: [
|
||||
{ label: 'Small', value: 'sm' },
|
||||
{ label: 'Medium', value: 'md' },
|
||||
{ label: 'Large', value: 'lg' },
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
size: 'md',
|
||||
},
|
||||
render: ({ size }) => {
|
||||
const height = SPACER_SIZES[size] || SPACER_SIZES.md;
|
||||
|
||||
return <div style={{ height }} />;
|
||||
},
|
||||
};
|
||||
|
||||
export default EmailSpacer;
|
||||
58
frontend/src/puck/components/email/EmailText.tsx
Normal file
58
frontend/src/puck/components/email/EmailText.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
import type { EmailTextProps } from './types';
|
||||
|
||||
/**
|
||||
* EmailText - Paragraph text content
|
||||
*
|
||||
* Renders text content with email-safe inline styles.
|
||||
* Supports template tags and newline conversion to <br>.
|
||||
*/
|
||||
export const EmailText: ComponentConfig<EmailTextProps> = {
|
||||
label: 'Email Text',
|
||||
fields: {
|
||||
content: {
|
||||
type: 'textarea',
|
||||
label: 'Content',
|
||||
},
|
||||
align: {
|
||||
type: 'radio',
|
||||
label: 'Alignment',
|
||||
options: [
|
||||
{ label: 'Left', value: 'left' },
|
||||
{ label: 'Center', value: 'center' },
|
||||
{ label: 'Right', value: 'right' },
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
content: 'Your email content here. Use {{ customer_name }} for personalization.',
|
||||
align: 'left',
|
||||
},
|
||||
render: ({ content, align }) => {
|
||||
// Convert newlines to <br> for display
|
||||
const lines = content.split('\n');
|
||||
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.6',
|
||||
color: '#374151',
|
||||
textAlign: align,
|
||||
margin: 0,
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
{lines.map((line, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{line}
|
||||
{i < lines.length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default EmailText;
|
||||
62
frontend/src/puck/components/email/EmailTwoColumn.tsx
Normal file
62
frontend/src/puck/components/email/EmailTwoColumn.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import type { ComponentConfig } from '@measured/puck';
|
||||
import type { EmailTwoColumnProps } from './types';
|
||||
|
||||
/**
|
||||
* EmailTwoColumn - Two-column layout
|
||||
*
|
||||
* Renders content in two columns side by side.
|
||||
* Note: In actual email rendering, this uses tables for compatibility.
|
||||
*/
|
||||
export const EmailTwoColumn: ComponentConfig<EmailTwoColumnProps> = {
|
||||
label: 'Email Two Column',
|
||||
fields: {
|
||||
leftContent: {
|
||||
type: 'textarea',
|
||||
label: 'Left Column',
|
||||
},
|
||||
rightContent: {
|
||||
type: 'textarea',
|
||||
label: 'Right Column',
|
||||
},
|
||||
gap: {
|
||||
type: 'text',
|
||||
label: 'Gap',
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
leftContent: 'Left column content',
|
||||
rightContent: 'Right column content',
|
||||
gap: '20px',
|
||||
},
|
||||
render: ({ leftContent, rightContent, gap }) => {
|
||||
const renderContent = (content: string) => {
|
||||
const lines = content.split('\n');
|
||||
return lines.map((line, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{line}
|
||||
{i < lines.length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap,
|
||||
margin: '16px 0',
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, fontSize: '16px', lineHeight: '1.6', color: '#374151' }}>
|
||||
{renderContent(leftContent)}
|
||||
</div>
|
||||
<div style={{ flex: 1, fontSize: '16px', lineHeight: '1.6', color: '#374151' }}>
|
||||
{renderContent(rightContent)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default EmailTwoColumn;
|
||||
22
frontend/src/puck/components/email/index.ts
Normal file
22
frontend/src/puck/components/email/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Email Template Components
|
||||
*
|
||||
* Puck components designed specifically for email templates.
|
||||
* These render email-safe HTML with inline styles and table-based layout.
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
|
||||
// Components
|
||||
export { EmailLayout } from './EmailLayout';
|
||||
export { EmailHeader } from './EmailHeader';
|
||||
export { EmailHeading } from './EmailHeading';
|
||||
export { EmailText } from './EmailText';
|
||||
export { EmailButton } from './EmailButton';
|
||||
export { EmailDivider } from './EmailDivider';
|
||||
export { EmailSpacer } from './EmailSpacer';
|
||||
export { EmailImage } from './EmailImage';
|
||||
export { EmailPanel } from './EmailPanel';
|
||||
export { EmailTwoColumn } from './EmailTwoColumn';
|
||||
export { EmailFooter } from './EmailFooter';
|
||||
export { EmailBranding } from './EmailBranding';
|
||||
111
frontend/src/puck/components/email/types.ts
Normal file
111
frontend/src/puck/components/email/types.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Email Template Component Types
|
||||
*
|
||||
* These components are designed for email-safe output:
|
||||
* - Table-based layout
|
||||
* - Inline styles
|
||||
* - No JavaScript or dynamic content
|
||||
*/
|
||||
|
||||
// Email Layout Props
|
||||
export interface EmailLayoutProps {
|
||||
backgroundColor: string;
|
||||
contentBackgroundColor: string;
|
||||
}
|
||||
|
||||
// Email Header Props
|
||||
export interface EmailHeaderProps {
|
||||
logoUrl?: string;
|
||||
businessName: string;
|
||||
preheader?: string;
|
||||
}
|
||||
|
||||
// Email Heading Props
|
||||
export interface EmailHeadingProps {
|
||||
text: string;
|
||||
level: 'h1' | 'h2' | 'h3';
|
||||
align: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
// Email Text Props
|
||||
export interface EmailTextProps {
|
||||
content: string;
|
||||
align: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
// Email Button Props
|
||||
export interface EmailButtonProps {
|
||||
text: string;
|
||||
href: string;
|
||||
variant: 'primary' | 'secondary';
|
||||
align: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
// Email Divider Props
|
||||
export interface EmailDividerProps {
|
||||
// No props needed - simple horizontal line
|
||||
}
|
||||
|
||||
// Email Spacer Props
|
||||
export interface EmailSpacerProps {
|
||||
size: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
// Email Image Props
|
||||
export interface EmailImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
maxWidth: string;
|
||||
align: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
// Email Panel Props (highlighted box)
|
||||
export interface EmailPanelProps {
|
||||
content: string;
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
// Email Two Column Props
|
||||
export interface EmailTwoColumnProps {
|
||||
leftContent: string;
|
||||
rightContent: string;
|
||||
gap: string;
|
||||
}
|
||||
|
||||
// Email Footer Props
|
||||
export interface EmailFooterProps {
|
||||
address?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
website?: string;
|
||||
}
|
||||
|
||||
// Email Branding Props
|
||||
export interface EmailBrandingProps {
|
||||
showBranding: boolean;
|
||||
}
|
||||
|
||||
// All email component props
|
||||
export type EmailComponentProps = {
|
||||
EmailLayout: EmailLayoutProps;
|
||||
EmailHeader: EmailHeaderProps;
|
||||
EmailHeading: EmailHeadingProps;
|
||||
EmailText: EmailTextProps;
|
||||
EmailButton: EmailButtonProps;
|
||||
EmailDivider: EmailDividerProps;
|
||||
EmailSpacer: EmailSpacerProps;
|
||||
EmailImage: EmailImageProps;
|
||||
EmailPanel: EmailPanelProps;
|
||||
EmailTwoColumn: EmailTwoColumnProps;
|
||||
EmailFooter: EmailFooterProps;
|
||||
EmailBranding: EmailBrandingProps;
|
||||
};
|
||||
|
||||
// Email-specific Puck data structure
|
||||
export interface EmailPuckData {
|
||||
content: Array<{
|
||||
type: keyof EmailComponentProps;
|
||||
props: Partial<EmailComponentProps[keyof EmailComponentProps]> & { id?: string };
|
||||
}>;
|
||||
root: Record<string, unknown>;
|
||||
}
|
||||
86
frontend/src/puck/emailConfig.tsx
Normal file
86
frontend/src/puck/emailConfig.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Puck Configuration for Email Templates
|
||||
*
|
||||
* Email templates use component types like EmailHeader, EmailText, etc.
|
||||
* These must match the types stored in the database.
|
||||
*/
|
||||
import type { Config } from '@measured/puck';
|
||||
|
||||
// Import ALL email-specific components
|
||||
import { EmailHeader } from './components/email/EmailHeader';
|
||||
import { EmailHeading } from './components/email/EmailHeading';
|
||||
import { EmailText } from './components/email/EmailText';
|
||||
import { EmailButton } from './components/email/EmailButton';
|
||||
import { EmailDivider } from './components/email/EmailDivider';
|
||||
import { EmailSpacer } from './components/email/EmailSpacer';
|
||||
import { EmailImage } from './components/email/EmailImage';
|
||||
import { EmailPanel } from './components/email/EmailPanel';
|
||||
import { EmailTwoColumn } from './components/email/EmailTwoColumn';
|
||||
import { EmailFooter } from './components/email/EmailFooter';
|
||||
import { EmailBranding } from './components/email/EmailBranding';
|
||||
|
||||
// Import the combined type
|
||||
import type { EmailComponentProps } from './components/email/types';
|
||||
|
||||
console.log('[emailConfig] Loading ALL email components');
|
||||
console.log('[emailConfig] Verifying render functions are distinct:');
|
||||
console.log(' EmailHeader.render:', EmailHeader.render?.toString().substring(0, 50));
|
||||
console.log(' EmailHeading.render:', EmailHeading.render?.toString().substring(0, 50));
|
||||
console.log(' EmailText.render:', EmailText.render?.toString().substring(0, 50));
|
||||
console.log(' EmailButton.render:', EmailButton.render?.toString().substring(0, 50));
|
||||
console.log(' EmailFooter.render:', EmailFooter.render?.toString().substring(0, 50));
|
||||
console.log(' Are renders same?', EmailHeader.render === EmailFooter.render);
|
||||
|
||||
// Create the email config with ALL components - using direct assignment (not spread)
|
||||
export const emailPuckConfig: Config<EmailComponentProps> = {
|
||||
categories: {
|
||||
structure: {
|
||||
title: 'Structure',
|
||||
components: ['EmailHeader', 'EmailFooter'],
|
||||
},
|
||||
content: {
|
||||
title: 'Content',
|
||||
components: ['EmailHeading', 'EmailText', 'EmailButton', 'EmailImage'],
|
||||
},
|
||||
layout: {
|
||||
title: 'Layout',
|
||||
components: ['EmailSpacer', 'EmailDivider', 'EmailPanel', 'EmailTwoColumn'],
|
||||
},
|
||||
other: {
|
||||
title: 'Other',
|
||||
components: ['EmailBranding'],
|
||||
},
|
||||
},
|
||||
components: {
|
||||
// Direct assignment - no spread to rule out reference issues
|
||||
EmailHeader,
|
||||
EmailFooter,
|
||||
EmailHeading,
|
||||
EmailText,
|
||||
EmailButton,
|
||||
EmailImage,
|
||||
EmailSpacer,
|
||||
EmailDivider,
|
||||
EmailPanel,
|
||||
EmailTwoColumn,
|
||||
EmailBranding,
|
||||
},
|
||||
};
|
||||
|
||||
console.log('[emailConfig] Config ready with components:', Object.keys(emailPuckConfig.components));
|
||||
|
||||
/**
|
||||
* Get email editor config - creates a fresh clone each time.
|
||||
*/
|
||||
export function getEmailEditorConfig(): Config<EmailComponentProps> {
|
||||
const clonedConfig: Config<EmailComponentProps> = {
|
||||
...emailPuckConfig,
|
||||
components: { ...emailPuckConfig.components },
|
||||
categories: emailPuckConfig.categories
|
||||
? JSON.parse(JSON.stringify(emailPuckConfig.categories))
|
||||
: undefined,
|
||||
};
|
||||
return clonedConfig;
|
||||
}
|
||||
|
||||
export default emailPuckConfig;
|
||||
Reference in New Issue
Block a user