payload-plugin-newsletter
Version:
Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration
1,668 lines • 60.4 kB
JavaScript
"use strict";
"use client";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/exports/components.ts
var components_exports = {};
__export(components_exports, {
BroadcastEditor: () => BroadcastEditor,
EmailPreview: () => EmailPreview,
EmailPreviewField: () => EmailPreviewField,
MagicLinkVerify: () => MagicLinkVerify,
NewsletterForm: () => NewsletterForm,
PreferencesForm: () => PreferencesForm,
createMagicLinkVerify: () => createMagicLinkVerify,
createNewsletterForm: () => createNewsletterForm,
createPreferencesForm: () => createPreferencesForm,
useNewsletterAuth: () => useNewsletterAuth
});
module.exports = __toCommonJS(components_exports);
// src/components/NewsletterForm.tsx
var import_react = require("react");
var import_jsx_runtime = require("react/jsx-runtime");
var defaultStyles = {
form: {
display: "flex",
flexDirection: "column",
gap: "1rem",
maxWidth: "400px",
margin: "0 auto"
},
inputGroup: {
display: "flex",
flexDirection: "column",
gap: "0.5rem"
},
label: {
fontSize: "0.875rem",
fontWeight: "500",
color: "#374151"
},
input: {
padding: "0.5rem 0.75rem",
fontSize: "1rem",
border: "1px solid #e5e7eb",
borderRadius: "0.375rem",
outline: "none",
transition: "border-color 0.2s"
},
button: {
padding: "0.75rem 1.5rem",
fontSize: "1rem",
fontWeight: "500",
color: "#ffffff",
backgroundColor: "#3b82f6",
border: "none",
borderRadius: "0.375rem",
cursor: "pointer",
transition: "background-color 0.2s"
},
buttonDisabled: {
opacity: 0.5,
cursor: "not-allowed"
},
error: {
fontSize: "0.875rem",
color: "#ef4444",
marginTop: "0.25rem"
},
success: {
fontSize: "0.875rem",
color: "#10b981",
marginTop: "0.25rem"
},
checkbox: {
display: "flex",
alignItems: "center",
gap: "0.5rem"
},
checkboxInput: {
width: "1rem",
height: "1rem"
},
checkboxLabel: {
fontSize: "0.875rem",
color: "#374151"
}
};
var NewsletterForm = ({
onSuccess,
onError,
showName = false,
showPreferences = false,
leadMagnet,
className,
styles: customStyles = {},
apiEndpoint = "/api/newsletter/subscribe",
buttonText = "Subscribe",
loadingText = "Subscribing...",
successMessage = "Successfully subscribed!",
placeholders = {
email: "Enter your email",
name: "Enter your name"
},
labels = {
email: "Email",
name: "Name",
newsletter: "Newsletter updates",
announcements: "Product announcements"
}
}) => {
const [email, setEmail] = (0, import_react.useState)("");
const [name, setName] = (0, import_react.useState)("");
const [preferences, setPreferences] = (0, import_react.useState)({
newsletter: true,
announcements: true
});
const [loading, setLoading] = (0, import_react.useState)(false);
const [error, setError] = (0, import_react.useState)(null);
const [success, setSuccess] = (0, import_react.useState)(false);
const styles = {
form: { ...defaultStyles.form, ...customStyles.form },
inputGroup: { ...defaultStyles.inputGroup, ...customStyles.inputGroup },
label: { ...defaultStyles.label, ...customStyles.label },
input: { ...defaultStyles.input, ...customStyles.input },
button: { ...defaultStyles.button, ...customStyles.button },
buttonDisabled: { ...defaultStyles.buttonDisabled, ...customStyles.buttonDisabled },
error: { ...defaultStyles.error, ...customStyles.error },
success: { ...defaultStyles.success, ...customStyles.success },
checkbox: { ...defaultStyles.checkbox, ...customStyles.checkbox },
checkboxInput: { ...defaultStyles.checkboxInput, ...customStyles.checkboxInput },
checkboxLabel: { ...defaultStyles.checkboxLabel, ...customStyles.checkboxLabel }
};
const handleSubmit = async (e) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const payload = {
email,
...showName && name && { name },
...showPreferences && { preferences },
...leadMagnet && { leadMagnet: leadMagnet.id },
metadata: {
signupPage: window.location.href,
...typeof window !== "undefined" && window.location.search && {
utmParams: Object.fromEntries(new URLSearchParams(window.location.search))
}
}
};
const response = await fetch(apiEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || data.errors?.join(", ") || "Subscription failed");
}
setSuccess(true);
setEmail("");
setName("");
if (onSuccess) {
onSuccess(data.subscriber);
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "An error occurred";
setError(errorMessage);
if (onError) {
onError(new Error(errorMessage));
}
} finally {
setLoading(false);
}
};
if (success && !showPreferences) {
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style: styles.form, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.success, children: successMessage }) });
}
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: handleSubmit, className, style: styles.form, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.inputGroup, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "email", style: styles.label, children: labels.email }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"input",
{
id: "email",
type: "email",
value: email,
onChange: (e) => setEmail(e.target.value),
placeholder: placeholders.email,
required: true,
disabled: loading,
style: {
...styles.input,
...loading && { opacity: 0.5 }
}
}
)
] }),
showName && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.inputGroup, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "name", style: styles.label, children: labels.name }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"input",
{
id: "name",
type: "text",
value: name,
onChange: (e) => setName(e.target.value),
placeholder: placeholders.name,
disabled: loading,
style: {
...styles.input,
...loading && { opacity: 0.5 }
}
}
)
] }),
showPreferences && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.inputGroup, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { style: styles.label, children: "Email Preferences" }),
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.checkbox, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"input",
{
id: "newsletter",
type: "checkbox",
checked: preferences.newsletter,
onChange: (e) => setPreferences({ ...preferences, newsletter: e.target.checked }),
disabled: loading,
style: styles.checkboxInput
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "newsletter", style: styles.checkboxLabel, children: labels.newsletter })
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.checkbox, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"input",
{
id: "announcements",
type: "checkbox",
checked: preferences.announcements,
onChange: (e) => setPreferences({ ...preferences, announcements: e.target.checked }),
disabled: loading,
style: styles.checkboxInput
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "announcements", style: styles.checkboxLabel, children: labels.announcements })
] })
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"button",
{
type: "submit",
disabled: loading,
style: {
...styles.button,
...loading && styles.buttonDisabled
},
children: loading ? loadingText : buttonText
}
),
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, children: error }),
success && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.success, children: successMessage })
] });
};
function createNewsletterForm(defaultProps) {
return (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NewsletterForm, { ...defaultProps, ...props });
}
// src/components/PreferencesForm.tsx
var import_react2 = require("react");
var import_jsx_runtime2 = require("react/jsx-runtime");
var defaultStyles2 = {
container: {
maxWidth: "600px",
margin: "0 auto",
padding: "2rem"
},
heading: {
fontSize: "1.5rem",
fontWeight: "600",
marginBottom: "1.5rem",
color: "#111827"
},
form: {
display: "flex",
flexDirection: "column",
gap: "1.5rem"
},
section: {
padding: "1.5rem",
backgroundColor: "#f9fafb",
borderRadius: "0.5rem",
border: "1px solid #e5e7eb"
},
sectionTitle: {
fontSize: "1.125rem",
fontWeight: "500",
marginBottom: "1rem",
color: "#111827"
},
inputGroup: {
display: "flex",
flexDirection: "column",
gap: "0.5rem"
},
label: {
fontSize: "0.875rem",
fontWeight: "500",
color: "#374151"
},
input: {
padding: "0.5rem 0.75rem",
fontSize: "1rem",
border: "1px solid #e5e7eb",
borderRadius: "0.375rem",
outline: "none",
transition: "border-color 0.2s"
},
select: {
padding: "0.5rem 0.75rem",
fontSize: "1rem",
border: "1px solid #e5e7eb",
borderRadius: "0.375rem",
outline: "none",
backgroundColor: "#ffffff"
},
checkbox: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "0.5rem"
},
checkboxInput: {
width: "1rem",
height: "1rem"
},
checkboxLabel: {
fontSize: "0.875rem",
color: "#374151"
},
buttonGroup: {
display: "flex",
gap: "1rem",
marginTop: "1rem"
},
button: {
padding: "0.75rem 1.5rem",
fontSize: "1rem",
fontWeight: "500",
borderRadius: "0.375rem",
cursor: "pointer",
transition: "all 0.2s",
border: "none"
},
primaryButton: {
color: "#ffffff",
backgroundColor: "#3b82f6"
},
secondaryButton: {
color: "#374151",
backgroundColor: "#ffffff",
border: "1px solid #e5e7eb"
},
dangerButton: {
color: "#ffffff",
backgroundColor: "#ef4444"
},
error: {
fontSize: "0.875rem",
color: "#ef4444",
marginTop: "0.5rem"
},
success: {
fontSize: "0.875rem",
color: "#10b981",
marginTop: "0.5rem"
},
info: {
fontSize: "0.875rem",
color: "#6b7280",
marginTop: "0.5rem"
}
};
var PreferencesForm = ({
subscriber: initialSubscriber,
onSuccess,
onError,
className,
styles: customStyles = {},
sessionToken,
apiEndpoint = "/api/newsletter/preferences",
showUnsubscribe = true,
locales = ["en"],
labels = {
title: "Newsletter Preferences",
personalInfo: "Personal Information",
emailPreferences: "Email Preferences",
name: "Name",
language: "Preferred Language",
newsletter: "Newsletter updates",
announcements: "Product announcements",
saveButton: "Save Preferences",
unsubscribeButton: "Unsubscribe",
saving: "Saving...",
saved: "Preferences saved successfully!",
unsubscribeConfirm: "Are you sure you want to unsubscribe? This cannot be undone."
}
}) => {
const [subscriber, setSubscriber] = (0, import_react2.useState)(initialSubscriber || {});
const [loading, setLoading] = (0, import_react2.useState)(false);
const [loadingData, setLoadingData] = (0, import_react2.useState)(!initialSubscriber);
const [error, setError] = (0, import_react2.useState)(null);
const [success, setSuccess] = (0, import_react2.useState)(false);
const styles = {
container: { ...defaultStyles2.container, ...customStyles.container },
heading: { ...defaultStyles2.heading, ...customStyles.heading },
form: { ...defaultStyles2.form, ...customStyles.form },
section: { ...defaultStyles2.section, ...customStyles.section },
sectionTitle: { ...defaultStyles2.sectionTitle, ...customStyles.sectionTitle },
inputGroup: { ...defaultStyles2.inputGroup, ...customStyles.inputGroup },
label: { ...defaultStyles2.label, ...customStyles.label },
input: { ...defaultStyles2.input, ...customStyles.input },
select: { ...defaultStyles2.select, ...customStyles.select },
checkbox: { ...defaultStyles2.checkbox, ...customStyles.checkbox },
checkboxInput: { ...defaultStyles2.checkboxInput, ...customStyles.checkboxInput },
checkboxLabel: { ...defaultStyles2.checkboxLabel, ...customStyles.checkboxLabel },
buttonGroup: { ...defaultStyles2.buttonGroup, ...customStyles.buttonGroup },
button: { ...defaultStyles2.button, ...customStyles.button },
primaryButton: { ...defaultStyles2.primaryButton, ...customStyles.primaryButton },
secondaryButton: { ...defaultStyles2.secondaryButton, ...customStyles.secondaryButton },
dangerButton: { ...defaultStyles2.dangerButton, ...customStyles.dangerButton },
error: { ...defaultStyles2.error, ...customStyles.error },
success: { ...defaultStyles2.success, ...customStyles.success },
info: { ...defaultStyles2.info, ...customStyles.info }
};
(0, import_react2.useEffect)(() => {
if (!initialSubscriber && sessionToken) {
fetchPreferences();
}
}, []);
const fetchPreferences = async () => {
try {
const response = await fetch(apiEndpoint, {
headers: {
"Authorization": `Bearer ${sessionToken}`
}
});
if (!response.ok) {
throw new Error("Failed to load preferences");
}
const data = await response.json();
setSubscriber(data.subscriber);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load preferences");
if (onError) {
onError(err instanceof Error ? err : new Error("Failed to load preferences"));
}
} finally {
setLoadingData(false);
}
};
const handleSave = async (e) => {
e.preventDefault();
setError(null);
setSuccess(false);
setLoading(true);
try {
const response = await fetch(apiEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${sessionToken}`
},
body: JSON.stringify({
name: subscriber.name,
locale: subscriber.locale,
emailPreferences: subscriber.emailPreferences
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Failed to save preferences");
}
setSubscriber(data.subscriber);
setSuccess(true);
if (onSuccess) {
onSuccess(data.subscriber);
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "An error occurred";
setError(errorMessage);
if (onError) {
onError(new Error(errorMessage));
}
} finally {
setLoading(false);
}
};
const handleUnsubscribe = async () => {
if (!window.confirm(labels.unsubscribeConfirm)) {
return;
}
setLoading(true);
setError(null);
try {
const response = await fetch("/api/newsletter/unsubscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${sessionToken}`
},
body: JSON.stringify({
email: subscriber.email
})
});
if (!response.ok) {
throw new Error("Failed to unsubscribe");
}
setSubscriber({ ...subscriber, subscriptionStatus: "unsubscribed" });
if (onSuccess) {
onSuccess({ ...subscriber, subscriptionStatus: "unsubscribed" });
}
} catch (err) {
setError("Failed to unsubscribe. Please try again.");
if (onError) {
onError(err instanceof Error ? err : new Error("Failed to unsubscribe"));
}
} finally {
setLoading(false);
}
};
if (loadingData) {
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, style: styles.container, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles.info, children: "Loading preferences..." }) });
}
if (subscriber.subscriptionStatus === "unsubscribed") {
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className, style: styles.container, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { style: styles.heading, children: "Unsubscribed" }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles.info, children: "You have been unsubscribed from all emails. To resubscribe, please sign up again." })
] });
}
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className, style: styles.container, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { style: styles.heading, children: labels.title }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { onSubmit: handleSave, style: styles.form, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.section, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { style: styles.sectionTitle, children: labels.personalInfo }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.inputGroup, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: "name", style: styles.label, children: labels.name }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"input",
{
id: "name",
type: "text",
value: subscriber.name || "",
onChange: (e) => setSubscriber({ ...subscriber, name: e.target.value }),
disabled: loading,
style: styles.input
}
)
] }),
locales.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.inputGroup, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: "locale", style: styles.label, children: labels.language }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"select",
{
id: "locale",
value: subscriber.locale || locales[0],
onChange: (e) => setSubscriber({ ...subscriber, locale: e.target.value }),
disabled: loading,
style: styles.select,
children: locales.map((locale) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: locale, children: locale.toUpperCase() }, locale))
}
)
] })
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.section, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { style: styles.sectionTitle, children: labels.emailPreferences }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.checkbox, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"input",
{
id: "pref-newsletter",
type: "checkbox",
checked: subscriber.emailPreferences?.newsletter ?? true,
onChange: (e) => setSubscriber({
...subscriber,
emailPreferences: {
...subscriber.emailPreferences,
newsletter: e.target.checked
}
}),
disabled: loading,
style: styles.checkboxInput
}
),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: "pref-newsletter", style: styles.checkboxLabel, children: labels.newsletter })
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.checkbox, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"input",
{
id: "pref-announcements",
type: "checkbox",
checked: subscriber.emailPreferences?.announcements ?? true,
onChange: (e) => setSubscriber({
...subscriber,
emailPreferences: {
...subscriber.emailPreferences,
announcements: e.target.checked
}
}),
disabled: loading,
style: styles.checkboxInput
}
),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: "pref-announcements", style: styles.checkboxLabel, children: labels.announcements })
] })
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.buttonGroup, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"button",
{
type: "submit",
disabled: loading,
style: {
...styles.button,
...styles.primaryButton,
...loading && { opacity: 0.5, cursor: "not-allowed" }
},
children: loading ? labels.saving : labels.saveButton
}
),
showUnsubscribe && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"button",
{
type: "button",
onClick: handleUnsubscribe,
disabled: loading,
style: {
...styles.button,
...styles.dangerButton,
...loading && { opacity: 0.5, cursor: "not-allowed" }
},
children: labels.unsubscribeButton
}
)
] }),
error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles.error, children: error }),
success && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles.success, children: labels.saved })
] })
] });
};
function createPreferencesForm(defaultProps) {
return (props) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PreferencesForm, { ...defaultProps, ...props });
}
// src/components/MagicLinkVerify.tsx
var import_react3 = require("react");
var import_jsx_runtime3 = require("react/jsx-runtime");
var defaultStyles3 = {
container: {
maxWidth: "400px",
margin: "4rem auto",
padding: "2rem",
textAlign: "center"
},
heading: {
fontSize: "1.5rem",
fontWeight: "600",
marginBottom: "1rem",
color: "#111827"
},
message: {
fontSize: "1rem",
color: "#6b7280",
marginBottom: "1.5rem"
},
error: {
fontSize: "1rem",
color: "#ef4444",
marginBottom: "1.5rem"
},
button: {
padding: "0.75rem 1.5rem",
fontSize: "1rem",
fontWeight: "500",
color: "#ffffff",
backgroundColor: "#3b82f6",
border: "none",
borderRadius: "0.375rem",
cursor: "pointer",
transition: "background-color 0.2s"
}
};
var MagicLinkVerify = ({
token: propToken,
onSuccess,
onError,
apiEndpoint = "/api/newsletter/verify-magic-link",
className,
styles: customStyles = {},
labels = {
verifying: "Verifying your magic link...",
success: "Successfully verified! Redirecting...",
error: "Failed to verify magic link",
expired: "This magic link has expired. Please request a new one.",
invalid: "This magic link is invalid. Please request a new one.",
redirecting: "Redirecting to your preferences...",
tryAgain: "Try Again"
}
}) => {
const [status, setStatus] = (0, import_react3.useState)("verifying");
const [error, setError] = (0, import_react3.useState)(null);
const [_sessionToken, setSessionToken] = (0, import_react3.useState)(null);
const styles = {
container: { ...defaultStyles3.container, ...customStyles.container },
heading: { ...defaultStyles3.heading, ...customStyles.heading },
message: { ...defaultStyles3.message, ...customStyles.message },
error: { ...defaultStyles3.error, ...customStyles.error },
button: { ...defaultStyles3.button, ...customStyles.button }
};
(0, import_react3.useEffect)(() => {
const token = propToken || new URLSearchParams(window.location.search).get("token");
if (token) {
verifyToken(token);
} else {
setStatus("error");
setError(labels.invalid || "Invalid magic link");
}
}, [propToken]);
const verifyToken = async (token) => {
try {
const response = await fetch(apiEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ token })
});
const data = await response.json();
if (!response.ok) {
if (data.error?.includes("expired")) {
throw new Error(labels.expired);
}
throw new Error(data.error || labels.error);
}
setStatus("success");
setSessionToken(data.sessionToken);
if (typeof window !== "undefined" && data.sessionToken) {
localStorage.setItem("newsletter_session", data.sessionToken);
}
if (onSuccess) {
onSuccess(data.sessionToken, data.subscriber);
}
} catch (err) {
setStatus("error");
const errorMessage = err instanceof Error ? err.message : labels.error || "Verification failed";
setError(errorMessage);
if (onError) {
onError(err instanceof Error ? err : new Error(errorMessage));
}
}
};
const handleTryAgain = () => {
window.location.href = "/";
};
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className, style: styles.container, children: [
status === "verifying" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: styles.heading, children: "Verifying" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles.message, children: labels.verifying })
] }),
status === "success" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: styles.heading, children: "Success!" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles.message, children: labels.success })
] }),
status === "error" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: styles.heading, children: "Verification Failed" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles.error, children: error }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: handleTryAgain, style: styles.button, children: labels.tryAgain })
] })
] });
};
function createMagicLinkVerify(defaultProps) {
return (props) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MagicLinkVerify, { ...defaultProps, ...props });
}
// src/hooks/useNewsletterAuth.ts
var import_react4 = require("react");
function useNewsletterAuth(_options = {}) {
const [subscriber, setSubscriber] = (0, import_react4.useState)(null);
const [isLoading, setIsLoading] = (0, import_react4.useState)(true);
const [error, setError] = (0, import_react4.useState)(null);
const checkAuth = (0, import_react4.useCallback)(async () => {
try {
const response = await fetch("/api/newsletter/me", {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json"
}
});
if (response.ok) {
const data = await response.json();
setSubscriber(data.subscriber);
setError(null);
} else {
setSubscriber(null);
if (response.status !== 401) {
setError(new Error("Failed to check authentication"));
}
}
} catch (err) {
console.error("Auth check failed:", err);
setError(err instanceof Error ? err : new Error("An error occurred"));
setSubscriber(null);
} finally {
setIsLoading(false);
}
}, []);
(0, import_react4.useEffect)(() => {
checkAuth();
}, [checkAuth]);
const signOut = (0, import_react4.useCallback)(async () => {
try {
const response = await fetch("/api/newsletter/signout", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json"
}
});
if (response.ok) {
setSubscriber(null);
setError(null);
} else {
throw new Error("Failed to sign out");
}
} catch (err) {
console.error("Sign out error:", err);
setError(err instanceof Error ? err : new Error("Sign out failed"));
throw err;
}
}, []);
const refreshAuth = (0, import_react4.useCallback)(async () => {
setIsLoading(true);
await checkAuth();
}, [checkAuth]);
const login = (0, import_react4.useCallback)(async (_token) => {
await refreshAuth();
}, [refreshAuth]);
return {
subscriber,
isAuthenticated: !!subscriber,
isLoading,
loading: isLoading,
// Alias for backward compatibility
error,
signOut,
logout: signOut,
// Alias for backward compatibility
refreshAuth,
refreshSubscriber: refreshAuth,
// Alias for backward compatibility
login
// For backward compatibility
};
}
// src/components/Broadcasts/EmailPreview.tsx
var import_react5 = require("react");
// src/utils/emailSafeHtml.ts
var import_isomorphic_dompurify = __toESM(require("isomorphic-dompurify"), 1);
var EMAIL_SAFE_CONFIG = {
ALLOWED_TAGS: [
"p",
"br",
"strong",
"b",
"em",
"i",
"u",
"strike",
"s",
"span",
"a",
"h1",
"h2",
"h3",
"ul",
"ol",
"li",
"blockquote",
"hr"
],
ALLOWED_ATTR: ["href", "style", "target", "rel", "align"],
ALLOWED_STYLES: {
"*": [
"color",
"background-color",
"font-size",
"font-weight",
"font-style",
"text-decoration",
"text-align",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"line-height",
"border-left",
"border-left-width",
"border-left-style",
"border-left-color"
]
},
FORBID_TAGS: ["script", "style", "iframe", "object", "embed", "form", "input"],
FORBID_ATTR: ["class", "id", "onclick", "onload", "onerror"]
};
async function convertToEmailSafeHtml(editorState, options) {
const rawHtml = await lexicalToEmailHtml(editorState);
const sanitizedHtml = import_isomorphic_dompurify.default.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
if (options?.wrapInTemplate) {
return wrapInEmailTemplate(sanitizedHtml, options.preheader);
}
return sanitizedHtml;
}
async function lexicalToEmailHtml(editorState) {
const { root } = editorState;
if (!root || !root.children) {
return "";
}
const html = root.children.map((node) => convertNode(node)).join("");
return html;
}
function convertNode(node) {
switch (node.type) {
case "paragraph":
return convertParagraph(node);
case "heading":
return convertHeading(node);
case "list":
return convertList(node);
case "listitem":
return convertListItem(node);
case "blockquote":
return convertBlockquote(node);
case "text":
return convertText(node);
case "link":
return convertLink(node);
case "linebreak":
return "<br>";
default:
if (node.children) {
return node.children.map(convertNode).join("");
}
return "";
}
}
function convertParagraph(node) {
const align = getAlignment(node.format);
const children = node.children?.map(convertNode).join("") || "";
if (!children.trim()) {
return '<p style="margin: 0 0 16px 0; min-height: 1em;"> </p>';
}
return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
}
function convertHeading(node) {
const tag = node.tag || "h1";
const align = getAlignment(node.format);
const children = node.children?.map(convertNode).join("") || "";
const styles = {
h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
h3: "font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;"
};
const style = `${styles[tag] || styles.h3} text-align: ${align};`;
return `<${tag} style="${style}">${children}</${tag}>`;
}
function convertList(node) {
const tag = node.listType === "number" ? "ol" : "ul";
const children = node.children?.map(convertNode).join("") || "";
const style = tag === "ul" ? "margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc;" : "margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal;";
return `<${tag} style="${style}">${children}</${tag}>`;
}
function convertListItem(node) {
const children = node.children?.map(convertNode).join("") || "";
return `<li style="margin: 0 0 8px 0;">${children}</li>`;
}
function convertBlockquote(node) {
const children = node.children?.map(convertNode).join("") || "";
const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
return `<blockquote style="${style}">${children}</blockquote>`;
}
function convertText(node) {
let text = escapeHtml(node.text || "");
if (node.format & 1) {
text = `<strong>${text}</strong>`;
}
if (node.format & 2) {
text = `<em>${text}</em>`;
}
if (node.format & 8) {
text = `<u>${text}</u>`;
}
if (node.format & 4) {
text = `<strike>${text}</strike>`;
}
return text;
}
function convertLink(node) {
const children = node.children?.map(convertNode).join("") || "";
const url = node.fields?.url || "#";
return `<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="color: #2563eb; text-decoration: underline;">${children}</a>`;
}
function getAlignment(format) {
if (!format) return "left";
if (format & 2) return "center";
if (format & 3) return "right";
if (format & 4) return "justify";
return "left";
}
function escapeHtml(text) {
const map = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
function wrapInEmailTemplate(content, preheader) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email</title>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
</head>
<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: #333333; background-color: #f3f4f6;">
${preheader ? `<div style="display: none; max-height: 0; overflow: hidden;">${escapeHtml(preheader)}</div>` : ""}
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0;">
<tr>
<td align="center" style="padding: 20px 0;">
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
<tr>
<td style="padding: 40px 30px;">
${content}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`;
}
function replacePersonalizationTags(html, sampleData) {
return html.replace(/\{\{([^}]+)\}\}/g, (match, tag) => {
const trimmedTag = tag.trim();
return sampleData[trimmedTag] || match;
});
}
// src/utils/validateEmailHtml.ts
function validateEmailHtml(html) {
const warnings = [];
const errors = [];
const sizeInBytes = new Blob([html]).size;
if (sizeInBytes > 102400) {
warnings.push(`Email size (${Math.round(sizeInBytes / 1024)}KB) exceeds Gmail's 102KB limit - email may be clipped`);
}
if (html.includes("position:") && (html.includes("position: absolute") || html.includes("position: fixed"))) {
errors.push("Absolute/fixed positioning is not supported in most email clients");
}
if (html.includes("display: flex") || html.includes("display: grid")) {
errors.push("Flexbox and Grid layouts are not supported in many email clients");
}
if (html.includes("@media")) {
warnings.push("Media queries may not work in all email clients");
}
const hasJavaScript = html.includes("<script") || html.includes("onclick") || html.includes("onload") || html.includes("javascript:");
if (hasJavaScript) {
errors.push("JavaScript is not supported in email and will be stripped by email clients");
}
const hasExternalStyles = html.includes("<link") && html.includes("stylesheet");
if (hasExternalStyles) {
errors.push("External stylesheets are not supported - use inline styles only");
}
if (html.includes("<form") || html.includes("<input") || html.includes("<button")) {
errors.push("Forms and form elements are not reliably supported in email");
}
const unsupportedTags = [
"video",
"audio",
"iframe",
"embed",
"object",
"canvas",
"svg"
];
for (const tag of unsupportedTags) {
if (html.includes(`<${tag}`)) {
errors.push(`<${tag}> tags are not supported in email`);
}
}
const imageCount = (html.match(/<img/g) || []).length;
const linkCount = (html.match(/<a/g) || []).length;
if (imageCount > 20) {
warnings.push(`High number of images (${imageCount}) may affect email performance`);
}
const imagesWithoutAlt = (html.match(/<img(?![^>]*\balt\s*=)[^>]*>/g) || []).length;
if (imagesWithoutAlt > 0) {
warnings.push(`${imagesWithoutAlt} image(s) missing alt text - important for accessibility`);
}
const linksWithoutTarget = (html.match(/<a(?![^>]*\btarget\s*=)[^>]*>/g) || []).length;
if (linksWithoutTarget > 0) {
warnings.push(`${linksWithoutTarget} link(s) missing target="_blank" attribute`);
}
if (html.includes("margin: auto") || html.includes("margin:auto")) {
warnings.push('margin: auto is not supported in Outlook - use align="center" or tables for centering');
}
if (html.includes("background-image")) {
warnings.push("Background images are not reliably supported - consider using <img> tags instead");
}
if (html.match(/\d+\s*(rem|em)/)) {
warnings.push("rem/em units may render inconsistently - use px for reliable sizing");
}
if (html.match(/margin[^:]*:\s*-\d+/)) {
errors.push("Negative margins are not supported in many email clients");
}
const personalizationTags = html.match(/\{\{([^}]+)\}\}/g) || [];
const validTags = ["subscriber.name", "subscriber.email", "subscriber.firstName", "subscriber.lastName"];
for (const tag of personalizationTags) {
const tagContent = tag.replace(/[{}]/g, "").trim();
if (!validTags.includes(tagContent)) {
warnings.push(`Unknown personalization tag: ${tag}`);
}
}
return {
valid: errors.length === 0,
warnings,
errors,
stats: {
sizeInBytes,
imageCount,
linkCount,
hasExternalStyles,
hasJavaScript
}
};
}
// src/components/Broadcasts/EmailPreview.tsx
var import_jsx_runtime4 = require("react/jsx-runtime");
var SAMPLE_DATA = {
"subscriber.name": "John Doe",
"subscriber.firstName": "John",
"subscriber.lastName": "Doe",
"subscriber.email": "john.doe@example.com"
};
var VIEWPORT_SIZES = {
desktop: { width: 600, scale: 1 },
mobile: { width: 320, scale: 0.8 }
};
var EmailPreview = ({
content,
subject,
preheader,
channel,
mode = "desktop",
onValidation
}) => {
const [html, setHtml] = (0, import_react5.useState)("");
const [loading, setLoading] = (0, import_react5.useState)(false);
const [validationResult, setValidationResult] = (0, import_react5.useState)(null);
const iframeRef = (0, import_react5.useRef)(null);
(0, import_react5.useEffect)(() => {
const convertContent = async () => {
if (!content) {
setHtml("");
return;
}
setLoading(true);
try {
const emailHtml = await convertToEmailSafeHtml(content, {
wrapInTemplate: true,
preheader
});
const personalizedHtml = replacePersonalizationTags(emailHtml, SAMPLE_DATA);
const previewHtml = addEmailHeader(personalizedHtml, {
subject,
from: channel ? `${channel.fromName} <${channel.fromEmail}>` : "Newsletter <noreply@example.com>",
to: SAMPLE_DATA["subscriber.email"]
});
setHtml(previewHtml);
const validation = validateEmailHtml(emailHtml);
setValidationResult(validation);
onValidation?.(validation);
} catch (error) {
console.error("Failed to convert content to HTML:", error);
setHtml("<p>Error converting content to HTML</p>");
} finally {
setLoading(false);
}
};
convertContent();
}, [content, subject, preheader, channel, onValidation]);
(0, import_react5.useEffect)(() => {
if (iframeRef.current && html) {
const doc = iframeRef.current.contentDocument;
if (doc) {
doc.open();
doc.write(html);
doc.close();
}
}
}, [html]);
const viewport = VIEWPORT_SIZES[mode];
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
validationResult && (validationResult.errors.length > 0 || validationResult.warnings.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { padding: "16px", borderBottom: "1px solid #e5e7eb" }, children: [
validationResult.errors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginBottom: "12px" }, children: [
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("h4", { style: { color: "#dc2626", margin: "0 0 8px 0", fontSize: "14px" }, children: [
"Errors (",
validationResult.errors.length,
")"
] }),
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { style: { margin: 0, paddingLeft: "20px", fontSize: "13px", color: "#dc2626" }, children: validationResult.errors.map((error, index) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: error }, index)) })
] }),
validationResult.warnings.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("h4", { style: { color: "#d97706", margin: "0 0 8px 0", fontSize: "14px" }, children: [
"Warnings (",
validationResult.warnings.length,
")"
] }),
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { style: { margin: 0, paddingLeft: "20px", fontSize: "13px", color: "#d97706" }, children: validationResult.warnings.map((warning, index) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: warning }, index)) })
] })
] }),
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#f3f4f6",
padding: "20px",
overflow: "auto"
}, children: loading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { textAlign: "center", color: "#6b7280" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { children: "Loading preview..." }) }) : html ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
backgroundColor: "white",
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
borderRadius: "8px",
overflow: "hidden",
transform: `scale(${viewport.scale})`,
transformOrigin: "top center"
}, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"iframe",
{
ref: iframeRef,
title: "Email Preview",
style: {
width: `${viewport.width}px`,
height: "800px",
border: "none",
display: "block"
},
sandbox: "allow-same-origin"
}
) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { textAlign: "center", color: "#6b7280" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { children: "Start typing to see the email preview" }) }) }),
validationResult && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
padding: "12px 16px",
borderTop: "1px solid #e5e7eb",
fontSize: "13px",
color: "#6b7280",
display: "flex",
gap: "24px"
}, children: [
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
"Size: ",
Math.round(validationResult.stats.sizeInBytes / 1024),
"KB"
] }),
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
"Links: ",
validationResult.stats.linkCount
] }),
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
"Images: ",
validationResult.stats.imageCount
] }),
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
"Viewport: ",
mode === "desktop" ? "600px" : "320px"
] })
] })
] });
};
function addEmailHeader(html, headers) {
const headerHtml = `
<div style="background-color: #f9fafb; border-bottom: 1px solid #e5e7eb; padding: 16px; font-family: monospace; font-size: 13px;">
<div style="margin-bottom: 8px;"><strong>Subject:</strong> ${escapeHtml2(headers.subject)}</div>
<div style="margin-bottom: 8px;"><strong>From:</strong> ${escapeHtml2(headers.from)}</div>
<div><strong>To:</strong> ${escapeHtml2(headers.to)}</div>
</div>
`;
return html.replace(/<body[^>]*>/, `$&${headerHtml}`);
}
function escapeHtml2(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// src/components/Broadcasts/EmailPreviewField.tsx
var import_react6 = require("react");
var import_ui = require("@payloadcms/ui");
var import_jsx_runtime5 = require("react/jsx-runtime");
var EmailPreviewField = () => {
const [previewMode, setPreviewMode] = (0, import_react6.useState)("desktop");
const [isValid, setIsValid] = (0, import_react6.useState)(true);
const [validationSummary, setValidationSummary] = (0, import_react6.useState)("");
const fields = (0, import_ui.useFormFields)(([fields2]) => ({
content: fields2.content,
subject: fields2.subject,
preheader: fields2.preheader,
channel: fields2.channel
}));
const handleValidation = (result) => {
setIsValid(result.valid);
const errorCount = result.errors.length;
const warningCount = result.warnings.length;
if (errorCount > 0) {
setValidationSummary(`${errorCount} error${errorCount !== 1 ? "s" : ""}, ${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
} else if (warningCount > 0) {
setValidationSummary(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
} else {
setValidationSummary("");
}
};
const handleTestEmail = async () => {
const pathParts = window.location.pathname.split("/");
const broadcastId = pathParts[pathParts.length - 1];
if (!broadcastId || broadcastId === "create") {
alert("Please save the broadcast before sending a test email");
return;
}
try {
const response = await fetch(`/api/broadcasts/${broadcastId}/test`, {
method: "POST",
headers: {
"Content-Type": "application/json"
}
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || "Failed to send test email");
}
alert("Test email sent successfully! Check your inbox.");
} catch (error) {
alert(error instanceof Error ? error.message : "Failed to send test email");
}
};
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: {
marginTop: "24px",
border: "1px solid #e5e7eb",
borderRadius: "8px",
overflow: "hidden"
}, children: [
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "12px 16px",
borderBottom: "1px solid #e5e7eb",
backgroundColor: "#f9fafb"
}, children: [
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "16px" }, children: [
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { style: { margin: 0, fontSize: "16px", fontWeight: 600 }, children: "Email Preview" }),
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
"button",
{
type: "button",
onClick: () => setPreviewMode("desktop"),
style: {
padding: "6px 12px",
backgroundColor: previewMode === "desktop" ? "#6366f1" : "#e5e7eb",
color: previewMode === "desktop" ? "white" : "#374151",
border: "none",
borderRadius: "4px 0 0 4px",
fontSize: "14px",
cursor: "pointer"
},
children: "Desktop"
}
),
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
"button",
{
type: "button",
onClick: () => setPreviewMode("mobile"),
style: {
padding: "6px 12px",
backgroundColor: previewMode === "mobile" ? "#6366f1" : "#e5e7eb",
color: previewMode === "mobile" ? "white" : "#374151",
border: "none",
borderRadius: "0 4px 4px 0",
fontSize: "14px",
cursor: "pointer"
},
children: "Mobile"
}
)
] }),
validationSummary && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: {
padding: "6px 12px",
backgroundColor: isValid ? "#fef3c7" : "#fee2e2",
color: isValid ? "#92400e" : "#991b1b",
borderRadius: "4px",
fontSize: "13px"
}, children: validationSummary })
] }),
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
"button",
{
type: "button",
onClick: handleTestEmail,
style: {
padding: "6px 12px",
backgroundColor: "#10b981",
color: "white",
border: "none",
borderRadius: "4px",
fontSize: "14px",
cursor: "pointer"
},
children: "Send Test Email"
}
)
] }),
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { height: "600px" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
EmailPreview,
{
content: fields.content?.value || null,
subject: fields.subject?.value || "Email Subject",
preheader: fields.preheader?.value,
channel: fields.channel?.value,
mode: previewMode,
onValidation: handleValidation
}
) })
] });
};
// src/components/Broadcasts/BroadcastEditor.tsx
var import_react7 = require("react");
var import_ui2 = require("@payloadcms/ui");
var import_jsx_runtime6 = require("react/jsx-runtime");
var BroadcastEditor = (props) => {
const { value } = (0, import_ui2.useField)({ path: props.path });
const [showPreview, setShowPreview] = (0, import_react7.useState)(true);
const [previewMode, setPreviewMode] = (0, import_react7.useState)("desktop");
const [isValid, setIsValid] = (0, import_react7.useState)(true);
const [validationSummary, setValidationSummary] = (0, import_react7.useState)("");
const fields = (0, import_ui2.useFormFields)(([fields2]) => ({
subject: fields2.subject,
preheader: fields2.preheader,
channel: fields2.channel
}));
const handleValidation = (0, import_react7.useCallback)((result) => {
setIsValid(result.valid);
const errorCount = result.errors.length;
const warningCount = result.warnings.length;
if (errorCount > 0) {
setValidationSummary(`${errorCount} error${errorCount !== 1 ? "s" : ""}, ${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
} else if (warningCount > 0) {
setValidationSummary(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
} else {
setValidationSummary("");
}
}, []);
const handleTestEmail = async () => {
const pathParts = window.location.pathname.split("/");
const broadcastId = pathParts[pathParts.length - 1];
if (!broadcastId || broadcastId === "create") {
alert("Please save the broadcast before sending a test email");
return;
}
try {
const response = await fetch(`/api/broadcasts/${broadcastId}/test`, {
method: "POST",
headers: {
"Content-Type": "application/json"
}
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || "Failed to send test email");
}
alert("Test email sent successfully! Check your inbox.");
} catch (error) {
alert(error instanceof Error ? error.message : "Failed to send test email");
}
};
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { height: "600px", display: "flex", flexDirection: "column" }, children: [
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "12px 16px",
borderBottom: "1px solid #e5e7eb",
backgroundColor: "#f9fafb"
}, children: [
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "16px" }, children: [
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
"button",
{
type: "button",
onClick: () => setShowPreview(!showPreview),
style: {
padding: "6px 12px",
backgroundColor: showPreview ? "#3b82f6" : "#e5e7eb",
color: showPreview ? "white" : "#374151",
border: "none",
borderRadius: "4px",
fontSize: "14px",
cursor: "pointer"
},
children: showPreview ? "Hide Preview" : "Show Preview"
}
),
showPreview && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
"button",
{
type: "button",
onClick: () => setPreviewMode("desktop"),
style: {
padding: "6px 12px",
backgroundColor: previewMode === "desktop" ? "#6366f1" : "#e5e7eb",
color: previewMode === "desktop" ? "white" : "#374151",
border: "none",
borderRadius: "4px 0 0 4px",
fontSize: "14px",
cursor: "pointer"
},
children: "Desktop"
}
),
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
"button",
{
type: "button",
onClick: () => setPreviewMode("mobile"),
style: {
padding: "6px 12px",
backgroundColor: previewMode === "mobile" ? "#6366f1" : "#e5e7eb",
color: previewMode === "mobile" ? "white" : "#374151",
border: "none",
borderRadius: "0 4px 4px 0",
fontSize: "14px",
cursor: "pointer"
},
children: "Mobile"
}
)
] }),
showPreview && validationSummary && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
padding: "6px 12px",
backgroundColor: isValid ? "#fef3c7" : "#fee2e2",
color: isValid ? "#92400e" : "#991b1b",
borderRadius: "4px",
fontSize: "13px"
}, children: validationSummary })
] }),
showPreview && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
"button",
{
type: "button",
onClick: handleTestEmail,
style: {
padding: "6px 12px",
backgroundColor: "#10b981",
color: "white",
border: "none",
borderRadius: "4px",
fontSize: "14px",
cursor: "pointer"
},
children: "Send Test Email"
}
)
] }),
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
flex: showPreview ? "0 0 50%" : "1",
overflow: "auto",
borderRight: showPreview ? "1px solid #e5e7eb" : "none"
}, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { padding: "16px" }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "rich-text-lexical" }) }) }),
showPreview && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { flex: "0 0 50%", overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
EmailPreview,
{
content: value,
subject: fields.subject?.value || "Email Subject",
preheader: fields.preheader?.value,
channel: fields.channel?.value,
mode: previewMode,
onValidation: handleValidation
}
) })
] })
] });
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BroadcastEditor,
EmailPreview,
EmailPreviewField,
MagicLinkVerify,
NewsletterForm,
PreferencesForm,
createMagicLinkVerify,
createNewsletterForm,
createPreferencesForm,
useNewsletterAuth
});
//# sourceMappingURL=components.cjs.map