payload-plugin-newsletter 0.27.0 → 0.28.0

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.d.ts CHANGED
@@ -24,4 +24,43 @@ declare const EmailPreview: React.FC<EmailPreviewProps>;
24
24
 
25
25
  declare const WebhookConfiguration: React.FC;
26
26
 
27
- export { BroadcastInlinePreview, type BroadcastInlinePreviewProps, EmailPreview, type EmailPreviewProps, StatusBadge, type StatusBadgeProps, WebhookConfiguration };
27
+ /**
28
+ * Field component that shows scheduling controls based on broadcast status
29
+ * Shows Schedule button for drafts, Cancel button for scheduled broadcasts
30
+ */
31
+ declare const BroadcastScheduleField: React.FC;
32
+
33
+ interface BroadcastScheduleButtonProps {
34
+ broadcastId: string;
35
+ sendStatus?: string;
36
+ providerId?: string;
37
+ }
38
+ /**
39
+ * Button to open the schedule modal for a broadcast
40
+ * Only shows for draft broadcasts that have been synced to the provider
41
+ */
42
+ declare const BroadcastScheduleButton: React.FC<BroadcastScheduleButtonProps>;
43
+
44
+ interface CancelScheduleButtonProps {
45
+ broadcastId: string;
46
+ sendStatus?: string;
47
+ scheduledAt?: string;
48
+ }
49
+ /**
50
+ * Button to cancel a scheduled broadcast
51
+ * Only shows for broadcasts with 'scheduled' status
52
+ */
53
+ declare const CancelScheduleButton: React.FC<CancelScheduleButtonProps>;
54
+
55
+ interface ScheduleModalProps {
56
+ broadcastId: string;
57
+ onScheduled?: () => void;
58
+ onClose: () => void;
59
+ }
60
+ /**
61
+ * Modal for scheduling a broadcast
62
+ * Uses native HTML date-time inputs for compatibility
63
+ */
64
+ declare const ScheduleModal: React.FC<ScheduleModalProps>;
65
+
66
+ export { BroadcastInlinePreview, type BroadcastInlinePreviewProps, BroadcastScheduleButton, BroadcastScheduleField, CancelScheduleButton, EmailPreview, type EmailPreviewProps, ScheduleModal, StatusBadge, type StatusBadgeProps, WebhookConfiguration };
package/dist/admin.js CHANGED
@@ -332,9 +332,550 @@ var WebhookConfiguration = () => {
332
332
  ] })
333
333
  ] }) });
334
334
  };
