diff --git a/admin-spa/src/App.tsx b/admin-spa/src/App.tsx
index 2676a5f..7b8e5ac 100644
--- a/admin-spa/src/App.tsx
+++ b/admin-spa/src/App.tsx
@@ -203,6 +203,7 @@ import SettingsLocalPickup from '@/routes/Settings/LocalPickup';
import SettingsNotifications from '@/routes/Settings/Notifications';
import StaffNotifications from '@/routes/Settings/Notifications/Staff';
import CustomerNotifications from '@/routes/Settings/Notifications/Customer';
+import EditTemplate from '@/routes/Settings/Notifications/EditTemplate';
import SettingsDeveloper from '@/routes/Settings/Developer';
import MorePage from '@/routes/More';
@@ -492,6 +493,7 @@ function AppRoutes() {
} />
} />
} />
+ } />
} />
} />
diff --git a/admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx b/admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx
new file mode 100644
index 0000000..5bf90e1
--- /dev/null
+++ b/admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx
@@ -0,0 +1,298 @@
+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 { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { RichTextEditor } from '@/components/ui/rich-text-editor';
+import { Label } from '@/components/ui/label';
+import { Badge } from '@/components/ui/badge';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { ArrowLeft, Eye, Edit, Save, RotateCcw } from 'lucide-react';
+import { toast } from 'sonner';
+import { __ } from '@/lib/i18n';
+
+export default function EditTemplate() {
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const queryClient = useQueryClient();
+
+ const eventId = searchParams.get('event');
+ const channelId = searchParams.get('channel');
+
+ const [subject, setSubject] = useState('');
+ const [body, setBody] = useState('');
+ const [variables, setVariables] = useState<{ [key: string]: string }>({});
+ const [activeTab, setActiveTab] = useState('editor');
+
+ // Fetch template
+ const { data: template, isLoading } = useQuery({
+ queryKey: ['notification-template', eventId, channelId],
+ queryFn: async () => {
+ const response = await api.get(`/notifications/templates/${eventId}/${channelId}`);
+ return response.data;
+ },
+ enabled: !!eventId && !!channelId,
+ });
+
+ useEffect(() => {
+ if (template) {
+ setSubject(template.subject || '');
+ setBody(template.body || '');
+ setVariables(template.variables || {});
+ }
+ }, [template]);
+
+ const saveMutation = useMutation({
+ mutationFn: async () => {
+ return api.post('/notifications/templates', {
+ eventId,
+ channelId,
+ subject,
+ body,
+ });
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
+ queryClient.invalidateQueries({ queryKey: ['notification-template', eventId, channelId] });
+ toast.success(__('Template saved successfully'));
+ navigate(-1);
+ },
+ onError: (error: any) => {
+ toast.error(error?.message || __('Failed to save template'));
+ },
+ });
+
+ const resetMutation = useMutation({
+ mutationFn: async () => {
+ return api.del(`/notifications/templates/${eventId}/${channelId}`);
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
+ queryClient.invalidateQueries({ queryKey: ['notification-template', eventId, channelId] });
+ toast.success(__('Template reset to default'));
+ navigate(-1);
+ },
+ onError: (error: any) => {
+ toast.error(error?.message || __('Failed to reset template'));
+ },
+ });
+
+ // Get variable keys for the rich text editor
+ const variableKeys = Object.keys(variables);
+
+ // Parse [card] tags for preview
+ const parseCardsForPreview = (content: string) => {
+ const cardRegex = /\[card([^\]]*)\](.*?)\[\/card\]/gs;
+
+ return content.replace(cardRegex, (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;` : '';
+
+ return `
${cardContent}
`;
+ });
+ };
+
+ // Generate preview HTML
+ const generatePreviewHTML = () => {
+ let previewBody = body;
+
+ // 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]) => {
+ previewBody = previewBody.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
+ });
+
+ // Highlight dynamic variables (non-store variables)
+ Object.keys(variables).forEach(key => {
+ if (!storeVariables[key]) {
+ const sampleValue = `[${key}]`;
+ previewBody = previewBody.replace(new RegExp(`\\{${key}\\}`, 'g'), sampleValue);
+ }
+ });
+
+ // Parse [card] tags
+ previewBody = parseCardsForPreview(previewBody);
+
+ return `
+
+
+
+
+
+
+
+
+
+ ${previewBody}
+
+
+
+
+
+ `;
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!eventId || !channelId) {
+ return (
+
+
{__('Invalid template parameters')}
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+ {__('Edit Template')}
+
+
+ {template?.event_label} - {template?.channel_label}
+
+
+
+
+
+
+
+
+
+ {/* Subject */}
+
+
+ setSubject(e.target.value)}
+ placeholder={__('Enter notification subject')}
+ className="mt-2"
+ />
+
+
+
+ {/* Body - Tabs */}
+
+
+
+
+
+ {__('Editor')}
+
+
+
+ {__('Preview')}
+
+
+
+ {/* Editor Tab */}
+
+
+
+
+
+
+
+ {/* Preview Tab */}
+
+
+
+
+
+
+ {__('This is how your email will look. Dynamic variables are highlighted in yellow.')}
+
+
+
+
+
+
+ );
+}
diff --git a/admin-spa/src/routes/Settings/Notifications/TemplateEditor.tsx b/admin-spa/src/routes/Settings/Notifications/TemplateEditor.tsx
index a9bd007..fe3d143 100644
--- a/admin-spa/src/routes/Settings/Notifications/TemplateEditor.tsx
+++ b/admin-spa/src/routes/Settings/Notifications/TemplateEditor.tsx
@@ -96,16 +96,53 @@ export default function TemplateEditor({
// Get variable keys for the rich text editor
const variableKeys = Object.keys(variables);
+
+ // Parse [card] tags for preview
+ const parseCardsForPreview = (content: string) => {
+ // Match [card ...] ... [/card] patterns
+ const cardRegex = /\[card([^\]]*)\](.*?)\[\/card\]/gs;
+
+ return content.replace(cardRegex, (match, attributes, cardContent) => {
+ // Parse attributes
+ 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;` : '';
+
+ return `${cardContent}
`;
+ });
+ };
// Generate preview HTML
const generatePreviewHTML = () => {
- // Simple preview - replace variables with sample data
let previewBody = body;
- Object.keys(variables).forEach(key => {
- const sampleValue = `[${key}]`;
- previewBody = previewBody.replace(new RegExp(`\\{${key}\\}`, 'g'), sampleValue);
+
+ // 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]) => {
+ previewBody = previewBody.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
});
+ // Highlight dynamic variables (non-store variables)
+ Object.keys(variables).forEach(key => {
+ if (!storeVariables[key]) {
+ const sampleValue = `[${key}]`;
+ previewBody = previewBody.replace(new RegExp(`\\{${key}\\}`, 'g'), sampleValue);
+ }
+ });
+
+ // Parse [card] tags
+ previewBody = parseCardsForPreview(previewBody);
+
return `
@@ -115,16 +152,17 @@ export default function TemplateEditor({
.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; }
+ .card { background: #ffffff; border-radius: 8px; margin-bottom: 24px; padding: 32px 40px; }
.card-success { background: #e8f5e9; border: 1px solid #4caf50; }
.card-highlight { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; }
+ .card-highlight * { color: #fff !important; }
.card-info { background: #f0f7ff; border: 1px solid #0071e3; }
.card-warning { background: #fff8e1; border: 1px solid #ff9800; }
- .content { padding: 32px 40px; }
- .content h1 { font-size: 26px; margin-top: 0; }
- .content h2 { font-size: 18px; margin-top: 0; }
- .content p { font-size: 16px; line-height: 1.6; color: #555; }
- .button { display: inline-block; background: #7f54b3; color: #fff; padding: 14px 28px; border-radius: 6px; text-decoration: none; }
+ 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: #7f54b3; color: #fff !important; padding: 14px 28px; border-radius: 6px; text-decoration: none; font-weight: 600; }
.info-box { background: #f6f6f6; border-radius: 6px; padding: 20px; margin: 16px 0; }
.footer { padding: 32px; text-align: center; color: #888; font-size: 13px; }
@@ -132,13 +170,13 @@ export default function TemplateEditor({
@@ -189,22 +227,6 @@ export default function TemplateEditor({
placeholder={__('Enter notification message')}
variables={variableKeys}
/>
-
-
- {__('Available Variables:')}
-
-
- {Object.keys(variables).map((key) => (
-
- {`{${key}}`}
-
- ))}
-
-
@@ -215,8 +237,16 @@ export default function TemplateEditor({
diff --git a/admin-spa/src/routes/Settings/Notifications/Templates.tsx b/admin-spa/src/routes/Settings/Notifications/Templates.tsx
index 3918c87..99598a0 100644
--- a/admin-spa/src/routes/Settings/Notifications/Templates.tsx
+++ b/admin-spa/src/routes/Settings/Notifications/Templates.tsx
@@ -1,4 +1,5 @@
-import React, { useState } from 'react';
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { SettingsCard } from '../components/SettingsCard';
@@ -12,7 +13,6 @@ import {
} from '@/components/ui/accordion';
import { RefreshCw, Mail, MessageCircle, Send, Bell, Edit } from 'lucide-react';
import { __ } from '@/lib/i18n';
-import TemplateEditor from './TemplateEditor';
interface NotificationChannel {
id: string;
@@ -24,10 +24,7 @@ interface NotificationChannel {
}
export default function NotificationTemplates() {
- const [editorOpen, setEditorOpen] = useState(false);
- const [selectedEvent, setSelectedEvent] = useState(null);
- const [selectedChannel, setSelectedChannel] = useState(null);
- const [selectedTemplate, setSelectedTemplate] = useState(null);
+ const navigate = useNavigate();
// Fetch channels
const { data: channels, isLoading: channelsLoading } = useQuery({
@@ -47,20 +44,9 @@ export default function NotificationTemplates() {
queryFn: () => api.get('/notifications/templates'),
});
- const openEditor = async (event: any, channel: any) => {
- setSelectedEvent(event);
- setSelectedChannel(channel);
-
- // Fetch template
- try {
- const template = await api.get(`/notifications/templates/${event.id}/${channel.id}`);
- setSelectedTemplate(template);
- } catch (error) {
- console.error('Failed to fetch template:', error);
- setSelectedTemplate(null);
- }
-
- setEditorOpen(true);
+ const openEditor = (event: any, channel: any) => {
+ // Navigate to edit template subpage
+ navigate(`/settings/notifications/edit-template?event=${event.id}&channel=${channel.id}`);
};
const getChannelIcon = (channelId: string) => {
@@ -182,24 +168,6 @@ export default function NotificationTemplates() {
})}
-
- {/* Template Editor Dialog */}
- {selectedEvent && selectedChannel && (
- {
- setEditorOpen(false);
- setSelectedEvent(null);
- setSelectedChannel(null);
- setSelectedTemplate(null);
- }}
- eventId={selectedEvent.id}
- channelId={selectedChannel.id}
- eventLabel={selectedEvent.label}
- channelLabel={selectedChannel.label}
- template={selectedTemplate}
- />
- )}
);
}