payload-plugin-newsletter 0.20.2 → 0.20.4

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,241 @@
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
5
+ import { useState, useEffect, useRef } from "react";
9
6
  import { jsx, jsxs } from "react/jsx-runtime";
10
- var PreviewControls = ({
11
- onUpdate,
12
- device,
13
- onDeviceChange,
14
- isLoading = false
7
+ var BroadcastInlinePreview = ({
8
+ data,
9
+ field: _field,
10
+ path: _path,
11
+ schemaPath: _schemaPath,
12
+ ..._props
15
13
  }) => {
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);
14
+ const [previewHtml, setPreviewHtml] = useState("");
15
+ const [loading, setLoading] = useState(false);
104
16
  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(
317
- "span",
318
- {
319
- style: {
320
- display: "inline-flex",
321
- alignItems: "center",
322
- padding: "2px 10px",
323
- borderRadius: "12px",
324
- fontSize: "12px",
325
- fontWeight: "500",
326
- color: config.color,
327
- backgroundColor: config.backgroundColor
328
- },
329
- children: config.label
330
- }
331
- );
332
- };
333
-
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
- };
927
- var EmailPreview = ({
928
- content,
929
- subject,
930
- preheader,
931
- mode = "desktop",
932
- onValidation,
933
- pluginConfig: propPluginConfig
934
- }) => {
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
17
  const iframeRef = useRef(null);
941
18
  useEffect(() => {
942
- const convertContent = async () => {
943
- if (!content) {
944
- setHtml("");
19
+ const fetchPreview = async () => {
20
+ if (!data?.content || !data.content.root?.children?.length) {
21
+ setPreviewHtml("");
945
22
  return;
946
23
  }
947
24
  setLoading(true);
25
+ setError(null);
948
26
  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"]
27
+ const response = await fetch("/api/broadcasts/preview", {
28
+ method: "POST",
29
+ headers: {
30
+ "Content-Type": "application/json"
31
+ },
32
+ credentials: "same-origin",
33
+ // Include cookies for auth
34
+ body: JSON.stringify({
35
+ content: data.content,
36
+ subject: data.subject || "",
37
+ preheader: data.preheader || ""
38
+ })
962
39
  });
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>");
40
+ if (!response.ok) {
41
+ const errorData = await response.json().catch(() => ({}));
42
+ throw new Error(errorData.error || `Failed to fetch preview: ${response.status}`);
43
+ }
44
+ const result = await response.json();
45
+ if (result.success && result.html) {
46
+ setPreviewHtml(result.html);
47
+ } else {
48
+ throw new Error(result.error || "No preview HTML returned");
49
+ }
50
+ } catch (err) {
51
+ console.error("Preview fetch error:", err);
52
+ setError(err instanceof Error ? err.message : "Failed to load preview");
970
53
  } finally {
971
54
  setLoading(false);
972
55
  }
973
56
  };
974
- convertContent();
975
- }, [content, subject, preheader, onValidation, pluginConfig]);
57
+ const timeoutId = setTimeout(fetchPreview, 500);
58
+ return () => clearTimeout(timeoutId);
59
+ }, [data?.content, data?.subject, data?.preheader]);
976
60
  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
- ] })
1005
- ] }),
1006
- /* @__PURE__ */ jsx5("div", { style: {
1007
- flex: 1,
61
+ if (iframeRef.current && previewHtml) {
62
+ const iframe = iframeRef.current;
63
+ const resizeIframe = () => {
64
+ try {
65
+ const body = iframe.contentDocument?.body;
66
+ if (body) {
67
+ iframe.style.height = `${body.scrollHeight + 40}px`;
68
+ }
69
+ } catch {
70
+ iframe.style.height = "600px";
71
+ }
72
+ };
73
+ iframe.onload = resizeIframe;
74
+ const contentWindow = iframe.contentWindow;
75
+ if (contentWindow) {
76
+ contentWindow.addEventListener("resize", resizeIframe);
77
+ return () => contentWindow.removeEventListener("resize", resizeIframe);
78
+ }
79
+ }
80
+ }, [previewHtml]);
81
+ if (!data?.content) {
82
+ return /* @__PURE__ */ jsx("div", { style: {
83
+ padding: "2rem",
84
+ textAlign: "center",
85
+ color: "#666",
86
+ border: "1px dashed #ddd",
87
+ borderRadius: "4px",
88
+ backgroundColor: "#f9f9f9"
89
+ }, children: /* @__PURE__ */ jsx("p", { children: "Start adding content to see the email preview" }) });
90
+ }
91
+ if (loading) {
92
+ return /* @__PURE__ */ jsx("div", { style: {
93
+ padding: "2rem",
94
+ textAlign: "center",
95
+ border: "1px solid #e0e0e0",
96
+ borderRadius: "4px",
97
+ backgroundColor: "#f9f9f9"
98
+ }, children: /* @__PURE__ */ jsx("div", { style: { display: "inline-block" }, children: "Loading preview..." }) });
99
+ }
100
+ if (error) {
101
+ return /* @__PURE__ */ jsxs("div", { style: {
102
+ padding: "1rem",
103
+ border: "1px solid #fee",
104
+ borderRadius: "4px",
105
+ backgroundColor: "#fef2f2",
106
+ color: "#dc2626"
107
+ }, children: [
108
+ /* @__PURE__ */ jsx("strong", { children: "Preview Error:" }),
109
+ " ",
110
+ error
111
+ ] });
112
+ }
113
+ return /* @__PURE__ */ jsxs("div", { className: "broadcast-preview", style: { marginTop: "1rem" }, children: [
114
+ /* @__PURE__ */ jsxs("div", { style: {
115
+ marginBottom: "0.5rem",
1008
116
  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",
117
+ justifyContent: "space-between",
118
+ alignItems: "center"
119
+ }, children: [
120
+ /* @__PURE__ */ jsx("h3", { style: { margin: 0, fontSize: "1rem", fontWeight: "600" }, children: "Email Preview" }),
121
+ /* @__PURE__ */ jsx("div", { style: { fontSize: "0.875rem", color: "#666" }, children: data.subject && /* @__PURE__ */ jsxs("span", { children: [
122
+ "Subject: ",
123
+ data.subject
124
+ ] }) })
125
+ ] }),
126
+ /* @__PURE__ */ jsx("div", { style: {
127
+ border: "1px solid #e0e0e0",
128
+ borderRadius: "4px",
1018
129
  overflow: "hidden",
1019
- transform: `scale(${viewport.scale})`,
1020
- transformOrigin: "top center"
1021
- }, children: /* @__PURE__ */ jsx5(
130
+ backgroundColor: "#fff"
131
+ }, children: /* @__PURE__ */ jsx(
1022
132
  "iframe",
1023
133
  {
1024
134
  ref: iframeRef,
1025
- title: "Email Preview",
135
+ srcDoc: previewHtml,
1026
136
  style: {
1027
- width: `${viewport.width}px`,
1028
- height: "800px",
137
+ width: "100%",
138
+ minHeight: "400px",
1029
139
  border: "none",
1030
140
  display: "block"
1031
141
  },
142
+ title: "Email Preview",
1032
143
  sandbox: "allow-same-origin"
1033
144
  }
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"
1042
- }, 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"
1059
- ] })
1060
- ] })
145
+ ) }),
146
+ /* @__PURE__ */ jsx("div", { style: {
147
+ marginTop: "0.5rem",
148
+ fontSize: "0.75rem",
149
+ color: "#666",
150
+ textAlign: "center"
151
+ }, children: "This preview shows how your email will appear when sent" })
1061
152
  ] });
