/** * Email Preview Frame Component * Renders a live preview of the email template with applied styles. * * When a `body` prop is supplied (e.g. Email Verification) the preview * renders that text instead of the default subscription sample content. * Placeholders like {{user_name}} are swapped for sample values and * {{verification_link}} becomes a CTA button. */ import { useMemo } from 'react'; import type { EmailCustomizerSettings, DevicePreview } from '../types'; import type { ProductCardConfig, CouponDisplayConfig } from '../sections'; import { getGoogleFontLinkTag } from '../types'; interface EmailPreviewFrameProps { settings: EmailCustomizerSettings; device: DevicePreview; darkMode?: boolean; emailType?: string; heading?: string; /** Optional user-editable body text (with placeholders). */ body?: string; preheaderText?: string; additionalContent?: string; /** Optional product card config for product-oriented emails. */ productCard?: ProductCardConfig; /** Optional coupon display config for recovery emails. */ couponDisplay?: CouponDisplayConfig; } // --------------------------------------------------------------------------- // Sample content per email type (subscription defaults) // --------------------------------------------------------------------------- function getEmailTypeContent(emailType: string) { const emailContents: Record = { new_subscription: { intro: 'Thank you for subscribing!', body: 'Your subscription for Premium Membership has been created successfully. Your first billing date is January 15, 2025.', buttonText: 'View Subscription', }, subscription_activated: { intro: 'Great news!', body: 'Your subscription for Premium Membership is now active. Your next billing date is February 15, 2025.', buttonText: 'View Subscription', }, subscription_cancelled: { intro: 'We\'re sorry to see you go.', body: 'Your subscription for Premium Membership has been cancelled as requested. You will continue to have access until January 15, 2025.', alert: { type: 'warning', message: 'Access expires: January 15, 2025' }, buttonText: 'Resubscribe', }, subscription_expired: { intro: 'Your subscription period has ended.', body: 'Your subscription for Premium Membership has expired. To continue enjoying our services, please renew your subscription.', alert: { type: 'error', message: 'Subscription expired' }, buttonText: 'Renew Subscription', }, subscription_on_hold: { intro: 'Your subscription has been paused.', body: 'Your subscription for Premium Membership is currently on hold. This may be due to a payment issue or at your request.', alert: { type: 'warning', message: 'Your access is temporarily suspended' }, buttonText: 'Resume Subscription', }, renewal_reminder: { intro: 'This is a friendly reminder.', body: 'Your subscription for Premium Membership will automatically renew on January 15, 2025 for $29.99.', alert: { type: 'info', message: 'Renewal Date: January 15, 2025 • Amount: $29.99' }, buttonText: 'Manage Subscription', }, trial_ending: { intro: 'Your trial period is almost over.', body: 'Your free trial for Premium Membership ends on January 15, 2025. After that, your subscription will begin billing at $29.99 automatically.', alert: { type: 'info', message: 'Trial ends: January 15, 2025' }, buttonText: 'Manage Subscription', }, payment_failed: { intro: 'We were unable to process your payment.', body: 'The renewal payment for your Premium Membership subscription has failed. Please update your payment method to avoid service interruption.', alert: { type: 'error', message: '⚠ Payment failed - action required' }, buttonText: 'Update Payment Method', }, payment_retry: { intro: 'We are retrying your payment.', body: 'We will attempt to process your payment for Premium Membership again shortly. No action is required at this time.', alert: { type: 'info', message: 'Next retry: Tomorrow at 9:00 AM' }, buttonText: 'View Payment Details', }, }; return emailContents[emailType] || emailContents.subscription_activated; } // --------------------------------------------------------------------------- // Process a user-editable body template for preview // --------------------------------------------------------------------------- /** Sample values used to replace common placeholders in the preview. */ const samplePlaceholders: Record = { '{{user_name}}': 'John', '{{user_email}}': 'john@example.com', '{{site_name}}': 'Your Store', '{{site_url}}': 'https://yourstore.com', '{{expiry_time}}': '24 hours', '{customer_name}': 'John Doe', '{site_name}': 'Your Store', '{site_url}': 'https://yourstore.com', '{product_name}': 'Blue Running Shoes', '{product_price}': '$49.99', '{product_url}': '#', '{customer_email}': 'john@example.com', }; /** * Convert plain-text body with placeholders into HTML for the preview pane. * - Known placeholders are replaced with sample values. * - `{{verification_link}}` becomes a styled CTA button. * - Double newlines become paragraph breaks; single newlines become `
`. */ function processBodyForPreview( body: string, buttonBg: string, buttonText: string, borderRadius: number, fontSize: number, textColor: string, ): string { let text = body; // Replace known placeholders (except verification_link). for (const [tag, val] of Object.entries(samplePlaceholders)) { text = text.split(tag).join(val); } const pStyle = `font-size: ${fontSize}px; color: ${textColor}; margin: 0 0 16px 0;`; // Split into paragraphs on double-newline. const paragraphs = text.split(/\n\n+/); return paragraphs .map((p) => { const trimmed = p.trim(); if (!trimmed) return ''; // Render {{verification_link}} as a CTA button. if (trimmed === '{{verification_link}}') { return `
Verify Email Address
`; } // Normal paragraph — single newlines become
. const withBr = trimmed.replace(/\n/g, '
'); return `

${withBr}

