diff --git a/admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx b/admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx index 629ff00..74684fa 100644 --- a/admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx +++ b/admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx @@ -18,7 +18,7 @@ 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(); @@ -28,63 +28,15 @@ export default function EditTemplate() { 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([]); // Visual mode view (derived from markdown) const [activeTab, setActiveTab] = useState('preview'); - - // All available template variables - const availableVariables = [ - // Order variables - 'order_number', - 'order_id', - 'order_date', - 'order_total', - 'order_subtotal', - 'order_tax', - 'order_shipping', - 'order_discount', - 'order_status', - 'order_url', - 'order_items_table', - 'completion_date', - 'estimated_delivery', - // Customer variables - 'customer_name', - 'customer_first_name', - 'customer_last_name', - 'customer_email', - 'customer_phone', - 'billing_address', - 'shipping_address', - // Payment variables - 'payment_method', - 'payment_status', - 'payment_date', - 'transaction_id', - 'payment_retry_url', - // Shipping/Tracking variables - 'tracking_number', - 'tracking_url', - 'shipping_carrier', - 'shipping_method', - // URL variables - 'review_url', - 'shop_url', - 'my_account_url', - // Store variables - 'site_name', - 'site_title', - 'store_name', - 'store_url', - 'support_email', - 'current_year', - ]; // Fetch email customization settings const { data: emailSettings } = useQuery({ @@ -101,20 +53,20 @@ export default function EditTemplate() { 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, @@ -123,11 +75,11 @@ export default function EditTemplate() { 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); @@ -151,7 +103,7 @@ export default function EditTemplate() { 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'] }); @@ -168,7 +120,7 @@ export default function EditTemplate() { 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 @@ -176,9 +128,11 @@ export default function EditTemplate() { setBlocks(newBlocks); // Keep blocks in sync }; - // Variable keys for the rich text editor dropdown - const variableKeys = availableVariables; - + // 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 @@ -187,7 +141,7 @@ export default function EditTemplate() { const htmlContent = markdownToHtml(cardContent.trim()); return `
${htmlContent}
`; }); - + // Parse card blocks - old [card type="..."] syntax (backward compatibility) parsed = parsed.replace(/\[card([^\]]*)\](.*?)\[\/card\]/gs, (match, attributes, cardContent) => { let cardClass = 'card'; @@ -195,27 +149,27 @@ export default function EditTemplate() { 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 `
${htmlContent}
`; }); - + // 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 `

${text.trim()}

`; }); - + // 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 `

${text.trim()}

`; }); - + return parsed; }; @@ -223,19 +177,19 @@ export default function EditTemplate() { 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', store_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', @@ -311,23 +265,23 @@ export default function EditTemplate() { store_email: 'store@example.com', support_email: 'support@example.com', }; - + 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 - availableVariables.forEach(key => { + variableKeys.forEach((key: string) => { if (!storeVariables[key] && !sampleData[key]) { const sampleValue = `[${key}]`; previewBody = previewBody.replace(new RegExp(`\\{${key}\\}`, 'g'), sampleValue); } }); - + // Parse [card] tags previewBody = parseCardsForPreview(previewBody); - + // Get email settings for preview const settings = emailSettings || {}; const primaryColor = settings.primary_color || '#7f54b3'; @@ -342,10 +296,10 @@ export default function EditTemplate() { 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 const pluginUrl = (window as any).woonoowData?.pluginUrl || @@ -360,7 +314,7 @@ export default function EditTemplate() { `).join('')} ` : ''; - + return ` @@ -416,7 +370,7 @@ export default function EditTemplate() { `; }; - + // Helper function to get social icon emoji const getSocialIcon = (platform: string) => { const icons: Record = { @@ -492,91 +446,91 @@ export default function EditTemplate() { } > - - {/* Subject */} -
- - setSubject(e.target.value)} - placeholder={__('Enter notification subject')} - /> -

- {channelId === 'email' - ? __('Email subject line') - : __('Push notification title')} -

+ + {/* Subject */} +
+ + setSubject(e.target.value)} + placeholder={__('Enter notification subject')} + /> +

+ {channelId === 'email' + ? __('Email subject line') + : __('Push notification title')} +

+
+ + {/* Body */} +
+ {/* Three-tab system: Preview | Visual | Markdown */} +
+ + + + + + {__('Preview')} + + + + {__('Visual')} + + + + {__('Markdown')} + + +
- {/* Body */} -
- {/* Three-tab system: Preview | Visual | Markdown */} -
- - - - - - {__('Preview')} - - - - {__('Visual')} - - - - {__('Markdown')} - - - + {/* Preview Tab */} + {activeTab === 'preview' && ( +
+