632 lines
26 KiB
TypeScript
632 lines
26 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||
import { api } from '@/lib/api';
|
||
import { SettingsLayout } from '../components/SettingsLayout';
|
||
import { Card, CardContent } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { EmailBuilder, EmailBlock, blocksToMarkdown, markdownToBlocks } from '@/components/EmailBuilder';
|
||
import { CodeEditor } from '@/components/ui/code-editor';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||
import { ArrowLeft, Eye, Edit, RotateCcw, FileText, Send } from 'lucide-react';
|
||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||
import { toast } from 'sonner';
|
||
import { __ } from '@/lib/i18n';
|
||
import { markdownToHtml } from '@/lib/markdown-utils';
|
||
|
||
export default function EditTemplate() {
|
||
// Mobile responsive check
|
||
const [isMobile, setIsMobile] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||
checkMobile();
|
||
window.addEventListener('resize', checkMobile);
|
||
return () => window.removeEventListener('resize', checkMobile);
|
||
}, []);
|
||
const navigate = useNavigate();
|
||
const [searchParams] = useSearchParams();
|
||
const queryClient = useQueryClient();
|
||
|
||
const eventId = searchParams.get('event');
|
||
const channelId = searchParams.get('channel');
|
||
const recipientType = searchParams.get('recipient') || 'customer'; // Default to customer
|
||
|
||
const [subject, setSubject] = useState('');
|
||
const [markdownContent, setMarkdownContent] = useState(''); // Source of truth: Markdown
|
||
const [blocks, setBlocks] = useState<EmailBlock[]>([]); // Visual mode view (derived from markdown)
|
||
const [activeTab, setActiveTab] = useState('preview');
|
||
|
||
// Send Test Email state
|
||
const [testEmailDialogOpen, setTestEmailDialogOpen] = useState(false);
|
||
const [testEmail, setTestEmail] = useState('');
|
||
|
||
// Fetch email customization settings (for non-color settings like logo, footer, social links)
|
||
const { data: emailSettings } = useQuery({
|
||
queryKey: ['email-settings'],
|
||
queryFn: () => api.get('/notifications/email-settings'),
|
||
});
|
||
|
||
// Fetch appearance settings for unified colors
|
||
const { data: appearanceSettings } = useQuery({
|
||
queryKey: ['appearance-settings'],
|
||
queryFn: () => api.get('/appearance/settings'),
|
||
});
|
||
|
||
// Fetch template
|
||
const { data: template, isLoading, error } = useQuery({
|
||
queryKey: ['notification-template', eventId, channelId, recipientType],
|
||
queryFn: async () => {
|
||
console.log('Fetching template for:', eventId, channelId, recipientType);
|
||
const response = await api.get(`/notifications/templates/${eventId}/${channelId}?recipient=${recipientType}`);
|
||
console.log('API Response:', response);
|
||
console.log('API Response.data:', response.data);
|
||
console.log('API Response type:', typeof response);
|
||
|
||
// The api.get might already unwrap response.data
|
||
// Return the response directly if it has the template fields
|
||
if (response && (response.subject !== undefined || response.body !== undefined)) {
|
||
console.log('Returning response directly:', response);
|
||
return response;
|
||
}
|
||
|
||
// Otherwise return response.data
|
||
if (response && response.data) {
|
||
console.log('Returning response.data:', response.data);
|
||
return response.data;
|
||
}
|
||
|
||
return null;
|
||
},
|
||
enabled: !!eventId && !!channelId,
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (template) {
|
||
setSubject(template.subject || '');
|
||
|
||
// Always treat body as markdown (source of truth)
|
||
const markdown = template.body || '';
|
||
setMarkdownContent(markdown);
|
||
|
||
// Convert to blocks for visual mode
|
||
const initialBlocks = markdownToBlocks(markdown);
|
||
setBlocks(initialBlocks);
|
||
}
|
||
}, [template]);
|
||
|
||
const handleSave = async () => {
|
||
try {
|
||
await api.post(`/notifications/templates/${eventId}/${channelId}`, {
|
||
subject,
|
||
body: markdownContent, // Save markdown (source of truth)
|
||
recipient: recipientType,
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
|
||
queryClient.invalidateQueries({ queryKey: ['notification-template', eventId, channelId, recipientType] });
|
||
toast.success(__('Template saved successfully'));
|
||
} catch (error: any) {
|
||
toast.error(error?.message || __('Failed to save template'));
|
||
}
|
||
};
|
||
|
||
const handleReset = async () => {
|
||
if (!confirm(__('Are you sure you want to reset this template to default?'))) return;
|
||
|
||
try {
|
||
await api.del(`/notifications/templates/${eventId}/${channelId}?recipient=${recipientType}`);
|
||
queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
|
||
queryClient.invalidateQueries({ queryKey: ['notification-template', eventId, channelId, recipientType] });
|
||
toast.success(__('Template reset to default'));
|
||
} catch (error: any) {
|
||
toast.error(error?.message || __('Failed to reset template'));
|
||
}
|
||
};
|
||
|
||
// Send test email mutation
|
||
const sendTestMutation = useMutation({
|
||
mutationFn: async (email: string) => {
|
||
return api.post(`/notifications/templates/${eventId}/${channelId}/send-test`, {
|
||
email,
|
||
recipient: recipientType,
|
||
});
|
||
},
|
||
onSuccess: (data: any) => {
|
||
toast.success(data.message || __('Test email sent successfully'));
|
||
setTestEmailDialogOpen(false);
|
||
setTestEmail('');
|
||
},
|
||
onError: (error: any) => {
|
||
toast.error(error?.message || __('Failed to send test email'));
|
||
},
|
||
});
|
||
|
||
const handleSendTest = () => {
|
||
if (!testEmail || !testEmail.includes('@')) {
|
||
toast.error(__('Please enter a valid email address'));
|
||
return;
|
||
}
|
||
sendTestMutation.mutate(testEmail);
|
||
};
|
||
|
||
// Visual mode: Update blocks → Markdown (source of truth)
|
||
const handleBlocksChange = (newBlocks: EmailBlock[]) => {
|
||
setBlocks(newBlocks);
|
||
const markdown = blocksToMarkdown(newBlocks);
|
||
setMarkdownContent(markdown); // Update markdown (source of truth)
|
||
};
|
||
|
||
// Markdown mode: Update markdown → Blocks (for visual sync)
|
||
const handleMarkdownChange = (newMarkdown: string) => {
|
||
setMarkdownContent(newMarkdown); // Update source of truth
|
||
const newBlocks = markdownToBlocks(newMarkdown);
|
||
setBlocks(newBlocks); // Keep blocks in sync
|
||
};
|
||
|
||
// Variable keys for the rich text editor dropdown - from API (contextual per event)
|
||
const variableKeys = template?.available_variables
|
||
? Object.keys(template.available_variables).map(k => k.replace(/^\{|}$/g, ''))
|
||
: [];
|
||
|
||
// Parse [card] tags and [button] shortcodes for preview
|
||
const parseCardsForPreview = (content: string) => {
|
||
// Parse card blocks - new [card:type] syntax
|
||
let parsed = content.replace(/\[card:(\w+)\](.*?)\[\/card\]/gs, (match, type, cardContent) => {
|
||
const cardClass = `card card-${type}`;
|
||
const htmlContent = markdownToHtml(cardContent.trim());
|
||
return `<div class="${cardClass}">${htmlContent}</div>`;
|
||
});
|
||
|
||
// Parse card blocks - old [card type="..."] syntax (backward compatibility)
|
||
parsed = parsed.replace(/\[card([^\]]*)\](.*?)\[\/card\]/gs, (match, attributes, cardContent) => {
|
||
let cardClass = 'card';
|
||
const typeMatch = attributes.match(/type=["']([^"']+)["']/);
|
||
if (typeMatch) {
|
||
cardClass += ` card-${typeMatch[1]}`;
|
||
}
|
||
|
||
const bgMatch = attributes.match(/bg=["']([^"']+)["']/);
|
||
const bgStyle = bgMatch ? `background-image: url(${bgMatch[1]}); background-size: cover; background-position: center;` : '';
|
||
|
||
// Convert markdown inside card to HTML
|
||
const htmlContent = markdownToHtml(cardContent.trim());
|
||
return `<div class="${cardClass}" style="${bgStyle}">${htmlContent}</div>`;
|
||
});
|
||
|
||
// Parse button shortcodes - new [button:style](url)Text[/button] syntax
|
||
parsed = parsed.replace(/\[button:(\w+)\]\(([^)]+)\)([^\[]+)\[\/button\]/g, (match, style, url, text) => {
|
||
const buttonClass = style === 'outline' ? 'button-outline' : 'button';
|
||
return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`;
|
||
});
|
||
|
||
// Parse button shortcodes - old [button url="..."]Text[/button] syntax (backward compatibility)
|
||
parsed = parsed.replace(/\[button\s+url=["']([^"']+)["'](?:\s+style=["'](solid|outline)["'])?\]([^\[]+)\[\/button\]/g, (match, url, style, text) => {
|
||
const buttonClass = style === 'outline' ? 'button-outline' : 'button';
|
||
return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`;
|
||
});
|
||
|
||
return parsed;
|
||
};
|
||
|
||
// Generate preview HTML
|
||
const generatePreviewHTML = () => {
|
||
// Convert markdown to HTML for preview
|
||
let previewBody = parseCardsForPreview(markdownContent);
|
||
|
||
// Replace store-identity variables with actual data
|
||
const storeVariables: { [key: string]: string } = {
|
||
store_name: 'My WordPress Store',
|
||
site_url: window.location.origin,
|
||
store_email: 'store@example.com',
|
||
};
|
||
|
||
Object.entries(storeVariables).forEach(([key, value]) => {
|
||
const regex = new RegExp(`\\{${key}\\}`, 'g');
|
||
previewBody = previewBody.replace(regex, value);
|
||
});
|
||
|
||
// Replace dynamic variables with sample data (not just highlighting)
|
||
const sampleData: { [key: string]: string } = {
|
||
order_number: '12345',
|
||
order_total: '$99.99',
|
||
order_status: 'Processing',
|
||
order_date: new Date().toLocaleDateString(),
|
||
order_url: '#',
|
||
completion_date: new Date().toLocaleDateString(),
|
||
order_items_list: `<ul style="list-style: none; padding: 0; margin: 16px 0;">
|
||
<li style="padding: 12px; background: #f9f9f9; border-radius: 6px; margin-bottom: 8px;">
|
||
<strong>Premium T-Shirt</strong> × 2<br>
|
||
<span style="color: #666;">Size: L, Color: Blue</span><br>
|
||
<span style="font-weight: 600;">$49.98</span>
|
||
</li>
|
||
<li style="padding: 12px; background: #f9f9f9; border-radius: 6px; margin-bottom: 8px;">
|
||
<strong>Classic Jeans</strong> × 1<br>
|
||
<span style="color: #666;">Size: 32, Color: Dark Blue</span><br>
|
||
<span style="font-weight: 600;">$79.99</span>
|
||
</li>
|
||
</ul>`,
|
||
order_items_table: `<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
|
||
<thead>
|
||
<tr style="background: #f5f5f5;">
|
||
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #ddd;">Product</th>
|
||
<th style="padding: 12px; text-align: center; border-bottom: 2px solid #ddd;">Qty</th>
|
||
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #ddd;">Price</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">
|
||
<strong>Premium T-Shirt</strong><br>
|
||
<span style="color: #666; font-size: 13px;">Size: L, Color: Blue</span>
|
||
</td>
|
||
<td style="padding: 12px; text-align: center; border-bottom: 1px solid #eee;">2</td>
|
||
<td style="padding: 12px; text-align: right; border-bottom: 1px solid #eee;">$49.98</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">
|
||
<strong>Classic Jeans</strong><br>
|
||
<span style="color: #666; font-size: 13px;">Size: 32, Color: Dark Blue</span>
|
||
</td>
|
||
<td style="padding: 12px; text-align: center; border-bottom: 1px solid #eee;">1</td>
|
||
<td style="padding: 12px; text-align: right; border-bottom: 1px solid #eee;">$79.99</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>`,
|
||
customer_name: 'John Doe',
|
||
customer_email: 'john@example.com',
|
||
customer_phone: '+1 234 567 8900',
|
||
payment_method: 'Credit Card',
|
||
payment_url: '#',
|
||
shipping_method: 'Standard Shipping',
|
||
tracking_number: 'TRACK123456',
|
||
tracking_url: '#',
|
||
shipping_carrier: 'Standard Shipping',
|
||
refund_amount: '$50.00',
|
||
billing_address: '123 Main St, City, State 12345',
|
||
shipping_address: '123 Main St, City, State 12345',
|
||
transaction_id: 'TXN123456789',
|
||
payment_date: new Date().toLocaleDateString(),
|
||
payment_status: 'Completed',
|
||
review_url: '#',
|
||
shop_url: '#',
|
||
my_account_url: '#',
|
||
payment_retry_url: '#',
|
||
vip_dashboard_url: '#',
|
||
vip_free_shipping_threshold: '$50',
|
||
current_year: new Date().getFullYear().toString(),
|
||
site_name: 'My WordPress Store',
|
||
store_name: 'My WordPress Store',
|
||
site_url: '#',
|
||
store_email: 'store@example.com',
|
||
support_email: 'support@example.com',
|
||
// Account-related URLs and variables
|
||
login_url: '#',
|
||
reset_link: '#',
|
||
reset_key: 'abc123xyz',
|
||
user_login: 'johndoe',
|
||
user_email: 'john@example.com',
|
||
user_temp_password: '••••••••',
|
||
customer_first_name: 'John',
|
||
customer_last_name: 'Doe',
|
||
// Campaign/Newsletter variables
|
||
content: '<p>This is sample content that would be replaced with your actual campaign content.</p>',
|
||
campaign_title: 'Newsletter Campaign',
|
||
};
|
||
|
||
Object.keys(sampleData).forEach((key) => {
|
||
const regex = new RegExp(`\\{${key}\\}`, 'g');
|
||
previewBody = previewBody.replace(regex, sampleData[key]);
|
||
});
|
||
|
||
// Highlight variables that don't have sample data
|
||
// Use plain text [variable] instead of HTML spans to avoid breaking href attributes
|
||
variableKeys.forEach((key: string) => {
|
||
if (!storeVariables[key] && !sampleData[key]) {
|
||
previewBody = previewBody.replace(new RegExp(`\\{${key}\\}`, 'g'), `[${key}]`);
|
||
}
|
||
});
|
||
|
||
// Get email settings for preview - use UNIFIED appearance settings for colors
|
||
const settings = emailSettings || {};
|
||
const appearColors = appearanceSettings?.data?.general?.colors || appearanceSettings?.general?.colors || {};
|
||
const primaryColor = appearColors.primary || '#7f54b3';
|
||
const secondaryColor = appearColors.secondary || '#7f54b3';
|
||
const heroGradientStart = appearColors.gradientStart || '#667eea';
|
||
const heroGradientEnd = appearColors.gradientEnd || '#764ba2';
|
||
const heroTextColor = '#ffffff'; // Always white on gradient
|
||
const buttonTextColor = '#ffffff'; // Always white on primary
|
||
const bodyBgColor = settings.body_bg_color || '#f8f8f8';
|
||
const socialIconColor = settings.social_icon_color || 'white';
|
||
const logoUrl = settings.logo_url || '';
|
||
const headerText = settings.header_text || 'My WordPress Store';
|
||
const footerText = settings.footer_text || `© ${new Date().getFullYear()} My WordPress Store. All rights reserved.`;
|
||
const socialLinks = settings.social_links || [];
|
||
|
||
// Replace {current_year} in footer
|
||
const processedFooter = footerText.replace('{current_year}', new Date().getFullYear().toString());
|
||
|
||
// Generate social icons HTML with PNG images
|
||
// Get plugin URL from config, with fallback
|
||
const pluginUrl =
|
||
(window as any).woonoowData?.pluginUrl ||
|
||
(window as any).WNW_CONFIG?.pluginUrl ||
|
||
'/wp-content/plugins/woonoow/';
|
||
const socialIconsHtml = socialLinks.length > 0 ? `
|
||
<div style="margin-top: 16px;">
|
||
${socialLinks.map((link: any) => `
|
||
<a href="${link.url}" style="display: inline-block; margin: 0 8px; text-decoration: none;">
|
||
<img src="${pluginUrl}assets/icons/mage--${link.platform}-${socialIconColor}.png" alt="${link.platform}" style="width: 24px; height: 24px;" />
|
||
</a>
|
||
`).join('')}
|
||
</div>
|
||
` : '';
|
||
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<style>
|
||
body { font-family: 'Inter', Arial, sans-serif; background: ${bodyBgColor}; margin: 0; padding: 20px; }
|
||
.container { max-width: 600px; margin: 0 auto; }
|
||
.header { padding: 32px; text-align: center; background: #f8f8f8; }
|
||
.card-gutter { padding: 0 16px; }
|
||
.card { background: #ffffff; border-radius: 8px; margin-bottom: 24px; padding: 32px 40px; }
|
||
|
||
/* Mobile responsive */
|
||
@media only screen and (max-width: 600px) {
|
||
body { padding: 8px; }
|
||
.card-gutter { padding: 0 8px; }
|
||
.card { padding: 20px 16px; }
|
||
.header { padding: 20px 16px; }
|
||
.footer { padding: 20px 16px; }
|
||
}
|
||
.card-success { background-color: #f0fdf4; }
|
||
.card-highlight { background: linear-gradient(135deg, ${heroGradientStart} 0%, ${heroGradientEnd} 100%); color: ${heroTextColor}; }
|
||
.card-highlight * { color: ${heroTextColor} !important; }
|
||
.card-hero { background: linear-gradient(135deg, ${heroGradientStart} 0%, ${heroGradientEnd} 100%); color: ${heroTextColor}; }
|
||
.card-hero * { color: ${heroTextColor} !important; }
|
||
.card-info { background-color: #f0f7ff; }
|
||
.card-warning { background-color: #fff8e1; }
|
||
.card-basic { background: none; border: none; padding: 0; margin: 16px 0; }
|
||
h1 { font-size: 26px; margin-top: 0; margin-bottom: 16px; color: #333; }
|
||
h2 { font-size: 18px; margin-top: 0; margin-bottom: 16px; color: #333; }
|
||
h3 { font-size: 16px; margin-top: 0; margin-bottom: 8px; color: #333; }
|
||
p { font-size: 16px; line-height: 1.6; color: #555; margin-bottom: 16px; }
|
||
.button { display: inline-block; background: ${primaryColor}; color: ${buttonTextColor} !important; padding: 14px 28px; border-radius: 6px; text-decoration: none; font-weight: 600; }
|
||
.button-outline { display: inline-block; background: transparent; color: ${secondaryColor} !important; padding: 12px 26px; border: 2px solid ${secondaryColor}; border-radius: 6px; text-decoration: none; font-weight: 600; }
|
||
.text-link { color: ${primaryColor}; text-decoration: underline; }
|
||
.info-box { background: #f6f6f6; border-radius: 6px; padding: 20px; margin: 16px 0; }
|
||
.footer { padding: 32px; text-align: center; color: #888; font-size: 13px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
${logoUrl ? `<img src="${logoUrl}" alt="${headerText}" style="max-width: 200px; max-height: 60px;">` : `<strong style="font-size: 24px; color: #333;">${headerText}</strong>`}
|
||
</div>
|
||
<div class="card-gutter">
|
||
${previewBody}
|
||
</div>
|
||
<div class="footer">
|
||
<p>${processedFooter}</p>
|
||
${socialIconsHtml}
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`;
|
||
};
|
||
|
||
// Helper function to get social icon emoji
|
||
const getSocialIcon = (platform: string) => {
|
||
const icons: Record<string, string> = {
|
||
facebook: '📘',
|
||
twitter: '🐦',
|
||
instagram: '📷',
|
||
linkedin: '💼',
|
||
youtube: '📺',
|
||
website: '🌐',
|
||
};
|
||
return icons[platform] || '🔗';
|
||
};
|
||
|
||
if (!eventId || !channelId) {
|
||
return (
|
||
<SettingsLayout
|
||
title={__('Edit Template')}
|
||
description={__('Template editor')}
|
||
>
|
||
<div className="text-muted-foreground">{__('Invalid template parameters')}</div>
|
||
</SettingsLayout>
|
||
);
|
||
}
|
||
|
||
// Don't render form until template data is loaded
|
||
if (isLoading || !template) {
|
||
return (
|
||
<SettingsLayout
|
||
title={__('Edit Template')}
|
||
description={__('Loading template...')}
|
||
isLoading={true}
|
||
>
|
||
<div className="text-muted-foreground">{__('Loading template data...')}</div>
|
||
</SettingsLayout>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<SettingsLayout
|
||
title={template.event_label || __('Edit Template')}
|
||
description={`${template.channel_label || ''} - ${__('Customize the notification template. Use variables like {customer_name} to personalize messages.')}`}
|
||
onSave={handleSave}
|
||
saveLabel={__('Save Template')}
|
||
isLoading={false}
|
||
action={
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => {
|
||
// Determine if staff or customer based on event category
|
||
const isStaffEvent = template.event_category === 'staff' || eventId?.includes('admin') || eventId?.includes('staff');
|
||
const page = isStaffEvent ? 'staff' : 'customer';
|
||
navigate(`/settings/notifications/${page}?tab=events`);
|
||
}}
|
||
className="gap-2"
|
||
title={__('Back')}
|
||
>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
<span className="hidden sm:inline">{__('Back')}</span>
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleReset}
|
||
className="gap-2"
|
||
title={__('Reset to Default')}
|
||
>
|
||
<RotateCcw className="h-4 w-4" />
|
||
<span className="hidden sm:inline">{__('Reset to Default')}</span>
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => setTestEmailDialogOpen(true)}
|
||
className="gap-2"
|
||
title={__('Send Test')}
|
||
>
|
||
<Send className="h-4 w-4" />
|
||
<span className="hidden sm:inline">{__('Send Test')}</span>
|
||
</Button>
|
||
</div>
|
||
}
|
||
>
|
||
<Card>
|
||
<CardContent className="pt-6 space-y-6">
|
||
{/* Subject */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="subject">{__('Subject / Title')}</Label>
|
||
<Input
|
||
id="subject"
|
||
value={subject}
|
||
onChange={(e) => setSubject(e.target.value)}
|
||
placeholder={__('Enter notification subject')}
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
{channelId === 'email'
|
||
? __('Email subject line')
|
||
: __('Push notification title')}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Body */}
|
||
<div className="space-y-4">
|
||
{/* Three-tab system: Preview | Visual | Markdown */}
|
||
<div className="flex items-center justify-between">
|
||
<Label>{__('Message Body')}</Label>
|
||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-auto">
|
||
<TabsList className="grid grid-cols-3">
|
||
<TabsTrigger value="preview" className="flex items-center gap-1 text-xs">
|
||
<Eye className="h-3 w-3" />
|
||
{__('Preview')}
|
||
</TabsTrigger>
|
||
<TabsTrigger value="visual" className="flex items-center gap-1 text-xs">
|
||
<Edit className="h-3 w-3" />
|
||
{__('Visual')}
|
||
</TabsTrigger>
|
||
<TabsTrigger value="markdown" className="flex items-center gap-1 text-xs">
|
||
<FileText className="h-3 w-3" />
|
||
{__('Markdown')}
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
</Tabs>
|
||
</div>
|
||
|
||
{/* Preview Tab */}
|
||
{activeTab === 'preview' && (
|
||
<div className="border rounded-md overflow-hidden">
|
||
<iframe
|
||
srcDoc={generatePreviewHTML()}
|
||
className="w-full min-h-[600px] overflow-hidden bg-white"
|
||
title={__('Email Preview')}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Visual Tab */}
|
||
{activeTab === 'visual' && (
|
||
<div>
|
||
<EmailBuilder
|
||
blocks={blocks}
|
||
onChange={handleBlocksChange}
|
||
variables={variableKeys}
|
||
/>
|
||
<p className="text-xs text-muted-foreground mt-2">
|
||
{__('Build your email visually. Add blocks, edit content, and switch to Preview to see your branding.')}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Markdown Tab */}
|
||
{activeTab === 'markdown' && (
|
||
<div className="space-y-2">
|
||
<CodeEditor
|
||
value={markdownContent}
|
||
onChange={handleMarkdownChange}
|
||
placeholder={__('Write in Markdown... Easy and mobile-friendly!')}
|
||
supportMarkdown={true}
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
{__('Write in Markdown - easy to type, even on mobile! Use **bold**, ## headings, [card]...[/card], etc.')}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
{__('All changes are automatically synced between Visual and Markdown modes.')}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</SettingsLayout>
|
||
|
||
{/* Send Test Email Dialog */}
|
||
<Dialog open={testEmailDialogOpen} onOpenChange={setTestEmailDialogOpen}>
|
||
<DialogContent className="sm:max-w-md">
|
||
<DialogHeader>
|
||
<DialogTitle>{__('Send Test Email')}</DialogTitle>
|
||
<DialogDescription>
|
||
{__('Send a test email with sample data to verify the template looks correct.')}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-4 p-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="test-email">{__('Email Address')}</Label>
|
||
<Input
|
||
id="test-email"
|
||
type="email"
|
||
value={testEmail}
|
||
onChange={(e) => setTestEmail(e.target.value)}
|
||
placeholder="you@example.com"
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
{__('The subject will be prefixed with [TEST]')}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setTestEmailDialogOpen(false)}>
|
||
{__('Cancel')}
|
||
</Button>
|
||
<Button onClick={handleSendTest} disabled={sendTestMutation.isPending}>
|
||
{sendTestMutation.isPending ? __('Sending...') : __('Send Test')}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</>
|
||
);
|
||
}
|