fix: Template editor UX improvements
## ✅ All 5 Issues Fixed! ### 1. Default Value in RichTextEditor ✅ - Added `useEffect` to sync content prop with editor - Editor now properly displays default template content - Fixed: `editor.commands.setContent(content)` when prop changes ### 2. Removed Duplicate Variable Section ✅ - Removed "Variable Reference" section (was redundant) - Variables already available in rich text editor toolbar - Kept small badge list under editor for quick reference ### 3. User-Friendly Preview ✅ - Preview now renders HTML (not raw code) - Subject separated in dialog header - Complete email template preview (header + content + footer) - Variables highlighted in yellow for clarity - Uses iframe with full base.html styling ### 4. Fixed Dialog Scrolling ✅ **New Structure:** ``` [Header] ← Fixed (title + subject input) [Body] ← Scrollable (tabs: editor/preview) [Footer] ← Fixed (action buttons) ``` - No more annoying full-dialog scroll - Each section scrolls independently - Better UX with fixed header/footer ### 5. Editor/Preview Tabs ✅ **Tabs Implementation:** - [Editor] tab: Rich text editor + variable badges - [Preview] tab: Full email preview with styling - Clean separation of editing vs previewing - Preview shows complete email (not just content) - 500px iframe height for comfortable viewing --- **Benefits:** - ✨ Default content loads properly - 🎨 Beautiful HTML preview - 📱 Better scrolling UX - 👁️ See exactly how email looks - 🚀 Professional editing experience **Next:** Email appearance settings + card insert buttons
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
import { useEditor, EditorContent } from '@tiptap/react';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
@@ -55,6 +55,13 @@ export function RichTextEditor({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update editor content when prop changes (fix for default value not showing)
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor && content !== editor.getHTML()) {
|
||||||
|
editor.commands.setContent(content);
|
||||||
|
}
|
||||||
|
}, [content, editor]);
|
||||||
|
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { X, Plus } from 'lucide-react';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { X, Plus, Eye, Edit } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { __ } from '@/lib/i18n';
|
import { __ } from '@/lib/i18n';
|
||||||
|
|
||||||
@@ -48,14 +49,17 @@ export default function TemplateEditor({
|
|||||||
const [subject, setSubject] = useState('');
|
const [subject, setSubject] = useState('');
|
||||||
const [body, setBody] = useState('');
|
const [body, setBody] = useState('');
|
||||||
const [variables, setVariables] = useState<{ [key: string]: string }>({});
|
const [variables, setVariables] = useState<{ [key: string]: string }>({});
|
||||||
|
const [activeTab, setActiveTab] = useState('editor');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialTemplate) {
|
if (initialTemplate) {
|
||||||
setSubject(initialTemplate.subject || '');
|
setSubject(initialTemplate.subject || '');
|
||||||
setBody(initialTemplate.body || '');
|
// Set body with default value - ensure it's set properly
|
||||||
|
const defaultBody = initialTemplate.body || '';
|
||||||
|
setBody(defaultBody);
|
||||||
setVariables(initialTemplate.variables || {});
|
setVariables(initialTemplate.variables || {});
|
||||||
}
|
}
|
||||||
}, [initialTemplate]);
|
}, [initialTemplate, open]);
|
||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@@ -93,80 +97,138 @@ export default function TemplateEditor({
|
|||||||
// Get variable keys for the rich text editor
|
// Get variable keys for the rich text editor
|
||||||
const variableKeys = Object.keys(variables);
|
const variableKeys = Object.keys(variables);
|
||||||
|
|
||||||
|
// Generate preview HTML
|
||||||
|
const generatePreviewHTML = () => {
|
||||||
|
// Simple preview - replace variables with sample data
|
||||||
|
let previewBody = body;
|
||||||
|
Object.keys(variables).forEach(key => {
|
||||||
|
const sampleValue = `<span style="background: #fef3c7; padding: 2px 4px; border-radius: 2px;">[${key}]</span>`;
|
||||||
|
previewBody = previewBody.replace(new RegExp(`\\{${key}\\}`, 'g'), sampleValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Inter', Arial, sans-serif; background: #f8f8f8; 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; }
|
||||||
|
.card-success { background: #e8f5e9; border: 1px solid #4caf50; }
|
||||||
|
.card-highlight { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; }
|
||||||
|
.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; }
|
||||||
|
.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">
|
||||||
|
<strong style="font-size: 24px; color: #333;">[Your Store Name]</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-gutter">
|
||||||
|
${previewBody}
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© ${new Date().getFullYear()} [Your Store Name]. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-4xl h-[85vh] flex flex-col p-0">
|
||||||
<DialogHeader>
|
{/* Header - Fixed */}
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-4 border-b">
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{__('Edit Template')}: {eventLabel} - {channelLabel}
|
{__('Edit Template')}: {eventLabel} - {channelLabel}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<div className="space-y-2 pt-2">
|
||||||
{__('Customize the notification template. Use variables like {customer_name} to personalize messages.')}
|
<Label htmlFor="subject" className="text-sm">{__('Subject / Title')}</Label>
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-6 py-4">
|
|
||||||
{/* Subject */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="subject">{__('Subject / Title')}</Label>
|
|
||||||
<Input
|
<Input
|
||||||
id="subject"
|
id="subject"
|
||||||
value={subject}
|
value={subject}
|
||||||
onChange={(e) => setSubject(e.target.value)}
|
onChange={(e) => setSubject(e.target.value)}
|
||||||
placeholder={__('Enter notification subject')}
|
placeholder={__('Enter notification subject')}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{channelId === 'email'
|
|
||||||
? __('Email subject line')
|
|
||||||
: __('Push notification title')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body - Scrollable */}
|
||||||
<div className="space-y-2">
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||||
<Label htmlFor="body">{__('Message Body')}</Label>
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<RichTextEditor
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
content={body}
|
<TabsTrigger value="editor" className="flex items-center gap-2">
|
||||||
onChange={setBody}
|
<Edit className="h-4 w-4" />
|
||||||
placeholder={__('Enter notification message')}
|
{__('Editor')}
|
||||||
variables={variableKeys}
|
</TabsTrigger>
|
||||||
/>
|
<TabsTrigger value="preview" className="flex items-center gap-2">
|
||||||
<p className="text-xs text-muted-foreground">
|
<Eye className="h-4 w-4" />
|
||||||
{__('Click variables below to insert them into your message')}
|
{__('Preview')}
|
||||||
</p>
|
</TabsTrigger>
|
||||||
</div>
|
</TabsList>
|
||||||
|
|
||||||
{/* Variable Reference */}
|
{/* Editor Tab */}
|
||||||
<div className="space-y-3">
|
<TabsContent value="editor" className="space-y-4 mt-4">
|
||||||
<Label>{__('Variable Reference')}</Label>
|
<div className="space-y-2">
|
||||||
<div className="flex flex-wrap gap-2">
|
<Label htmlFor="body">{__('Message Body')}</Label>
|
||||||
{Object.entries(variables).map(([key, label]) => (
|
<RichTextEditor
|
||||||
<Badge
|
content={body}
|
||||||
key={key}
|
onChange={setBody}
|
||||||
variant="secondary"
|
placeholder={__('Enter notification message')}
|
||||||
className="cursor-default"
|
variables={variableKeys}
|
||||||
>
|
/>
|
||||||
<Plus className="h-3 w-3 mr-1" />
|
<div className="mt-2">
|
||||||
{`{${key}}`}
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
</Badge>
|
{__('Available Variables:')}
|
||||||
))}
|
</p>
|
||||||
</div>
|
<div className="flex flex-wrap gap-2">
|
||||||
<p className="text-xs text-muted-foreground">
|
{Object.keys(variables).map((key) => (
|
||||||
{__('Click a variable to insert it at cursor position')}
|
<Badge
|
||||||
</p>
|
key={key}
|
||||||
</div>
|
variant="outline"
|
||||||
|
className="text-xs font-mono"
|
||||||
|
>
|
||||||
|
{`{${key}}`}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
{/* Preview */}
|
{/* Preview Tab */}
|
||||||
<div className="space-y-2">
|
<TabsContent value="preview" className="mt-4">
|
||||||
<Label>{__('Preview')}</Label>
|
<div className="space-y-2">
|
||||||
<div className="rounded-lg border bg-muted/50 p-4 space-y-2">
|
<Label>{__('Email Preview')}</Label>
|
||||||
<div className="font-medium text-sm">{subject || __('(No subject)')}</div>
|
<div className="rounded-lg border bg-white overflow-hidden">
|
||||||
<div className="text-sm whitespace-pre-wrap">{body || __('(No message)')}</div>
|
<iframe
|
||||||
</div>
|
srcDoc={generatePreviewHTML()}
|
||||||
</div>
|
className="w-full h-[500px] border-0"
|
||||||
|
title="Email Preview"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{__('This is how your email will look. Variables are highlighted in yellow.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2">
|
{/* Footer - Fixed */}
|
||||||
|
<DialogFooter className="px-6 py-4 border-t gap-2">
|
||||||
<Button variant="outline" onClick={() => resetMutation.mutate()} disabled={saveMutation.isPending || resetMutation.isPending}>
|
<Button variant="outline" onClick={() => resetMutation.mutate()} disabled={saveMutation.isPending || resetMutation.isPending}>
|
||||||
{__('Reset to Default')}
|
{__('Reset to Default')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user