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 ` + + + + + + +
+
+ My WordPress Store +
+
+ ${previewBody} +
+ +
+ + + `; + }; + + if (isLoading) { + return ( +
+
{__('Loading...')}
+
+ ); + } + + 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 */} + +
+ +
+