PluginProbe
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services / 8.8.1
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services v8.8.1
8.8.1 8.8.0 8.7.9 8.7.8 8.7.7 8.7.6 8.7.5 8.7.4 8.7.3 8.7.2 8.7.1 8.7.0 8.6.9 8.6.8 8.6.7 8.6.6 8.6.5 8.6.4 8.6.2 8.6.1 8.6.0 8.5.9 8.5.8 8.5.7 8.5.6 All 537 releases
chatbot / addons / automator / src / components / email-builder / EmailDesigner.jsx

EmailDesigner.jsx in WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services 8.8.1, at addons/automator/src/components/email-builder/EmailDesigner.jsx

499 lines 20.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState, useEffect } from 'react';
2 import { ArrowLeft, Save, Plus, Trash2, Settings2, Eye, Layout, Type, Image as ImageIcon, MousePointer2, Minus } from 'lucide-react';
3
4 const EmailDesigner = ({ template, onBack }) => {
5 const [name, setName] = useState(template?.name || 'Untitled Template');
6 const [subject, setSubject] = useState(template?.subject || '');
7 const [blocks, setBlocks] = useState(() => {
8 if (template?.body_json) {
9 try {
10 return JSON.parse(template.body_json);
11 } catch (e) {
12 console.error('Failed to parse body_json:', e);
13 }
14 }
15 return [
16 { id: '1', type: 'text', content: '<h1>Welcome to our Newsletter!</h1><p>Start designing your beautiful email here.</p>' }
17 ];
18 });
19 const [selectedBlockId, setSelectedBlockId] = useState(null);
20 const [saving, setSaving] = useState(false);
21 const [uploadingImage, setUploadingImage] = useState(false);
22 const [uploadError, setUploadError] = useState(null);
23 const [showPreview, setShowPreview] = useState(false);
24
25 const blockTypes = [
26 { type: 'text', icon: Type, label: 'Text Block' },
27 { type: 'button', icon: MousePointer2, label: 'Button' },
28 { type: 'image', icon: ImageIcon, label: 'Image' },
29 { type: 'divider', icon: Minus, label: 'Divider' },
30 { type: 'spacer', icon: Layout, label: 'Spacer' },
31 ];
32
33 const addBlock = (type) => {
34 let content = '';
35 let styles = {};
36
37 switch (type) {
38 case 'text':
39 content = '<p>Enter your text here...</p>';
40 break;
41 case 'button':
42 content = 'Click Me';
43 styles = { backgroundColor: '#3b82f6', color: '#ffffff', padding: '12px 24px', borderRadius: '6px', url: '#' };
44 break;
45 case 'image':
46 content = 'https://via.placeholder.com/600x300?text=Your+Image';
47 break;
48 case 'divider':
49 content = '';
50 styles = { borderTop: '1px solid #e5e7eb', margin: '20px 0' };
51 break;
52 case 'spacer':
53 styles = { height: '30px' };
54 break;
55 }
56
57 const newBlock = {
58 id: Math.random().toString(36).substr(2, 9),
59 type,
60 content,
61 styles
62 };
63 setBlocks([...blocks, newBlock]);
64 setSelectedBlockId(newBlock.id);
65 };
66
67 const removeBlock = (id) => {
68 setBlocks(blocks.filter(b => b.id !== id));
69 if (selectedBlockId === id) setSelectedBlockId(null);
70 };
71
72 const updateBlock = (id, updates) => {
73 setBlocks(blocks.map(b => b.id === id ? { ...b, ...updates } : b));
74 };
75
76 const getMediaApiUrl = () => {
77 if (!window.wpbotAutomator?.apiUrl) {
78 return '/wp-json/wp/v2/media';
79 }
80
81 try {
82 const apiUrl = new URL(window.wpbotAutomator.apiUrl);
83 const mediaPath = apiUrl.pathname.replace(/\/wpbot-automator\/v1\/?$/, '/wp/v2/');
84 return `${apiUrl.origin}${mediaPath}media`;
85 } catch (error) {
86 return '/wp-json/wp/v2/media';
87 }
88 };
89
90 const uploadImageFile = async (file) => {
91 if (!file) {
92 return null;
93 }
94
95 setUploadingImage(true);
96 setUploadError(null);
97
98 try {
99 const formData = new FormData();
100 formData.append('file', file);
101
102 const response = await fetch(getMediaApiUrl(), {
103 method: 'POST',
104 headers: {
105 'X-WP-Nonce': window.wpbotAutomator.nonce,
106 },
107 body: formData,
108 });
109
110 const result = await response.json();
111
112 if (!response.ok || !result?.source_url) {
113 throw new Error(result?.message || 'Failed to upload image');
114 }
115
116 if (selectedBlockId) {
117 updateBlock(selectedBlockId, { content: result.source_url });
118 }
119
120 return result.source_url;
121 } catch (error) {
122 console.error('Image upload failed:', error);
123 setUploadError(error.message || 'Failed to upload image.');
124 return null;
125 } finally {
126 setUploadingImage(false);
127 }
128 };
129
130 const handleImageFileChange = async (event) => {
131 const file = event.target.files?.[0];
132 if (!file) {
133 return;
134 }
135
136 await uploadImageFile(file);
137 event.target.value = '';
138 };
139
140 const handleSave = async () => {
141 setSaving(true);
142 const html = generateHTML();
143 const data = {
144 name,
145 subject,
146 body_html: html,
147 body_json: JSON.stringify(blocks)
148 };
149
150 try {
151 const baseUrl = window.wpbotAutomator.apiUrl.replace(/\/$/, '');
152 const url = template?.id
153 ? `${baseUrl}/email-templates/${template.id}`
154 : `${baseUrl}/email-templates`;
155
156 const response = await fetch(url, {
157 method: template?.id ? 'PUT' : 'POST',
158 headers: {
159 'Content-Type': 'application/json',
160 'X-WP-Nonce': window.wpbotAutomator.nonce,
161 },
162 body: JSON.stringify(data),
163 });
164 const result = await response.json();
165 if (result.success) {
166 alert('Template saved successfully!');
167 if (!template?.id) onBack(); // Go back if it was new
168 } else {
169 alert(result.message || 'Failed to save template.');
170 }
171 } catch (error) {
172 console.error('Error saving template:', error);
173 } finally {
174 setSaving(false);
175 }
176 };
177
178 const generateHTML = () => {
179 let html = `
180 <!DOCTYPE html>
181 <html>
182 <head>
183 <meta charset="utf-8">
184 <style>
185 body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 0; background-color: #f4f7f9; }
186 .container { max-width: 600px; margin: 40px auto; background: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
187 .content { padding: 40px; }
188 .btn { display: inline-block; text-decoration: none; font-weight: bold; text-align: center; }
189 img { max-width: 100%; height: auto; display: block; }
190 @media only screen and (max-width: 600px) { .container { margin: 0; border-radius: 0; } }
191 </style>
192 </head>
193 <body>
194 <div class="container">
195 <div class="content">
196 `;
197
198 blocks.forEach(block => {
199 switch (block.type) {
200 case 'text':
201 html += `<div class="block-text">${block.content}</div>`;
202 break;
203 case 'button':
204 const s = block.styles;
205 html += `<div style="text-align: center; margin: 20px 0;"><a href="${s.url}" class="btn" style="background-color: ${s.backgroundColor}; color: ${s.color}; padding: ${s.padding}; border-radius: ${s.borderRadius};">${block.content}</a></div>`;
206 break;
207 case 'image':
208 html += `<div style="margin: 20px 0;"><img src="${block.content}" alt="Image"></div>`;
209 break;
210 case 'divider':
211 html += `<hr style="border: none; border-top: ${block.styles.borderTop}; margin: ${block.styles.margin};">`;
212 break;
213 case 'spacer':
214 html += `<div style="height: ${block.styles.height};"></div>`;
215 break;
216 }
217 });
218
219 html += `
220 </div>
221 <div style="background: #f9fafb; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; border-top: 1px solid #f3f4f6;">
222 Sent by ${window.wpbotAutomator.blogname || 'Our Website'}
223 </div>
224 </div>
225 </body>
226 </html>
227 `;
228 return html;
229 };
230
231 const selectedBlock = blocks.find(b => b.id === selectedBlockId);
232
233 return (
234 <div className="flex flex-col h-[calc(100vh-100px)]">
235 {/* Header */}
236 <div className="flex justify-between items-center mb-6 bg-card p-4 rounded-xl border border-border shadow-sm">
237 <div className="flex items-center space-x-4">
238 <button onClick={onBack} className="p-2 hover:bg-muted rounded-full transition-colors">
239 <ArrowLeft className="w-5 h-5" />
240 </button>
241 <div>
242 <input
243 value={name}
244 onChange={(e) => setName(e.target.value)}
245 className="text-xl font-bold bg-transparent border-none focus:ring-0 p-0 hover:bg-muted/50 rounded px-2 cursor-text transition-colors"
246 />
247 <div className="flex items-center mt-0.5 px-2">
248 <span className="text-xs text-muted-foreground mr-2 font-medium">Subject:</span>
249 <input
250 value={subject}
251 onChange={(e) => setSubject(e.target.value)}
252 placeholder="Enter email subject..."
253 className="text-xs text-primary bg-transparent border-none focus:ring-0 p-0 w-64 placeholder:text-muted-foreground/50"
254 />
255 </div>
256 </div>
257 </div>
258 <div className="flex items-center space-x-3">
259 <button
260 onClick={() => setShowPreview(!showPreview)}
261 className="flex items-center space-x-2 px-4 py-2 border border-border rounded-lg hover:bg-muted transition-colors text-sm font-medium"
262 >
263 {showPreview ? <Layout className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
264 <span>{showPreview ? 'Editor' : 'Preview'}</span>
265 </button>
266 <button
267 onClick={handleSave}
268 disabled={saving}
269 className="flex items-center space-x-2 px-6 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-all font-medium disabled:opacity-50 shadow-sm"
270 >
271 {saving ? <Plus className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
272 <span>{saving ? 'Saving...' : 'Save Template'}</span>
273 </button>
274 </div>
275 </div>
276
277 {showPreview ? (
278 <div className="flex-1 overflow-auto p-8 bg-muted/30 rounded-xl border border-border">
279 <div className="max-w-[600px] mx-auto shadow-2xl rounded-lg overflow-hidden bg-white">
280 <iframe
281 title="Email Preview"
282 srcDoc={generateHTML()}
283 className="w-full h-[800px] border-none"
284 />
285 </div>
286 </div>
287 ) : (
288 <div className="flex flex-1 space-x-6 overflow-hidden">
289 {/* Main Canvas */}
290 <div className="flex-1 bg-muted/20 border border-border rounded-xl overflow-y-auto p-12 designer-canvas">
291 <div className="max-w-[600px] mx-auto bg-white shadow-lg rounded-lg min-h-[600px] flex flex-col p-10 border border-border/50">
292 <div className="space-y-4">
293 {blocks.map((block) => (
294 <div
295 key={block.id}
296 onClick={() => setSelectedBlockId(block.id)}
297 className={`relative group cursor-pointer border-2 rounded-lg transition-all ${
298 selectedBlockId === block.id
299 ? 'border-primary ring-4 ring-primary/10 shadow-md'
300 : 'border-transparent hover:border-primary/30'
301 }`}
302 >
303 {/* Render Block Content */}
304 <div className="p-4">
305 {block.type === 'text' && (
306 <div dangerouslySetInnerHTML={{ __html: block.content }} />
307 )}
308 {block.type === 'button' && (
309 <div className="text-center my-4">
310 <span
311 style={{
312 backgroundColor: block.styles.backgroundColor,
313 color: block.styles.color,
314 padding: block.styles.padding,
315 borderRadius: block.styles.borderRadius,
316 display: 'inline-block',
317 fontWeight: 'bold'
318 }}
319 >
320 {block.content}
321 </span>
322 </div>
323 )}
324 {block.type === 'image' && (
325 <div className="my-4">
326 <img src={block.content} alt="Block" className="mx-auto rounded-md shadow-sm" />
327 </div>
328 )}
329 {block.type === 'divider' && (
330 <hr style={block.styles} />
331 )}
332 {block.type === 'spacer' && (
333 <div style={block.styles}></div>
334 )}
335 </div>
336
337 {/* Block Toolbar */}
338 {selectedBlockId === block.id && (
339 <div className="absolute -right-12 top-0 flex flex-col space-y-2">
340 <button
341 onClick={(e) => { e.stopPropagation(); removeBlock(block.id); }}
342 className="p-2 bg-rose-500 text-white rounded-lg shadow-lg hover:bg-rose-600 transition-colors"
343 >
344 <Trash2 className="w-4 h-4" />
345 </button>
346 </div>
347 )}
348 </div>
349 ))}
350 </div>
351
352 {blocks.length === 0 && (
353 <div className="flex flex-col items-center justify-center flex-1 text-muted-foreground opacity-50 space-y-4">
354 <Layout className="w-12 h-12" />
355 <p className="text-sm font-medium">Your canvas is empty. Drag or click blocks to start.</p>
356 </div>
357 )}
358 </div>
359 </div>
360
361 {/* Right Panels */}
362 <div className="w-80 flex flex-col space-y-6">
363 {/* Widget Picker */}
364 <div className="bg-card border border-border rounded-xl p-5 shadow-sm">
365 <h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-4 flex items-center">
366 <Plus className="w-4 h-4 mr-2" />
367 Add Blocks
368 </h3>
369 <div className="grid grid-cols-2 gap-3">
370 {blockTypes.map((bt) => (
371 <button
372 key={bt.type}
373 onClick={() => addBlock(bt.type)}
374 className="flex flex-col items-center justify-center p-4 border border-border rounded-xl hover:bg-primary hover:text-primary-foreground hover:scale-105 hover:shadow-md transition-all group"
375 >
376 <bt.icon className="w-6 h-6 mb-2 group-hover:animate-pulse" />
377 <span className="text-[10px] font-bold uppercase tracking-tight">{bt.label}</span>
378 </button>
379 ))}
380 </div>
381 </div>
382
383 {/* Inspector */}
384 <div className="bg-card border border-border rounded-xl flex-1 p-5 shadow-sm overflow-y-auto">
385 <h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-4 flex items-center border-b border-border pb-2">
386 <Settings2 className="w-4 h-4 mr-2" />
387 Settings
388 </h3>
389
390 {selectedBlock ? (
391 <div className="space-y-6">
392 <div>
393 <label className="text-xs font-bold text-muted-foreground uppercase mb-2 block">Content</label>
394 {selectedBlock.type === 'text' ? (
395 <textarea
396 value={selectedBlock.content}
397 onChange={(e) => updateBlock(selectedBlock.id, { content: e.target.value })}
398 className="w-full p-3 border border-border rounded-lg bg-background text-sm min-h-[150px] focus:ring-2 focus:ring-primary/20 transition-all font-mono"
399 placeholder="Supports HTML..."
400 />
401 ) : (
402 <input
403 value={selectedBlock.content}
404 onChange={(e) => updateBlock(selectedBlock.id, { content: e.target.value })}
405 className="w-full p-3 border border-border rounded-lg bg-background text-sm focus:ring-2 focus:ring-primary/20 transition-all"
406 />
407 )}
408 </div>
409
410 {selectedBlock.type === 'image' && (
411 <div className="space-y-4">
412 <div>
413 <label className="text-xs font-bold text-muted-foreground uppercase mb-2 block">Upload Image</label>
414 <input
415 type="file"
416 accept="image/*"
417 onChange={handleImageFileChange}
418 className="w-full p-2 border border-border rounded-lg bg-background text-sm"
419 />
420 {uploadingImage && (
421 <p className="text-xs text-muted-foreground mt-2">Uploading image…</p>
422 )}
423 {uploadError && (
424 <p className="text-xs text-rose-500 mt-2">{uploadError}</p>
425 )}
426 </div>
427 <div>
428 <label className="text-xs font-bold text-muted-foreground uppercase mb-2 block">Image URL</label>
429 <input
430 value={selectedBlock.content}
431 onChange={(e) => updateBlock(selectedBlock.id, { content: e.target.value })}
432 placeholder="Paste image URL or upload a file"
433 className="w-full p-3 border border-border rounded-lg bg-background text-sm focus:ring-2 focus:ring-primary/20 transition-all"
434 />
435 </div>
436 </div>
437 )}
438
439 {selectedBlock.type === 'button' && (
440 <>
441 <div>
442 <label className="text-xs font-bold text-muted-foreground uppercase mb-2 block">Target URL</label>
443 <input
444 value={selectedBlock.styles.url}
445 onChange={(e) => updateBlock(selectedBlock.id, { styles: { ...selectedBlock.styles, url: e.target.value } })}
446 className="w-full p-3 border border-border rounded-lg bg-background text-sm font-mono"
447 />
448 </div>
449 <div className="grid grid-cols-2 gap-4">
450 <div>
451 <label className="text-xs font-bold text-muted-foreground uppercase mb-2 block">BG Color</label>
452 <input
453 type="color"
454 value={selectedBlock.styles.backgroundColor}
455 onChange={(e) => updateBlock(selectedBlock.id, { styles: { ...selectedBlock.styles, backgroundColor: e.target.value } })}
456 className="w-full h-10 p-1 border border-border rounded-lg bg-background"
457 />
458 </div>
459 <div>
460 <label className="text-xs font-bold text-muted-foreground uppercase mb-2 block">Text Color</label>
461 <input
462 type="color"
463 value={selectedBlock.styles.color}
464 onChange={(e) => updateBlock(selectedBlock.id, { styles: { ...selectedBlock.styles, color: e.target.value } })}
465 className="w-full h-10 p-1 border border-border rounded-lg bg-background"
466 />
467 </div>
468 </div>
469 </>
470 )}
471
472 {selectedBlock.type === 'spacer' && (
473 <div>
474 <label className="text-xs font-bold text-muted-foreground uppercase mb-2 block">Height (px)</label>
475 <input
476 type="number"
477 value={parseInt(selectedBlock.styles.height)}
478 onChange={(e) => updateBlock(selectedBlock.id, { styles: { ...selectedBlock.styles, height: e.target.value + 'px' } })}
479 className="w-full p-3 border border-border rounded-lg bg-background text-sm"
480 />
481 </div>
482 )}
483 </div>
484 ) : (
485 <div className="py-20 text-center opacity-40">
486 <MousePointer2 className="w-10 h-10 mx-auto mb-3" />
487 <p className="text-xs font-bold">Select a block to edit its properties</p>
488 </div>
489 )}
490 </div>
491 </div>
492 </div>
493 )}
494 </div>
495 );
496 };
497
498 export default EmailDesigner;
499