payload-plugin-newsletter
Version:
Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration
1,153 lines (1,139 loc) • 40.6 kB
JavaScript
"use client";
"use client";
// src/admin/components/BroadcastInlinePreview.tsx
import { useState, useCallback, useEffect } from "react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
var BroadcastInlinePreview = () => {
const [preview, setPreview] = useState("");
const [loading, setLoading] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [error, setError] = useState(null);
const [documentId, setDocumentId] = useState(null);
useEffect(() => {
const pathParts = window.location.pathname.split("/");
const broadcastsIndex = pathParts.indexOf("broadcasts");
if (broadcastsIndex !== -1 && pathParts[broadcastsIndex + 1]) {
const id = pathParts[broadcastsIndex + 1];
if (id !== "create" && id.length > 10) {
setDocumentId(id);
}
}
}, []);
const generatePreview = useCallback(async () => {
if (!documentId) {
setError("Cannot generate preview: Document must be saved first. Save the document and try again.");
return;
}
try {
setLoading(true);
setError(null);
const docResponse = await fetch(`/api/broadcasts/${documentId}`);
if (!docResponse.ok) {
throw new Error(`Failed to fetch document: ${docResponse.statusText}`);
}
const docData = await docResponse.json();
console.log("[BroadcastPreview] Document data:", {
id: docData.id,
subject: docData.subject,
hasContent: !!docData.contentSection?.content
});
const content = docData.contentSection?.content || docData.content;
if (!content) {
setError("No content available to preview. Add some content and save the document first.");
setLoading(false);
return;
}
const response = await fetch("/api/broadcasts/preview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content,
documentData: docData
})
});
if (!response.ok) {
throw new Error(`Preview failed: ${response.statusText}`);
}
const data = await response.json();
setPreview(data.html || "");
setShowPreview(true);
} catch (err) {
console.error("Preview generation error:", err);
setError(err instanceof Error ? err.message : "Failed to generate preview");
} finally {
setLoading(false);
}
}, [documentId]);
return /* @__PURE__ */ jsxs("div", { className: "field-type", children: [
/* @__PURE__ */ jsx("div", { className: "field-label", children: "Email Preview" }),
!showPreview ? /* @__PURE__ */ jsxs("div", { className: "preview-controls", children: [
/* @__PURE__ */ jsx(
"button",
{
type: "button",
onClick: generatePreview,
disabled: loading,
className: "btn btn--style-primary btn--icon-style-without-border btn--size-small",
style: { marginBottom: "1rem" },
children: loading ? "Generating Preview..." : "Generate Preview"
}
),
!documentId && /* @__PURE__ */ jsx("div", { style: { color: "#6b7280", fontSize: "0.875rem", marginTop: "0.5rem" }, children: "Save the document first to enable preview." }),
error && /* @__PURE__ */ jsx("div", { className: "error-message", style: { color: "#dc2626", marginTop: "0.5rem" }, children: error })
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsxs("div", { className: "preview-controls", style: { marginBottom: "1rem" }, children: [
/* @__PURE__ */ jsx(
"button",
{
type: "button",
onClick: () => setShowPreview(false),
className: "btn btn--style-secondary btn--icon-style-without-border btn--size-small",
style: { marginRight: "0.5rem" },
children: "Hide Preview"
}
),
/* @__PURE__ */ jsx(
"button",
{
type: "button",
onClick: generatePreview,
disabled: loading,
className: "btn btn--style-primary btn--icon-style-without-border btn--size-small",
children: loading ? "Regenerating..." : "Refresh Preview"
}
)
] }),
/* @__PURE__ */ jsx("div", { className: "email-preview-container", children: /* @__PURE__ */ jsx(
"iframe",
{
srcDoc: preview,
style: {
width: "100%",
height: "600px",
border: "1px solid #e5e7eb",
borderRadius: "0.375rem",
backgroundColor: "white"
},
title: "Email Preview"
}
) })
] })
] });
};
// src/admin/components/StatusBadge.tsx
import { jsx as jsx2 } from "react/jsx-runtime";
var StatusBadge = (props) => {
const status = props.cellData || "draft";
const getStatusColor = (status2) => {
switch (status2) {
case "sent":
return "#22c55e";
case "scheduled":
return "#3b82f6";
case "draft":
return "#6b7280";
case "failed":
return "#ef4444";
default:
return "#6b7280";
}
};
const getStatusLabel = (status2) => {
switch (status2) {
case "sent":
return "Sent";
case "scheduled":
return "Scheduled";
case "draft":
return "Draft";
case "failed":
return "Failed";
default:
return status2;
}
};
return /* @__PURE__ */ jsx2(
"span",
{
style: {
display: "inline-block",
padding: "4px 8px",
borderRadius: "12px",
fontSize: "12px",
fontWeight: "500",
color: "#fff",
backgroundColor: getStatusColor(status),
textTransform: "capitalize"
},
children: getStatusLabel(status)
}
);
};
// src/admin/components/EmailPreview.tsx
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
var EmailPreview = ({
content,
subject,
preheader
}) => {
return /* @__PURE__ */ jsxs2("div", { className: "email-preview", style: { padding: "1rem" }, children: [
/* @__PURE__ */ jsxs2("div", { style: { marginBottom: "1rem" }, children: [
/* @__PURE__ */ jsx3("strong", { children: "Subject:" }),
" ",
subject || "No subject"
] }),
preheader && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: "1rem", color: "#666" }, children: [
/* @__PURE__ */ jsx3("strong", { children: "Preheader:" }),
" ",
preheader
] }),
/* @__PURE__ */ jsxs2("div", { style: {
border: "1px solid #e0e0e0",
borderRadius: "4px",
padding: "1rem",
backgroundColor: "#f9f9f9"
}, children: [
/* @__PURE__ */ jsx3("div", { children: "Email content will be rendered here" }),
content && /* @__PURE__ */ jsxs2("div", { style: { marginTop: "1rem", fontSize: "14px", color: "#666" }, children: [
"Content type: ",
typeof content
] })
] })
] });
};
// src/admin/components/WebhookConfiguration.tsx
import { useState as useState2 } from "react";
import { useFormFields } from "@payloadcms/ui";
import { Button } from "@payloadcms/ui";
import { toast } from "@payloadcms/ui";
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
var WebhookConfiguration = () => {
const [showInstructions, setShowInstructions] = useState2(false);
const [verifying, setVerifying] = useState2(false);
const fields = useFormFields(([fields2]) => ({
webhookUrl: fields2?.broadcastSettings?.webhookUrl,
webhookStatus: fields2?.broadcastSettings?.webhookStatus,
lastWebhookReceived: fields2?.broadcastSettings?.lastWebhookReceived
}));
const handleVerify = async () => {
setVerifying(true);
try {
const response = await fetch("/api/newsletter/webhooks/verify", {
method: "POST",
headers: {
"Content-Type": "application/json"
}
});
const data = await response.json();
if (data.success) {
toast.success(data.message || "Webhook verified");
} else {
toast.error(data.error || "Verification failed");
}
} catch {
toast.error("Failed to verify webhook");
} finally {
setVerifying(false);
}
};
const getStatusColor = (status) => {
switch (status) {
case "verified":
return "green";
case "configured":
return "yellow";
case "error":
return "red";
default:
return "gray";
}
};
const getStatusLabel = (status) => {
switch (status) {
case "verified":
return "Verified \u2713";
case "configured":
return "Configured";
case "error":
return "Error";
default:
return "Not Configured";
}
};
return /* @__PURE__ */ jsx4("div", { className: "field-type", children: /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "1rem" }, children: [
/* @__PURE__ */ jsx4("label", { className: "field-label", children: "Webhook Configuration" }),
/* @__PURE__ */ jsxs3("div", { style: {
background: "#f5f5f5",
padding: "1rem",
borderRadius: "4px",
marginTop: "0.5rem"
}, children: [
/* @__PURE__ */ jsxs3("div", { style: { marginBottom: "0.5rem" }, children: [
/* @__PURE__ */ jsx4("strong", { children: "Webhook URL:" }),
/* @__PURE__ */ jsx4("code", { style: {
display: "block",
padding: "0.5rem",
background: "#fff",
border: "1px solid #ddd",
borderRadius: "4px",
marginTop: "0.25rem",
fontSize: "0.875rem"
}, children: fields.webhookUrl || "Save settings to generate URL" })
] }),
/* @__PURE__ */ jsxs3("div", { style: { marginBottom: "0.5rem" }, children: [
/* @__PURE__ */ jsx4("strong", { children: "Status:" }),
" ",
/* @__PURE__ */ jsx4("span", { style: { color: getStatusColor(fields.webhookStatus || "not_configured") }, children: getStatusLabel(fields.webhookStatus || "not_configured") })
] }),
fields.lastWebhookReceived && /* @__PURE__ */ jsxs3("div", { style: { fontSize: "0.875rem", color: "#666" }, children: [
"Last event: ",
new Date(fields.lastWebhookReceived).toLocaleString()
] })
] }),
/* @__PURE__ */ jsxs3("div", { style: { marginTop: "1rem", display: "flex", gap: "0.5rem" }, children: [
/* @__PURE__ */ jsxs3(
Button,
{
onClick: () => setShowInstructions(!showInstructions),
buttonStyle: "secondary",
size: "small",
children: [
showInstructions ? "Hide" : "Show",
" Instructions"
]
}
),
/* @__PURE__ */ jsx4(
Button,
{
onClick: handleVerify,
buttonStyle: "primary",
size: "small",
disabled: verifying || !fields.webhookUrl,
children: verifying ? "Verifying..." : "Verify Webhook"
}
)
] }),
showInstructions && /* @__PURE__ */ jsxs3("div", { style: {
marginTop: "1rem",
padding: "1rem",
background: "#f0f8ff",
border: "1px solid #b0d4ff",
borderRadius: "4px"
}, children: [
/* @__PURE__ */ jsx4("h4", { style: { marginTop: 0 }, children: "Configure Broadcast Webhook" }),
/* @__PURE__ */ jsxs3("ol", { children: [
/* @__PURE__ */ jsx4("li", { children: "Copy the webhook URL above" }),
/* @__PURE__ */ jsx4("li", { children: "Go to your Broadcast dashboard" }),
/* @__PURE__ */ jsx4("li", { children: 'Navigate to "Webhook Endpoints"' }),
/* @__PURE__ */ jsx4("li", { children: 'Click "Add Webhook Endpoint"' }),
/* @__PURE__ */ jsx4("li", { children: "Paste the URL" }),
/* @__PURE__ */ jsxs3("li", { children: [
"Select these events:",
/* @__PURE__ */ jsxs3("ul", { children: [
/* @__PURE__ */ jsx4("li", { children: "Subscriber Events: subscribed, unsubscribed" }),
/* @__PURE__ */ jsx4("li", { children: "Broadcast Events: All" })
] })
] }),
/* @__PURE__ */ jsx4("li", { children: 'Click "Create Webhook"' }),
/* @__PURE__ */ jsx4("li", { children: "Copy the webhook secret shown" }),
/* @__PURE__ */ jsx4("li", { children: "Paste it in the Webhook Secret field below" }),
/* @__PURE__ */ jsx4("li", { children: "Save these settings" }),
/* @__PURE__ */ jsx4("li", { children: 'Click "Verify Webhook" to test the connection' })
] })
] })
] }) });
};
// src/components/Broadcasts/BroadcastScheduleField.tsx
import { useDocumentInfo, useFormFields as useFormFields2 } from "@payloadcms/ui";
// src/components/Broadcasts/BroadcastScheduleButton.tsx
import { useState as useState4 } from "react";
// src/components/Broadcasts/ScheduleModal.tsx
import { useState as useState3, useCallback as useCallback2 } from "react";
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
var ScheduleModal = ({
broadcastId,
onScheduled,
onClose
}) => {
const [selectedDate, setSelectedDate] = useState3("");
const [selectedTime, setSelectedTime] = useState3("");
const [timezone, setTimezone] = useState3(
Intl.DateTimeFormat().resolvedOptions().timeZone
);
const [isLoading, setIsLoading] = useState3(false);
const [error, setError] = useState3(null);
const getMinDate = () => {
const today = /* @__PURE__ */ new Date();
return today.toISOString().split("T")[0];
};
const getMinTime = () => {
if (!selectedDate) return void 0;
const today = /* @__PURE__ */ new Date();
const selectedDateObj = new Date(selectedDate);
if (selectedDateObj.toDateString() === today.toDateString()) {
const minTime = new Date(today.getTime() + 5 * 60 * 1e3);
return `${String(minTime.getHours()).padStart(2, "0")}:${String(minTime.getMinutes()).padStart(2, "0")}`;
}
return void 0;
};
const handleSchedule = useCallback2(async () => {
if (!selectedDate || !selectedTime) {
setError("Please select both date and time");
return;
}
setIsLoading(true);
setError(null);
try {
const scheduledAt = /* @__PURE__ */ new Date(`${selectedDate}T${selectedTime}:00`);
if (scheduledAt <= /* @__PURE__ */ new Date()) {
setError("Scheduled time must be in the future");
setIsLoading(false);
return;
}
const response = await fetch(`/api/broadcasts/${broadcastId}/schedule`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scheduledAt: scheduledAt.toISOString(),
timezone
})
});
const data = await response.json();
if (!data.success) {
setError(data.error || "Failed to schedule broadcast");
return;
}
onScheduled?.();
onClose();
window.location.reload();
} catch (err) {
setError("Network error. Please try again.");
} finally {
setIsLoading(false);
}
}, [selectedDate, selectedTime, timezone, broadcastId, onScheduled, onClose]);
const commonTimezones = [
"America/New_York",
"America/Chicago",
"America/Denver",
"America/Los_Angeles",
"Europe/London",
"Europe/Paris",
"Asia/Tokyo",
"Australia/Sydney"
];
return /* @__PURE__ */ jsx5(
"div",
{
style: {
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1e4
},
onClick: (e) => {
if (e.target === e.currentTarget) onClose();
},
children: /* @__PURE__ */ jsxs4(
"div",
{
style: {
backgroundColor: "var(--theme-elevation-0, #fff)",
borderRadius: "8px",
padding: "24px",
maxWidth: "400px",
width: "100%",
boxShadow: "0 4px 20px rgba(0, 0, 0, 0.15)"
},
children: [
/* @__PURE__ */ jsx5(
"h2",
{
style: {
margin: "0 0 16px 0",
fontSize: "18px",
fontWeight: "600",
color: "var(--theme-elevation-1000, #000)"
},
children: "Schedule Broadcast"
}
),
error && /* @__PURE__ */ jsx5(
"div",
{
style: {
padding: "12px",
marginBottom: "16px",
backgroundColor: "var(--theme-error-100, #fef2f2)",
color: "var(--theme-error-500, #ef4444)",
borderRadius: "4px",
fontSize: "14px"
},
children: error
}
),
/* @__PURE__ */ jsxs4("div", { style: { marginBottom: "16px" }, children: [
/* @__PURE__ */ jsx5(
"label",
{
style: {
display: "block",
marginBottom: "4px",
fontSize: "14px",
fontWeight: "500",
color: "var(--theme-elevation-800, #333)"
},
children: "Date"
}
),
/* @__PURE__ */ jsx5(
"input",
{
type: "date",
value: selectedDate,
onChange: (e) => setSelectedDate(e.target.value),
min: getMinDate(),
style: {
width: "100%",
padding: "8px 12px",
border: "1px solid var(--theme-elevation-150, #ddd)",
borderRadius: "4px",
fontSize: "14px",
backgroundColor: "var(--theme-input-bg, #fff)",
color: "var(--theme-elevation-1000, #000)"
}
}
)
] }),
/* @__PURE__ */ jsxs4("div", { style: { marginBottom: "16px" }, children: [
/* @__PURE__ */ jsx5(
"label",
{
style: {
display: "block",
marginBottom: "4px",
fontSize: "14px",
fontWeight: "500",
color: "var(--theme-elevation-800, #333)"
},
children: "Time"
}
),
/* @__PURE__ */ jsx5(
"input",
{
type: "time",
value: selectedTime,
onChange: (e) => setSelectedTime(e.target.value),
min: getMinTime(),
style: {
width: "100%",
padding: "8px 12px",
border: "1px solid var(--theme-elevation-150, #ddd)",
borderRadius: "4px",
fontSize: "14px",
backgroundColor: "var(--theme-input-bg, #fff)",
color: "var(--theme-elevation-1000, #000)"
}
}
)
] }),
/* @__PURE__ */ jsxs4("div", { style: { marginBottom: "24px" }, children: [
/* @__PURE__ */ jsx5(
"label",
{
style: {
display: "block",
marginBottom: "4px",
fontSize: "14px",
fontWeight: "500",
color: "var(--theme-elevation-800, #333)"
},
children: "Timezone"
}
),
/* @__PURE__ */ jsxs4(
"select",
{
value: timezone,
onChange: (e) => setTimezone(e.target.value),
style: {
width: "100%",
padding: "8px 12px",
border: "1px solid var(--theme-elevation-150, #ddd)",
borderRadius: "4px",
fontSize: "14px",
backgroundColor: "var(--theme-input-bg, #fff)",
color: "var(--theme-elevation-1000, #000)"
},
children: [
commonTimezones.includes(timezone) ? null : /* @__PURE__ */ jsx5("option", { value: timezone, children: timezone }),
commonTimezones.map((tz) => /* @__PURE__ */ jsx5("option", { value: tz, children: tz.replace("_", " ") }, tz))
]
}
)
] }),
/* @__PURE__ */ jsxs4(
"div",
{
style: {
display: "flex",
gap: "12px",
justifyContent: "flex-end"
},
children: [
/* @__PURE__ */ jsx5(
"button",
{
onClick: onClose,
disabled: isLoading,
style: {
padding: "8px 16px",
border: "1px solid var(--theme-elevation-150, #ddd)",
borderRadius: "4px",
backgroundColor: "transparent",
color: "var(--theme-elevation-800, #333)",
fontSize: "14px",
cursor: "pointer"
},
children: "Cancel"
}
),
/* @__PURE__ */ jsx5(
"button",
{
onClick: handleSchedule,
disabled: isLoading || !selectedDate || !selectedTime,
style: {
padding: "8px 16px",
border: "none",
borderRadius: "4px",
backgroundColor: isLoading || !selectedDate || !selectedTime ? "var(--theme-elevation-200, #ccc)" : "var(--theme-success-500, #22c55e)",
color: "#fff",
fontSize: "14px",
fontWeight: "500",
cursor: isLoading || !selectedDate || !selectedTime ? "not-allowed" : "pointer"
},
children: isLoading ? "Scheduling..." : "Schedule"
}
)
]
}
)
]
}
)
}
);
};
// src/components/Broadcasts/BroadcastScheduleButton.tsx
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
var BroadcastScheduleButton = ({
broadcastId,
sendStatus = "draft" /* DRAFT */,
providerId
}) => {
const [showModal, setShowModal] = useState4(false);
if (sendStatus !== "draft" /* DRAFT */) {
return null;
}
if (!providerId) {
return /* @__PURE__ */ jsxs5(
"button",
{
disabled: true,
title: "Save the broadcast first to sync with the email provider",
style: {
padding: "8px 16px",
border: "none",
borderRadius: "4px",
backgroundColor: "var(--theme-elevation-200, #ccc)",
color: "var(--theme-elevation-500, #666)",
fontSize: "14px",
fontWeight: "500",
cursor: "not-allowed",
display: "inline-flex",
alignItems: "center",
gap: "6px"
},
children: [
/* @__PURE__ */ jsx6("span", { children: "\u{1F4C5}" }),
"Schedule (save first)"
]
}
);
}
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
/* @__PURE__ */ jsxs5(
"button",
{
onClick: () => setShowModal(true),
style: {
padding: "8px 16px",
border: "none",
borderRadius: "4px",
backgroundColor: "var(--theme-success-500, #22c55e)",
color: "#fff",
fontSize: "14px",
fontWeight: "500",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: "6px"
},
children: [
/* @__PURE__ */ jsx6("span", { children: "\u{1F4C5}" }),
"Schedule Send"
]
}
),
showModal && /* @__PURE__ */ jsx6(
ScheduleModal,
{
broadcastId,
onClose: () => setShowModal(false)
}
)
] });
};
// src/components/Broadcasts/CancelScheduleButton.tsx
import { useState as useState5, useCallback as useCallback3 } from "react";
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
var CancelScheduleButton = ({
broadcastId,
sendStatus = "draft" /* DRAFT */,
scheduledAt
}) => {
const [isLoading, setIsLoading] = useState5(false);
if (sendStatus !== "scheduled" /* SCHEDULED */) {
return null;
}
const handleCancel = useCallback3(async () => {
const formattedDate = scheduledAt ? new Date(scheduledAt).toLocaleString() : "unknown time";
const confirmed = window.confirm(
`Are you sure you want to cancel this scheduled broadcast?
It was scheduled for: ${formattedDate}
The broadcast will be returned to draft status.`
);
if (!confirmed) return;
setIsLoading(true);
try {
const response = await fetch(`/api/broadcasts/${broadcastId}/schedule`, {
method: "DELETE"
});
const data = await response.json();
if (!data.success) {
alert(data.error || "Failed to cancel schedule");
return;
}
window.location.reload();
} catch (err) {
alert("Network error. Please try again.");
} finally {
setIsLoading(false);
}
}, [broadcastId, scheduledAt]);
const formattedScheduledAt = scheduledAt ? new Date(scheduledAt).toLocaleString() : null;
return /* @__PURE__ */ jsxs6(
"div",
{
style: {
display: "flex",
alignItems: "center",
gap: "16px"
},
children: [
formattedScheduledAt && /* @__PURE__ */ jsxs6(
"span",
{
style: {
fontSize: "14px",
color: "var(--theme-elevation-600, #666)"
},
children: [
"Scheduled for: ",
/* @__PURE__ */ jsx7("strong", { children: formattedScheduledAt })
]
}
),
/* @__PURE__ */ jsxs6(
"button",
{
onClick: handleCancel,
disabled: isLoading,
style: {
padding: "8px 16px",
border: "1px solid var(--theme-error-500, #ef4444)",
borderRadius: "4px",
backgroundColor: "transparent",
color: "var(--theme-error-500, #ef4444)",
fontSize: "14px",
fontWeight: "500",
cursor: isLoading ? "not-allowed" : "pointer",
display: "inline-flex",
alignItems: "center",
gap: "6px",
opacity: isLoading ? 0.6 : 1
},
children: [
/* @__PURE__ */ jsx7("span", { children: "\u2715" }),
isLoading ? "Cancelling..." : "Cancel Schedule"
]
}
)
]
}
);
};
// src/components/Broadcasts/BroadcastScheduleField.tsx
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
var BroadcastScheduleField = () => {
const { id } = useDocumentInfo();
const sendStatusField = useFormFields2(([fields]) => fields.sendStatus);
const providerIdField = useFormFields2(([fields]) => fields.providerId);
const scheduledAtField = useFormFields2(([fields]) => fields.scheduledAt);
const emailOnlyField = useFormFields2(([fields]) => fields.emailOnly);
const sendStatus = sendStatusField?.value;
const providerId = providerIdField?.value;
const scheduledAt = scheduledAtField?.value;
const emailOnly = emailOnlyField?.value;
if (!id) {
return /* @__PURE__ */ jsx8(
"div",
{
style: {
padding: "16px",
backgroundColor: "var(--theme-elevation-50, #f9f9f9)",
borderRadius: "4px",
fontSize: "14px",
color: "var(--theme-elevation-600, #666)"
},
children: "Save the broadcast to enable scheduling options."
}
);
}
if (sendStatus === "sent" /* SENT */ || sendStatus === "sending" /* SENDING */) {
return /* @__PURE__ */ jsx8(
"div",
{
style: {
padding: "16px",
backgroundColor: "var(--theme-elevation-50, #f9f9f9)",
borderRadius: "4px",
fontSize: "14px",
color: "var(--theme-elevation-600, #666)"
},
children: sendStatus === "sent" /* SENT */ ? "This broadcast has been sent and cannot be rescheduled." : "This broadcast is currently being sent."
}
);
}
if (sendStatus === "failed" /* FAILED */) {
return /* @__PURE__ */ jsx8(
"div",
{
style: {
padding: "16px",
backgroundColor: "var(--theme-error-100, #fef2f2)",
borderRadius: "4px",
fontSize: "14px",
color: "var(--theme-error-600, #dc2626)"
},
children: "This broadcast failed to send. Edit and save to return it to draft status."
}
);
}
if (!emailOnly) {
return /* @__PURE__ */ jsxs7(
"div",
{
style: {
padding: "16px",
backgroundColor: "var(--theme-elevation-50, #f9f9f9)",
borderRadius: "4px"
},
children: [
/* @__PURE__ */ jsx8("div", { style: { marginBottom: "8px", fontWeight: 500, fontSize: "14px" }, children: "Email Scheduling" }),
sendStatus === "scheduled" /* SCHEDULED */ && scheduledAt ? /* @__PURE__ */ jsxs7("div", { style: { fontSize: "14px", color: "var(--theme-elevation-600, #666)" }, children: [
/* @__PURE__ */ jsxs7("div", { style: { marginBottom: "8px" }, children: [
"\u2713 Email scheduled to send:",
" ",
/* @__PURE__ */ jsx8("strong", { children: new Date(scheduledAt).toLocaleString() })
] }),
/* @__PURE__ */ jsx8("div", { style: { fontSize: "13px", color: "var(--theme-elevation-500, #888)" }, children: 'This email will be sent when the website publish time is reached. To change the time, use the "Schedule Publish" button in the sidebar. To cancel, remove the scheduled publish date.' })
] }) : sendStatus === "draft" /* DRAFT */ ? /* @__PURE__ */ jsxs7("div", { style: { fontSize: "14px", color: "var(--theme-elevation-600, #666)" }, children: [
/* @__PURE__ */ jsxs7("div", { style: { marginBottom: "8px" }, children: [
"Use Payload's ",
/* @__PURE__ */ jsx8("strong", { children: "Schedule Publish" }),
" feature (in the sidebar) to schedule both the website publish and email send together."
] }),
/* @__PURE__ */ jsxs7("div", { style: { fontSize: "13px", color: "var(--theme-elevation-500, #888)" }, children: [
'\u2022 Click "Publish" to publish the website and send the email immediately',
/* @__PURE__ */ jsx8("br", {}),
'\u2022 Click "Schedule Publish" to schedule both for a future date/time'
] })
] }) : /* @__PURE__ */ jsxs7("div", { style: { fontSize: "14px", color: "var(--theme-elevation-600, #666)" }, children: [
"Status: ",
sendStatus || "unknown"
] })
]
}
);
}
return /* @__PURE__ */ jsxs7(
"div",
{
style: {
padding: "16px",
backgroundColor: "var(--theme-elevation-50, #f9f9f9)",
borderRadius: "4px"
},
children: [
/* @__PURE__ */ jsx8("div", { style: { marginBottom: "8px", fontWeight: 500, fontSize: "14px" }, children: "Email-Only Scheduling" }),
sendStatus === "scheduled" /* SCHEDULED */ ? /* @__PURE__ */ jsx8(
CancelScheduleButton,
{
broadcastId: String(id),
sendStatus,
scheduledAt
}
) : /* @__PURE__ */ jsx8(
BroadcastScheduleButton,
{
broadcastId: String(id),
sendStatus,
providerId
}
)
]
}
);
};
// src/components/Broadcasts/SyncStatusField.tsx
import { useState as useState6, useCallback as useCallback4 } from "react";
import { useDocumentInfo as useDocumentInfo2, useFormFields as useFormFields3 } from "@payloadcms/ui";
import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
var SyncStatusField = () => {
const { id } = useDocumentInfo2();
const [isRetrying, setIsRetrying] = useState6(false);
const [retryResult, setRetryResult] = useState6(null);
const syncStatusField = useFormFields3(([fields]) => fields.providerSyncStatus);
const syncErrorField = useFormFields3(([fields]) => fields.providerSyncError);
const lastSyncField = useFormFields3(([fields]) => fields.lastSyncAttempt);
const syncStatus = syncStatusField?.value;
const syncError = syncErrorField?.value;
const lastSyncAttempt = lastSyncField?.value;
const handleRetrySync = useCallback4(async () => {
if (!id) return;
setIsRetrying(true);
setRetryResult(null);
try {
const response = await fetch(`/api/broadcasts/${id}/retry-sync`, {
method: "POST"
});
const data = await response.json();
if (data.success) {
setRetryResult({ success: true, message: "Sync successful! Refreshing..." });
setTimeout(() => window.location.reload(), 1500);
} else {
setRetryResult({ success: false, message: data.error || "Retry failed" });
}
} catch (err) {
setRetryResult({ success: false, message: "Network error. Please try again." });
} finally {
setIsRetrying(false);
}
}, [id]);
const formattedLastSync = lastSyncAttempt ? new Date(lastSyncAttempt).toLocaleString() : null;
if (syncStatus === "pending" || !syncStatus) {
return /* @__PURE__ */ jsxs8(
"div",
{
style: {
padding: "12px 16px",
backgroundColor: "var(--theme-elevation-100, #f5f5f5)",
borderRadius: "4px",
fontSize: "14px",
color: "var(--theme-elevation-600, #666)"
},
children: [
/* @__PURE__ */ jsxs8("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
/* @__PURE__ */ jsx9("span", { style: { fontSize: "16px" }, children: "\u23F3" }),
/* @__PURE__ */ jsx9("span", { children: "Pending sync with email provider" })
] }),
/* @__PURE__ */ jsx9("div", { style: { fontSize: "12px", marginTop: "4px", color: "var(--theme-elevation-500, #888)" }, children: "Save the broadcast with content to sync with the provider." })
]
}
);
}
if (syncStatus === "synced") {
return /* @__PURE__ */ jsxs8(
"div",
{
style: {
padding: "12px 16px",
backgroundColor: "var(--theme-success-100, #dcfce7)",
borderRadius: "4px",
fontSize: "14px",
color: "var(--theme-success-700, #15803d)"
},
children: [
/* @__PURE__ */ jsxs8("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
/* @__PURE__ */ jsx9("span", { style: { fontSize: "16px" }, children: "\u2713" }),
/* @__PURE__ */ jsx9("span", { children: "Synced with email provider" })
] }),
formattedLastSync && /* @__PURE__ */ jsxs8("div", { style: { fontSize: "12px", marginTop: "4px", opacity: 0.8 }, children: [
"Last synced: ",
formattedLastSync
] })
]
}
);
}
if (syncStatus === "failed") {
return /* @__PURE__ */ jsxs8(
"div",
{
style: {
padding: "12px 16px",
backgroundColor: "var(--theme-error-100, #fef2f2)",
borderRadius: "4px",
fontSize: "14px",
border: "1px solid var(--theme-error-200, #fecaca)"
},
children: [
/* @__PURE__ */ jsxs8(
"div",
{
style: {
display: "flex",
alignItems: "center",
gap: "8px",
color: "var(--theme-error-700, #b91c1c)",
fontWeight: 500
},
children: [
/* @__PURE__ */ jsx9("span", { style: { fontSize: "16px" }, children: "\u26A0" }),
/* @__PURE__ */ jsx9("span", { children: "Sync failed" })
]
}
),
syncError && /* @__PURE__ */ jsx9(
"div",
{
style: {
fontSize: "13px",
marginTop: "8px",
padding: "8px",
backgroundColor: "var(--theme-error-50, #fff5f5)",
borderRadius: "4px",
color: "var(--theme-error-600, #dc2626)",
fontFamily: "monospace",
wordBreak: "break-word"
},
children: syncError
}
),
formattedLastSync && /* @__PURE__ */ jsxs8(
"div",
{
style: {
fontSize: "12px",
marginTop: "8px",
color: "var(--theme-elevation-500, #888)"
},
children: [
"Last attempt: ",
formattedLastSync
]
}
),
/* @__PURE__ */ jsxs8("div", { style: { marginTop: "12px", display: "flex", gap: "8px", alignItems: "center" }, children: [
/* @__PURE__ */ jsx9(
"button",
{
onClick: handleRetrySync,
disabled: isRetrying,
style: {
padding: "6px 12px",
border: "none",
borderRadius: "4px",
backgroundColor: "var(--theme-elevation-800, #333)",
color: "#fff",
fontSize: "13px",
fontWeight: 500,
cursor: isRetrying ? "not-allowed" : "pointer",
opacity: isRetrying ? 0.6 : 1,
display: "inline-flex",
alignItems: "center",
gap: "6px"
},
children: isRetrying ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
/* @__PURE__ */ jsx9("span", { style: { animation: "spin 1s linear infinite" }, children: "\u21BB" }),
"Retrying..."
] }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
/* @__PURE__ */ jsx9("span", { children: "\u21BB" }),
"Retry Sync"
] })
}
),
retryResult && /* @__PURE__ */ jsx9(
"span",
{
style: {
fontSize: "13px",
color: retryResult.success ? "var(--theme-success-600, #16a34a)" : "var(--theme-error-600, #dc2626)"
},
children: retryResult.message
}
)
] }),
/* @__PURE__ */ jsx9(
"div",
{
style: {
fontSize: "12px",
marginTop: "8px",
color: "var(--theme-elevation-500, #888)"
},
children: "The email content may be out of sync with the provider. Try saving again or click Retry."
}
)
]
}
);
}
return /* @__PURE__ */ jsxs8(
"div",
{
style: {
padding: "12px 16px",
backgroundColor: "var(--theme-elevation-100, #f5f5f5)",
borderRadius: "4px",
fontSize: "14px",
color: "var(--theme-elevation-600, #666)"
},
children: [
"Status: ",
syncStatus || "unknown"
]
}
);
};
export {
BroadcastInlinePreview,
BroadcastScheduleButton,
BroadcastScheduleField,
CancelScheduleButton,
EmailPreview,
ScheduleModal,
StatusBadge,
SyncStatusField,
WebhookConfiguration
};