payload-plugin-newsletter 0.20.2 → 0.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/admin.js CHANGED
@@ -1,1792 +1,104 @@
1
1
  "use client";
2
2
  "use client";
3
3
 
4
- // src/components/Broadcasts/BroadcastInlinePreview.tsx
5
- import { useState, useCallback } from "react";
6
- import { useFormFields } from "@payloadcms/ui";
7
-
8
- // src/components/Broadcasts/PreviewControls.tsx
4
+ // src/admin/components/BroadcastInlinePreview.tsx
9
5
  import { jsx, jsxs } from "react/jsx-runtime";
10
- var PreviewControls = ({
11
- onUpdate,
12
- device,
13
- onDeviceChange,
14
- isLoading = false
6
+ var BroadcastInlinePreview = ({
7
+ field: _field,
8
+ data: _data,
9
+ ..._props
15
10
  }) => {
16
- const controlsStyle = {
17
- display: "flex",
18
- alignItems: "center",
19
- justifyContent: "space-between",
20
- padding: "1rem",
21
- background: "white",
22
- borderBottom: "1px solid #e5e7eb"
23
- };
24
- const updateButtonStyle = {
25
- padding: "0.5rem 1rem",
26
- background: "#10b981",
27
- color: "white",
28
- border: "none",
29
- borderRadius: "4px",
30
- cursor: isLoading ? "not-allowed" : "pointer",
31
- fontSize: "14px",
32
- fontWeight: 500,
33
- opacity: isLoading ? 0.6 : 1
34
- };
35
- const deviceSelectorStyle = {
36
- display: "flex",
37
- gap: "0.5rem"
38
- };
39
- const deviceButtonStyle = (isActive) => ({
40
- display: "flex",
41
- alignItems: "center",
42
- gap: "0.5rem",
43
- padding: "0.5rem 0.75rem",
44
- background: isActive ? "#1f2937" : "white",
45
- color: isActive ? "white" : "#374151",
46
- border: `1px solid ${isActive ? "#1f2937" : "#e5e7eb"}`,
47
- borderRadius: "4px",
48
- cursor: "pointer",
49
- fontSize: "14px"
50
- });
51
- return /* @__PURE__ */ jsxs("div", { style: controlsStyle, children: [
52
- /* @__PURE__ */ jsx(
53
- "button",
54
- {
55
- style: updateButtonStyle,
56
- onClick: onUpdate,
57
- disabled: isLoading,
58
- children: isLoading ? "Updating..." : "Update Preview"
59
- }
60
- ),
61
- /* @__PURE__ */ jsxs("div", { style: deviceSelectorStyle, children: [
62
- /* @__PURE__ */ jsxs(
63
- "button",
64
- {
65
- style: deviceButtonStyle(device === "desktop"),
66
- onClick: () => onDeviceChange("desktop"),
67
- "aria-label": "Desktop view",
68
- children: [
69
- /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
70
- /* @__PURE__ */ jsx("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2", ry: "2" }),
71
- /* @__PURE__ */ jsx("line", { x1: "8", y1: "21", x2: "16", y2: "21" }),
72
- /* @__PURE__ */ jsx("line", { x1: "12", y1: "17", x2: "12", y2: "21" })
73
- ] }),
74
- "Desktop"
75
- ]
76
- }
77
- ),
78
- /* @__PURE__ */ jsxs(
79
- "button",
80
- {
81
- style: deviceButtonStyle(device === "mobile"),
82
- onClick: () => onDeviceChange("mobile"),
83
- "aria-label": "Mobile view",
84
- children: [
85
- /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
86
- /* @__PURE__ */ jsx("rect", { x: "5", y: "2", width: "14", height: "20", rx: "2", ry: "2" }),
87
- /* @__PURE__ */ jsx("line", { x1: "12", y1: "18", x2: "12", y2: "18" })
88
- ] }),
89
- "Mobile"
90
- ]
91
- }
92
- )
93
- ] })
94
- ] });
95
- };
96
-
97
- // src/components/Broadcasts/BroadcastInlinePreview.tsx
98
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
99
- var BroadcastInlinePreview = () => {
100
- const [device, setDevice] = useState("desktop");
101
- const [isLoading, setIsLoading] = useState(false);
102
- const [showPreview, setShowPreview] = useState(false);
103
- const [previewHtml, setPreviewHtml] = useState(null);
104
- const [error, setError] = useState(null);
105
- const fields = useFormFields(([fields2]) => ({
106
- subject: fields2["subject"]?.value,
107
- preheader: fields2["contentSection.preheader"]?.value,
108
- content: fields2["contentSection.content"]?.value
109
- }));
110
- const updatePreview = useCallback(async () => {
111
- if (!fields.content) {
112
- setError(new Error("Please add some content before previewing"));
113
- return;
114
- }
115
- setIsLoading(true);
116
- setError(null);
117
- try {
118
- const response = await fetch("/api/broadcasts/preview", {
119
- method: "POST",
120
- headers: {
121
- "Content-Type": "application/json"
122
- },
123
- body: JSON.stringify({
124
- content: fields.content,
125
- preheader: fields.preheader,
126
- subject: fields.subject
127
- })
128
- });
129
- const data = await response.json();
130
- if (!response.ok || !data.success) {
131
- throw new Error(data.error || "Failed to generate preview");
132
- }
133
- setPreviewHtml(data.preview.html);
134
- setShowPreview(true);
135
- } catch (err) {
136
- setError(err);
137
- console.error("Failed to update preview:", err);
138
- } finally {
139
- setIsLoading(false);
140
- }
141
- }, [fields]);
142
- const containerStyle = {
143
- border: "1px solid #e5e7eb",
144
- borderRadius: "8px",
145
- overflow: "hidden",
146
- height: "100%",
147
- display: "flex",
148
- flexDirection: "column"
149
- };
150
- const headerStyle = {
151
- display: "flex",
152
- alignItems: "center",
153
- justifyContent: "space-between",
154
- padding: "1rem",
155
- background: "#f9fafb",
156
- borderBottom: "1px solid #e5e7eb"
157
- };
158
- const titleStyle = {
159
- fontSize: "16px",
160
- fontWeight: 600,
161
- color: "#1f2937",
162
- margin: 0
163
- };
164
- const previewContainerStyle = {
165
- flex: 1,
166
- display: "flex",
167
- flexDirection: "column",
168
- background: "#f3f4f6",
169
- overflow: "hidden"
170
- };
171
- const errorStyle = {
172
- padding: "2rem",
173
- textAlign: "center"
174
- };
175
- const toggleButtonStyle = {
176
- padding: "0.5rem 1rem",
177
- background: showPreview ? "#ef4444" : "#3b82f6",
178
- color: "white",
179
- border: "none",
180
- borderRadius: "4px",
181
- cursor: "pointer",
182
- fontSize: "14px",
183
- fontWeight: 500
184
- };
185
- return /* @__PURE__ */ jsxs2("div", { style: containerStyle, children: [
186
- /* @__PURE__ */ jsxs2("div", { style: headerStyle, children: [
187
- /* @__PURE__ */ jsx2("h3", { style: titleStyle, children: "Email Preview" }),
188
- /* @__PURE__ */ jsx2(
189
- "button",
190
- {
191
- onClick: () => showPreview ? setShowPreview(false) : updatePreview(),
192
- style: toggleButtonStyle,
193
- disabled: isLoading,
194
- children: isLoading ? "Loading..." : showPreview ? "Hide Preview" : "Show Preview"
195
- }
196
- )
197
- ] }),
198
- showPreview && /* @__PURE__ */ jsx2("div", { style: previewContainerStyle, children: error ? /* @__PURE__ */ jsxs2("div", { style: errorStyle, children: [
199
- /* @__PURE__ */ jsx2("p", { style: { color: "#ef4444", margin: "0 0 1rem" }, children: error.message }),
200
- /* @__PURE__ */ jsx2(
201
- "button",
202
- {
203
- onClick: updatePreview,
204
- style: {
205
- padding: "0.5rem 1rem",
206
- background: "#3b82f6",
207
- color: "white",
208
- border: "none",
209
- borderRadius: "4px",
210
- cursor: "pointer"
211
- },
212
- children: "Retry"
213
- }
214
- )
215
- ] }) : previewHtml ? /* @__PURE__ */ jsxs2(Fragment, { children: [
216
- /* @__PURE__ */ jsx2(
217
- PreviewControls,
218
- {
219
- onUpdate: updatePreview,
220
- device,
221
- onDeviceChange: setDevice,
222
- isLoading
223
- }
224
- ),
225
- /* @__PURE__ */ jsx2(
226
- "div",
227
- {
228
- style: {
229
- flex: 1,
230
- padding: device === "mobile" ? "1rem" : "2rem",
231
- display: "flex",
232
- justifyContent: "center",
233
- overflow: "auto"
234
- },
235
- children: /* @__PURE__ */ jsx2(
236
- "div",
237
- {
238
- style: {
239
- width: device === "mobile" ? "375px" : "600px",
240
- maxWidth: "100%",
241
- background: "white",
242
- boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
243
- borderRadius: "8px",
244
- overflow: "hidden"
245
- },
246
- children: /* @__PURE__ */ jsx2(
247
- "iframe",
248
- {
249
- srcDoc: previewHtml,
250
- style: {
251
- width: "100%",
252
- height: "100%",
253
- minHeight: "600px",
254
- border: "none"
255
- },
256
- title: "Email Preview"
257
- }
258
- )
259
- }
260
- )
261
- }
262
- )
263
- ] }) : null })
264
- ] });
265
- };
266
-
267
- // src/components/Broadcasts/StatusBadge.tsx
268
- import { jsx as jsx3 } from "react/jsx-runtime";
269
- var statusConfig = {
270
- ["draft" /* DRAFT */]: {
271
- label: "Draft",
272
- color: "#6B7280",
273
- // gray
274
- backgroundColor: "#F3F4F6"
275
- },
276
- ["scheduled" /* SCHEDULED */]: {
277
- label: "Scheduled",
278
- color: "#2563EB",
279
- // blue
280
- backgroundColor: "#DBEAFE"
281
- },
282
- ["sending" /* SENDING */]: {
283
- label: "Sending",
284
- color: "#D97706",
285
- // yellow/orange
286
- backgroundColor: "#FEF3C7"
287
- },
288
- ["sent" /* SENT */]: {
289
- label: "Sent",
290
- color: "#059669",
291
- // green
292
- backgroundColor: "#D1FAE5"
293
- },
294
- ["failed" /* FAILED */]: {
295
- label: "Failed",
296
- color: "#DC2626",
297
- // red
298
- backgroundColor: "#FEE2E2"
299
- },
300
- ["paused" /* PAUSED */]: {
301
- label: "Paused",
302
- color: "#9333EA",
303
- // purple
304
- backgroundColor: "#EDE9FE"
305
- },
306
- ["canceled" /* CANCELED */]: {
307
- label: "Canceled",
308
- color: "#6B7280",
309
- // gray
310
- backgroundColor: "#F3F4F6"
311
- }
312
- };
313
- var StatusBadge = ({ cellData }) => {
314
- const status = cellData;
315
- const config = statusConfig[status] || statusConfig["draft" /* DRAFT */];
316
- return /* @__PURE__ */ jsx3(
11
+ return /* @__PURE__ */ jsx("div", { className: "broadcast-preview", children: /* @__PURE__ */ jsxs("div", { style: { padding: "1rem", border: "1px solid #e0e0e0", borderRadius: "4px" }, children: [
12
+ /* @__PURE__ */ jsx("h3", { children: "Email Preview" }),
13
+ /* @__PURE__ */ jsx("p", { children: "This is a simplified preview component for the admin bundle." }),
14
+ /* @__PURE__ */ jsx("p", { children: "Full preview functionality will be available in the complete admin interface." })
15
+ ] }) });
16
+ };
17
+
18
+ // src/admin/components/StatusBadge.tsx
19
+ import { jsx as jsx2 } from "react/jsx-runtime";
20
+ var StatusBadge = (props) => {
21
+ const status = props.cellData || "draft";
22
+ const getStatusColor = (status2) => {
23
+ switch (status2) {
24
+ case "sent":
25
+ return "#22c55e";
26
+ case "scheduled":
27
+ return "#3b82f6";
28
+ case "draft":
29
+ return "#6b7280";
30
+ case "failed":
31
+ return "#ef4444";
32
+ default:
33
+ return "#6b7280";
34
+ }
35
+ };
36
+ const getStatusLabel = (status2) => {
37
+ switch (status2) {
38
+ case "sent":
39
+ return "Sent";
40
+ case "scheduled":
41
+ return "Scheduled";
42
+ case "draft":
43
+ return "Draft";
44
+ case "failed":
45
+ return "Failed";
46
+ default:
47
+ return status2;
48
+ }
49
+ };
50
+ return /* @__PURE__ */ jsx2(
317
51
  "span",
318
52
  {
319
53
  style: {
320
- display: "inline-flex",
321
- alignItems: "center",
322
- padding: "2px 10px",
54
+ display: "inline-block",
55
+ padding: "4px 8px",
323
56
  borderRadius: "12px",
324
57
  fontSize: "12px",
325
58
  fontWeight: "500",
326
- color: config.color,
327
- backgroundColor: config.backgroundColor
59
+ color: "#fff",
60
+ backgroundColor: getStatusColor(status),
61
+ textTransform: "capitalize"
328
62
  },
329
- children: config.label
63
+ children: getStatusLabel(status)
330
64
  }
331
65
  );
332
66
  };
333
67
 
334
- // src/components/Broadcasts/EmailPreview.tsx
335
- import { useState as useState2, useEffect, useRef } from "react";
336
-
337
- // src/utils/emailSafeHtml.ts
338
- import DOMPurify from "isomorphic-dompurify";
339
- var EMAIL_SAFE_CONFIG = {
340
- ALLOWED_TAGS: [
341
- "p",
342
- "br",
343
- "strong",
344
- "b",
345
- "em",
346
- "i",
347
- "u",
348
- "strike",
349
- "s",
350
- "span",
351
- "a",
352
- "h1",
353
- "h2",
354
- "h3",
355
- "ul",
356
- "ol",
357
- "li",
358
- "blockquote",
359
- "hr",
360
- "img",
361
- "div",
362
- "table",
363
- "tr",
364
- "td",
365
- "th",
366
- "tbody",
367
- "thead"
368
- ],
369
- ALLOWED_ATTR: ["href", "style", "target", "rel", "align", "src", "alt", "width", "height", "border", "cellpadding", "cellspacing"],
370
- ALLOWED_STYLES: {
371
- "*": [
372
- "color",
373
- "background-color",
374
- "font-size",
375
- "font-weight",
376
- "font-style",
377
- "text-decoration",
378
- "text-align",
379
- "margin",
380
- "margin-top",
381
- "margin-right",
382
- "margin-bottom",
383
- "margin-left",
384
- "padding",
385
- "padding-top",
386
- "padding-right",
387
- "padding-bottom",
388
- "padding-left",
389
- "line-height",
390
- "border-left",
391
- "border-left-width",
392
- "border-left-style",
393
- "border-left-color"
394
- ]
395
- },
396
- FORBID_TAGS: ["script", "style", "iframe", "object", "embed", "form", "input"],
397
- FORBID_ATTR: ["class", "id", "onclick", "onload", "onerror"]
398
- };
399
- async function convertToEmailSafeHtml(editorState, options) {
400
- if (!editorState) {
401
- return "";
402
- }
403
- const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter);
404
- const sanitizedHtml = DOMPurify.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
405
- if (options?.wrapInTemplate) {
406
- if (options.customWrapper) {
407
- return await Promise.resolve(options.customWrapper(sanitizedHtml, {
408
- preheader: options.preheader,
409
- subject: options.subject
410
- }));
411
- }
412
- return wrapInEmailTemplate(sanitizedHtml, options.preheader);
413
- }
414
- return sanitizedHtml;
415
- }
416
- async function lexicalToEmailHtml(editorState, mediaUrl, customBlockConverter) {
417
- const { root } = editorState;
418
- if (!root || !root.children) {
419
- return "";
420
- }
421
- const htmlParts = await Promise.all(
422
- root.children.map((node) => convertNode(node, mediaUrl, customBlockConverter))
423
- );
424
- return htmlParts.join("");
425
- }
426
- async function convertNode(node, mediaUrl, customBlockConverter) {
427
- switch (node.type) {
428
- case "paragraph":
429
- return convertParagraph(node, mediaUrl, customBlockConverter);
430
- case "heading":
431
- return convertHeading(node, mediaUrl, customBlockConverter);
432
- case "list":
433
- return convertList(node, mediaUrl, customBlockConverter);
434
- case "listitem":
435
- return convertListItem(node, mediaUrl, customBlockConverter);
436
- case "blockquote":
437
- return convertBlockquote(node, mediaUrl, customBlockConverter);
438
- case "text":
439
- return convertText(node);
440
- case "link":
441
- return convertLink(node, mediaUrl, customBlockConverter);
442
- case "linebreak":
443
- return "<br>";
444
- case "upload":
445
- return convertUpload(node, mediaUrl);
446
- case "block":
447
- return await convertBlock(node, mediaUrl, customBlockConverter);
448
- default:
449
- if (node.children) {
450
- const childParts = await Promise.all(
451
- node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
452
- );
453
- return childParts.join("");
454
- }
455
- return "";
456
- }
457
- }
458
- async function convertParagraph(node, mediaUrl, customBlockConverter) {
459
- const align = getAlignment(node.format);
460
- const childParts = await Promise.all(
461
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
462
- );
463
- const children = childParts.join("");
464
- if (!children.trim()) {
465
- return '<p class="mobile-margin-bottom-16" style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
466
- }
467
- return `<p class="mobile-margin-bottom-16" style="margin: 0 0 16px 0; text-align: ${align}; font-size: 16px; line-height: 1.5;">${children}</p>`;
468
- }
469
- async function convertHeading(node, mediaUrl, customBlockConverter) {
470
- const tag = node.tag || "h1";
471
- const align = getAlignment(node.format);
472
- const childParts = await Promise.all(
473
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
474
- );
475
- const children = childParts.join("");
476
- const styles = {
477
- h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
478
- h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
479
- h3: "font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;"
480
- };
481
- const mobileClasses = {
482
- h1: "mobile-font-size-24",
483
- h2: "mobile-font-size-20",
484
- h3: "mobile-font-size-16"
485
- };
486
- const style = `${styles[tag] || styles.h3} text-align: ${align};`;
487
- const mobileClass = mobileClasses[tag] || mobileClasses.h3;
488
- return `<${tag} class="${mobileClass}" style="${style}">${children}</${tag}>`;
489
- }
490
- async function convertList(node, mediaUrl, customBlockConverter) {
491
- const tag = node.listType === "number" ? "ol" : "ul";
492
- const childParts = await Promise.all(
493
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
494
- );
495
- const children = childParts.join("");
496
- const style = tag === "ul" ? "margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc; font-size: 16px; line-height: 1.5;" : "margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal; font-size: 16px; line-height: 1.5;";
497
- return `<${tag} class="mobile-margin-bottom-16" style="${style}">${children}</${tag}>`;
498
- }
499
- async function convertListItem(node, mediaUrl, customBlockConverter) {
500
- const childParts = await Promise.all(
501
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
502
- );
503
- const children = childParts.join("");
504
- return `<li style="margin: 0 0 8px 0;">${children}</li>`;
505
- }
506
- async function convertBlockquote(node, mediaUrl, customBlockConverter) {
507
- const childParts = await Promise.all(
508
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
509
- );
510
- const children = childParts.join("");
511
- const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
512
- return `<blockquote style="${style}">${children}</blockquote>`;
513
- }
514
- function convertText(node) {
515
- let text = escapeHtml(node.text || "");
516
- if (node.format & 1) {
517
- text = `<strong>${text}</strong>`;
518
- }
519
- if (node.format & 2) {
520
- text = `<em>${text}</em>`;
521
- }
522
- if (node.format & 8) {
523
- text = `<u>${text}</u>`;
524
- }
525
- if (node.format & 4) {
526
- text = `<strike>${text}</strike>`;
527
- }
528
- return text;
529
- }
530
- async function convertLink(node, mediaUrl, customBlockConverter) {
531
- const childParts = await Promise.all(
532
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
533
- );
534
- const children = childParts.join("");
535
- const url = node.fields?.url || "#";
536
- const newTab = node.fields?.newTab ?? false;
537
- const targetAttr = newTab ? ' target="_blank"' : "";
538
- const relAttr = newTab ? ' rel="noopener noreferrer"' : "";
539
- return `<a href="${escapeHtml(url)}"${targetAttr}${relAttr} style="color: #2563eb; text-decoration: underline;">${children}</a>`;
540
- }
541
- function convertUpload(node, mediaUrl) {
542
- const upload = node.value;
543
- if (!upload) return "";
544
- let src = "";
545
- if (typeof upload === "string") {
546
- src = upload;
547
- } else if (upload.url) {
548
- src = upload.url;
549
- } else if (upload.filename && mediaUrl) {
550
- src = `${mediaUrl}/${upload.filename}`;
551
- }
552
- const alt = node.fields?.altText || upload.alt || "";
553
- const caption = node.fields?.caption || "";
554
- const imgHtml = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" class="mobile-width-100" style="max-width: 100%; height: auto; display: block; margin: 0 auto; border-radius: 6px;" />`;
555
- if (caption) {
556
- return `
557
- <div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">
558
- ${imgHtml}
559
- <p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic; text-align: center;" class="mobile-font-size-14">${escapeHtml(caption)}</p>
560
- </div>
561
- `;
562
- }
563
- return `<div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">${imgHtml}</div>`;
564
- }
565
- async function convertBlock(node, mediaUrl, customBlockConverter) {
566
- const blockType = node.fields?.blockName || node.blockName;
567
- if (customBlockConverter) {
568
- try {
569
- const customHtml = await customBlockConverter(node, mediaUrl);
570
- if (customHtml) {
571
- return customHtml;
572
- }
573
- } catch (error) {
574
- console.error(`Custom block converter error for ${blockType}:`, error);
575
- }
576
- }
577
- switch (blockType) {
578
- case "button":
579
- return convertButtonBlock(node.fields);
580
- case "divider":
581
- return convertDividerBlock(node.fields);
582
- default:
583
- if (node.children) {
584
- const childParts = await Promise.all(
585
- node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
586
- );
587
- return childParts.join("");
588
- }
589
- return "";
590
- }
591
- }
592
- function convertButtonBlock(fields) {
593
- const text = fields?.text || "Click here";
594
- const url = fields?.url || "#";
595
- const style = fields?.style || "primary";
596
- const styles = {
597
- primary: "background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;",
598
- secondary: "background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;",
599
- outline: "background-color: transparent; color: #2563eb; border: 2px solid #2563eb;"
600
- };
601
- const buttonStyle = `${styles[style] || styles.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`;
602
- return `
603
- <div style="margin: 0 0 16px 0; text-align: center;">
604
- <a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
605
- </div>
606
- `;
607
- }
608
- function convertDividerBlock(fields) {
609
- const style = fields?.style || "solid";
610
- const styles = {
611
- solid: "border-top: 1px solid #e5e7eb;",
612
- dashed: "border-top: 1px dashed #e5e7eb;",
613
- dotted: "border-top: 1px dotted #e5e7eb;"
614
- };
615
- return `<hr style="${styles[style] || styles.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
616
- }
617
- function getAlignment(format) {
618
- if (!format) return "left";
619
- if (format & 2) return "center";
620
- if (format & 3) return "right";
621
- if (format & 4) return "justify";
622
- return "left";
623
- }
624
- function escapeHtml(text) {
625
- const map = {
626
- "&": "&amp;",
627
- "<": "&lt;",
628
- ">": "&gt;",
629
- '"': "&quot;",
630
- "'": "&#039;"
631
- };
632
- return text.replace(/[&<>"']/g, (m) => map[m]);
633
- }
634
- function wrapInEmailTemplate(content, preheader) {
635
- return `<!DOCTYPE html>
636
- <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
637
- <head>
638
- <meta charset="UTF-8">
639
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
640
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
641
- <meta name="x-apple-disable-message-reformatting">
642
- <title>Newsletter</title>
643
-
644
- <!--[if mso]>
645
- <noscript>
646
- <xml>
647
- <o:OfficeDocumentSettings>
648
- <o:PixelsPerInch>96</o:PixelsPerInch>
649
- </o:OfficeDocumentSettings>
650
- </xml>
651
- </noscript>
652
- <![endif]-->
653
-
654
- <style>
655
- /* Reset and base styles */
656
- * {
657
- -webkit-text-size-adjust: 100%;
658
- -ms-text-size-adjust: 100%;
659
- }
660
-
661
- body {
662
- margin: 0 !important;
663
- padding: 0 !important;
664
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
665
- font-size: 16px;
666
- line-height: 1.5;
667
- color: #1A1A1A;
668
- background-color: #f8f9fa;
669
- -webkit-font-smoothing: antialiased;
670
- -moz-osx-font-smoothing: grayscale;
671
- }
672
-
673
- table {
674
- border-spacing: 0 !important;
675
- border-collapse: collapse !important;
676
- table-layout: fixed !important;
677
- margin: 0 auto !important;
678
- }
679
-
680
- table table table {
681
- table-layout: auto;
682
- }
683
-
684
- img {
685
- -ms-interpolation-mode: bicubic;
686
- max-width: 100%;
687
- height: auto;
688
- border: 0;
689
- outline: none;
690
- text-decoration: none;
691
- }
692
-
693
- /* Responsive styles */
694
- @media only screen and (max-width: 640px) {
695
- .mobile-hide {
696
- display: none !important;
697
- }
698
-
699
- .mobile-center {
700
- text-align: center !important;
701
- }
702
-
703
- .mobile-width-100 {
704
- width: 100% !important;
705
- max-width: 100% !important;
706
- }
707
-
708
- .mobile-padding {
709
- padding: 20px !important;
710
- }
711
-
712
- .mobile-padding-sm {
713
- padding: 16px !important;
714
- }
715
-
716
- .mobile-font-size-14 {
717
- font-size: 14px !important;
718
- }
719
-
720
- .mobile-font-size-16 {
721
- font-size: 16px !important;
722
- }
723
-
724
- .mobile-font-size-20 {
725
- font-size: 20px !important;
726
- line-height: 1.3 !important;
727
- }
728
-
729
- .mobile-font-size-24 {
730
- font-size: 24px !important;
731
- line-height: 1.2 !important;
732
- }
733
-
734
- /* Stack sections on mobile */
735
- .mobile-stack {
736
- display: block !important;
737
- width: 100% !important;
738
- }
739
-
740
- /* Mobile-specific spacing */
741
- .mobile-margin-bottom-16 {
742
- margin-bottom: 16px !important;
743
- }
744
-
745
- .mobile-margin-bottom-20 {
746
- margin-bottom: 20px !important;
747
- }
748
- }
749
-
750
- /* Dark mode support */
751
- @media (prefers-color-scheme: dark) {
752
- .dark-mode-bg {
753
- background-color: #1a1a1a !important;
754
- }
755
-
756
- .dark-mode-text {
757
- color: #ffffff !important;
758
- }
759
-
760
- .dark-mode-border {
761
- border-color: #333333 !important;
762
- }
763
- }
764
-
765
- /* Outlook-specific fixes */
766
- <!--[if mso]>
767
- <style>
768
- table {
769
- border-collapse: collapse;
770
- border-spacing: 0;
771
- border: none;
772
- margin: 0;
773
- }
774
-
775
- div, p {
776
- margin: 0;
777
- }
778
- </style>
779
- <![endif]-->
780
- </style>
781
- </head>
782
- <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #1A1A1A; background-color: #f8f9fa;">
783
- ${preheader ? `
784
- <!-- Preheader text -->
785
- <div style="display: none; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: transparent;">
786
- ${escapeHtml(preheader)}
787
- </div>
788
- ` : ""}
789
-
790
- <!-- Main container -->
791
- <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0; background-color: #f8f9fa;">
792
- <tr>
793
- <td align="center" style="padding: 20px 10px;">
794
- <!-- Email wrapper -->
795
- <table role="presentation" cellpadding="0" cellspacing="0" width="600" class="mobile-width-100" style="margin: 0 auto; max-width: 600px;">
796
- <tr>
797
- <td class="mobile-padding" style="padding: 0;">
798
- <!-- Content area with light background -->
799
- <div style="background-color: #ffffff; padding: 40px 30px; border-radius: 8px;" class="mobile-padding">
800
- ${content}
801
- </div>
802
- </td>
803
- </tr>
804
- </table>
805
- </td>
806
- </tr>
807
- </table>
808
- </body>
809
- </html>`;
810
- }
811
- function replacePersonalizationTags(html, sampleData) {
812
- return html.replace(/\{\{([^}]+)\}\}/g, (match, tag) => {
813
- const trimmedTag = tag.trim();
814
- return sampleData[trimmedTag] || match;
815
- });
816
- }
817
-
818
- // src/utils/validateEmailHtml.ts
819
- function validateEmailHtml(html) {
820
- const warnings = [];
821
- const errors = [];
822
- const sizeInBytes = new Blob([html]).size;
823
- if (sizeInBytes > 102400) {
824
- warnings.push(`Email size (${Math.round(sizeInBytes / 1024)}KB) exceeds Gmail's 102KB limit - email may be clipped`);
825
- }
826
- if (html.includes("position:") && (html.includes("position: absolute") || html.includes("position: fixed"))) {
827
- errors.push("Absolute/fixed positioning is not supported in most email clients");
828
- }
829
- if (html.includes("display: flex") || html.includes("display: grid")) {
830
- errors.push("Flexbox and Grid layouts are not supported in many email clients");
831
- }
832
- if (html.includes("@media")) {
833
- warnings.push("Media queries may not work in all email clients");
834
- }
835
- const hasJavaScript = html.includes("<script") || html.includes("onclick") || html.includes("onload") || html.includes("javascript:");
836
- if (hasJavaScript) {
837
- errors.push("JavaScript is not supported in email and will be stripped by email clients");
838
- }
839
- const hasExternalStyles = html.includes("<link") && html.includes("stylesheet");
840
- if (hasExternalStyles) {
841
- errors.push("External stylesheets are not supported - use inline styles only");
842
- }
843
- if (html.includes("<form") || html.includes("<input") || html.includes("<button")) {
844
- errors.push("Forms and form elements are not reliably supported in email");
845
- }
846
- const unsupportedTags = [
847
- "video",
848
- "audio",
849
- "iframe",
850
- "embed",
851
- "object",
852
- "canvas",
853
- "svg"
854
- ];
855
- for (const tag of unsupportedTags) {
856
- if (html.includes(`<${tag}`)) {
857
- errors.push(`<${tag}> tags are not supported in email`);
858
- }
859
- }
860
- const imageCount = (html.match(/<img/g) || []).length;
861
- const linkCount = (html.match(/<a/g) || []).length;
862
- if (imageCount > 20) {
863
- warnings.push(`High number of images (${imageCount}) may affect email performance`);
864
- }
865
- const imagesWithoutAlt = (html.match(/<img(?![^>]*\balt\s*=)[^>]*>/g) || []).length;
866
- if (imagesWithoutAlt > 0) {
867
- warnings.push(`${imagesWithoutAlt} image(s) missing alt text - important for accessibility`);
868
- }
869
- const linksWithoutTarget = (html.match(/<a(?![^>]*\btarget\s*=)[^>]*>/g) || []).length;
870
- if (linksWithoutTarget > 0) {
871
- warnings.push(`${linksWithoutTarget} link(s) missing target="_blank" attribute`);
872
- }
873
- if (html.includes("margin: auto") || html.includes("margin:auto")) {
874
- warnings.push('margin: auto is not supported in Outlook - use align="center" or tables for centering');
875
- }
876
- if (html.includes("background-image")) {
877
- warnings.push("Background images are not reliably supported - consider using <img> tags instead");
878
- }
879
- if (html.match(/\d+\s*(rem|em)/)) {
880
- warnings.push("rem/em units may render inconsistently - use px for reliable sizing");
881
- }
882
- if (html.match(/margin[^:]*:\s*-\d+/)) {
883
- errors.push("Negative margins are not supported in many email clients");
884
- }
885
- const personalizationTags = html.match(/\{\{([^}]+)\}\}/g) || [];
886
- const validTags = ["subscriber.name", "subscriber.email", "subscriber.firstName", "subscriber.lastName"];
887
- for (const tag of personalizationTags) {
888
- const tagContent = tag.replace(/[{}]/g, "").trim();
889
- if (!validTags.includes(tagContent)) {
890
- warnings.push(`Unknown personalization tag: ${tag}`);
891
- }
892
- }
893
- return {
894
- valid: errors.length === 0,
895
- warnings,
896
- errors,
897
- stats: {
898
- sizeInBytes,
899
- imageCount,
900
- linkCount,
901
- hasExternalStyles,
902
- hasJavaScript
903
- }
904
- };
905
- }
906
-
907
- // src/contexts/PluginConfigContext.tsx
908
- import { createContext, useContext } from "react";
909
- import { jsx as jsx4 } from "react/jsx-runtime";
910
- var PluginConfigContext = createContext(null);
911
- var usePluginConfigOptional = () => {
912
- return useContext(PluginConfigContext);
913
- };
914
-
915
- // src/components/Broadcasts/EmailPreview.tsx
916
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
917
- var SAMPLE_DATA = {
918
- "subscriber.name": "John Doe",
919
- "subscriber.firstName": "John",
920
- "subscriber.lastName": "Doe",
921
- "subscriber.email": "john.doe@example.com"
922
- };
923
- var VIEWPORT_SIZES = {
924
- desktop: { width: 600, scale: 1 },
925
- mobile: { width: 320, scale: 0.8 }
926
- };
68
+ // src/admin/components/EmailPreview.tsx
69
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
927
70
  var EmailPreview = ({
928
71
  content,
929
72
  subject,
930
- preheader,
931
- mode = "desktop",
932
- onValidation,
933
- pluginConfig: propPluginConfig
73
+ preheader
934
74
  }) => {
935
- const contextPluginConfig = usePluginConfigOptional();
936
- const pluginConfig = propPluginConfig || contextPluginConfig;
937
- const [html, setHtml] = useState2("");
938
- const [loading, setLoading] = useState2(false);
939
- const [validationResult, setValidationResult] = useState2(null);
940
- const iframeRef = useRef(null);
941
- useEffect(() => {
942
- const convertContent = async () => {
943
- if (!content) {
944
- setHtml("");
945
- return;
946
- }
947
- setLoading(true);
948
- try {
949
- const emailPreviewConfig = pluginConfig?.customizations?.broadcasts?.emailPreview;
950
- const emailHtml = await convertToEmailSafeHtml(content, {
951
- wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
952
- preheader,
953
- subject,
954
- customWrapper: emailPreviewConfig?.customWrapper,
955
- customBlockConverter: pluginConfig?.customizations?.broadcasts?.customBlockConverter
956
- });
957
- const personalizedHtml = replacePersonalizationTags(emailHtml, SAMPLE_DATA);
958
- const previewHtml = addEmailHeader(personalizedHtml, {
959
- subject,
960
- from: "Newsletter <noreply@example.com>",
961
- to: SAMPLE_DATA["subscriber.email"]
962
- });
963
- setHtml(previewHtml);
964
- const validation = validateEmailHtml(emailHtml);
965
- setValidationResult(validation);
966
- onValidation?.(validation);
967
- } catch (error) {
968
- console.error("Failed to convert content to HTML:", error);
969
- setHtml("<p>Error converting content to HTML</p>");
970
- } finally {
971
- setLoading(false);
972
- }
973
- };
974
- convertContent();
975
- }, [content, subject, preheader, onValidation, pluginConfig]);
976
- useEffect(() => {
977
- if (iframeRef.current && html) {
978
- const doc = iframeRef.current.contentDocument;
979
- if (doc) {
980
- doc.open();
981
- doc.write(html);
982
- doc.close();
983
- }
984
- }
985
- }, [html]);
986
- const viewport = VIEWPORT_SIZES[mode];
987
- return /* @__PURE__ */ jsxs3("div", { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
988
- validationResult && (validationResult.errors.length > 0 || validationResult.warnings.length > 0) && /* @__PURE__ */ jsxs3("div", { style: { padding: "16px", borderBottom: "1px solid #e5e7eb" }, children: [
989
- validationResult.errors.length > 0 && /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "12px" }, children: [
990
- /* @__PURE__ */ jsxs3("h4", { style: { color: "#dc2626", margin: "0 0 8px 0", fontSize: "14px" }, children: [
991
- "Errors (",
992
- validationResult.errors.length,
993
- ")"
994
- ] }),
995
- /* @__PURE__ */ jsx5("ul", { style: { margin: 0, paddingLeft: "20px", fontSize: "13px", color: "#dc2626" }, children: validationResult.errors.map((error, index) => /* @__PURE__ */ jsx5("li", { children: error }, index)) })
996
- ] }),
997
- validationResult.warnings.length > 0 && /* @__PURE__ */ jsxs3("div", { children: [
998
- /* @__PURE__ */ jsxs3("h4", { style: { color: "#d97706", margin: "0 0 8px 0", fontSize: "14px" }, children: [
999
- "Warnings (",
1000
- validationResult.warnings.length,
1001
- ")"
1002
- ] }),
1003
- /* @__PURE__ */ jsx5("ul", { style: { margin: 0, paddingLeft: "20px", fontSize: "13px", color: "#d97706" }, children: validationResult.warnings.map((warning, index) => /* @__PURE__ */ jsx5("li", { children: warning }, index)) })
1004
- ] })
75
+ return /* @__PURE__ */ jsxs2("div", { className: "email-preview", style: { padding: "1rem" }, children: [
76
+ /* @__PURE__ */ jsxs2("div", { style: { marginBottom: "1rem" }, children: [
77
+ /* @__PURE__ */ jsx3("strong", { children: "Subject:" }),
78
+ " ",
79
+ subject || "No subject"
1005
80
  ] }),
1006
- /* @__PURE__ */ jsx5("div", { style: {
1007
- flex: 1,
1008
- display: "flex",
1009
- alignItems: "center",
1010
- justifyContent: "center",
1011
- backgroundColor: "#f3f4f6",
1012
- padding: "20px",
1013
- overflow: "auto"
1014
- }, children: loading ? /* @__PURE__ */ jsx5("div", { style: { textAlign: "center", color: "#6b7280" }, children: /* @__PURE__ */ jsx5("p", { children: "Loading preview..." }) }) : html ? /* @__PURE__ */ jsx5("div", { style: {
1015
- backgroundColor: "white",
1016
- boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
1017
- borderRadius: "8px",
1018
- overflow: "hidden",
1019
- transform: `scale(${viewport.scale})`,
1020
- transformOrigin: "top center"
1021
- }, children: /* @__PURE__ */ jsx5(
1022
- "iframe",
1023
- {
1024
- ref: iframeRef,
1025
- title: "Email Preview",
1026
- style: {
1027
- width: `${viewport.width}px`,
1028
- height: "800px",
1029
- border: "none",
1030
- display: "block"
1031
- },
1032
- sandbox: "allow-same-origin"
1033
- }
1034
- ) }) : /* @__PURE__ */ jsx5("div", { style: { textAlign: "center", color: "#6b7280" }, children: /* @__PURE__ */ jsx5("p", { children: "Start typing to see the email preview" }) }) }),
1035
- validationResult && /* @__PURE__ */ jsxs3("div", { style: {
1036
- padding: "12px 16px",
1037
- borderTop: "1px solid #e5e7eb",
1038
- fontSize: "13px",
1039
- color: "#6b7280",
1040
- display: "flex",
1041
- gap: "24px"
81
+ preheader && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: "1rem", color: "#666" }, children: [
82
+ /* @__PURE__ */ jsx3("strong", { children: "Preheader:" }),
83
+ " ",
84
+ preheader
85
+ ] }),
86
+ /* @__PURE__ */ jsxs2("div", { style: {
87
+ border: "1px solid #e0e0e0",
88
+ borderRadius: "4px",
89
+ padding: "1rem",
90
+ backgroundColor: "#f9f9f9"
1042
91
  }, children: [
1043
- /* @__PURE__ */ jsxs3("span", { children: [
1044
- "Size: ",
1045
- Math.round(validationResult.stats.sizeInBytes / 1024),
1046
- "KB"
1047
- ] }),
1048
- /* @__PURE__ */ jsxs3("span", { children: [
1049
- "Links: ",
1050
- validationResult.stats.linkCount
1051
- ] }),
1052
- /* @__PURE__ */ jsxs3("span", { children: [
1053
- "Images: ",
1054
- validationResult.stats.imageCount
1055
- ] }),
1056
- /* @__PURE__ */ jsxs3("span", { children: [
1057
- "Viewport: ",
1058
- mode === "desktop" ? "600px" : "320px"
92
+ /* @__PURE__ */ jsx3("div", { children: "Email content will be rendered here" }),
93
+ content && /* @__PURE__ */ jsxs2("div", { style: { marginTop: "1rem", fontSize: "14px", color: "#666" }, children: [
94
+ "Content type: ",
95
+ typeof content
1059
96
  ] })
1060
97
  ] })
1061
98
  ] });
1062
99
  };
1063
- function addEmailHeader(html, headers) {
1064
- const headerHtml = `
1065
- <div style="background-color: #f9fafb; border-bottom: 1px solid #e5e7eb; padding: 16px; font-family: monospace; font-size: 13px;">
1066
- <div style="margin-bottom: 8px;"><strong>Subject:</strong> ${escapeHtml2(headers.subject)}</div>
1067
- <div style="margin-bottom: 8px;"><strong>From:</strong> ${escapeHtml2(headers.from)}</div>
1068
- <div><strong>To:</strong> ${escapeHtml2(headers.to)}</div>
1069
- </div>
1070
- `;
1071
- return html.replace(/<body[^>]*>/, `$&${headerHtml}`);
1072
- }
1073
- function escapeHtml2(text) {
1074
- const div = document.createElement("div");
1075
- div.textContent = text;
1076
- return div.innerHTML;
1077
- }
1078
-
1079
- // src/components/Broadcasts/BroadcastEditor.tsx
1080
- import { useState as useState3, useCallback as useCallback2 } from "react";
1081
- import { useField, useFormFields as useFormFields2 } from "@payloadcms/ui";
1082
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1083
- var BroadcastEditor = (props) => {
1084
- const { value } = useField({ path: props.path });
1085
- const [showPreview, setShowPreview] = useState3(true);
1086
- const [previewMode, setPreviewMode] = useState3("desktop");
1087
- const [isValid, setIsValid] = useState3(true);
1088
- const [validationSummary, setValidationSummary] = useState3("");
1089
- const fields = useFormFields2(([fields2]) => ({
1090
- subject: fields2["subject"],
1091
- preheader: fields2["contentSection.preheader"]
1092
- }));
1093
- const handleValidation = useCallback2((result) => {
1094
- setIsValid(result.valid);
1095
- const errorCount = result.errors.length;
1096
- const warningCount = result.warnings.length;
1097
- if (errorCount > 0) {
1098
- setValidationSummary(`${errorCount} error${errorCount !== 1 ? "s" : ""}, ${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
1099
- } else if (warningCount > 0) {
1100
- setValidationSummary(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
1101
- } else {
1102
- setValidationSummary("");
1103
- }
1104
- }, []);
1105
- const handleTestEmail = async () => {
1106
- const pathParts = window.location.pathname.split("/");
1107
- const broadcastId = pathParts[pathParts.length - 1];
1108
- if (!broadcastId || broadcastId === "create") {
1109
- alert("Please save the broadcast before sending a test email");
1110
- return;
1111
- }
1112
- try {
1113
- const response = await fetch(`/api/broadcasts/${broadcastId}/test`, {
1114
- method: "POST",
1115
- headers: {
1116
- "Content-Type": "application/json"
1117
- }
1118
- });
1119
- if (!response.ok) {
1120
- const data = await response.json();
1121
- throw new Error(data.error || "Failed to send test email");
1122
- }
1123
- alert("Test email sent successfully! Check your inbox.");
1124
- } catch (error) {
1125
- alert(error instanceof Error ? error.message : "Failed to send test email");
1126
- }
1127
- };
1128
- return /* @__PURE__ */ jsxs4("div", { style: { height: "600px", display: "flex", flexDirection: "column" }, children: [
1129
- /* @__PURE__ */ jsxs4("div", { style: {
1130
- display: "flex",
1131
- alignItems: "center",
1132
- justifyContent: "space-between",
1133
- padding: "12px 16px",
1134
- borderBottom: "1px solid #e5e7eb",
1135
- backgroundColor: "#f9fafb"
1136
- }, children: [
1137
- /* @__PURE__ */ jsxs4("div", { style: { display: "flex", alignItems: "center", gap: "16px" }, children: [
1138
- /* @__PURE__ */ jsx6(
1139
- "button",
1140
- {
1141
- type: "button",
1142
- onClick: () => setShowPreview(!showPreview),
1143
- style: {
1144
- padding: "6px 12px",
1145
- backgroundColor: showPreview ? "#3b82f6" : "#e5e7eb",
1146
- color: showPreview ? "white" : "#374151",
1147
- border: "none",
1148
- borderRadius: "4px",
1149
- fontSize: "14px",
1150
- cursor: "pointer"
1151
- },
1152
- children: showPreview ? "Hide Preview" : "Show Preview"
1153
- }
1154
- ),
1155
- showPreview && /* @__PURE__ */ jsxs4("div", { style: { display: "flex", gap: "8px" }, children: [
1156
- /* @__PURE__ */ jsx6(
1157
- "button",
1158
- {
1159
- type: "button",
1160
- onClick: () => setPreviewMode("desktop"),
1161
- style: {
1162
- padding: "6px 12px",
1163
- backgroundColor: previewMode === "desktop" ? "#6366f1" : "#e5e7eb",
1164
- color: previewMode === "desktop" ? "white" : "#374151",
1165
- border: "none",
1166
- borderRadius: "4px 0 0 4px",
1167
- fontSize: "14px",
1168
- cursor: "pointer"
1169
- },
1170
- children: "Desktop"
1171
- }
1172
- ),
1173
- /* @__PURE__ */ jsx6(
1174
- "button",
1175
- {
1176
- type: "button",
1177
- onClick: () => setPreviewMode("mobile"),
1178
- style: {
1179
- padding: "6px 12px",
1180
- backgroundColor: previewMode === "mobile" ? "#6366f1" : "#e5e7eb",
1181
- color: previewMode === "mobile" ? "white" : "#374151",
1182
- border: "none",
1183
- borderRadius: "0 4px 4px 0",
1184
- fontSize: "14px",
1185
- cursor: "pointer"
1186
- },
1187
- children: "Mobile"
1188
- }
1189
- )
1190
- ] }),
1191
- showPreview && validationSummary && /* @__PURE__ */ jsx6("div", { style: {
1192
- padding: "6px 12px",
1193
- backgroundColor: isValid ? "#fef3c7" : "#fee2e2",
1194
- color: isValid ? "#92400e" : "#991b1b",
1195
- borderRadius: "4px",
1196
- fontSize: "13px"
1197
- }, children: validationSummary })
1198
- ] }),
1199
- showPreview && /* @__PURE__ */ jsx6(
1200
- "button",
1201
- {
1202
- type: "button",
1203
- onClick: handleTestEmail,
1204
- style: {
1205
- padding: "6px 12px",
1206
- backgroundColor: "#10b981",
1207
- color: "white",
1208
- border: "none",
1209
- borderRadius: "4px",
1210
- fontSize: "14px",
1211
- cursor: "pointer"
1212
- },
1213
- children: "Send Test Email"
1214
- }
1215
- )
1216
- ] }),
1217
- /* @__PURE__ */ jsxs4("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
1218
- /* @__PURE__ */ jsx6("div", { style: {
1219
- flex: showPreview ? "0 0 50%" : "1",
1220
- overflow: "auto",
1221
- borderRight: showPreview ? "1px solid #e5e7eb" : "none"
1222
- }, children: /* @__PURE__ */ jsx6("div", { style: { padding: "16px" }, children: /* @__PURE__ */ jsx6("div", { className: "rich-text-lexical" }) }) }),
1223
- showPreview && /* @__PURE__ */ jsx6("div", { style: { flex: "0 0 50%", overflow: "hidden" }, children: /* @__PURE__ */ jsx6(
1224
- EmailPreview,
1225
- {
1226
- content: value,
1227
- subject: fields.subject?.value || "Email Subject",
1228
- preheader: fields.preheader?.value,
1229
- mode: previewMode,
1230
- onValidation: handleValidation
1231
- }
1232
- ) })
1233
- ] })
1234
- ] });
1235
- };
1236
-
1237
- // src/components/Broadcasts/EmailPreviewField.tsx
1238
- import { useState as useState4 } from "react";
1239
- import { useFormFields as useFormFields3 } from "@payloadcms/ui";
1240
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1241
- var EmailPreviewField = () => {
1242
- const [previewMode, setPreviewMode] = useState4("desktop");
1243
- const [isValid, setIsValid] = useState4(true);
1244
- const [validationSummary, setValidationSummary] = useState4("");
1245
- const pluginConfig = usePluginConfigOptional();
1246
- const fields = useFormFields3(([fields2]) => ({
1247
- content: fields2["contentSection.content"],
1248
- subject: fields2["subject"],
1249
- preheader: fields2["contentSection.preheader"],
1250
- channel: fields2.channel
1251
- }));
1252
- const handleValidation = (result) => {
1253
- setIsValid(result.valid);
1254
- const errorCount = result.errors.length;
1255
- const warningCount = result.warnings.length;
1256
- if (errorCount > 0) {
1257
- setValidationSummary(`${errorCount} error${errorCount !== 1 ? "s" : ""}, ${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
1258
- } else if (warningCount > 0) {
1259
- setValidationSummary(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
1260
- } else {
1261
- setValidationSummary("");
1262
- }
1263
- };
1264
- const handleTestEmail = async () => {
1265
- const pathParts = window.location.pathname.split("/");
1266
- const broadcastId = pathParts[pathParts.length - 1];
1267
- if (!broadcastId || broadcastId === "create") {
1268
- alert("Please save the broadcast before sending a test email");
1269
- return;
1270
- }
1271
- try {
1272
- const response = await fetch(`/api/broadcasts/${broadcastId}/test`, {
1273
- method: "POST",
1274
- headers: {
1275
- "Content-Type": "application/json"
1276
- }
1277
- });
1278
- if (!response.ok) {
1279
- const data = await response.json();
1280
- throw new Error(data.error || "Failed to send test email");
1281
- }
1282
- alert("Test email sent successfully! Check your inbox.");
1283
- } catch (error) {
1284
- alert(error instanceof Error ? error.message : "Failed to send test email");
1285
- }
1286
- };
1287
- return /* @__PURE__ */ jsxs5("div", { style: {
1288
- marginTop: "24px",
1289
- border: "1px solid #e5e7eb",
1290
- borderRadius: "8px",
1291
- overflow: "hidden"
1292
- }, children: [
1293
- /* @__PURE__ */ jsxs5("div", { style: {
1294
- display: "flex",
1295
- alignItems: "center",
1296
- justifyContent: "space-between",
1297
- padding: "12px 16px",
1298
- borderBottom: "1px solid #e5e7eb",
1299
- backgroundColor: "#f9fafb"
1300
- }, children: [
1301
- /* @__PURE__ */ jsxs5("div", { style: { display: "flex", alignItems: "center", gap: "16px" }, children: [
1302
- /* @__PURE__ */ jsx7("h3", { style: { margin: 0, fontSize: "16px", fontWeight: 600 }, children: "Email Preview" }),
1303
- /* @__PURE__ */ jsxs5("div", { style: { display: "flex", gap: "8px" }, children: [
1304
- /* @__PURE__ */ jsx7(
1305
- "button",
1306
- {
1307
- type: "button",
1308
- onClick: () => setPreviewMode("desktop"),
1309
- style: {
1310
- padding: "6px 12px",
1311
- backgroundColor: previewMode === "desktop" ? "#6366f1" : "#e5e7eb",
1312
- color: previewMode === "desktop" ? "white" : "#374151",
1313
- border: "none",
1314
- borderRadius: "4px 0 0 4px",
1315
- fontSize: "14px",
1316
- cursor: "pointer"
1317
- },
1318
- children: "Desktop"
1319
- }
1320
- ),
1321
- /* @__PURE__ */ jsx7(
1322
- "button",
1323
- {
1324
- type: "button",
1325
- onClick: () => setPreviewMode("mobile"),
1326
- style: {
1327
- padding: "6px 12px",
1328
- backgroundColor: previewMode === "mobile" ? "#6366f1" : "#e5e7eb",
1329
- color: previewMode === "mobile" ? "white" : "#374151",
1330
- border: "none",
1331
- borderRadius: "0 4px 4px 0",
1332
- fontSize: "14px",
1333
- cursor: "pointer"
1334
- },
1335
- children: "Mobile"
1336
- }
1337
- )
1338
- ] }),
1339
- validationSummary && /* @__PURE__ */ jsx7("div", { style: {
1340
- padding: "6px 12px",
1341
- backgroundColor: isValid ? "#fef3c7" : "#fee2e2",
1342
- color: isValid ? "#92400e" : "#991b1b",
1343
- borderRadius: "4px",
1344
- fontSize: "13px"
1345
- }, children: validationSummary })
1346
- ] }),
1347
- /* @__PURE__ */ jsx7(
1348
- "button",
1349
- {
1350
- type: "button",
1351
- onClick: handleTestEmail,
1352
- style: {
1353
- padding: "6px 12px",
1354
- backgroundColor: "#10b981",
1355
- color: "white",
1356
- border: "none",
1357
- borderRadius: "4px",
1358
- fontSize: "14px",
1359
- cursor: "pointer"
1360
- },
1361
- children: "Send Test Email"
1362
- }
1363
- )
1364
- ] }),
1365
- /* @__PURE__ */ jsx7("div", { style: { height: "600px" }, children: /* @__PURE__ */ jsx7(
1366
- EmailPreview,
1367
- {
1368
- content: fields.content?.value || null,
1369
- subject: fields.subject?.value || "Email Subject",
1370
- preheader: fields.preheader?.value,
1371
- mode: previewMode,
1372
- onValidation: handleValidation,
1373
- pluginConfig: pluginConfig || void 0
1374
- }
1375
- ) })
1376
- ] });
1377
- };
1378
-
1379
- // src/components/Broadcasts/BroadcastPreviewField.tsx
1380
- import { jsx as jsx8 } from "react/jsx-runtime";
1381
- var BroadcastPreviewField = () => {
1382
- return /* @__PURE__ */ jsx8("div", { style: {
1383
- padding: "1rem",
1384
- background: "#f9fafb",
1385
- borderRadius: "4px",
1386
- fontSize: "14px",
1387
- color: "#6b7280"
1388
- }, children: "Email preview is available inline below the content editor." });
1389
- };
1390
-
1391
- // src/contexts/ClientContext.tsx
1392
- import { createContext as createContext2, useContext as useContext2 } from "react";
1393
- import { jsx as jsx9 } from "react/jsx-runtime";
1394
- var PluginConfigContext2 = createContext2(null);
1395
- var PluginConfigProvider = ({ config, children }) => {
1396
- return /* @__PURE__ */ jsx9(PluginConfigContext2.Provider, { value: config, children });
1397
- };
1398
- var usePluginConfig = () => {
1399
- const context = useContext2(PluginConfigContext2);
1400
- if (!context) {
1401
- throw new Error("usePluginConfig must be used within a PluginConfigProvider");
1402
- }
1403
- return context;
1404
- };
1405
- var usePluginConfigOptional2 = () => {
1406
- return useContext2(PluginConfigContext2);
1407
- };
1408
-
1409
- // src/fields/broadcastInlinePreview.ts
1410
- var createBroadcastInlinePreviewField = () => {
1411
- return {
1412
- name: "broadcastInlinePreview",
1413
- type: "ui",
1414
- admin: {
1415
- components: {
1416
- Field: "payload-plugin-newsletter/components#BroadcastInlinePreview"
1417
- }
1418
- }
1419
- };
1420
- };
1421
-
1422
- // src/fields/broadcastPreview.ts
1423
- var createBroadcastPreviewField = () => {
1424
- return {
1425
- name: "broadcastPreview",
1426
- type: "ui",
1427
- admin: {
1428
- components: {
1429
- Field: "payload-plugin-newsletter/components#BroadcastPreviewField"
1430
- },
1431
- position: "sidebar"
1432
- }
1433
- };
1434
- };
1435
-
1436
- // src/fields/emailContent.ts
1437
- import {
1438
- BoldFeature,
1439
- ItalicFeature,
1440
- UnderlineFeature,
1441
- StrikethroughFeature,
1442
- LinkFeature,
1443
- OrderedListFeature,
1444
- UnorderedListFeature,
1445
- HeadingFeature,
1446
- ParagraphFeature,
1447
- AlignFeature,
1448
- BlockquoteFeature,
1449
- BlocksFeature,
1450
- UploadFeature,
1451
- FixedToolbarFeature,
1452
- InlineToolbarFeature,
1453
- lexicalEditor
1454
- } from "@payloadcms/richtext-lexical";
1455
-
1456
- // src/utils/blockValidation.ts
1457
- var EMAIL_INCOMPATIBLE_TYPES = [
1458
- "chart",
1459
- "dataTable",
1460
- "interactive",
1461
- "streamable",
1462
- "video",
1463
- "iframe",
1464
- "form",
1465
- "carousel",
1466
- "tabs",
1467
- "accordion",
1468
- "map"
1469
- ];
1470
- var validateEmailBlocks = (blocks) => {
1471
- blocks.forEach((block) => {
1472
- if (EMAIL_INCOMPATIBLE_TYPES.includes(block.slug)) {
1473
- console.warn(`\u26A0\uFE0F Block "${block.slug}" may not be email-compatible. Consider creating an email-specific version.`);
1474
- }
1475
- const hasComplexFields = block.fields?.some((field) => {
1476
- const complexTypes = ["code", "json", "richText", "blocks", "array"];
1477
- return complexTypes.includes(field.type);
1478
- });
1479
- if (hasComplexFields) {
1480
- console.warn(`\u26A0\uFE0F Block "${block.slug}" contains complex field types that may not render consistently in email clients.`);
1481
- }
1482
- });
1483
- };
1484
- var createEmailSafeBlocks = (customBlocks = []) => {
1485
- validateEmailBlocks(customBlocks);
1486
- const baseBlocks = [
1487
- {
1488
- slug: "button",
1489
- fields: [
1490
- {
1491
- name: "text",
1492
- type: "text",
1493
- label: "Button Text",
1494
- required: true
1495
- },
1496
- {
1497
- name: "url",
1498
- type: "text",
1499
- label: "Button URL",
1500
- required: true,
1501
- admin: {
1502
- description: "Enter the full URL (including https://)"
1503
- }
1504
- },
1505
- {
1506
- name: "style",
1507
- type: "select",
1508
- label: "Button Style",
1509
- defaultValue: "primary",
1510
- options: [
1511
- { label: "Primary", value: "primary" },
1512
- { label: "Secondary", value: "secondary" },
1513
- { label: "Outline", value: "outline" }
1514
- ]
1515
- }
1516
- ],
1517
- interfaceName: "EmailButton",
1518
- labels: {
1519
- singular: "Button",
1520
- plural: "Buttons"
1521
- }
1522
- },
1523
- {
1524
- slug: "divider",
1525
- fields: [
1526
- {
1527
- name: "style",
1528
- type: "select",
1529
- label: "Divider Style",
1530
- defaultValue: "solid",
1531
- options: [
1532
- { label: "Solid", value: "solid" },
1533
- { label: "Dashed", value: "dashed" },
1534
- { label: "Dotted", value: "dotted" }
1535
- ]
1536
- }
1537
- ],
1538
- interfaceName: "EmailDivider",
1539
- labels: {
1540
- singular: "Divider",
1541
- plural: "Dividers"
1542
- }
1543
- }
1544
- ];
1545
- return [
1546
- ...baseBlocks,
1547
- ...customBlocks
1548
- ];
1549
- };
1550
-
1551
- // src/fields/emailContent.ts
1552
- var createEmailSafeFeatures = (additionalBlocks) => {
1553
- const baseBlocks = [
1554
- {
1555
- slug: "button",
1556
- fields: [
1557
- {
1558
- name: "text",
1559
- type: "text",
1560
- label: "Button Text",
1561
- required: true
1562
- },
1563
- {
1564
- name: "url",
1565
- type: "text",
1566
- label: "Button URL",
1567
- required: true,
1568
- admin: {
1569
- description: "Enter the full URL (including https://)"
1570
- }
1571
- },
1572
- {
1573
- name: "style",
1574
- type: "select",
1575
- label: "Button Style",
1576
- defaultValue: "primary",
1577
- options: [
1578
- { label: "Primary", value: "primary" },
1579
- { label: "Secondary", value: "secondary" },
1580
- { label: "Outline", value: "outline" }
1581
- ]
1582
- }
1583
- ],
1584
- interfaceName: "EmailButton",
1585
- labels: {
1586
- singular: "Button",
1587
- plural: "Buttons"
1588
- }
1589
- },
1590
- {
1591
- slug: "divider",
1592
- fields: [
1593
- {
1594
- name: "style",
1595
- type: "select",
1596
- label: "Divider Style",
1597
- defaultValue: "solid",
1598
- options: [
1599
- { label: "Solid", value: "solid" },
1600
- { label: "Dashed", value: "dashed" },
1601
- { label: "Dotted", value: "dotted" }
1602
- ]
1603
- }
1604
- ],
1605
- interfaceName: "EmailDivider",
1606
- labels: {
1607
- singular: "Divider",
1608
- plural: "Dividers"
1609
- }
1610
- }
1611
- ];
1612
- const allBlocks = [
1613
- ...baseBlocks,
1614
- ...additionalBlocks || []
1615
- ];
1616
- return [
1617
- // Toolbars
1618
- FixedToolbarFeature(),
1619
- // Fixed toolbar at the top
1620
- InlineToolbarFeature(),
1621
- // Floating toolbar when text is selected
1622
- // Basic text formatting
1623
- BoldFeature(),
1624
- ItalicFeature(),
1625
- UnderlineFeature(),
1626
- StrikethroughFeature(),
1627
- // Links with enhanced configuration
1628
- LinkFeature({
1629
- fields: [
1630
- {
1631
- name: "url",
1632
- type: "text",
1633
- required: true,
1634
- admin: {
1635
- description: "Enter the full URL (including https://)"
1636
- }
1637
- },
1638
- {
1639
- name: "newTab",
1640
- type: "checkbox",
1641
- label: "Open in new tab",
1642
- defaultValue: false
1643
- }
1644
- ]
1645
- }),
1646
- // Lists
1647
- OrderedListFeature(),
1648
- UnorderedListFeature(),
1649
- // Headings - limited to h1, h2, h3 for email compatibility
1650
- HeadingFeature({
1651
- enabledHeadingSizes: ["h1", "h2", "h3"]
1652
- }),
1653
- // Basic paragraph and alignment
1654
- ParagraphFeature(),
1655
- AlignFeature(),
1656
- // Blockquotes
1657
- BlockquoteFeature(),
1658
- // Upload feature for images
1659
- UploadFeature({
1660
- collections: {
1661
- media: {
1662
- fields: [
1663
- {
1664
- name: "caption",
1665
- type: "text",
1666
- admin: {
1667
- description: "Optional caption for the image"
1668
- }
1669
- },
1670
- {
1671
- name: "altText",
1672
- type: "text",
1673
- label: "Alt Text",
1674
- required: true,
1675
- admin: {
1676
- description: "Alternative text for accessibility and when image cannot be displayed"
1677
- }
1678
- }
1679
- ]
1680
- }
1681
- }
1682
- }),
1683
- // Custom blocks for email-specific content
1684
- BlocksFeature({
1685
- blocks: allBlocks
1686
- })
1687
- ];
1688
- };
1689
- var createEmailLexicalEditor = (customBlocks = []) => {
1690
- const emailSafeBlocks = createEmailSafeBlocks(customBlocks);
1691
- return lexicalEditor({
1692
- features: [
1693
- // Toolbars
1694
- FixedToolbarFeature(),
1695
- InlineToolbarFeature(),
1696
- // Basic text formatting
1697
- BoldFeature(),
1698
- ItalicFeature(),
1699
- UnderlineFeature(),
1700
- StrikethroughFeature(),
1701
- // Links with enhanced configuration
1702
- LinkFeature({
1703
- fields: [
1704
- {
1705
- name: "url",
1706
- type: "text",
1707
- required: true,
1708
- admin: {
1709
- description: "Enter the full URL (including https://)"
1710
- }
1711
- },
1712
- {
1713
- name: "newTab",
1714
- type: "checkbox",
1715
- label: "Open in new tab",
1716
- defaultValue: false
1717
- }
1718
- ]
1719
- }),
1720
- // Lists
1721
- OrderedListFeature(),
1722
- UnorderedListFeature(),
1723
- // Headings - limited to h1, h2, h3 for email compatibility
1724
- HeadingFeature({
1725
- enabledHeadingSizes: ["h1", "h2", "h3"]
1726
- }),
1727
- // Basic paragraph and alignment
1728
- ParagraphFeature(),
1729
- AlignFeature(),
1730
- // Blockquotes
1731
- BlockquoteFeature(),
1732
- // Upload feature for images
1733
- UploadFeature({
1734
- collections: {
1735
- media: {
1736
- fields: [
1737
- {
1738
- name: "caption",
1739
- type: "text",
1740
- admin: {
1741
- description: "Optional caption for the image"
1742
- }
1743
- },
1744
- {
1745
- name: "altText",
1746
- type: "text",
1747
- label: "Alt Text",
1748
- required: true,
1749
- admin: {
1750
- description: "Alternative text for accessibility and when image cannot be displayed"
1751
- }
1752
- }
1753
- ]
1754
- }
1755
- }
1756
- }),
1757
- // Email-safe blocks (processed server-side)
1758
- BlocksFeature({
1759
- blocks: emailSafeBlocks
1760
- })
1761
- ]
1762
- });
1763
- };
1764
- var emailSafeFeatures = createEmailSafeFeatures();
1765
- var createEmailContentField = (overrides) => {
1766
- const editor = overrides?.editor || createEmailLexicalEditor(overrides?.additionalBlocks);
1767
- return {
1768
- name: "content",
1769
- type: "richText",
1770
- required: true,
1771
- editor,
1772
- admin: {
1773
- description: "Email content with limited formatting for compatibility",
1774
- ...overrides?.admin
1775
- },
1776
- ...overrides
1777
- };
1778
- };
1779
100
  export {
1780
- BroadcastEditor,
1781
101
  BroadcastInlinePreview,
1782
- BroadcastPreviewField,
1783
102
  EmailPreview,
1784
- EmailPreviewField,
1785
- PluginConfigProvider,
1786
- StatusBadge,
1787
- createBroadcastInlinePreviewField,
1788
- createBroadcastPreviewField,
1789
- createEmailContentField,
1790
- usePluginConfig,
1791
- usePluginConfigOptional2 as usePluginConfigOptional
103
+ StatusBadge
1792
104
  };