335
+
336
+ // src/components/Broadcasts/BroadcastScheduleField.tsx
337
+ import { useDocumentInfo, useFormFields as useFormFields3 } from "@payloadcms/ui";
338
+
339
+ // src/components/Broadcasts/BroadcastScheduleButton.tsx
340
+ import { useState as useState4 } from "react";
341
+
342
+ // src/components/Broadcasts/ScheduleModal.tsx
343
+ import { useState as useState3, useCallback as useCallback2 } from "react";
344
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
345
+ var ScheduleModal = ({
346
+ broadcastId,
347
+ onScheduled,
348
+ onClose
349
+ }) => {
350
+ const [selectedDate, setSelectedDate] = useState3("");
351
+ const [selectedTime, setSelectedTime] = useState3("");
352
+ const [timezone, setTimezone] = useState3(
353
+ Intl.DateTimeFormat().resolvedOptions().timeZone
354
+ );
355
+ const [isLoading, setIsLoading] = useState3(false);
356
+ const [error, setError] = useState3(null);
357
+ const getMinDate = () => {
358
+ const today = /* @__PURE__ */ new Date();
359
+ return today.toISOString().split("T")[0];
360
+ };
361
+ const getMinTime = () => {
362
+ if (!selectedDate) return void 0;
363
+ const today = /* @__PURE__ */ new Date();
364
+ const selectedDateObj = new Date(selectedDate);
365
+ if (selectedDateObj.toDateString() === today.toDateString()) {
366
+ const minTime = new Date(today.getTime() + 5 * 60 * 1e3);
367
+ return `${String(minTime.getHours()).padStart(2, "0")}:${String(minTime.getMinutes()).padStart(2, "0")}`;
368
+ }
369
+ return void 0;
370
+ };
371
+ const handleSchedule = useCallback2(async () => {
372
+ if (!selectedDate || !selectedTime) {
373
+ setError("Please select both date and time");
374
+ return;
375
+ }
376
+ setIsLoading(true);
377
+ setError(null);
378
+ try {
379
+ const scheduledAt = /* @__PURE__ */ new Date(`${selectedDate}T${selectedTime}:00`);
380
+ if (scheduledAt <= /* @__PURE__ */ new Date()) {
381
+ setError("Scheduled time must be in the future");
382
+ setIsLoading(false);
383
+ return;
384
+ }
385
+ const response = await fetch(`/api/broadcasts/${broadcastId}/schedule`, {
386
+ method: "POST",
387
+ headers: { "Content-Type": "application/json" },
388
+ body: JSON.stringify({
389
+ scheduledAt: scheduledAt.toISOString(),
390
+ timezone
391
+ })
392
+ });
393
+ const data = await response.json();
394
+ if (!data.success) {
395
+ setError(data.error || "Failed to schedule broadcast");
396
+ return;
397
+ }
398
+ onScheduled?.();
399
+ onClose();
400
+ window.location.reload();
401
+ } catch (err) {
402
+ setError("Network error. Please try again.");
403
+ } finally {
404
+ setIsLoading(false);
405
+ }
406
+ }, [selectedDate, selectedTime, timezone, broadcastId, onScheduled, onClose]);
407
+ const commonTimezones = [
408
+ "America/New_York",
409
+ "America/Chicago",
410
+ "America/Denver",
411
+ "America/Los_Angeles",
412
+ "Europe/London",
413
+ "Europe/Paris",
414
+ "Asia/Tokyo",
415
+ "Australia/Sydney"
416
+ ];
417
+ return /* @__PURE__ */ jsx5(
418
+ "div",
419
+ {
420
+ style: {
421
+ position: "fixed",
422
+ top: 0,
423
+ left: 0,
424
+ right: 0,
425
+ bottom: 0,
426
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
427
+ display: "flex",
428
+ alignItems: "center",
429
+ justifyContent: "center",
430
+ zIndex: 1e4
431
+ },
432
+ onClick: (e) => {
433
+ if (e.target === e.currentTarget) onClose();
434
+ },
435
+ children: /* @__PURE__ */ jsxs4(
436
+ "div",
437
+ {
438
+ style: {
439
+ backgroundColor: "var(--theme-elevation-0, #fff)",
440
+ borderRadius: "8px",
441
+ padding: "24px",
442
+ maxWidth: "400px",
443
+ width: "100%",
444
+ boxShadow: "0 4px 20px rgba(0, 0, 0, 0.15)"
445
+ },
446
+ children: [
447
+ /* @__PURE__ */ jsx5(
448
+ "h2",
449
+ {
450
+ style: {
451
+ margin: "0 0 16px 0",
452
+ fontSize: "18px",
453
+ fontWeight: "600",
454
+ color: "var(--theme-elevation-1000, #000)"
455
+ },
456
+ children: "Schedule Broadcast"
457
+ }
458
+ ),
459
+ error && /* @__PURE__ */ jsx5(
460
+ "div",
461
+ {
462
+ style: {
463
+ padding: "12px",
464
+ marginBottom: "16px",
465
+ backgroundColor: "var(--theme-error-100, #fef2f2)",
466
+ color: "var(--theme-error-500, #ef4444)",
467
+ borderRadius: "4px",
468
+ fontSize: "14px"
469
+ },
470
+ children: error
471
+ }
472
+ ),
473
+ /* @__PURE__ */ jsxs4("div", { style: { marginBottom: "16px" }, children: [
474
+ /* @__PURE__ */ jsx5(
475
+ "label",
476
+ {
477
+ style: {
478
+ display: "block",
479
+ marginBottom: "4px",
480
+ fontSize: "14px",
481
+ fontWeight: "500",
482
+ color: "var(--theme-elevation-800, #333)"
483
+ },
484
+ children: "Date"
485
+ }
486
+ ),
487
+ /* @__PURE__ */ jsx5(
488
+ "input",
489
+ {
490
+ type: "date",
491
+ value: selectedDate,
492
+ onChange: (e) => setSelectedDate(e.target.value),
493
+ min: getMinDate(),
494
+ style: {
495
+ width: "100%",
496
+ padding: "8px 12px",
497
+ border: "1px solid var(--theme-elevation-150, #ddd)",
498
+ borderRadius: "4px",
499
+ fontSize: "14px",
500
+ backgroundColor: "var(--theme-input-bg, #fff)",
501
+ color: "var(--theme-elevation-1000, #000)"
502
+ }
503
+ }
504
+ )
505
+ ] }),
506
+ /* @__PURE__ */ jsxs4("div", { style: { marginBottom: "16px" }, children: [
507
+ /* @__PURE__ */ jsx5(
508
+ "label",
509
+ {
510
+ style: {
511
+ display: "block",
512
+ marginBottom: "4px",
513
+ fontSize: "14px",
514
+ fontWeight: "500",
515
+ color: "var(--theme-elevation-800, #333)"
516
+ },
517
+ children: "Time"
518
+ }
519
+ ),
520
+ /* @__PURE__ */ jsx5(
521
+ "input",
522
+ {
523
+ type: "time",
524
+ value: selectedTime,
525
+ onChange: (e) => setSelectedTime(e.target.value),
526
+ min: getMinTime(),
527
+ style: {
528
+ width: "100%",
529
+ padding: "8px 12px",
530
+ border: "1px solid var(--theme-elevation-150, #ddd)",
531
+ borderRadius: "4px",
532
+ fontSize: "14px",
533
+ backgroundColor: "var(--theme-input-bg, #fff)",
534
+ color: "var(--theme-elevation-1000, #000)"
535
+ }
536
+ }
537
+ )
538
+ ] }),
539
+ /* @__PURE__ */ jsxs4("div", { style: { marginBottom: "24px" }, children: [
540
+ /* @__PURE__ */ jsx5(
541
+ "label",
542
+ {
543
+ style: {
544
+ display: "block",
545
+ marginBottom: "4px",
546
+ fontSize: "14px",
547
+ fontWeight: "500",
548
+ color: "var(--theme-elevation-800, #333)"
549
+ },
550
+ children: "Timezone"
551
+ }
552
+ ),
553
+ /* @__PURE__ */ jsxs4(
554
+ "select",
555
+ {
556
+ value: timezone,
557
+ onChange: (e) => setTimezone(e.target.value),
558
+ style: {
559
+ width: "100%",
560
+ padding: "8px 12px",
561
+ border: "1px solid var(--theme-elevation-150, #ddd)",
562
+ borderRadius: "4px",
563
+ fontSize: "14px",
564
+ backgroundColor: "var(--theme-input-bg, #fff)",
565
+ color: "var(--theme-elevation-1000, #000)"
566
+ },
567
+ children: [
568
+ commonTimezones.includes(timezone) ? null : /* @__PURE__ */ jsx5("option", { value: timezone, children: timezone }),
569
+ commonTimezones.map((tz) => /* @__PURE__ */ jsx5("option", { value: tz, children: tz.replace("_", " ") }, tz))
570
+ ]
571
+ }
572
+ )
573
+ ] }),
574
+ /* @__PURE__ */ jsxs4(
575
+ "div",
576
+ {
577
+ style: {
578
+ display: "flex",
579
+ gap: "12px",
580
+ justifyContent: "flex-end"
581
+ },
582
+ children: [
583
+ /* @__PURE__ */ jsx5(
584
+ "button",
585
+ {
586
+ onClick: onClose,
587
+ disabled: isLoading,
588
+ style: {
589
+ padding: "8px 16px",
590
+ border: "1px solid var(--theme-elevation-150, #ddd)",
591
+ borderRadius: "4px",
592
+ backgroundColor: "transparent",
593
+ color: "var(--theme-elevation-800, #333)",
594
+ fontSize: "14px",
595
+ cursor: "pointer"
596
+ },
597
+ children: "Cancel"
598
+ }
599
+ ),
600
+ /* @__PURE__ */ jsx5(
601
+ "button",
602
+ {
603
+ onClick: handleSchedule,
604
+ disabled: isLoading || !selectedDate || !selectedTime,
605
+ style: {
606
+ padding: "8px 16px",
607
+ border: "none",
608
+ borderRadius: "4px",
609
+ backgroundColor: isLoading || !selectedDate || !selectedTime ? "var(--theme-elevation-200, #ccc)" : "var(--theme-success-500, #22c55e)",
610
+ color: "#fff",
611
+ fontSize: "14px",
612
+ fontWeight: "500",
613
+ cursor: isLoading || !selectedDate || !selectedTime ? "not-allowed" : "pointer"
614
+ },
615
+ children: isLoading ? "Scheduling..." : "Schedule"
616
+ }
617
+ )
618
+ ]
619
+ }
620
+ )
621
+ ]
622
+ }
623
+ )
624
+ }
625
+ );
626
+ };
627
+
628
+ // src/components/Broadcasts/BroadcastScheduleButton.tsx
629
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
630
+ var BroadcastScheduleButton = ({
631
+ broadcastId,
632
+ sendStatus = "draft",
633
+ providerId
634
+ }) => {
635
+ const [showModal, setShowModal] = useState4(false);
636
+ if (sendStatus !== "draft") {
637
+ return null;
638
+ }
639
+ if (!providerId) {
640
+ return /* @__PURE__ */ jsxs5(
641
+ "button",
642
+ {
643
+ disabled: true,
644
+ title: "Save the broadcast first to sync with the email provider",
645
+ style: {
646
+ padding: "8px 16px",
647
+ border: "none",
648
+ borderRadius: "4px",
649
+ backgroundColor: "var(--theme-elevation-200, #ccc)",
650
+ color: "var(--theme-elevation-500, #666)",
651
+ fontSize: "14px",
652
+ fontWeight: "500",
653
+ cursor: "not-allowed",
654
+ display: "inline-flex",
655
+ alignItems: "center",
656
+ gap: "6px"
657
+ },
658
+ children: [
659
+ /* @__PURE__ */ jsx6("span", { children: "\u{1F4C5}" }),
660
+ "Schedule (save first)"
661
+ ]
662
+ }
663
+ );
664
+ }
665
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
666
+ /* @__PURE__ */ jsxs5(
667
+ "button",
668
+ {
669
+ onClick: () => setShowModal(true),
670
+ style: {
671
+ padding: "8px 16px",
672
+ border: "none",
673
+ borderRadius: "4px",
674
+ backgroundColor: "var(--theme-success-500, #22c55e)",
675
+ color: "#fff",
676
+ fontSize: "14px",
677
+ fontWeight: "500",
678
+ cursor: "pointer",
679
+ display: "inline-flex",
680
+ alignItems: "center",
681
+ gap: "6px"
682
+ },
683
+ children: [
684
+ /* @__PURE__ */ jsx6("span", { children: "\u{1F4C5}" }),
685
+ "Schedule Send"
686
+ ]
687
+ }
688
+ ),
689
+ showModal && /* @__PURE__ */ jsx6(
690
+ ScheduleModal,
691
+ {
692
+ broadcastId,
693
+ onClose: () => setShowModal(false)
694
+ }
695
+ )
696
+ ] });
697
+ };
698
+
699
+ // src/components/Broadcasts/CancelScheduleButton.tsx
700
+ import { useState as useState5, useCallback as useCallback3 } from "react";
701
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
702
+ var CancelScheduleButton = ({
703
+ broadcastId,
704
+ sendStatus = "draft",
705
+ scheduledAt
706
+ }) => {
707
+ const [isLoading, setIsLoading] = useState5(false);
708
+ if (sendStatus !== "scheduled") {
709
+ return null;
710
+ }
711
+ const handleCancel = useCallback3(async () => {
712
+ const formattedDate = scheduledAt ? new Date(scheduledAt).toLocaleString() : "unknown time";
713
+ const confirmed = window.confirm(
714
+ `Are you sure you want to cancel this scheduled broadcast?
715
+
716
+ It was scheduled for: ${formattedDate}
717
+
718
+ The broadcast will be returned to draft status.`
719
+ );
720
+ if (!confirmed) return;
721
+ setIsLoading(true);
722
+ try {
723
+ const response = await fetch(`/api/broadcasts/${broadcastId}/schedule`, {
724
+ method: "DELETE"
725
+ });
726
+ const data = await response.json();
727
+ if (!data.success) {
728
+ alert(data.error || "Failed to cancel schedule");
729
+ return;
730
+ }
731
+ window.location.reload();
732
+ } catch (err) {
733
+ alert("Network error. Please try again.");
734
+ } finally {
735
+ setIsLoading(false);
736
+ }
737
+ }, [broadcastId, scheduledAt]);
738
+ const formattedScheduledAt = scheduledAt ? new Date(scheduledAt).toLocaleString() : null;
739
+ return /* @__PURE__ */ jsxs6(
740
+ "div",
741
+ {
742
+ style: {
743
+ display: "flex",
744
+ alignItems: "center",
745
+ gap: "16px"
746
+ },
747
+ children: [
748
+ formattedScheduledAt && /* @__PURE__ */ jsxs6(
749
+ "span",
750
+ {
751
+ style: {
752
+ fontSize: "14px",
753
+ color: "var(--theme-elevation-600, #666)"
754
+ },
755
+ children: [
756
+ "Scheduled for: ",
757
+ /* @__PURE__ */ jsx7("strong", { children: formattedScheduledAt })
758
+ ]
759
+ }
760
+ ),
761
+ /* @__PURE__ */ jsxs6(
762
+ "button",
763
+ {
764
+ onClick: handleCancel,
765
+ disabled: isLoading,
766
+ style: {
767
+ padding: "8px 16px",
768
+ border: "1px solid var(--theme-error-500, #ef4444)",
769
+ borderRadius: "4px",
770
+ backgroundColor: "transparent",
771
+ color: "var(--theme-error-500, #ef4444)",
772
+ fontSize: "14px",
773
+ fontWeight: "500",
774
+ cursor: isLoading ? "not-allowed" : "pointer",
775
+ display: "inline-flex",
776
+ alignItems: "center",
777
+ gap: "6px",
778
+ opacity: isLoading ? 0.6 : 1
779
+ },
780
+ children: [
781
+ /* @__PURE__ */ jsx7("span", { children: "\u2715" }),
782
+ isLoading ? "Cancelling..." : "Cancel Schedule"
783
+ ]
784
+ }
785
+ )
786
+ ]
787
+ }
788
+ );
789
+ };
790
+
791
+ // src/components/Broadcasts/BroadcastScheduleField.tsx
792
+ import { jsx as jsx8 } from "react/jsx-runtime";
793
+ var BroadcastScheduleField = () => {
794
+ const { id } = useDocumentInfo();
795
+ const sendStatusField = useFormFields3(([fields]) => fields.sendStatus);
796
+ const providerIdField = useFormFields3(([fields]) => fields.providerId);
797
+ const scheduledAtField = useFormFields3(([fields]) => fields.scheduledAt);
798
+ const sendStatus = sendStatusField?.value;
799
+ const providerId = providerIdField?.value;
800
+ const scheduledAt = scheduledAtField?.value;
801
+ if (!id) {
802
+ return /* @__PURE__ */ jsx8(
803
+ "div",
804
+ {
805
+ style: {
806
+ padding: "16px",
807
+ backgroundColor: "var(--theme-elevation-50, #f9f9f9)",
808
+ borderRadius: "4px",
809
+ fontSize: "14px",
810
+ color: "var(--theme-elevation-600, #666)"
811
+ },
812
+ children: "Save the broadcast to enable scheduling options."
813
+ }
814
+ );
815
+ }
816
+ if (sendStatus === "sent" || sendStatus === "sending") {
817
+ return /* @__PURE__ */ jsx8(
818
+ "div",
819
+ {
820
+ style: {
821
+ padding: "16px",
822
+ backgroundColor: "var(--theme-elevation-50, #f9f9f9)",
823
+ borderRadius: "4px",
824
+ fontSize: "14px",
825
+ color: "var(--theme-elevation-600, #666)"
826
+ },
827
+ children: sendStatus === "sent" ? "This broadcast has been sent and cannot be rescheduled." : "This broadcast is currently being sent."
828
+ }
829
+ );
830
+ }
831
+ if (sendStatus === "failed") {
832
+ return /* @__PURE__ */ jsx8(
833
+ "div",
834
+ {
835
+ style: {
836
+ padding: "16px",
837
+ backgroundColor: "var(--theme-error-100, #fef2f2)",
838
+ borderRadius: "4px",
839
+ fontSize: "14px",
840
+ color: "var(--theme-error-600, #dc2626)"
841
+ },
842
+ children: "This broadcast failed to send. Edit and save to return it to draft status."
843
+ }
844
+ );
845
+ }
846
+ return /* @__PURE__ */ jsx8(
847
+ "div",
848
+ {
849
+ style: {
850
+ padding: "16px",
851
+ backgroundColor: "var(--theme-elevation-50, #f9f9f9)",
852
+ borderRadius: "4px"
853
+ },
854
+ children: sendStatus === "scheduled" ? /* @__PURE__ */ jsx8(
855
+ CancelScheduleButton,
856
+ {
857
+ broadcastId: String(id),
858
+ sendStatus,
859
+ scheduledAt
860
+ }
861
+ ) : /* @__PURE__ */ jsx8(
862
+ BroadcastScheduleButton,
863
+ {
864
+ broadcastId: String(id),
865
+ sendStatus,
866
+ providerId
867
+ }
868
+ )
869
+ }
870
+ );
871
+ };
335
872
  export {
336
873
  BroadcastInlinePreview,
874
+ BroadcastScheduleButton,
875
+ BroadcastScheduleField,
876
+ CancelScheduleButton,
337
877
  EmailPreview,
878
+ ScheduleModal,
338
879
  StatusBadge,
339
880
  WebhookConfiguration
340
881
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-plugin-newsletter",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",