`; }) .filter(Boolean) .join('\n'); } export function EmailPreviewFrame({ settings, device, darkMode = false, emailType = 'subscription_activated', heading = 'Your Subscription is Active', body, preheaderText = '', additionalContent = '', productCard, couponDisplay, }: EmailPreviewFrameProps) { const { logo, colors, typography, layout, footer, table } = settings; // Should we render the user-editable body instead of subscription sample? const useCustomBody = body !== undefined; // Calculate container width based on device const containerWidth = device === 'mobile' ? 320 : layout.containerWidth; // Dark mode color overrides - simulates how email clients render in dark mode const darkModeColors = darkMode ? { bodyBackground: '#1a1a1a', headerBackground: '#2d2d2d', contentBackground: '#262626', primaryText: '#e5e5e5', secondaryText: '#a3a3a3', borderColor: '#404040', } : null; // Get plugin URL for local assets const pluginUrl = window.swiftCommerceData?.pluginUrl || ''; const iconBaseUrl = `${pluginUrl}assets/img/icons/`; // Get content based on email type const emailContent = getEmailTypeContent(emailType); // Process custom body for preview if provided. const processedBody = useCustomBody ? processBodyForPreview( body, colors.buttonBackground, colors.buttonText, layout.borderRadius, typography.bodySize, darkModeColors?.primaryText ?? colors.primaryText, ) : null; // Build product card HTML for preview if provided. const productCardHTML = useMemo(() => { if (!productCard || !useCustomBody) return ''; const { showImage, showTitle, showPrice, buttonText, sample } = productCard; if (!showImage && !showTitle && !showPrice) { // Still show the CTA button even without product details. return `
${buttonText}
`; } const placeholderImg = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 200 200'%3E%3Crect fill='%23f3f4f6' width='200' height='200'/%3E%3Cpath d='M70 130l30-50 30 50' fill='none' stroke='%23d1d5db' stroke-width='4' stroke-linejoin='round'/%3E%3Cpath d='M95 130l12-20 12 20' fill='none' stroke='%23d1d5db' stroke-width='4' stroke-linejoin='round'/%3E%3Ccircle cx='78' cy='82' r='8' fill='%23d1d5db'/%3E%3C/svg%3E"; const imageUrl = sample.imageUrl || placeholderImg; const cardBg = darkModeColors ? '#303030' : '#f9fafb'; const titleColor = darkModeColors?.primaryText ?? '#1f2937'; const priceColor = colors.buttonBackground; let cardInner = ''; // Image cell. if (showImage) { cardInner += ` ${sample.name} `; } // Details cell. let detailsContent = ''; if (showTitle) { detailsContent += `

${sample.name}

`; } if (showPrice) { detailsContent += `

${sample.price}

`; } if (detailsContent) { cardInner += `${detailsContent}`; } let html = ` ${cardInner}
`; // CTA button. html += `
${buttonText}
`; return html; }, [productCard, useCustomBody, colors, typography, layout, darkModeColors]); // Build coupon display HTML for preview if provided. const couponDisplayHTML = useMemo(() => { if (!couponDisplay || !useCustomBody) return ''; const msg = couponDisplay.messageText || 'Use code {coupon_code} at checkout for a special discount!'; const parts = msg.split('{coupon_code}'); const codeHtml = `${(settings as any)._couponPrefix || 'RECOVER'}-A1B2C3D4`; const messageHtml = parts.join(codeHtml); return `

${messageHtml}

`; }, [couponDisplay, useCustomBody, typography, settings]); // Generate the email HTML for preview const emailHTML = useMemo(() => { const socialLinksHTML = footer.showSocialLinks ? ` ` : ''; const copyrightText = footer.copyrightText .replace('{year}', new Date().getFullYear().toString()) .replace('{site_name}', 'Your Store') .replace('{site_url}', 'https://yourstore.com'); // Apply dark mode overrides const effectiveColors = { ...colors, ...(darkModeColors || {}), }; // Generate alert HTML if exists const alertColors: Record = darkMode ? { info: { bg: '#1e3a5f', border: '#2196f3', text: '#90caf9' }, warning: { bg: '#4a3000', border: '#ff9800', text: '#ffcc80' }, error: { bg: '#4a1515', border: '#f44336', text: '#ef9a9a' }, } : { info: { bg: '#e3f2fd', border: '#2196f3', text: '#1565c0' }, warning: { bg: '#fff3e0', border: '#ff9800', text: '#e65100' }, error: { bg: '#ffebee', border: '#f44336', text: '#c62828' }, }; const alertHTML = emailContent.alert ? `
${emailContent.alert.message}
` : ''; // Table colors for dark mode const effectiveTableColors = darkMode ? { headerBackground: '#2d2d2d', headerTextColor: '#e5e5e5', rowBackground: '#262626', alternateRowBackground: '#303030', borderColor: '#404040', } : table; return ` ${preheaderText ? `` : ''} ${getGoogleFontLinkTag(typography.fontFamily)} ${preheaderText ? `
${preheaderText}
` : ''} `; }, [settings, containerWidth, heading, preheaderText, additionalContent, emailType, emailContent, logo, colors, typography, layout, footer, table, iconBaseUrl, darkMode, darkModeColors, useCustomBody, processedBody, productCardHTML, couponDisplayHTML]); return (
{/* Device Frame */} {device === 'mobile' && (
)} {/* Email Preview iframe */}