payload-plugin-newsletter 0.20.1 → 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.
@@ -1,2418 +0,0 @@
1
- "use client";
2
-
3
- // src/components/NewsletterForm.tsx
4
- import { useState } from "react";
5
- import { jsx, jsxs } from "react/jsx-runtime";
6
- var defaultStyles = {
7
- form: {
8
- display: "flex",
9
- flexDirection: "column",
10
- gap: "1rem",
11
- maxWidth: "400px",
12
- margin: "0 auto"
13
- },
14
- inputGroup: {
15
- display: "flex",
16
- flexDirection: "column",
17
- gap: "0.5rem"
18
- },
19
- label: {
20
- fontSize: "0.875rem",
21
- fontWeight: "500",
22
- color: "#374151"
23
- },
24
- input: {
25
- padding: "0.5rem 0.75rem",
26
- fontSize: "1rem",
27
- border: "1px solid #e5e7eb",
28
- borderRadius: "0.375rem",
29
- outline: "none",
30
- transition: "border-color 0.2s"
31
- },
32
- button: {
33
- padding: "0.75rem 1.5rem",
34
- fontSize: "1rem",
35
- fontWeight: "500",
36
- color: "#ffffff",
37
- backgroundColor: "#3b82f6",
38
- border: "none",
39
- borderRadius: "0.375rem",
40
- cursor: "pointer",
41
- transition: "background-color 0.2s"
42
- },
43
- buttonDisabled: {
44
- opacity: 0.5,
45
- cursor: "not-allowed"
46
- },
47
- error: {
48
- fontSize: "0.875rem",
49
- color: "#ef4444",
50
- marginTop: "0.25rem"
51
- },
52
- success: {
53
- fontSize: "0.875rem",
54
- color: "#10b981",
55
- marginTop: "0.25rem"
56
- },
57
- checkbox: {
58
- display: "flex",
59
- alignItems: "center",
60
- gap: "0.5rem"
61
- },
62
- checkboxInput: {
63
- width: "1rem",
64
- height: "1rem"
65
- },
66
- checkboxLabel: {
67
- fontSize: "0.875rem",
68
- color: "#374151"
69
- }
70
- };
71
- var NewsletterForm = ({
72
- onSuccess,
73
- onError,
74
- showName = false,
75
- showPreferences = false,
76
- leadMagnet,
77
- className,
78
- styles: customStyles = {},
79
- apiEndpoint = "/api/newsletter/subscribe",
80
- buttonText = "Subscribe",
81
- loadingText = "Subscribing...",
82
- successMessage = "Successfully subscribed!",
83
- placeholders = {
84
- email: "Enter your email",
85
- name: "Enter your name"
86
- },
87
- labels = {
88
- email: "Email",
89
- name: "Name",
90
- newsletter: "Newsletter updates",
91
- announcements: "Product announcements"
92
- }
93
- }) => {
94
- const [email, setEmail] = useState("");
95
- const [name, setName] = useState("");
96
- const [preferences, setPreferences] = useState({
97
- newsletter: true,
98
- announcements: true
99
- });
100
- const [loading, setLoading] = useState(false);
101
- const [error, setError] = useState(null);
102
- const [success, setSuccess] = useState(false);
103
- const styles = {
104
- form: { ...defaultStyles.form, ...customStyles.form },
105
- inputGroup: { ...defaultStyles.inputGroup, ...customStyles.inputGroup },
106
- label: { ...defaultStyles.label, ...customStyles.label },
107
- input: { ...defaultStyles.input, ...customStyles.input },
108
- button: { ...defaultStyles.button, ...customStyles.button },
109
- buttonDisabled: { ...defaultStyles.buttonDisabled, ...customStyles.buttonDisabled },
110
- error: { ...defaultStyles.error, ...customStyles.error },
111
- success: { ...defaultStyles.success, ...customStyles.success },
112
- checkbox: { ...defaultStyles.checkbox, ...customStyles.checkbox },
113
- checkboxInput: { ...defaultStyles.checkboxInput, ...customStyles.checkboxInput },
114
- checkboxLabel: { ...defaultStyles.checkboxLabel, ...customStyles.checkboxLabel }
115
- };
116
- const handleSubmit = async (e) => {
117
- e.preventDefault();
118
- setError(null);
119
- setLoading(true);
120
- try {
121
- const payload = {
122
- email,
123
- ...showName && name && { name },
124
- ...showPreferences && { preferences },
125
- ...leadMagnet && { leadMagnet: leadMagnet.id },
126
- metadata: {
127
- signupPage: window.location.href,
128
- ...typeof window !== "undefined" && window.location.search && {
129
- utmParams: Object.fromEntries(new URLSearchParams(window.location.search))
130
- }
131
- }
132
- };
133
- const response = await fetch(apiEndpoint, {
134
- method: "POST",
135
- headers: {
136
- "Content-Type": "application/json"
137
- },
138
- body: JSON.stringify(payload)
139
- });
140
- const data = await response.json();
141
- if (!response.ok) {
142
- throw new Error(data.error || data.errors?.join(", ") || "Subscription failed");
143
- }
144
- setSuccess(true);
145
- setEmail("");
146
- setName("");
147
- if (onSuccess) {
148
- onSuccess(data.subscriber);
149
- }
150
- } catch (err) {
151
- const errorMessage = err instanceof Error ? err.message : "An error occurred";
152
- setError(errorMessage);
153
- if (onError) {
154
- onError(new Error(errorMessage));
155
- }
156
- } finally {
157
- setLoading(false);
158
- }
159
- };
160
- if (success && !showPreferences) {
161
- return /* @__PURE__ */ jsx("div", { className, style: styles.form, children: /* @__PURE__ */ jsx("p", { style: styles.success, children: successMessage }) });
162
- }
163
- return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className, style: styles.form, children: [
164
- /* @__PURE__ */ jsxs("div", { style: styles.inputGroup, children: [
165
- /* @__PURE__ */ jsx("label", { htmlFor: "email", style: styles.label, children: labels.email }),
166
- /* @__PURE__ */ jsx(
167
- "input",
168
- {
169
- id: "email",
170
- type: "email",
171
- value: email,
172
- onChange: (e) => setEmail(e.target.value),
173
- placeholder: placeholders.email,
174
- required: true,
175
- disabled: loading,
176
- style: {
177
- ...styles.input,
178
- ...loading && { opacity: 0.5 }
179
- }
180
- }
181
- )
182
- ] }),
183
- showName && /* @__PURE__ */ jsxs("div", { style: styles.inputGroup, children: [
184
- /* @__PURE__ */ jsx("label", { htmlFor: "name", style: styles.label, children: labels.name }),
185
- /* @__PURE__ */ jsx(
186
- "input",
187
- {
188
- id: "name",
189
- type: "text",
190
- value: name,
191
- onChange: (e) => setName(e.target.value),
192
- placeholder: placeholders.name,
193
- disabled: loading,
194
- style: {
195
- ...styles.input,
196
- ...loading && { opacity: 0.5 }
197
- }
198
- }
199
- )
200
- ] }),
201
- showPreferences && /* @__PURE__ */ jsxs("div", { style: styles.inputGroup, children: [
202
- /* @__PURE__ */ jsx("label", { style: styles.label, children: "Email Preferences" }),
203
- /* @__PURE__ */ jsxs("div", { style: styles.checkbox, children: [
204
- /* @__PURE__ */ jsx(
205
- "input",
206
- {
207
- id: "newsletter",
208
- type: "checkbox",
209
- checked: preferences.newsletter,
210
- onChange: (e) => setPreferences({ ...preferences, newsletter: e.target.checked }),
211
- disabled: loading,
212
- style: styles.checkboxInput
213
- }
214
- ),
215
- /* @__PURE__ */ jsx("label", { htmlFor: "newsletter", style: styles.checkboxLabel, children: labels.newsletter })
216
- ] }),
217
- /* @__PURE__ */ jsxs("div", { style: styles.checkbox, children: [
218
- /* @__PURE__ */ jsx(
219
- "input",
220
- {
221
- id: "announcements",
222
- type: "checkbox",
223
- checked: preferences.announcements,
224
- onChange: (e) => setPreferences({ ...preferences, announcements: e.target.checked }),
225
- disabled: loading,
226
- style: styles.checkboxInput
227
- }
228
- ),
229
- /* @__PURE__ */ jsx("label", { htmlFor: "announcements", style: styles.checkboxLabel, children: labels.announcements })
230
- ] })
231
- ] }),
232
- /* @__PURE__ */ jsx(
233
- "button",
234
- {
235
- type: "submit",
236
- disabled: loading,
237
- style: {
238
- ...styles.button,
239
- ...loading && styles.buttonDisabled
240
- },
241
- children: loading ? loadingText : buttonText
242
- }
243
- ),
244
- error && /* @__PURE__ */ jsx("p", { style: styles.error, children: error }),
245
- success && /* @__PURE__ */ jsx("p", { style: styles.success, children: successMessage })
246
- ] });
247
- };
248
- function createNewsletterForm(defaultProps) {
249
- return (props) => /* @__PURE__ */ jsx(NewsletterForm, { ...defaultProps, ...props });
250
- }
251
-
252
- // src/components/PreferencesForm.tsx
253
- import { useState as useState2, useEffect } from "react";
254
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
255
- var defaultStyles2 = {
256
- container: {
257
- maxWidth: "600px",
258
- margin: "0 auto",
259
- padding: "2rem"
260
- },
261
- heading: {
262
- fontSize: "1.5rem",
263
- fontWeight: "600",
264
- marginBottom: "1.5rem",
265
- color: "#111827"
266
- },
267
- form: {
268
- display: "flex",
269
- flexDirection: "column",
270
- gap: "1.5rem"
271
- },
272
- section: {
273
- padding: "1.5rem",
274
- backgroundColor: "#f9fafb",
275
- borderRadius: "0.5rem",
276
- border: "1px solid #e5e7eb"
277
- },
278
- sectionTitle: {
279
- fontSize: "1.125rem",
280
- fontWeight: "500",
281
- marginBottom: "1rem",
282
- color: "#111827"
283
- },
284
- inputGroup: {
285
- display: "flex",
286
- flexDirection: "column",
287
- gap: "0.5rem"
288
- },
289
- label: {
290
- fontSize: "0.875rem",
291
- fontWeight: "500",
292
- color: "#374151"
293
- },
294
- input: {
295
- padding: "0.5rem 0.75rem",
296
- fontSize: "1rem",
297
- border: "1px solid #e5e7eb",
298
- borderRadius: "0.375rem",
299
- outline: "none",
300
- transition: "border-color 0.2s"
301
- },
302
- select: {
303
- padding: "0.5rem 0.75rem",
304
- fontSize: "1rem",
305
- border: "1px solid #e5e7eb",
306
- borderRadius: "0.375rem",
307
- outline: "none",
308
- backgroundColor: "#ffffff"
309
- },
310
- checkbox: {
311
- display: "flex",
312
- alignItems: "center",
313
- gap: "0.5rem",
314
- marginBottom: "0.5rem"
315
- },
316
- checkboxInput: {
317
- width: "1rem",
318
- height: "1rem"
319
- },
320
- checkboxLabel: {
321
- fontSize: "0.875rem",
322
- color: "#374151"
323
- },
324
- buttonGroup: {
325
- display: "flex",
326
- gap: "1rem",
327
- marginTop: "1rem"
328
- },
329
- button: {
330
- padding: "0.75rem 1.5rem",
331
- fontSize: "1rem",
332
- fontWeight: "500",
333
- borderRadius: "0.375rem",
334
- cursor: "pointer",
335
- transition: "all 0.2s",
336
- border: "none"
337
- },
338
- primaryButton: {
339
- color: "#ffffff",
340
- backgroundColor: "#3b82f6"
341
- },
342
- secondaryButton: {
343
- color: "#374151",
344
- backgroundColor: "#ffffff",
345
- border: "1px solid #e5e7eb"
346
- },
347
- dangerButton: {
348
- color: "#ffffff",
349
- backgroundColor: "#ef4444"
350
- },
351
- error: {
352
- fontSize: "0.875rem",
353
- color: "#ef4444",
354
- marginTop: "0.5rem"
355
- },
356
- success: {
357
- fontSize: "0.875rem",
358
- color: "#10b981",
359
- marginTop: "0.5rem"
360
- },
361
- info: {
362
- fontSize: "0.875rem",
363
- color: "#6b7280",
364
- marginTop: "0.5rem"
365
- }
366
- };
367
- var PreferencesForm = ({
368
- subscriber: initialSubscriber,
369
- onSuccess,
370
- onError,
371
- className,
372
- styles: customStyles = {},
373
- sessionToken,
374
- apiEndpoint = "/api/newsletter/preferences",
375
- showUnsubscribe = true,
376
- locales = ["en"],
377
- labels = {
378
- title: "Newsletter Preferences",
379
- personalInfo: "Personal Information",
380
- emailPreferences: "Email Preferences",
381
- name: "Name",
382
- language: "Preferred Language",
383
- newsletter: "Newsletter updates",
384
- announcements: "Product announcements",
385
- saveButton: "Save Preferences",
386
- unsubscribeButton: "Unsubscribe",
387
- saving: "Saving...",
388
- saved: "Preferences saved successfully!",
389
- unsubscribeConfirm: "Are you sure you want to unsubscribe? This cannot be undone."
390
- }
391
- }) => {
392
- const [subscriber, setSubscriber] = useState2(initialSubscriber || {});
393
- const [loading, setLoading] = useState2(false);
394
- const [loadingData, setLoadingData] = useState2(!initialSubscriber);
395
- const [error, setError] = useState2(null);
396
- const [success, setSuccess] = useState2(false);
397
- const styles = {
398
- container: { ...defaultStyles2.container, ...customStyles.container },
399
- heading: { ...defaultStyles2.heading, ...customStyles.heading },
400
- form: { ...defaultStyles2.form, ...customStyles.form },
401
- section: { ...defaultStyles2.section, ...customStyles.section },
402
- sectionTitle: { ...defaultStyles2.sectionTitle, ...customStyles.sectionTitle },
403
- inputGroup: { ...defaultStyles2.inputGroup, ...customStyles.inputGroup },
404
- label: { ...defaultStyles2.label, ...customStyles.label },
405
- input: { ...defaultStyles2.input, ...customStyles.input },
406
- select: { ...defaultStyles2.select, ...customStyles.select },
407
- checkbox: { ...defaultStyles2.checkbox, ...customStyles.checkbox },
408
- checkboxInput: { ...defaultStyles2.checkboxInput, ...customStyles.checkboxInput },
409
- checkboxLabel: { ...defaultStyles2.checkboxLabel, ...customStyles.checkboxLabel },
410
- buttonGroup: { ...defaultStyles2.buttonGroup, ...customStyles.buttonGroup },
411
- button: { ...defaultStyles2.button, ...customStyles.button },
412
- primaryButton: { ...defaultStyles2.primaryButton, ...customStyles.primaryButton },
413
- secondaryButton: { ...defaultStyles2.secondaryButton, ...customStyles.secondaryButton },
414
- dangerButton: { ...defaultStyles2.dangerButton, ...customStyles.dangerButton },
415
- error: { ...defaultStyles2.error, ...customStyles.error },
416
- success: { ...defaultStyles2.success, ...customStyles.success },
417
- info: { ...defaultStyles2.info, ...customStyles.info }
418
- };
419
- useEffect(() => {
420
- if (!initialSubscriber && sessionToken) {
421
- fetchPreferences();
422
- }
423
- }, []);
424
- const fetchPreferences = async () => {
425
- try {
426
- const response = await fetch(apiEndpoint, {
427
- headers: {
428
- "Authorization": `Bearer ${sessionToken}`
429
- }
430
- });
431
- if (!response.ok) {
432
- throw new Error("Failed to load preferences");
433
- }
434
- const data = await response.json();
435
- setSubscriber(data.subscriber);
436
- } catch (err) {
437
- setError(err instanceof Error ? err.message : "Failed to load preferences");
438
- if (onError) {
439
- onError(err instanceof Error ? err : new Error("Failed to load preferences"));
440
- }
441
- } finally {
442
- setLoadingData(false);
443
- }
444
- };
445
- const handleSave = async (e) => {
446
- e.preventDefault();
447
- setError(null);
448
- setSuccess(false);
449
- setLoading(true);
450
- try {
451
- const response = await fetch(apiEndpoint, {
452
- method: "POST",
453
- headers: {
454
- "Content-Type": "application/json",
455
- "Authorization": `Bearer ${sessionToken}`
456
- },
457
- body: JSON.stringify({
458
- name: subscriber.name,
459
- locale: subscriber.locale,
460
- emailPreferences: subscriber.emailPreferences
461
- })
462
- });
463
- const data = await response.json();
464
- if (!response.ok) {
465
- throw new Error(data.error || "Failed to save preferences");
466
- }
467
- setSubscriber(data.subscriber);
468
- setSuccess(true);
469
- if (onSuccess) {
470
- onSuccess(data.subscriber);
471
- }
472
- } catch (err) {
473
- const errorMessage = err instanceof Error ? err.message : "An error occurred";
474
- setError(errorMessage);
475
- if (onError) {
476
- onError(new Error(errorMessage));
477
- }
478
- } finally {
479
- setLoading(false);
480
- }
481
- };
482
- const handleUnsubscribe = async () => {
483
- if (!window.confirm(labels.unsubscribeConfirm)) {
484
- return;
485
- }
486
- setLoading(true);
487
- setError(null);
488
- try {
489
- const response = await fetch("/api/newsletter/unsubscribe", {
490
- method: "POST",
491
- headers: {
492
- "Content-Type": "application/json",
493
- "Authorization": `Bearer ${sessionToken}`
494
- },
495
- body: JSON.stringify({
496
- email: subscriber.email
497
- })
498
- });
499
- if (!response.ok) {
500
- throw new Error("Failed to unsubscribe");
501
- }
502
- setSubscriber({ ...subscriber, subscriptionStatus: "unsubscribed" });
503
- if (onSuccess) {
504
- onSuccess({ ...subscriber, subscriptionStatus: "unsubscribed" });
505
- }
506
- } catch (err) {
507
- setError("Failed to unsubscribe. Please try again.");
508
- if (onError) {
509
- onError(err instanceof Error ? err : new Error("Failed to unsubscribe"));
510
- }
511
- } finally {
512
- setLoading(false);
513
- }
514
- };
515
- if (loadingData) {
516
- return /* @__PURE__ */ jsx2("div", { className, style: styles.container, children: /* @__PURE__ */ jsx2("p", { style: styles.info, children: "Loading preferences..." }) });
517
- }
518
- if (subscriber.subscriptionStatus === "unsubscribed") {
519
- return /* @__PURE__ */ jsxs2("div", { className, style: styles.container, children: [
520
- /* @__PURE__ */ jsx2("h2", { style: styles.heading, children: "Unsubscribed" }),
521
- /* @__PURE__ */ jsx2("p", { style: styles.info, children: "You have been unsubscribed from all emails. To resubscribe, please sign up again." })
522
- ] });
523
- }
524
- return /* @__PURE__ */ jsxs2("div", { className, style: styles.container, children: [
525
- /* @__PURE__ */ jsx2("h2", { style: styles.heading, children: labels.title }),
526
- /* @__PURE__ */ jsxs2("form", { onSubmit: handleSave, style: styles.form, children: [
527
- /* @__PURE__ */ jsxs2("div", { style: styles.section, children: [
528
- /* @__PURE__ */ jsx2("h3", { style: styles.sectionTitle, children: labels.personalInfo }),
529
- /* @__PURE__ */ jsxs2("div", { style: styles.inputGroup, children: [
530
- /* @__PURE__ */ jsx2("label", { htmlFor: "name", style: styles.label, children: labels.name }),
531
- /* @__PURE__ */ jsx2(
532
- "input",
533
- {
534
- id: "name",
535
- type: "text",
536
- value: subscriber.name || "",
537
- onChange: (e) => setSubscriber({ ...subscriber, name: e.target.value }),
538
- disabled: loading,
539
- style: styles.input
540
- }
541
- )
542
- ] }),
543
- locales.length > 1 && /* @__PURE__ */ jsxs2("div", { style: styles.inputGroup, children: [
544
- /* @__PURE__ */ jsx2("label", { htmlFor: "locale", style: styles.label, children: labels.language }),
545
- /* @__PURE__ */ jsx2(
546
- "select",
547
- {
548
- id: "locale",
549
- value: subscriber.locale || locales[0],
550
- onChange: (e) => setSubscriber({ ...subscriber, locale: e.target.value }),
551
- disabled: loading,
552
- style: styles.select,
553
- children: locales.map((locale) => /* @__PURE__ */ jsx2("option", { value: locale, children: locale.toUpperCase() }, locale))
554
- }
555
- )
556
- ] })
557
- ] }),
558
- /* @__PURE__ */ jsxs2("div", { style: styles.section, children: [
559
- /* @__PURE__ */ jsx2("h3", { style: styles.sectionTitle, children: labels.emailPreferences }),
560
- /* @__PURE__ */ jsxs2("div", { style: styles.checkbox, children: [
561
- /* @__PURE__ */ jsx2(
562
- "input",
563
- {
564
- id: "pref-newsletter",
565
- type: "checkbox",
566
- checked: subscriber.emailPreferences?.newsletter ?? true,
567
- onChange: (e) => setSubscriber({
568
- ...subscriber,
569
- emailPreferences: {
570
- ...subscriber.emailPreferences,
571
- newsletter: e.target.checked
572
- }
573
- }),
574
- disabled: loading,
575
- style: styles.checkboxInput
576
- }
577
- ),
578
- /* @__PURE__ */ jsx2("label", { htmlFor: "pref-newsletter", style: styles.checkboxLabel, children: labels.newsletter })
579
- ] }),
580
- /* @__PURE__ */ jsxs2("div", { style: styles.checkbox, children: [
581
- /* @__PURE__ */ jsx2(
582
- "input",
583
- {
584
- id: "pref-announcements",
585
- type: "checkbox",
586
- checked: subscriber.emailPreferences?.announcements ?? true,
587
- onChange: (e) => setSubscriber({
588
- ...subscriber,
589
- emailPreferences: {
590
- ...subscriber.emailPreferences,
591
- announcements: e.target.checked
592
- }
593
- }),
594
- disabled: loading,
595
- style: styles.checkboxInput
596
- }
597
- ),
598
- /* @__PURE__ */ jsx2("label", { htmlFor: "pref-announcements", style: styles.checkboxLabel, children: labels.announcements })
599
- ] })
600
- ] }),
601
- /* @__PURE__ */ jsxs2("div", { style: styles.buttonGroup, children: [
602
- /* @__PURE__ */ jsx2(
603
- "button",
604
- {
605
- type: "submit",
606
- disabled: loading,
607
- style: {
608
- ...styles.button,
609
- ...styles.primaryButton,
610
- ...loading && { opacity: 0.5, cursor: "not-allowed" }
611
- },
612
- children: loading ? labels.saving : labels.saveButton
613
- }
614
- ),
615
- showUnsubscribe && /* @__PURE__ */ jsx2(
616
- "button",
617
- {
618
- type: "button",
619
- onClick: handleUnsubscribe,
620
- disabled: loading,
621
- style: {
622
- ...styles.button,
623
- ...styles.dangerButton,
624
- ...loading && { opacity: 0.5, cursor: "not-allowed" }
625
- },
626
- children: labels.unsubscribeButton
627
- }
628
- )
629
- ] }),
630
- error && /* @__PURE__ */ jsx2("p", { style: styles.error, children: error }),
631
- success && /* @__PURE__ */ jsx2("p", { style: styles.success, children: labels.saved })
632
- ] })
633
- ] });
634
- };
635
- function createPreferencesForm(defaultProps) {
636
- return (props) => /* @__PURE__ */ jsx2(PreferencesForm, { ...defaultProps, ...props });
637
- }
638
-
639
- // src/components/MagicLinkVerify.tsx
640
- import { useState as useState3, useEffect as useEffect2 } from "react";
641
- import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
642
- var defaultStyles3 = {
643
- container: {
644
- maxWidth: "400px",
645
- margin: "4rem auto",
646
- padding: "2rem",
647
- textAlign: "center"
648
- },
649
- heading: {
650
- fontSize: "1.5rem",
651
- fontWeight: "600",
652
- marginBottom: "1rem",
653
- color: "#111827"
654
- },
655
- message: {
656
- fontSize: "1rem",
657
- color: "#6b7280",
658
- marginBottom: "1.5rem"
659
- },
660
- error: {
661
- fontSize: "1rem",
662
- color: "#ef4444",
663
- marginBottom: "1.5rem"
664
- },
665
- button: {
666
- padding: "0.75rem 1.5rem",
667
- fontSize: "1rem",
668
- fontWeight: "500",
669
- color: "#ffffff",
670
- backgroundColor: "#3b82f6",
671
- border: "none",
672
- borderRadius: "0.375rem",
673
- cursor: "pointer",
674
- transition: "background-color 0.2s"
675
- }
676
- };
677
- var MagicLinkVerify = ({
678
- token: propToken,
679
- onSuccess,
680
- onError,
681
- apiEndpoint = "/api/newsletter/verify-magic-link",
682
- className,
683
- styles: customStyles = {},
684
- labels = {
685
- verifying: "Verifying your magic link...",
686
- success: "Successfully verified! Redirecting...",
687
- error: "Failed to verify magic link",
688
- expired: "This magic link has expired. Please request a new one.",
689
- invalid: "This magic link is invalid. Please request a new one.",
690
- redirecting: "Redirecting to your preferences...",
691
- tryAgain: "Try Again"
692
- }
693
- }) => {
694
- const [status, setStatus] = useState3("verifying");
695
- const [error, setError] = useState3(null);
696
- const [_sessionToken, setSessionToken] = useState3(null);
697
- const styles = {
698
- container: { ...defaultStyles3.container, ...customStyles.container },
699
- heading: { ...defaultStyles3.heading, ...customStyles.heading },
700
- message: { ...defaultStyles3.message, ...customStyles.message },
701
- error: { ...defaultStyles3.error, ...customStyles.error },
702
- button: { ...defaultStyles3.button, ...customStyles.button }
703
- };
704
- useEffect2(() => {
705
- const token = propToken || new URLSearchParams(window.location.search).get("token");
706
- if (token) {
707
- verifyToken(token);
708
- } else {
709
- setStatus("error");
710
- setError(labels.invalid || "Invalid magic link");
711
- }
712
- }, [propToken]);
713
- const verifyToken = async (token) => {
714
- try {
715
- const response = await fetch(apiEndpoint, {
716
- method: "POST",
717
- headers: {
718
- "Content-Type": "application/json"
719
- },
720
- body: JSON.stringify({ token })
721
- });
722
- const data = await response.json();
723
- if (!response.ok) {
724
- if (data.error?.includes("expired")) {
725
- throw new Error(labels.expired);
726
- }
727
- throw new Error(data.error || labels.error);
728
- }
729
- setStatus("success");
730
- setSessionToken(data.sessionToken);
731
- if (typeof window !== "undefined" && data.sessionToken) {
732
- localStorage.setItem("newsletter_session", data.sessionToken);
733
- }
734
- if (onSuccess) {
735
- onSuccess(data.sessionToken, data.subscriber);
736
- }
737
- } catch (err) {
738
- setStatus("error");
739
- const errorMessage = err instanceof Error ? err.message : labels.error || "Verification failed";
740
- setError(errorMessage);
741
- if (onError) {
742
- onError(err instanceof Error ? err : new Error(errorMessage));
743
- }
744
- }
745
- };
746
- const handleTryAgain = () => {
747
- window.location.href = "/";
748
- };
749
- return /* @__PURE__ */ jsxs3("div", { className, style: styles.container, children: [
750
- status === "verifying" && /* @__PURE__ */ jsxs3(Fragment, { children: [
751
- /* @__PURE__ */ jsx3("h2", { style: styles.heading, children: "Verifying" }),
752
- /* @__PURE__ */ jsx3("p", { style: styles.message, children: labels.verifying })
753
- ] }),
754
- status === "success" && /* @__PURE__ */ jsxs3(Fragment, { children: [
755
- /* @__PURE__ */ jsx3("h2", { style: styles.heading, children: "Success!" }),
756
- /* @__PURE__ */ jsx3("p", { style: styles.message, children: labels.success })
757
- ] }),
758
- status === "error" && /* @__PURE__ */ jsxs3(Fragment, { children: [
759
- /* @__PURE__ */ jsx3("h2", { style: styles.heading, children: "Verification Failed" }),
760
- /* @__PURE__ */ jsx3("p", { style: styles.error, children: error }),
761
- /* @__PURE__ */ jsx3("button", { onClick: handleTryAgain, style: styles.button, children: labels.tryAgain })
762
- ] })
763
- ] });
764
- };
765
- function createMagicLinkVerify(defaultProps) {
766
- return (props) => /* @__PURE__ */ jsx3(MagicLinkVerify, { ...defaultProps, ...props });
767
- }
768
-
769
- // src/hooks/useNewsletterAuth.ts
770
- import { useState as useState4, useEffect as useEffect3, useCallback } from "react";
771
- function useNewsletterAuth(_options = {}) {
772
- const [subscriber, setSubscriber] = useState4(null);
773
- const [isLoading, setIsLoading] = useState4(true);
774
- const [error, setError] = useState4(null);
775
- const checkAuth = useCallback(async () => {
776
- try {
777
- const response = await fetch("/api/newsletter/me", {
778
- method: "GET",
779
- credentials: "include",
780
- headers: {
781
- "Content-Type": "application/json"
782
- }
783
- });
784
- if (response.ok) {
785
- const data = await response.json();
786
- setSubscriber(data.subscriber);
787
- setError(null);
788
- } else {
789
- setSubscriber(null);
790
- if (response.status !== 401) {
791
- setError(new Error("Failed to check authentication"));
792
- }
793
- }
794
- } catch (err) {
795
- console.error("Auth check failed:", err);
796
- setError(err instanceof Error ? err : new Error("An error occurred"));
797
- setSubscriber(null);
798
- } finally {
799
- setIsLoading(false);
800
- }
801
- }, []);
802
- useEffect3(() => {
803
- checkAuth();
804
- }, [checkAuth]);
805
- const signOut = useCallback(async () => {
806
- try {
807
- const response = await fetch("/api/newsletter/signout", {
808
- method: "POST",
809
- credentials: "include",
810
- headers: {
811
- "Content-Type": "application/json"
812
- }
813
- });
814
- if (response.ok) {
815
- setSubscriber(null);
816
- setError(null);
817
- } else {
818
- throw new Error("Failed to sign out");
819
- }
820
- } catch (err) {
821
- console.error("Sign out error:", err);
822
- setError(err instanceof Error ? err : new Error("Sign out failed"));
823
- throw err;
824
- }
825
- }, []);
826
- const refreshAuth = useCallback(async () => {
827
- setIsLoading(true);
828
- await checkAuth();
829
- }, [checkAuth]);
830
- const login = useCallback(async (_token) => {
831
- await refreshAuth();
832
- }, [refreshAuth]);
833
- return {
834
- subscriber,
835
- isAuthenticated: !!subscriber,
836
- isLoading,
837
- loading: isLoading,
838
- // Alias for backward compatibility
839
- error,
840
- signOut,
841
- logout: signOut,
842
- // Alias for backward compatibility
843
- refreshAuth,
844
- refreshSubscriber: refreshAuth,
845
- // Alias for backward compatibility
846
- login
847
- // For backward compatibility
848
- };
849
- }
850
-
851
- // src/components/Broadcasts/EmailPreview.tsx
852
- import { useState as useState5, useEffect as useEffect4, useRef } from "react";
853
-
854
- // src/utils/emailSafeHtml.ts
855
- import DOMPurify from "isomorphic-dompurify";
856
- var EMAIL_SAFE_CONFIG = {
857
- ALLOWED_TAGS: [
858
- "p",
859
- "br",
860
- "strong",
861
- "b",
862
- "em",
863
- "i",
864
- "u",
865
- "strike",
866
- "s",
867
- "span",
868
- "a",
869
- "h1",
870
- "h2",
871
- "h3",
872
- "ul",
873
- "ol",
874
- "li",
875
- "blockquote",
876
- "hr",
877
- "img",
878
- "div",
879
- "table",
880
- "tr",
881
- "td",
882
- "th",
883
- "tbody",
884
- "thead"
885
- ],
886
- ALLOWED_ATTR: ["href", "style", "target", "rel", "align", "src", "alt", "width", "height", "border", "cellpadding", "cellspacing"],
887
- ALLOWED_STYLES: {
888
- "*": [
889
- "color",
890
- "background-color",
891
- "font-size",
892
- "font-weight",
893
- "font-style",
894
- "text-decoration",
895
- "text-align",
896
- "margin",
897
- "margin-top",
898
- "margin-right",
899
- "margin-bottom",
900
- "margin-left",
901
- "padding",
902
- "padding-top",
903
- "padding-right",
904
- "padding-bottom",
905
- "padding-left",
906
- "line-height",
907
- "border-left",
908
- "border-left-width",
909
- "border-left-style",
910
- "border-left-color"
911
- ]
912
- },
913
- FORBID_TAGS: ["script", "style", "iframe", "object", "embed", "form", "input"],
914
- FORBID_ATTR: ["class", "id", "onclick", "onload", "onerror"]
915
- };
916
- async function convertToEmailSafeHtml(editorState, options) {
917
- if (!editorState) {
918
- return "";
919
- }
920
- const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter);
921
- const sanitizedHtml = DOMPurify.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
922
- if (options?.wrapInTemplate) {
923
- if (options.customWrapper) {
924
- return await Promise.resolve(options.customWrapper(sanitizedHtml, {
925
- preheader: options.preheader,
926
- subject: options.subject
927
- }));
928
- }
929
- return wrapInEmailTemplate(sanitizedHtml, options.preheader);
930
- }
931
- return sanitizedHtml;
932
- }
933
- async function lexicalToEmailHtml(editorState, mediaUrl, customBlockConverter) {
934
- const { root } = editorState;
935
- if (!root || !root.children) {
936
- return "";
937
- }
938
- const htmlParts = await Promise.all(
939
- root.children.map((node) => convertNode(node, mediaUrl, customBlockConverter))
940
- );
941
- return htmlParts.join("");
942
- }
943
- async function convertNode(node, mediaUrl, customBlockConverter) {
944
- switch (node.type) {
945
- case "paragraph":
946
- return convertParagraph(node, mediaUrl, customBlockConverter);
947
- case "heading":
948
- return convertHeading(node, mediaUrl, customBlockConverter);
949
- case "list":
950
- return convertList(node, mediaUrl, customBlockConverter);
951
- case "listitem":
952
- return convertListItem(node, mediaUrl, customBlockConverter);
953
- case "blockquote":
954
- return convertBlockquote(node, mediaUrl, customBlockConverter);
955
- case "text":
956
- return convertText(node);
957
- case "link":
958
- return convertLink(node, mediaUrl, customBlockConverter);
959
- case "linebreak":
960
- return "<br>";
961
- case "upload":
962
- return convertUpload(node, mediaUrl);
963
- case "block":
964
- return await convertBlock(node, mediaUrl, customBlockConverter);
965
- default:
966
- if (node.children) {
967
- const childParts = await Promise.all(
968
- node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
969
- );
970
- return childParts.join("");
971
- }
972
- return "";
973
- }
974
- }
975
- async function convertParagraph(node, mediaUrl, customBlockConverter) {
976
- const align = getAlignment(node.format);
977
- const childParts = await Promise.all(
978
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
979
- );
980
- const children = childParts.join("");
981
- if (!children.trim()) {
982
- return '<p class="mobile-margin-bottom-16" style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
983
- }
984
- 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>`;
985
- }
986
- async function convertHeading(node, mediaUrl, customBlockConverter) {
987
- const tag = node.tag || "h1";
988
- const align = getAlignment(node.format);
989
- const childParts = await Promise.all(
990
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
991
- );
992
- const children = childParts.join("");
993
- const styles = {
994
- h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
995
- h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
996
- h3: "font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;"
997
- };
998
- const mobileClasses = {
999
- h1: "mobile-font-size-24",
1000
- h2: "mobile-font-size-20",
1001
- h3: "mobile-font-size-16"
1002
- };
1003
- const style = `${styles[tag] || styles.h3} text-align: ${align};`;
1004
- const mobileClass = mobileClasses[tag] || mobileClasses.h3;
1005
- return `<${tag} class="${mobileClass}" style="${style}">${children}</${tag}>`;
1006
- }
1007
- async function convertList(node, mediaUrl, customBlockConverter) {
1008
- const tag = node.listType === "number" ? "ol" : "ul";
1009
- const childParts = await Promise.all(
1010
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1011
- );
1012
- const children = childParts.join("");
1013
- 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;";
1014
- return `<${tag} class="mobile-margin-bottom-16" style="${style}">${children}</${tag}>`;
1015
- }
1016
- async function convertListItem(node, mediaUrl, customBlockConverter) {
1017
- const childParts = await Promise.all(
1018
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1019
- );
1020
- const children = childParts.join("");
1021
- return `<li style="margin: 0 0 8px 0;">${children}</li>`;
1022
- }
1023
- async function convertBlockquote(node, mediaUrl, customBlockConverter) {
1024
- const childParts = await Promise.all(
1025
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1026
- );
1027
- const children = childParts.join("");
1028
- const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
1029
- return `<blockquote style="${style}">${children}</blockquote>`;
1030
- }
1031
- function convertText(node) {
1032
- let text = escapeHtml(node.text || "");
1033
- if (node.format & 1) {
1034
- text = `<strong>${text}</strong>`;
1035
- }
1036
- if (node.format & 2) {
1037
- text = `<em>${text}</em>`;
1038
- }
1039
- if (node.format & 8) {
1040
- text = `<u>${text}</u>`;
1041
- }
1042
- if (node.format & 4) {
1043
- text = `<strike>${text}</strike>`;
1044
- }
1045
- return text;
1046
- }
1047
- async function convertLink(node, mediaUrl, customBlockConverter) {
1048
- const childParts = await Promise.all(
1049
- (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1050
- );
1051
- const children = childParts.join("");
1052
- const url = node.fields?.url || "#";
1053
- const newTab = node.fields?.newTab ?? false;
1054
- const targetAttr = newTab ? ' target="_blank"' : "";
1055
- const relAttr = newTab ? ' rel="noopener noreferrer"' : "";
1056
- return `<a href="${escapeHtml(url)}"${targetAttr}${relAttr} style="color: #2563eb; text-decoration: underline;">${children}</a>`;
1057
- }
1058
- function convertUpload(node, mediaUrl) {
1059
- const upload = node.value;
1060
- if (!upload) return "";
1061
- let src = "";
1062
- if (typeof upload === "string") {
1063
- src = upload;
1064
- } else if (upload.url) {
1065
- src = upload.url;
1066
- } else if (upload.filename && mediaUrl) {
1067
- src = `${mediaUrl}/${upload.filename}`;
1068
- }
1069
- const alt = node.fields?.altText || upload.alt || "";
1070
- const caption = node.fields?.caption || "";
1071
- 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;" />`;
1072
- if (caption) {
1073
- return `
1074
- <div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">
1075
- ${imgHtml}
1076
- <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>
1077
- </div>
1078
- `;
1079
- }
1080
- return `<div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">${imgHtml}</div>`;
1081
- }
1082
- async function convertBlock(node, mediaUrl, customBlockConverter) {
1083
- const blockType = node.fields?.blockName || node.blockName;
1084
- if (customBlockConverter) {
1085
- try {
1086
- const customHtml = await customBlockConverter(node, mediaUrl);
1087
- if (customHtml) {
1088
- return customHtml;
1089
- }
1090
- } catch (error) {
1091
- console.error(`Custom block converter error for ${blockType}:`, error);
1092
- }
1093
- }
1094
- switch (blockType) {
1095
- case "button":
1096
- return convertButtonBlock(node.fields);
1097
- case "divider":
1098
- return convertDividerBlock(node.fields);
1099
- default:
1100
- if (node.children) {
1101
- const childParts = await Promise.all(
1102
- node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
1103
- );
1104
- return childParts.join("");
1105
- }
1106
- return "";
1107
- }
1108
- }
1109
- function convertButtonBlock(fields) {
1110
- const text = fields?.text || "Click here";
1111
- const url = fields?.url || "#";
1112
- const style = fields?.style || "primary";
1113
- const styles = {
1114
- primary: "background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;",
1115
- secondary: "background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;",
1116
- outline: "background-color: transparent; color: #2563eb; border: 2px solid #2563eb;"
1117
- };
1118
- 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;`;
1119
- return `
1120
- <div style="margin: 0 0 16px 0; text-align: center;">
1121
- <a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
1122
- </div>
1123
- `;
1124
- }
1125
- function convertDividerBlock(fields) {
1126
- const style = fields?.style || "solid";
1127
- const styles = {
1128
- solid: "border-top: 1px solid #e5e7eb;",
1129
- dashed: "border-top: 1px dashed #e5e7eb;",
1130
- dotted: "border-top: 1px dotted #e5e7eb;"
1131
- };
1132
- return `<hr style="${styles[style] || styles.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
1133
- }
1134
- function getAlignment(format) {
1135
- if (!format) return "left";
1136
- if (format & 2) return "center";
1137
- if (format & 3) return "right";
1138
- if (format & 4) return "justify";
1139
- return "left";
1140
- }
1141
- function escapeHtml(text) {
1142
- const map = {
1143
- "&": "&amp;",
1144
- "<": "&lt;",
1145
- ">": "&gt;",
1146
- '"': "&quot;",
1147
- "'": "&#039;"
1148
- };
1149
- return text.replace(/[&<>"']/g, (m) => map[m]);
1150
- }
1151
- function wrapInEmailTemplate(content, preheader) {
1152
- return `<!DOCTYPE html>
1153
- <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">
1154
- <head>
1155
- <meta charset="UTF-8">
1156
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1157
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
1158
- <meta name="x-apple-disable-message-reformatting">
1159
- <title>Newsletter</title>
1160
-
1161
- <!--[if mso]>
1162
- <noscript>
1163
- <xml>
1164
- <o:OfficeDocumentSettings>
1165
- <o:PixelsPerInch>96</o:PixelsPerInch>
1166
- </o:OfficeDocumentSettings>
1167
- </xml>
1168
- </noscript>
1169
- <![endif]-->
1170
-
1171
- <style>
1172
- /* Reset and base styles */
1173
- * {
1174
- -webkit-text-size-adjust: 100%;
1175
- -ms-text-size-adjust: 100%;
1176
- }
1177
-
1178
- body {
1179
- margin: 0 !important;
1180
- padding: 0 !important;
1181
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
1182
- font-size: 16px;
1183
- line-height: 1.5;
1184
- color: #1A1A1A;
1185
- background-color: #f8f9fa;
1186
- -webkit-font-smoothing: antialiased;
1187
- -moz-osx-font-smoothing: grayscale;
1188
- }
1189
-
1190
- table {
1191
- border-spacing: 0 !important;
1192
- border-collapse: collapse !important;
1193
- table-layout: fixed !important;
1194
- margin: 0 auto !important;
1195
- }
1196
-
1197
- table table table {
1198
- table-layout: auto;
1199
- }
1200
-
1201
- img {
1202
- -ms-interpolation-mode: bicubic;
1203
- max-width: 100%;
1204
- height: auto;
1205
- border: 0;
1206
- outline: none;
1207
- text-decoration: none;
1208
- }
1209
-
1210
- /* Responsive styles */
1211
- @media only screen and (max-width: 640px) {
1212
- .mobile-hide {
1213
- display: none !important;
1214
- }
1215
-
1216
- .mobile-center {
1217
- text-align: center !important;
1218
- }
1219
-
1220
- .mobile-width-100 {
1221
- width: 100% !important;
1222
- max-width: 100% !important;
1223
- }
1224
-
1225
- .mobile-padding {
1226
- padding: 20px !important;
1227
- }
1228
-
1229
- .mobile-padding-sm {
1230
- padding: 16px !important;
1231
- }
1232
-
1233
- .mobile-font-size-14 {
1234
- font-size: 14px !important;
1235
- }
1236
-
1237
- .mobile-font-size-16 {
1238
- font-size: 16px !important;
1239
- }
1240
-
1241
- .mobile-font-size-20 {
1242
- font-size: 20px !important;
1243
- line-height: 1.3 !important;
1244
- }
1245
-
1246
- .mobile-font-size-24 {
1247
- font-size: 24px !important;
1248
- line-height: 1.2 !important;
1249
- }
1250
-
1251
- /* Stack sections on mobile */
1252
- .mobile-stack {
1253
- display: block !important;
1254
- width: 100% !important;
1255
- }
1256
-
1257
- /* Mobile-specific spacing */
1258
- .mobile-margin-bottom-16 {
1259
- margin-bottom: 16px !important;
1260
- }
1261
-
1262
- .mobile-margin-bottom-20 {
1263
- margin-bottom: 20px !important;
1264
- }
1265
- }
1266
-
1267
- /* Dark mode support */
1268
- @media (prefers-color-scheme: dark) {
1269
- .dark-mode-bg {
1270
- background-color: #1a1a1a !important;
1271
- }
1272
-
1273
- .dark-mode-text {
1274
- color: #ffffff !important;
1275
- }
1276
-
1277
- .dark-mode-border {
1278
- border-color: #333333 !important;
1279
- }
1280
- }
1281
-
1282
- /* Outlook-specific fixes */
1283
- <!--[if mso]>
1284
- <style>
1285
- table {
1286
- border-collapse: collapse;
1287
- border-spacing: 0;
1288
- border: none;
1289
- margin: 0;
1290
- }
1291
-
1292
- div, p {
1293
- margin: 0;
1294
- }
1295
- </style>
1296
- <![endif]-->
1297
- </style>
1298
- </head>
1299
- <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;">
1300
- ${preheader ? `
1301
- <!-- Preheader text -->
1302
- <div style="display: none; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: transparent;">
1303
- ${escapeHtml(preheader)}
1304
- </div>
1305
- ` : ""}
1306
-
1307
- <!-- Main container -->
1308
- <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0; background-color: #f8f9fa;">
1309
- <tr>
1310
- <td align="center" style="padding: 20px 10px;">
1311
- <!-- Email wrapper -->
1312
- <table role="presentation" cellpadding="0" cellspacing="0" width="600" class="mobile-width-100" style="margin: 0 auto; max-width: 600px;">
1313
- <tr>
1314
- <td class="mobile-padding" style="padding: 0;">
1315
- <!-- Content area with light background -->
1316
- <div style="background-color: #ffffff; padding: 40px 30px; border-radius: 8px;" class="mobile-padding">
1317
- ${content}
1318
- </div>
1319
- </td>
1320
- </tr>
1321
- </table>
1322
- </td>
1323
- </tr>
1324
- </table>
1325
- </body>
1326
- </html>`;
1327
- }
1328
- function replacePersonalizationTags(html, sampleData) {
1329
- return html.replace(/\{\{([^}]+)\}\}/g, (match, tag) => {
1330
- const trimmedTag = tag.trim();
1331
- return sampleData[trimmedTag] || match;
1332
- });
1333
- }
1334
-
1335
- // src/utils/validateEmailHtml.ts
1336
- function validateEmailHtml(html) {
1337
- const warnings = [];
1338
- const errors = [];
1339
- const sizeInBytes = new Blob([html]).size;
1340
- if (sizeInBytes > 102400) {
1341
- warnings.push(`Email size (${Math.round(sizeInBytes / 1024)}KB) exceeds Gmail's 102KB limit - email may be clipped`);
1342
- }
1343
- if (html.includes("position:") && (html.includes("position: absolute") || html.includes("position: fixed"))) {
1344
- errors.push("Absolute/fixed positioning is not supported in most email clients");
1345
- }
1346
- if (html.includes("display: flex") || html.includes("display: grid")) {
1347
- errors.push("Flexbox and Grid layouts are not supported in many email clients");
1348
- }
1349
- if (html.includes("@media")) {
1350
- warnings.push("Media queries may not work in all email clients");
1351
- }
1352
- const hasJavaScript = html.includes("<script") || html.includes("onclick") || html.includes("onload") || html.includes("javascript:");
1353
- if (hasJavaScript) {
1354
- errors.push("JavaScript is not supported in email and will be stripped by email clients");
1355
- }
1356
- const hasExternalStyles = html.includes("<link") && html.includes("stylesheet");
1357
- if (hasExternalStyles) {
1358
- errors.push("External stylesheets are not supported - use inline styles only");
1359
- }
1360
- if (html.includes("<form") || html.includes("<input") || html.includes("<button")) {
1361
- errors.push("Forms and form elements are not reliably supported in email");
1362
- }
1363
- const unsupportedTags = [
1364
- "video",
1365
- "audio",
1366
- "iframe",
1367
- "embed",
1368
- "object",
1369
- "canvas",
1370
- "svg"
1371
- ];
1372
- for (const tag of unsupportedTags) {
1373
- if (html.includes(`<${tag}`)) {
1374
- errors.push(`<${tag}> tags are not supported in email`);
1375
- }
1376
- }
1377
- const imageCount = (html.match(/<img/g) || []).length;
1378
- const linkCount = (html.match(/<a/g) || []).length;
1379
- if (imageCount > 20) {
1380
- warnings.push(`High number of images (${imageCount}) may affect email performance`);
1381
- }
1382
- const imagesWithoutAlt = (html.match(/<img(?![^>]*\balt\s*=)[^>]*>/g) || []).length;
1383
- if (imagesWithoutAlt > 0) {
1384
- warnings.push(`${imagesWithoutAlt} image(s) missing alt text - important for accessibility`);
1385
- }
1386
- const linksWithoutTarget = (html.match(/<a(?![^>]*\btarget\s*=)[^>]*>/g) || []).length;
1387
- if (linksWithoutTarget > 0) {
1388
- warnings.push(`${linksWithoutTarget} link(s) missing target="_blank" attribute`);
1389
- }
1390
- if (html.includes("margin: auto") || html.includes("margin:auto")) {
1391
- warnings.push('margin: auto is not supported in Outlook - use align="center" or tables for centering');
1392
- }
1393
- if (html.includes("background-image")) {
1394
- warnings.push("Background images are not reliably supported - consider using <img> tags instead");
1395
- }
1396
- if (html.match(/\d+\s*(rem|em)/)) {
1397
- warnings.push("rem/em units may render inconsistently - use px for reliable sizing");
1398
- }
1399
- if (html.match(/margin[^:]*:\s*-\d+/)) {
1400
- errors.push("Negative margins are not supported in many email clients");
1401
- }
1402
- const personalizationTags = html.match(/\{\{([^}]+)\}\}/g) || [];
1403
- const validTags = ["subscriber.name", "subscriber.email", "subscriber.firstName", "subscriber.lastName"];
1404
- for (const tag of personalizationTags) {
1405
- const tagContent = tag.replace(/[{}]/g, "").trim();
1406
- if (!validTags.includes(tagContent)) {
1407
- warnings.push(`Unknown personalization tag: ${tag}`);
1408
- }
1409
- }
1410
- return {
1411
- valid: errors.length === 0,
1412
- warnings,
1413
- errors,
1414
- stats: {
1415
- sizeInBytes,
1416
- imageCount,
1417
- linkCount,
1418
- hasExternalStyles,
1419
- hasJavaScript
1420
- }
1421
- };
1422
- }
1423
-
1424
- // src/contexts/PluginConfigContext.tsx
1425
- import { createContext, useContext } from "react";
1426
- import { jsx as jsx4 } from "react/jsx-runtime";
1427
- var PluginConfigContext = createContext(null);
1428
- var usePluginConfigOptional = () => {
1429
- return useContext(PluginConfigContext);
1430
- };
1431
-
1432
- // src/components/Broadcasts/EmailPreview.tsx
1433
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1434
- var SAMPLE_DATA = {
1435
- "subscriber.name": "John Doe",
1436
- "subscriber.firstName": "John",
1437
- "subscriber.lastName": "Doe",
1438
- "subscriber.email": "john.doe@example.com"
1439
- };
1440
- var VIEWPORT_SIZES = {
1441
- desktop: { width: 600, scale: 1 },
1442
- mobile: { width: 320, scale: 0.8 }
1443
- };
1444
- var EmailPreview = ({
1445
- content,
1446
- subject,
1447
- preheader,
1448
- mode = "desktop",
1449
- onValidation,
1450
- pluginConfig: propPluginConfig
1451
- }) => {
1452
- const contextPluginConfig = usePluginConfigOptional();
1453
- const pluginConfig = propPluginConfig || contextPluginConfig;
1454
- const [html, setHtml] = useState5("");
1455
- const [loading, setLoading] = useState5(false);
1456
- const [validationResult, setValidationResult] = useState5(null);
1457
- const iframeRef = useRef(null);
1458
- useEffect4(() => {
1459
- const convertContent = async () => {
1460
- if (!content) {
1461
- setHtml("");
1462
- return;
1463
- }
1464
- setLoading(true);
1465
- try {
1466
- const emailPreviewConfig = pluginConfig?.customizations?.broadcasts?.emailPreview;
1467
- const emailHtml = await convertToEmailSafeHtml(content, {
1468
- wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
1469
- preheader,
1470
- subject,
1471
- customWrapper: emailPreviewConfig?.customWrapper,
1472
- customBlockConverter: pluginConfig?.customizations?.broadcasts?.customBlockConverter
1473
- });
1474
- const personalizedHtml = replacePersonalizationTags(emailHtml, SAMPLE_DATA);
1475
- const previewHtml = addEmailHeader(personalizedHtml, {
1476
- subject,
1477
- from: "Newsletter <noreply@example.com>",
1478
- to: SAMPLE_DATA["subscriber.email"]
1479
- });
1480
- setHtml(previewHtml);
1481
- const validation = validateEmailHtml(emailHtml);
1482
- setValidationResult(validation);
1483
- onValidation?.(validation);
1484
- } catch (error) {
1485
- console.error("Failed to convert content to HTML:", error);
1486
- setHtml("<p>Error converting content to HTML</p>");
1487
- } finally {
1488
- setLoading(false);
1489
- }
1490
- };
1491
- convertContent();
1492
- }, [content, subject, preheader, onValidation, pluginConfig]);
1493
- useEffect4(() => {
1494
- if (iframeRef.current && html) {
1495
- const doc = iframeRef.current.contentDocument;
1496
- if (doc) {
1497
- doc.open();
1498
- doc.write(html);
1499
- doc.close();
1500
- }
1501
- }
1502
- }, [html]);
1503
- const viewport = VIEWPORT_SIZES[mode];
1504
- return /* @__PURE__ */ jsxs4("div", { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
1505
- validationResult && (validationResult.errors.length > 0 || validationResult.warnings.length > 0) && /* @__PURE__ */ jsxs4("div", { style: { padding: "16px", borderBottom: "1px solid #e5e7eb" }, children: [
1506
- validationResult.errors.length > 0 && /* @__PURE__ */ jsxs4("div", { style: { marginBottom: "12px" }, children: [
1507
- /* @__PURE__ */ jsxs4("h4", { style: { color: "#dc2626", margin: "0 0 8px 0", fontSize: "14px" }, children: [
1508
- "Errors (",
1509
- validationResult.errors.length,
1510
- ")"
1511
- ] }),
1512
- /* @__PURE__ */ jsx5("ul", { style: { margin: 0, paddingLeft: "20px", fontSize: "13px", color: "#dc2626" }, children: validationResult.errors.map((error, index) => /* @__PURE__ */ jsx5("li", { children: error }, index)) })
1513
- ] }),
1514
- validationResult.warnings.length > 0 && /* @__PURE__ */ jsxs4("div", { children: [
1515
- /* @__PURE__ */ jsxs4("h4", { style: { color: "#d97706", margin: "0 0 8px 0", fontSize: "14px" }, children: [
1516
- "Warnings (",
1517
- validationResult.warnings.length,
1518
- ")"
1519
- ] }),
1520
- /* @__PURE__ */ jsx5("ul", { style: { margin: 0, paddingLeft: "20px", fontSize: "13px", color: "#d97706" }, children: validationResult.warnings.map((warning, index) => /* @__PURE__ */ jsx5("li", { children: warning }, index)) })
1521
- ] })
1522
- ] }),
1523
- /* @__PURE__ */ jsx5("div", { style: {
1524
- flex: 1,
1525
- display: "flex",
1526
- alignItems: "center",
1527
- justifyContent: "center",
1528
- backgroundColor: "#f3f4f6",
1529
- padding: "20px",
1530
- overflow: "auto"
1531
- }, children: loading ? /* @__PURE__ */ jsx5("div", { style: { textAlign: "center", color: "#6b7280" }, children: /* @__PURE__ */ jsx5("p", { children: "Loading preview..." }) }) : html ? /* @__PURE__ */ jsx5("div", { style: {
1532
- backgroundColor: "white",
1533
- boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
1534
- borderRadius: "8px",
1535
- overflow: "hidden",
1536
- transform: `scale(${viewport.scale})`,
1537
- transformOrigin: "top center"
1538
- }, children: /* @__PURE__ */ jsx5(
1539
- "iframe",
1540
- {
1541
- ref: iframeRef,
1542
- title: "Email Preview",
1543
- style: {
1544
- width: `${viewport.width}px`,
1545
- height: "800px",
1546
- border: "none",
1547
- display: "block"
1548
- },
1549
- sandbox: "allow-same-origin"
1550
- }
1551
- ) }) : /* @__PURE__ */ jsx5("div", { style: { textAlign: "center", color: "#6b7280" }, children: /* @__PURE__ */ jsx5("p", { children: "Start typing to see the email preview" }) }) }),
1552
- validationResult && /* @__PURE__ */ jsxs4("div", { style: {
1553
- padding: "12px 16px",
1554
- borderTop: "1px solid #e5e7eb",
1555
- fontSize: "13px",
1556
- color: "#6b7280",
1557
- display: "flex",
1558
- gap: "24px"
1559
- }, children: [
1560
- /* @__PURE__ */ jsxs4("span", { children: [
1561
- "Size: ",
1562
- Math.round(validationResult.stats.sizeInBytes / 1024),
1563
- "KB"
1564
- ] }),
1565
- /* @__PURE__ */ jsxs4("span", { children: [
1566
- "Links: ",
1567
- validationResult.stats.linkCount
1568
- ] }),
1569
- /* @__PURE__ */ jsxs4("span", { children: [
1570
- "Images: ",
1571
- validationResult.stats.imageCount
1572
- ] }),
1573
- /* @__PURE__ */ jsxs4("span", { children: [
1574
- "Viewport: ",
1575
- mode === "desktop" ? "600px" : "320px"
1576
- ] })
1577
- ] })
1578
- ] });
1579
- };
1580
- function addEmailHeader(html, headers) {
1581
- const headerHtml = `
1582
- <div style="background-color: #f9fafb; border-bottom: 1px solid #e5e7eb; padding: 16px; font-family: monospace; font-size: 13px;">
1583
- <div style="margin-bottom: 8px;"><strong>Subject:</strong> ${escapeHtml2(headers.subject)}</div>
1584
- <div style="margin-bottom: 8px;"><strong>From:</strong> ${escapeHtml2(headers.from)}</div>
1585
- <div><strong>To:</strong> ${escapeHtml2(headers.to)}</div>
1586
- </div>
1587
- `;
1588
- return html.replace(/<body[^>]*>/, `$&${headerHtml}`);
1589
- }
1590
- function escapeHtml2(text) {
1591
- const div = document.createElement("div");
1592
- div.textContent = text;
1593
- return div.innerHTML;
1594
- }
1595
-
1596
- // src/components/Broadcasts/EmailPreviewField.tsx
1597
- import { useState as useState6 } from "react";
1598
- import { useFormFields } from "@payloadcms/ui";
1599
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1600
- var EmailPreviewField = () => {
1601
- const [previewMode, setPreviewMode] = useState6("desktop");
1602
- const [isValid, setIsValid] = useState6(true);
1603
- const [validationSummary, setValidationSummary] = useState6("");
1604
- const pluginConfig = usePluginConfigOptional();
1605
- const fields = useFormFields(([fields2]) => ({
1606
- content: fields2["contentSection.content"],
1607
- subject: fields2["subject"],
1608
- preheader: fields2["contentSection.preheader"],
1609
- channel: fields2.channel
1610
- }));
1611
- const handleValidation = (result) => {
1612
- setIsValid(result.valid);
1613
- const errorCount = result.errors.length;
1614
- const warningCount = result.warnings.length;
1615
- if (errorCount > 0) {
1616
- setValidationSummary(`${errorCount} error${errorCount !== 1 ? "s" : ""}, ${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
1617
- } else if (warningCount > 0) {
1618
- setValidationSummary(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
1619
- } else {
1620
- setValidationSummary("");
1621
- }
1622
- };
1623
- const handleTestEmail = async () => {
1624
- const pathParts = window.location.pathname.split("/");
1625
- const broadcastId = pathParts[pathParts.length - 1];
1626
- if (!broadcastId || broadcastId === "create") {
1627
- alert("Please save the broadcast before sending a test email");
1628
- return;
1629
- }
1630
- try {
1631
- const response = await fetch(`/api/broadcasts/${broadcastId}/test`, {
1632
- method: "POST",
1633
- headers: {
1634
- "Content-Type": "application/json"
1635
- }
1636
- });
1637
- if (!response.ok) {
1638
- const data = await response.json();
1639
- throw new Error(data.error || "Failed to send test email");
1640
- }
1641
- alert("Test email sent successfully! Check your inbox.");
1642
- } catch (error) {
1643
- alert(error instanceof Error ? error.message : "Failed to send test email");
1644
- }
1645
- };
1646
- return /* @__PURE__ */ jsxs5("div", { style: {
1647
- marginTop: "24px",
1648
- border: "1px solid #e5e7eb",
1649
- borderRadius: "8px",
1650
- overflow: "hidden"
1651
- }, children: [
1652
- /* @__PURE__ */ jsxs5("div", { style: {
1653
- display: "flex",
1654
- alignItems: "center",
1655
- justifyContent: "space-between",
1656
- padding: "12px 16px",
1657
- borderBottom: "1px solid #e5e7eb",
1658
- backgroundColor: "#f9fafb"
1659
- }, children: [
1660
- /* @__PURE__ */ jsxs5("div", { style: { display: "flex", alignItems: "center", gap: "16px" }, children: [
1661
- /* @__PURE__ */ jsx6("h3", { style: { margin: 0, fontSize: "16px", fontWeight: 600 }, children: "Email Preview" }),
1662
- /* @__PURE__ */ jsxs5("div", { style: { display: "flex", gap: "8px" }, children: [
1663
- /* @__PURE__ */ jsx6(
1664
- "button",
1665
- {
1666
- type: "button",
1667
- onClick: () => setPreviewMode("desktop"),
1668
- style: {
1669
- padding: "6px 12px",
1670
- backgroundColor: previewMode === "desktop" ? "#6366f1" : "#e5e7eb",
1671
- color: previewMode === "desktop" ? "white" : "#374151",
1672
- border: "none",
1673
- borderRadius: "4px 0 0 4px",
1674
- fontSize: "14px",
1675
- cursor: "pointer"
1676
- },
1677
- children: "Desktop"
1678
- }
1679
- ),
1680
- /* @__PURE__ */ jsx6(
1681
- "button",
1682
- {
1683
- type: "button",
1684
- onClick: () => setPreviewMode("mobile"),
1685
- style: {
1686
- padding: "6px 12px",
1687
- backgroundColor: previewMode === "mobile" ? "#6366f1" : "#e5e7eb",
1688
- color: previewMode === "mobile" ? "white" : "#374151",
1689
- border: "none",
1690
- borderRadius: "0 4px 4px 0",
1691
- fontSize: "14px",
1692
- cursor: "pointer"
1693
- },
1694
- children: "Mobile"
1695
- }
1696
- )
1697
- ] }),
1698
- validationSummary && /* @__PURE__ */ jsx6("div", { style: {
1699
- padding: "6px 12px",
1700
- backgroundColor: isValid ? "#fef3c7" : "#fee2e2",
1701
- color: isValid ? "#92400e" : "#991b1b",
1702
- borderRadius: "4px",
1703
- fontSize: "13px"
1704
- }, children: validationSummary })
1705
- ] }),
1706
- /* @__PURE__ */ jsx6(
1707
- "button",
1708
- {
1709
- type: "button",
1710
- onClick: handleTestEmail,
1711
- style: {
1712
- padding: "6px 12px",
1713
- backgroundColor: "#10b981",
1714
- color: "white",
1715
- border: "none",
1716
- borderRadius: "4px",
1717
- fontSize: "14px",
1718
- cursor: "pointer"
1719
- },
1720
- children: "Send Test Email"
1721
- }
1722
- )
1723
- ] }),
1724
- /* @__PURE__ */ jsx6("div", { style: { height: "600px" }, children: /* @__PURE__ */ jsx6(
1725
- EmailPreview,
1726
- {
1727
- content: fields.content?.value || null,
1728
- subject: fields.subject?.value || "Email Subject",
1729
- preheader: fields.preheader?.value,
1730
- mode: previewMode,
1731
- onValidation: handleValidation,
1732
- pluginConfig: pluginConfig || void 0
1733
- }
1734
- ) })
1735
- ] });
1736
- };
1737
-
1738
- // src/components/Broadcasts/BroadcastEditor.tsx
1739
- import { useState as useState7, useCallback as useCallback2 } from "react";
1740
- import { useField, useFormFields as useFormFields2 } from "@payloadcms/ui";
1741
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1742
- var BroadcastEditor = (props) => {
1743
- const { value } = useField({ path: props.path });
1744
- const [showPreview, setShowPreview] = useState7(true);
1745
- const [previewMode, setPreviewMode] = useState7("desktop");
1746
- const [isValid, setIsValid] = useState7(true);
1747
- const [validationSummary, setValidationSummary] = useState7("");
1748
- const fields = useFormFields2(([fields2]) => ({
1749
- subject: fields2["subject"],
1750
- preheader: fields2["contentSection.preheader"]
1751
- }));
1752
- const handleValidation = useCallback2((result) => {
1753
- setIsValid(result.valid);
1754
- const errorCount = result.errors.length;
1755
- const warningCount = result.warnings.length;
1756
- if (errorCount > 0) {
1757
- setValidationSummary(`${errorCount} error${errorCount !== 1 ? "s" : ""}, ${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
1758
- } else if (warningCount > 0) {
1759
- setValidationSummary(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
1760
- } else {
1761
- setValidationSummary("");
1762
- }
1763
- }, []);
1764
- const handleTestEmail = async () => {
1765
- const pathParts = window.location.pathname.split("/");
1766
- const broadcastId = pathParts[pathParts.length - 1];
1767
- if (!broadcastId || broadcastId === "create") {
1768
- alert("Please save the broadcast before sending a test email");
1769
- return;
1770
- }
1771
- try {
1772
- const response = await fetch(`/api/broadcasts/${broadcastId}/test`, {
1773
- method: "POST",
1774
- headers: {
1775
- "Content-Type": "application/json"
1776
- }
1777
- });
1778
- if (!response.ok) {
1779
- const data = await response.json();
1780
- throw new Error(data.error || "Failed to send test email");
1781
- }
1782
- alert("Test email sent successfully! Check your inbox.");
1783
- } catch (error) {
1784
- alert(error instanceof Error ? error.message : "Failed to send test email");
1785
- }
1786
- };
1787
- return /* @__PURE__ */ jsxs6("div", { style: { height: "600px", display: "flex", flexDirection: "column" }, children: [
1788
- /* @__PURE__ */ jsxs6("div", { style: {
1789
- display: "flex",
1790
- alignItems: "center",
1791
- justifyContent: "space-between",
1792
- padding: "12px 16px",
1793
- borderBottom: "1px solid #e5e7eb",
1794
- backgroundColor: "#f9fafb"
1795
- }, children: [
1796
- /* @__PURE__ */ jsxs6("div", { style: { display: "flex", alignItems: "center", gap: "16px" }, children: [
1797
- /* @__PURE__ */ jsx7(
1798
- "button",
1799
- {
1800
- type: "button",
1801
- onClick: () => setShowPreview(!showPreview),
1802
- style: {
1803
- padding: "6px 12px",
1804
- backgroundColor: showPreview ? "#3b82f6" : "#e5e7eb",
1805
- color: showPreview ? "white" : "#374151",
1806
- border: "none",
1807
- borderRadius: "4px",
1808
- fontSize: "14px",
1809
- cursor: "pointer"
1810
- },
1811
- children: showPreview ? "Hide Preview" : "Show Preview"
1812
- }
1813
- ),
1814
- showPreview && /* @__PURE__ */ jsxs6("div", { style: { display: "flex", gap: "8px" }, children: [
1815
- /* @__PURE__ */ jsx7(
1816
- "button",
1817
- {
1818
- type: "button",
1819
- onClick: () => setPreviewMode("desktop"),
1820
- style: {
1821
- padding: "6px 12px",
1822
- backgroundColor: previewMode === "desktop" ? "#6366f1" : "#e5e7eb",
1823
- color: previewMode === "desktop" ? "white" : "#374151",
1824
- border: "none",
1825
- borderRadius: "4px 0 0 4px",
1826
- fontSize: "14px",
1827
- cursor: "pointer"
1828
- },
1829
- children: "Desktop"
1830
- }
1831
- ),
1832
- /* @__PURE__ */ jsx7(
1833
- "button",
1834
- {
1835
- type: "button",
1836
- onClick: () => setPreviewMode("mobile"),
1837
- style: {
1838
- padding: "6px 12px",
1839
- backgroundColor: previewMode === "mobile" ? "#6366f1" : "#e5e7eb",
1840
- color: previewMode === "mobile" ? "white" : "#374151",
1841
- border: "none",
1842
- borderRadius: "0 4px 4px 0",
1843
- fontSize: "14px",
1844
- cursor: "pointer"
1845
- },
1846
- children: "Mobile"
1847
- }
1848
- )
1849
- ] }),
1850
- showPreview && validationSummary && /* @__PURE__ */ jsx7("div", { style: {
1851
- padding: "6px 12px",
1852
- backgroundColor: isValid ? "#fef3c7" : "#fee2e2",
1853
- color: isValid ? "#92400e" : "#991b1b",
1854
- borderRadius: "4px",
1855
- fontSize: "13px"
1856
- }, children: validationSummary })
1857
- ] }),
1858
- showPreview && /* @__PURE__ */ jsx7(
1859
- "button",
1860
- {
1861
- type: "button",
1862
- onClick: handleTestEmail,
1863
- style: {
1864
- padding: "6px 12px",
1865
- backgroundColor: "#10b981",
1866
- color: "white",
1867
- border: "none",
1868
- borderRadius: "4px",
1869
- fontSize: "14px",
1870
- cursor: "pointer"
1871
- },
1872
- children: "Send Test Email"
1873
- }
1874
- )
1875
- ] }),
1876
- /* @__PURE__ */ jsxs6("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
1877
- /* @__PURE__ */ jsx7("div", { style: {
1878
- flex: showPreview ? "0 0 50%" : "1",
1879
- overflow: "auto",
1880
- borderRight: showPreview ? "1px solid #e5e7eb" : "none"
1881
- }, children: /* @__PURE__ */ jsx7("div", { style: { padding: "16px" }, children: /* @__PURE__ */ jsx7("div", { className: "rich-text-lexical" }) }) }),
1882
- showPreview && /* @__PURE__ */ jsx7("div", { style: { flex: "0 0 50%", overflow: "hidden" }, children: /* @__PURE__ */ jsx7(
1883
- EmailPreview,
1884
- {
1885
- content: value,
1886
- subject: fields.subject?.value || "Email Subject",
1887
- preheader: fields.preheader?.value,
1888
- mode: previewMode,
1889
- onValidation: handleValidation
1890
- }
1891
- ) })
1892
- ] })
1893
- ] });
1894
- };
1895
-
1896
- // src/components/Broadcasts/BroadcastInlinePreview.tsx
1897
- import { useState as useState8, useCallback as useCallback3 } from "react";
1898
- import { useFormFields as useFormFields3 } from "@payloadcms/ui";
1899
-
1900
- // src/components/Broadcasts/PreviewControls.tsx
1901
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1902
- var PreviewControls = ({
1903
- onUpdate,
1904
- device,
1905
- onDeviceChange,
1906
- isLoading = false
1907
- }) => {
1908
- const controlsStyle = {
1909
- display: "flex",
1910
- alignItems: "center",
1911
- justifyContent: "space-between",
1912
- padding: "1rem",
1913
- background: "white",
1914
- borderBottom: "1px solid #e5e7eb"
1915
- };
1916
- const updateButtonStyle = {
1917
- padding: "0.5rem 1rem",
1918
- background: "#10b981",
1919
- color: "white",
1920
- border: "none",
1921
- borderRadius: "4px",
1922
- cursor: isLoading ? "not-allowed" : "pointer",
1923
- fontSize: "14px",
1924
- fontWeight: 500,
1925
- opacity: isLoading ? 0.6 : 1
1926
- };
1927
- const deviceSelectorStyle = {
1928
- display: "flex",
1929
- gap: "0.5rem"
1930
- };
1931
- const deviceButtonStyle = (isActive) => ({
1932
- display: "flex",
1933
- alignItems: "center",
1934
- gap: "0.5rem",
1935
- padding: "0.5rem 0.75rem",
1936
- background: isActive ? "#1f2937" : "white",
1937
- color: isActive ? "white" : "#374151",
1938
- border: `1px solid ${isActive ? "#1f2937" : "#e5e7eb"}`,
1939
- borderRadius: "4px",
1940
- cursor: "pointer",
1941
- fontSize: "14px"
1942
- });
1943
- return /* @__PURE__ */ jsxs7("div", { style: controlsStyle, children: [
1944
- /* @__PURE__ */ jsx8(
1945
- "button",
1946
- {
1947
- style: updateButtonStyle,
1948
- onClick: onUpdate,
1949
- disabled: isLoading,
1950
- children: isLoading ? "Updating..." : "Update Preview"
1951
- }
1952
- ),
1953
- /* @__PURE__ */ jsxs7("div", { style: deviceSelectorStyle, children: [
1954
- /* @__PURE__ */ jsxs7(
1955
- "button",
1956
- {
1957
- style: deviceButtonStyle(device === "desktop"),
1958
- onClick: () => onDeviceChange("desktop"),
1959
- "aria-label": "Desktop view",
1960
- children: [
1961
- /* @__PURE__ */ jsxs7("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1962
- /* @__PURE__ */ jsx8("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2", ry: "2" }),
1963
- /* @__PURE__ */ jsx8("line", { x1: "8", y1: "21", x2: "16", y2: "21" }),
1964
- /* @__PURE__ */ jsx8("line", { x1: "12", y1: "17", x2: "12", y2: "21" })
1965
- ] }),
1966
- "Desktop"
1967
- ]
1968
- }
1969
- ),
1970
- /* @__PURE__ */ jsxs7(
1971
- "button",
1972
- {
1973
- style: deviceButtonStyle(device === "mobile"),
1974
- onClick: () => onDeviceChange("mobile"),
1975
- "aria-label": "Mobile view",
1976
- children: [
1977
- /* @__PURE__ */ jsxs7("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1978
- /* @__PURE__ */ jsx8("rect", { x: "5", y: "2", width: "14", height: "20", rx: "2", ry: "2" }),
1979
- /* @__PURE__ */ jsx8("line", { x1: "12", y1: "18", x2: "12", y2: "18" })
1980
- ] }),
1981
- "Mobile"
1982
- ]
1983
- }
1984
- )
1985
- ] })
1986
- ] });
1987
- };
1988
-
1989
- // src/components/Broadcasts/BroadcastInlinePreview.tsx
1990
- import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1991
- var BroadcastInlinePreview = () => {
1992
- const [device, setDevice] = useState8("desktop");
1993
- const [isLoading, setIsLoading] = useState8(false);
1994
- const [showPreview, setShowPreview] = useState8(false);
1995
- const [previewHtml, setPreviewHtml] = useState8(null);
1996
- const [error, setError] = useState8(null);
1997
- const fields = useFormFields3(([fields2]) => ({
1998
- subject: fields2["subject"]?.value,
1999
- preheader: fields2["contentSection.preheader"]?.value,
2000
- content: fields2["contentSection.content"]?.value
2001
- }));
2002
- const updatePreview = useCallback3(async () => {
2003
- if (!fields.content) {
2004
- setError(new Error("Please add some content before previewing"));
2005
- return;
2006
- }
2007
- setIsLoading(true);
2008
- setError(null);
2009
- try {
2010
- const response = await fetch("/api/broadcasts/preview", {
2011
- method: "POST",
2012
- headers: {
2013
- "Content-Type": "application/json"
2014
- },
2015
- body: JSON.stringify({
2016
- content: fields.content,
2017
- preheader: fields.preheader,
2018
- subject: fields.subject
2019
- })
2020
- });
2021
- const data = await response.json();
2022
- if (!response.ok || !data.success) {
2023
- throw new Error(data.error || "Failed to generate preview");
2024
- }
2025
- setPreviewHtml(data.preview.html);
2026
- setShowPreview(true);
2027
- } catch (err) {
2028
- setError(err);
2029
- console.error("Failed to update preview:", err);
2030
- } finally {
2031
- setIsLoading(false);
2032
- }
2033
- }, [fields]);
2034
- const containerStyle = {
2035
- border: "1px solid #e5e7eb",
2036
- borderRadius: "8px",
2037
- overflow: "hidden",
2038
- height: "100%",
2039
- display: "flex",
2040
- flexDirection: "column"
2041
- };
2042
- const headerStyle = {
2043
- display: "flex",
2044
- alignItems: "center",
2045
- justifyContent: "space-between",
2046
- padding: "1rem",
2047
- background: "#f9fafb",
2048
- borderBottom: "1px solid #e5e7eb"
2049
- };
2050
- const titleStyle = {
2051
- fontSize: "16px",
2052
- fontWeight: 600,
2053
- color: "#1f2937",
2054
- margin: 0
2055
- };
2056
- const previewContainerStyle = {
2057
- flex: 1,
2058
- display: "flex",
2059
- flexDirection: "column",
2060
- background: "#f3f4f6",
2061
- overflow: "hidden"
2062
- };
2063
- const errorStyle = {
2064
- padding: "2rem",
2065
- textAlign: "center"
2066
- };
2067
- const toggleButtonStyle = {
2068
- padding: "0.5rem 1rem",
2069
- background: showPreview ? "#ef4444" : "#3b82f6",
2070
- color: "white",
2071
- border: "none",
2072
- borderRadius: "4px",
2073
- cursor: "pointer",
2074
- fontSize: "14px",
2075
- fontWeight: 500
2076
- };
2077
- return /* @__PURE__ */ jsxs8("div", { style: containerStyle, children: [
2078
- /* @__PURE__ */ jsxs8("div", { style: headerStyle, children: [
2079
- /* @__PURE__ */ jsx9("h3", { style: titleStyle, children: "Email Preview" }),
2080
- /* @__PURE__ */ jsx9(
2081
- "button",
2082
- {
2083
- onClick: () => showPreview ? setShowPreview(false) : updatePreview(),
2084
- style: toggleButtonStyle,
2085
- disabled: isLoading,
2086
- children: isLoading ? "Loading..." : showPreview ? "Hide Preview" : "Show Preview"
2087
- }
2088
- )
2089
- ] }),
2090
- showPreview && /* @__PURE__ */ jsx9("div", { style: previewContainerStyle, children: error ? /* @__PURE__ */ jsxs8("div", { style: errorStyle, children: [
2091
- /* @__PURE__ */ jsx9("p", { style: { color: "#ef4444", margin: "0 0 1rem" }, children: error.message }),
2092
- /* @__PURE__ */ jsx9(
2093
- "button",
2094
- {
2095
- onClick: updatePreview,
2096
- style: {
2097
- padding: "0.5rem 1rem",
2098
- background: "#3b82f6",
2099
- color: "white",
2100
- border: "none",
2101
- borderRadius: "4px",
2102
- cursor: "pointer"
2103
- },
2104
- children: "Retry"
2105
- }
2106
- )
2107
- ] }) : previewHtml ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
2108
- /* @__PURE__ */ jsx9(
2109
- PreviewControls,
2110
- {
2111
- onUpdate: updatePreview,
2112
- device,
2113
- onDeviceChange: setDevice,
2114
- isLoading
2115
- }
2116
- ),
2117
- /* @__PURE__ */ jsx9(
2118
- "div",
2119
- {
2120
- style: {
2121
- flex: 1,
2122
- padding: device === "mobile" ? "1rem" : "2rem",
2123
- display: "flex",
2124
- justifyContent: "center",
2125
- overflow: "auto"
2126
- },
2127
- children: /* @__PURE__ */ jsx9(
2128
- "div",
2129
- {
2130
- style: {
2131
- width: device === "mobile" ? "375px" : "600px",
2132
- maxWidth: "100%",
2133
- background: "white",
2134
- boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
2135
- borderRadius: "8px",
2136
- overflow: "hidden"
2137
- },
2138
- children: /* @__PURE__ */ jsx9(
2139
- "iframe",
2140
- {
2141
- srcDoc: previewHtml,
2142
- style: {
2143
- width: "100%",
2144
- height: "100%",
2145
- minHeight: "600px",
2146
- border: "none"
2147
- },
2148
- title: "Email Preview"
2149
- }
2150
- )
2151
- }
2152
- )
2153
- }
2154
- )
2155
- ] }) : null })
2156
- ] });
2157
- };
2158
-
2159
- // src/components/Broadcasts/BroadcastPreviewField.tsx
2160
- import { jsx as jsx10 } from "react/jsx-runtime";
2161
- var BroadcastPreviewField = () => {
2162
- return /* @__PURE__ */ jsx10("div", { style: {
2163
- padding: "1rem",
2164
- background: "#f9fafb",
2165
- borderRadius: "4px",
2166
- fontSize: "14px",
2167
- color: "#6b7280"
2168
- }, children: "Email preview is available inline below the content editor." });
2169
- };
2170
-
2171
- // src/components/Broadcasts/EmailRenderer.tsx
2172
- import { useEffect as useEffect5, useState as useState9, useCallback as useCallback4, useRef as useRef2 } from "react";
2173
- import { render } from "@react-email/render";
2174
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2175
- var EmailRenderer = ({
2176
- template,
2177
- data,
2178
- device = "desktop",
2179
- onRender
2180
- }) => {
2181
- const [renderedHtml, setRenderedHtml] = useState9("");
2182
- const [error, setError] = useState9(null);
2183
- const iframeRef = useRef2(null);
2184
- const renderEmail = useCallback4(async () => {
2185
- try {
2186
- const TemplateComponent = template;
2187
- const element = /* @__PURE__ */ jsx11(TemplateComponent, { ...data });
2188
- const html = await render(element, {
2189
- pretty: true
2190
- });
2191
- setRenderedHtml(html);
2192
- onRender?.(html);
2193
- setError(null);
2194
- } catch (err) {
2195
- setError(err);
2196
- console.error("Failed to render email template:", err);
2197
- }
2198
- }, [template, data, onRender]);
2199
- useEffect5(() => {
2200
- renderEmail();
2201
- }, [renderEmail]);
2202
- useEffect5(() => {
2203
- if (iframeRef.current && renderedHtml) {
2204
- const iframe = iframeRef.current;
2205
- const doc = iframe.contentDocument || iframe.contentWindow?.document;
2206
- if (doc) {
2207
- doc.open();
2208
- doc.write(renderedHtml);
2209
- doc.close();
2210
- }
2211
- }
2212
- }, [renderedHtml]);
2213
- const containerStyle = {
2214
- width: "100%",
2215
- height: "100%",
2216
- display: "flex",
2217
- alignItems: "flex-start",
2218
- justifyContent: "center",
2219
- overflow: "auto",
2220
- padding: "2rem",
2221
- boxSizing: "border-box"
2222
- };
2223
- const iframeStyle = {
2224
- width: device === "mobile" ? "375px" : "600px",
2225
- height: "100%",
2226
- minHeight: "600px",
2227
- background: "white",
2228
- boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
2229
- borderRadius: device === "mobile" ? "20px" : "8px",
2230
- border: "none",
2231
- display: "block"
2232
- };
2233
- const errorStyle = {
2234
- background: "white",
2235
- border: "1px solid #ef4444",
2236
- borderRadius: "4px",
2237
- padding: "2rem",
2238
- maxWidth: "500px"
2239
- };
2240
- if (error) {
2241
- return /* @__PURE__ */ jsxs9("div", { style: errorStyle, children: [
2242
- /* @__PURE__ */ jsx11("h3", { style: { color: "#ef4444", margin: "0 0 1rem" }, children: "Template Render Error" }),
2243
- /* @__PURE__ */ jsx11("pre", { style: {
2244
- background: "#f9fafb",
2245
- padding: "1rem",
2246
- borderRadius: "4px",
2247
- overflowX: "auto",
2248
- fontSize: "12px",
2249
- color: "#374151",
2250
- margin: 0
2251
- }, children: error.message })
2252
- ] });
2253
- }
2254
- return /* @__PURE__ */ jsx11("div", { style: containerStyle, children: /* @__PURE__ */ jsx11(
2255
- "iframe",
2256
- {
2257
- ref: iframeRef,
2258
- style: iframeStyle,
2259
- sandbox: "allow-same-origin",
2260
- title: "Email Preview"
2261
- }
2262
- ) });
2263
- };
2264
-
2265
- // src/components/Broadcasts/StatusBadge.tsx
2266
- import { jsx as jsx12 } from "react/jsx-runtime";
2267
- var statusConfig = {
2268
- ["draft" /* DRAFT */]: {
2269
- label: "Draft",
2270
- color: "#6B7280",
2271
- // gray
2272
- backgroundColor: "#F3F4F6"
2273
- },
2274
- ["scheduled" /* SCHEDULED */]: {
2275
- label: "Scheduled",
2276
- color: "#2563EB",
2277
- // blue
2278
- backgroundColor: "#DBEAFE"
2279
- },
2280
- ["sending" /* SENDING */]: {
2281
- label: "Sending",
2282
- color: "#D97706",
2283
- // yellow/orange
2284
- backgroundColor: "#FEF3C7"
2285
- },
2286
- ["sent" /* SENT */]: {
2287
- label: "Sent",
2288
- color: "#059669",
2289
- // green
2290
- backgroundColor: "#D1FAE5"
2291
- },
2292
- ["failed" /* FAILED */]: {
2293
- label: "Failed",
2294
- color: "#DC2626",
2295
- // red
2296
- backgroundColor: "#FEE2E2"
2297
- },
2298
- ["paused" /* PAUSED */]: {
2299
- label: "Paused",
2300
- color: "#9333EA",
2301
- // purple
2302
- backgroundColor: "#EDE9FE"
2303
- },
2304
- ["canceled" /* CANCELED */]: {
2305
- label: "Canceled",
2306
- color: "#6B7280",
2307
- // gray
2308
- backgroundColor: "#F3F4F6"
2309
- }
2310
- };
2311
- var StatusBadge = ({ cellData }) => {
2312
- const status = cellData;
2313
- const config = statusConfig[status] || statusConfig["draft" /* DRAFT */];
2314
- return /* @__PURE__ */ jsx12(
2315
- "span",
2316
- {
2317
- style: {
2318
- display: "inline-flex",
2319
- alignItems: "center",
2320
- padding: "2px 10px",
2321
- borderRadius: "12px",
2322
- fontSize: "12px",
2323
- fontWeight: "500",
2324
- color: config.color,
2325
- backgroundColor: config.backgroundColor
2326
- },
2327
- children: config.label
2328
- }
2329
- );
2330
- };
2331
-
2332
- // src/components/Broadcasts/EmptyField.tsx
2333
- var EmptyField = () => {
2334
- return null;
2335
- };
2336
-
2337
- // src/email-templates/DefaultBroadcastTemplate.tsx
2338
- import {
2339
- Body,
2340
- Container,
2341
- Head,
2342
- Hr,
2343
- Html,
2344
- Link,
2345
- Preview,
2346
- Section,
2347
- Text
2348
- } from "@react-email/components";
2349
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
2350
- var DefaultBroadcastTemplate = ({
2351
- subject,
2352
- preheader,
2353
- content
2354
- }) => {
2355
- return /* @__PURE__ */ jsxs10(Html, { children: [
2356
- /* @__PURE__ */ jsx13(Head, {}),
2357
- /* @__PURE__ */ jsx13(Preview, { children: preheader || subject }),
2358
- /* @__PURE__ */ jsx13(Body, { style: main, children: /* @__PURE__ */ jsxs10(Container, { style: container, children: [
2359
- /* @__PURE__ */ jsx13(Section, { style: contentSection, children: /* @__PURE__ */ jsx13("div", { dangerouslySetInnerHTML: { __html: content } }) }),
2360
- /* @__PURE__ */ jsx13(Hr, { style: divider }),
2361
- /* @__PURE__ */ jsxs10(Section, { style: footer, children: [
2362
- /* @__PURE__ */ jsx13(Text, { style: footerText, children: "You're receiving this email because you subscribed to our newsletter." }),
2363
- /* @__PURE__ */ jsx13(Text, { style: footerText, children: /* @__PURE__ */ jsx13(Link, { href: "{{unsubscribe_url}}", style: footerLink, children: "Unsubscribe" }) })
2364
- ] })
2365
- ] }) })
2366
- ] });
2367
- };
2368
- var main = {
2369
- backgroundColor: "#ffffff",
2370
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
2371
- };
2372
- var container = {
2373
- margin: "0 auto",
2374
- padding: "40px 20px",
2375
- maxWidth: "600px"
2376
- };
2377
- var contentSection = {
2378
- fontSize: "16px",
2379
- lineHeight: "1.6",
2380
- color: "#374151"
2381
- };
2382
- var divider = {
2383
- borderColor: "#e5e7eb",
2384
- margin: "40px 0 20px"
2385
- };
2386
- var footer = {
2387
- textAlign: "center"
2388
- };
2389
- var footerText = {
2390
- fontSize: "14px",
2391
- lineHeight: "1.5",
2392
- color: "#6b7280",
2393
- margin: "0 0 10px"
2394
- };
2395
- var footerLink = {
2396
- color: "#6b7280",
2397
- textDecoration: "underline"
2398
- };
2399
- export {
2400
- BroadcastEditor,
2401
- BroadcastInlinePreview,
2402
- BroadcastPreviewField,
2403
- DefaultBroadcastTemplate,
2404
- EmailPreview,
2405
- EmailPreviewField,
2406
- EmailRenderer,
2407
- EmptyField,
2408
- MagicLinkVerify,
2409
- NewsletterForm,
2410
- PreferencesForm,
2411
- PreviewControls,
2412
- StatusBadge,
2413
- createMagicLinkVerify,
2414
- createNewsletterForm,
2415
- createPreferencesForm,
2416
- useNewsletterAuth
2417
- };
2418
- //# sourceMappingURL=components.js.map