1062
153
  };
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
154
 
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");
155
+ // src/admin/components/StatusBadge.tsx
156
+ import { jsx as jsx2 } from "react/jsx-runtime";
157
+ var StatusBadge = (props) => {
158
+ const status = props.cellData || "draft";
159
+ const getStatusColor = (status2) => {
160
+ switch (status2) {
161
+ case "sent":
162
+ return "#22c55e";
163
+ case "scheduled":
164
+ return "#3b82f6";
165
+ case "draft":
166
+ return "#6b7280";
167
+ case "failed":
168
+ return "#ef4444";
169
+ default:
170
+ return "#6b7280";
171
+ }
172
+ };
173
+ const getStatusLabel = (status2) => {
174
+ switch (status2) {
175
+ case "sent":
176
+ return "Sent";
177
+ case "scheduled":
178
+ return "Scheduled";
179
+ case "draft":
180
+ return "Draft";
181
+ case "failed":
182
+ return "Failed";
183
+ default:
184
+ return status2;
185
+ }
186
+ };
187
+ return /* @__PURE__ */ jsx2(
188
+ "span",
189
+ {
190
+ style: {
191
+ display: "inline-block",
192
+ padding: "4px 8px",
193
+ borderRadius: "12px",
194
+ fontSize: "12px",
195
+ fontWeight: "500",
196
+ color: "#fff",
197
+ backgroundColor: getStatusColor(status),
198
+ textTransform: "capitalize"
199
+ },
200
+ children: getStatusLabel(status)
1126
201
  }
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
- ] });
202
+ );
1235
203
  };
1236
204
 
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
- )
205
+ // src/admin/components/EmailPreview.tsx
206
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
207
+ var EmailPreview = ({
208
+ content,
209
+ subject,
210
+ preheader
211
+ }) => {
212
+ return /* @__PURE__ */ jsxs2("div", { className: "email-preview", style: { padding: "1rem" }, children: [
213
+ /* @__PURE__ */ jsxs2("div", { style: { marginBottom: "1rem" }, children: [
214
+ /* @__PURE__ */ jsx3("strong", { children: "Subject:" }),
215
+ " ",
216
+ subject || "No subject"
1364
217
  ] }),
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
- ) })
218
+ preheader && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: "1rem", color: "#666" }, children: [
219
+ /* @__PURE__ */ jsx3("strong", { children: "Preheader:" }),
220
+ " ",
221
+ preheader
222
+ ] }),
223
+ /* @__PURE__ */ jsxs2("div", { style: {
224
+ border: "1px solid #e0e0e0",
225
+ borderRadius: "4px",
226
+ padding: "1rem",
227
+ backgroundColor: "#f9f9f9"
228
+ }, children: [
229
+ /* @__PURE__ */ jsx3("div", { children: "Email content will be rendered here" }),
230
+ content && /* @__PURE__ */ jsxs2("div", { style: { marginTop: "1rem", fontSize: "14px", color: "#666" }, children: [
231
+ "Content type: ",
232
+ typeof content
233
+ ] })
234
+ ] })
1376
235
  ] });
1377
236
  };
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
237
  export {
1780
- BroadcastEditor,
1781
238
  BroadcastInlinePreview,
1782
- BroadcastPreviewField,
1783
239
  EmailPreview,
1784
- EmailPreviewField,
1785
- PluginConfigProvider,
1786
- StatusBadge,
1787
- createBroadcastInlinePreviewField,
1788
- createBroadcastPreviewField,
1789
- createEmailContentField,
1790
- usePluginConfig,
1791
- usePluginConfigOptional2 as usePluginConfigOptional
240
+ StatusBadge
1792
241
  };