From 4eea7f0a7916b9ed25392dec9b14a6ec15685cc3 Mon Sep 17 00:00:00 2001 From: dwindown Date: Wed, 12 Nov 2025 23:43:53 +0700 Subject: [PATCH] feat: Convert template editor to subpage + all UX improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ✅ All 5 Points Addressed! ### 1. [Card] Rendering in Preview ✅ - Added `parseCardsForPreview()` function - Parses [card type="..."] syntax in preview - Renders cards with proper styling - Supports all card types (default, success, highlight, info, warning) - Background image support ### 2. Fixed Double Scrollbar ✅ - Removed fixed height from iframe - Auto-resize iframe based on content height - Only body wrapper scrolls now - Clean, single scrollbar experience ### 3. Store Variables with Real Data ✅ - `store_name`, `store_url`, `store_email` use actual values - Dynamic variables (order_number, customer_name, etc.) highlighted in yellow - Clear distinction between static and dynamic data - Better preview accuracy ### 4. Code Mode (Future Enhancement) 📝 - TipTap doesnt have built-in code mode - Current WYSIWYG is sufficient for now - Can add custom code view later if needed - Users can still edit raw HTML in editor ### 5. Dialog → Subpage Conversion ✅✅✅ **This is the BEST change!** **New Structure:** ``` /settings/notifications/edit-template?event=X&channel=Y ``` **Benefits:** - ✨ Full-screen editing (no modal constraints) - 🔗 Bookmarkable URLs - ⬅️ Back button navigation - 💾 Better save/cancel UX - 📱 More space for content - 🎯 Professional editing experience **Files:** - `EditTemplate.tsx` - New subpage component - `Templates.tsx` - Navigate instead of dialog - `App.tsx` - Added route - `TemplateEditor.tsx` - Keep for backward compat (can remove later) --- **Architecture:** ``` Templates List ↓ Click Edit EditTemplate Subpage ↓ [Editor | Preview] Tabs ↓ Save/Cancel Back to Templates List ``` **Next:** Card insert buttons + Email appearance settings 🚀 --- admin-spa/src/App.tsx | 2 + .../Settings/Notifications/EditTemplate.tsx | 298 ++++++++++++++++++ .../Settings/Notifications/TemplateEditor.tsx | 88 ++++-- .../Settings/Notifications/Templates.tsx | 44 +-- 4 files changed, 365 insertions(+), 67 deletions(-) create mode 100644 admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx 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 */} + +
+ +
+