payload-plugin-newsletter 0.11.0 → 0.12.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/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [0.12.0] - 2025-07-20
2
+
3
+ ### Added
4
+ - Email preview feature for broadcasts with inline preview component
5
+ - React Email integration for reliable email template rendering
6
+ - Custom email template support
7
+ - Desktop and mobile preview modes
8
+
9
+ ### Changed
10
+ - Broadcast collection now includes inline email preview below content editor
11
+
1
12
  ## [0.11.0] - 2025-07-20
2
13
 
3
14
  ### Added
@@ -7,12 +18,25 @@
7
18
  - Image upload support with Media collection integration
8
19
  - Custom email blocks (Button and Divider)
9
20
  - Enhanced link feature with "open in new tab" option
21
+ - Email preview feature for broadcasts:
22
+ - Live preview with manual update button
23
+ - Desktop and mobile responsive views
24
+ - React Email template rendering
25
+ - Custom template support via `email-templates/broadcast-template.tsx`
26
+ - Inline preview below content editor
10
27
  - Comprehensive image handling in email HTML conversion:
11
28
  - Responsive images with proper email-safe HTML
12
29
  - Support for captions and alt text
13
30
  - Automatic media URL handling for different storage backends
14
- - Media collection setup documentation (`docs/guides/media-collection-setup.md`)
15
- - Prerequisites section in README mentioning Media collection requirement
31
+ - New utilities and components:
32
+ - `contentTransformer` for preview content processing
33
+ - `templateLoader` for custom template discovery
34
+ - `DefaultBroadcastTemplate` bundled email template
35
+ - `BroadcastInlinePreview` component
36
+ - Documentation:
37
+ - Media collection setup guide (`docs/guides/media-collection-setup.md`)
38
+ - Email preview feature guide (`docs/features/email-preview.md`)
39
+ - Prerequisites section in README mentioning Media collection requirement
16
40
 
17
41
  ### Changed
18
42
  - Removed 'name' field from Broadcasts collection (now uses 'subject' as title)
@@ -32,11 +32,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  var components_exports = {};
33
33
  __export(components_exports, {
34
34
  BroadcastEditor: () => BroadcastEditor,
35
+ BroadcastInlinePreview: () => BroadcastInlinePreview,
36
+ BroadcastPreviewField: () => BroadcastPreviewField,
37
+ DefaultBroadcastTemplate: () => DefaultBroadcastTemplate,
35
38
  EmailPreview: () => EmailPreview,
36
39
  EmailPreviewField: () => EmailPreviewField,
40
+ EmailRenderer: () => EmailRenderer,
37
41
  MagicLinkVerify: () => MagicLinkVerify,
38
42
  NewsletterForm: () => NewsletterForm,
39
43
  PreferencesForm: () => PreferencesForm,
44
+ PreviewControls: () => PreviewControls,
40
45
  createMagicLinkVerify: () => createMagicLinkVerify,
41
46
  createNewsletterForm: () => createNewsletterForm,
42
47
  createPreferencesForm: () => createPreferencesForm,
@@ -1726,14 +1731,453 @@ var BroadcastEditor = (props) => {
1726
1731
  ] })
1727
1732
  ] });
1728
1733
  };
1734
+
1735
+ // src/components/Broadcasts/BroadcastInlinePreview.tsx
1736
+ var import_react9 = require("react");
1737
+ var import_ui3 = require("@payloadcms/ui");
1738
+
1739
+ // src/utils/contentTransformer.ts
1740
+ async function transformContentForPreview(lexicalState, options = {}) {
1741
+ const html = await convertToEmailSafeHtml(lexicalState, {
1742
+ mediaUrl: options.mediaUrl
1743
+ });
1744
+ const processedHtml = processCustomBlocks(html);
1745
+ return processedHtml;
1746
+ }
1747
+ function processCustomBlocks(html) {
1748
+ return html;
1749
+ }
1750
+
1751
+ // src/email-templates/DefaultBroadcastTemplate.tsx
1752
+ var import_components2 = require("@react-email/components");
1753
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1754
+ var DefaultBroadcastTemplate = ({
1755
+ subject,
1756
+ preheader,
1757
+ content
1758
+ }) => {
1759
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_components2.Html, { children: [
1760
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_components2.Head, {}),
1761
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_components2.Preview, { children: preheader || subject }),
1762
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_components2.Body, { style: main, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_components2.Container, { style: container, children: [
1763
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_components2.Section, { style: contentSection, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { dangerouslySetInnerHTML: { __html: content } }) }),
1764
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_components2.Hr, { style: divider }),
1765
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_components2.Section, { style: footer, children: [
1766
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_components2.Text, { style: footerText, children: "You're receiving this email because you subscribed to our newsletter." }),
1767
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_components2.Text, { style: footerText, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_components2.Link, { href: "{{unsubscribe_url}}", style: footerLink, children: "Unsubscribe" }) })
1768
+ ] })
1769
+ ] }) })
1770
+ ] });
1771
+ };
1772
+ var main = {
1773
+ backgroundColor: "#ffffff",
1774
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
1775
+ };
1776
+ var container = {
1777
+ margin: "0 auto",
1778
+ padding: "40px 20px",
1779
+ maxWidth: "600px"
1780
+ };
1781
+ var contentSection = {
1782
+ fontSize: "16px",
1783
+ lineHeight: "1.6",
1784
+ color: "#374151"
1785
+ };
1786
+ var divider = {
1787
+ borderColor: "#e5e7eb",
1788
+ margin: "40px 0 20px"
1789
+ };
1790
+ var footer = {
1791
+ textAlign: "center"
1792
+ };
1793
+ var footerText = {
1794
+ fontSize: "14px",
1795
+ lineHeight: "1.5",
1796
+ color: "#6b7280",
1797
+ margin: "0 0 10px"
1798
+ };
1799
+ var footerLink = {
1800
+ color: "#6b7280",
1801
+ textDecoration: "underline"
1802
+ };
1803
+
1804
+ // src/utils/templateLoader.ts
1805
+ var TemplateLoader = class {
1806
+ constructor() {
1807
+ this.loadAttempted = false;
1808
+ this.defaultTemplate = DefaultBroadcastTemplate;
1809
+ }
1810
+ async loadTemplate() {
1811
+ if (!this.loadAttempted) {
1812
+ this.loadAttempted = true;
1813
+ await this.attemptCustomTemplateLoad();
1814
+ }
1815
+ return this.customTemplate || this.defaultTemplate;
1816
+ }
1817
+ async attemptCustomTemplateLoad() {
1818
+ try {
1819
+ const customTemplatePath = `${process.cwd()}/email-templates/broadcast-template`;
1820
+ const module2 = await import(
1821
+ /* @vite-ignore */
1822
+ /* webpackIgnore: true */
1823
+ customTemplatePath
1824
+ ).catch(() => null);
1825
+ if (module2) {
1826
+ this.customTemplate = module2.default || module2.BroadcastTemplate;
1827
+ console.info("Loaded custom broadcast template");
1828
+ }
1829
+ } catch (error) {
1830
+ console.info("Using default broadcast template");
1831
+ }
1832
+ }
1833
+ // Reset for testing
1834
+ reset() {
1835
+ this.customTemplate = void 0;
1836
+ this.loadAttempted = false;
1837
+ }
1838
+ };
1839
+ var templateLoader = new TemplateLoader();
1840
+ async function loadTemplate() {
1841
+ return templateLoader.loadTemplate();
1842
+ }
1843
+
1844
+ // src/components/Broadcasts/EmailRenderer.tsx
1845
+ var import_react8 = require("react");
1846
+ var import_render = require("@react-email/render");
1847
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1848
+ var EmailRenderer = ({
1849
+ template,
1850
+ data,
1851
+ device = "desktop",
1852
+ onRender
1853
+ }) => {
1854
+ const [renderedHtml, setRenderedHtml] = (0, import_react8.useState)("");
1855
+ const [error, setError] = (0, import_react8.useState)(null);
1856
+ const iframeRef = (0, import_react8.useRef)(null);
1857
+ const renderEmail = (0, import_react8.useCallback)(async () => {
1858
+ try {
1859
+ const TemplateComponent = template;
1860
+ const element = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TemplateComponent, { ...data });
1861
+ const html = await (0, import_render.render)(element, {
1862
+ pretty: true
1863
+ });
1864
+ setRenderedHtml(html);
1865
+ onRender?.(html);
1866
+ setError(null);
1867
+ } catch (err) {
1868
+ setError(err);
1869
+ console.error("Failed to render email template:", err);
1870
+ }
1871
+ }, [template, data, onRender]);
1872
+ (0, import_react8.useEffect)(() => {
1873
+ renderEmail();
1874
+ }, [renderEmail]);
1875
+ (0, import_react8.useEffect)(() => {
1876
+ if (iframeRef.current && renderedHtml) {
1877
+ const iframe = iframeRef.current;
1878
+ const doc = iframe.contentDocument || iframe.contentWindow?.document;
1879
+ if (doc) {
1880
+ doc.open();
1881
+ doc.write(renderedHtml);
1882
+ doc.close();
1883
+ }
1884
+ }
1885
+ }, [renderedHtml]);
1886
+ const containerStyle = {
1887
+ width: "100%",
1888
+ height: "100%",
1889
+ display: "flex",
1890
+ alignItems: "center",
1891
+ justifyContent: "center"
1892
+ };
1893
+ const iframeStyle = {
1894
+ width: device === "mobile" ? "375px" : "600px",
1895
+ maxWidth: "100%",
1896
+ height: "100%",
1897
+ background: "white",
1898
+ boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
1899
+ borderRadius: device === "mobile" ? "20px" : "8px",
1900
+ border: "none",
1901
+ display: "block"
1902
+ };
1903
+ const errorStyle = {
1904
+ background: "white",
1905
+ border: "1px solid #ef4444",
1906
+ borderRadius: "4px",
1907
+ padding: "2rem",
1908
+ maxWidth: "500px"
1909
+ };
1910
+ if (error) {
1911
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: errorStyle, children: [
1912
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { style: { color: "#ef4444", margin: "0 0 1rem" }, children: "Template Render Error" }),
1913
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("pre", { style: {
1914
+ background: "#f9fafb",
1915
+ padding: "1rem",
1916
+ borderRadius: "4px",
1917
+ overflowX: "auto",
1918
+ fontSize: "12px",
1919
+ color: "#374151",
1920
+ margin: 0
1921
+ }, children: error.message })
1922
+ ] });
1923
+ }
1924
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: containerStyle, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1925
+ "iframe",
1926
+ {
1927
+ ref: iframeRef,
1928
+ style: iframeStyle,
1929
+ sandbox: "allow-same-origin",
1930
+ title: "Email Preview"
1931
+ }
1932
+ ) });
1933
+ };
1934
+
1935
+ // src/components/Broadcasts/PreviewControls.tsx
1936
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1937
+ var PreviewControls = ({
1938
+ onUpdate,
1939
+ device,
1940
+ onDeviceChange,
1941
+ isLoading = false
1942
+ }) => {
1943
+ const controlsStyle = {
1944
+ display: "flex",
1945
+ alignItems: "center",
1946
+ justifyContent: "space-between",
1947
+ padding: "1rem",
1948
+ background: "white",
1949
+ borderBottom: "1px solid #e5e7eb"
1950
+ };
1951
+ const updateButtonStyle = {
1952
+ padding: "0.5rem 1rem",
1953
+ background: "#10b981",
1954
+ color: "white",
1955
+ border: "none",
1956
+ borderRadius: "4px",
1957
+ cursor: isLoading ? "not-allowed" : "pointer",
1958
+ fontSize: "14px",
1959
+ fontWeight: 500,
1960
+ opacity: isLoading ? 0.6 : 1
1961
+ };
1962
+ const deviceSelectorStyle = {
1963
+ display: "flex",
1964
+ gap: "0.5rem"
1965
+ };
1966
+ const deviceButtonStyle = (isActive) => ({
1967
+ display: "flex",
1968
+ alignItems: "center",
1969
+ gap: "0.5rem",
1970
+ padding: "0.5rem 0.75rem",
1971
+ background: isActive ? "#1f2937" : "white",
1972
+ color: isActive ? "white" : "#374151",
1973
+ border: `1px solid ${isActive ? "#1f2937" : "#e5e7eb"}`,
1974
+ borderRadius: "4px",
1975
+ cursor: "pointer",
1976
+ fontSize: "14px"
1977
+ });
1978
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: controlsStyle, children: [
1979
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1980
+ "button",
1981
+ {
1982
+ style: updateButtonStyle,
1983
+ onClick: onUpdate,
1984
+ disabled: isLoading,
1985
+ children: isLoading ? "Updating..." : "Update Preview"
1986
+ }
1987
+ ),
1988
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: deviceSelectorStyle, children: [
1989
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1990
+ "button",
1991
+ {
1992
+ style: deviceButtonStyle(device === "desktop"),
1993
+ onClick: () => onDeviceChange("desktop"),
1994
+ "aria-label": "Desktop view",
1995
+ children: [
1996
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1997
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2", ry: "2" }),
1998
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "8", y1: "21", x2: "16", y2: "21" }),
1999
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "12", y1: "17", x2: "12", y2: "21" })
2000
+ ] }),
2001
+ "Desktop"
2002
+ ]
2003
+ }
2004
+ ),
2005
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2006
+ "button",
2007
+ {
2008
+ style: deviceButtonStyle(device === "mobile"),
2009
+ onClick: () => onDeviceChange("mobile"),
2010
+ "aria-label": "Mobile view",
2011
+ children: [
2012
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
2013
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("rect", { x: "5", y: "2", width: "14", height: "20", rx: "2", ry: "2" }),
2014
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "12", y1: "18", x2: "12", y2: "18" })
2015
+ ] }),
2016
+ "Mobile"
2017
+ ]
2018
+ }
2019
+ )
2020
+ ] })
2021
+ ] });
2022
+ };
2023
+
2024
+ // src/components/Broadcasts/BroadcastInlinePreview.tsx
2025
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2026
+ var BroadcastInlinePreview = () => {
2027
+ const [device, setDevice] = (0, import_react9.useState)("desktop");
2028
+ const [isLoading, setIsLoading] = (0, import_react9.useState)(false);
2029
+ const [showPreview, setShowPreview] = (0, import_react9.useState)(false);
2030
+ const [previewData, setPreviewData] = (0, import_react9.useState)(null);
2031
+ const [error, setError] = (0, import_react9.useState)(null);
2032
+ const fields = (0, import_ui3.useFormFields)(([fields2]) => ({
2033
+ subject: fields2.subject?.value,
2034
+ preheader: fields2.preheader?.value,
2035
+ content: fields2.content?.value
2036
+ }));
2037
+ const updatePreview = (0, import_react9.useCallback)(async () => {
2038
+ if (!fields.content) {
2039
+ setError(new Error("Please add some content before previewing"));
2040
+ return;
2041
+ }
2042
+ setIsLoading(true);
2043
+ setError(null);
2044
+ try {
2045
+ const htmlContent = await transformContentForPreview(fields.content, {
2046
+ mediaUrl: "/api/media"
2047
+ });
2048
+ const template = await loadTemplate();
2049
+ setPreviewData({
2050
+ template,
2051
+ data: {
2052
+ subject: fields.subject || "",
2053
+ preheader: fields.preheader || "",
2054
+ content: htmlContent
2055
+ }
2056
+ });
2057
+ setShowPreview(true);
2058
+ } catch (err) {
2059
+ setError(err);
2060
+ console.error("Failed to update preview:", err);
2061
+ } finally {
2062
+ setIsLoading(false);
2063
+ }
2064
+ }, [fields]);
2065
+ const containerStyle = {
2066
+ marginTop: "2rem",
2067
+ border: "1px solid #e5e7eb",
2068
+ borderRadius: "8px",
2069
+ overflow: "hidden"
2070
+ };
2071
+ const headerStyle = {
2072
+ display: "flex",
2073
+ alignItems: "center",
2074
+ justifyContent: "space-between",
2075
+ padding: "1rem",
2076
+ background: "#f9fafb",
2077
+ borderBottom: "1px solid #e5e7eb"
2078
+ };
2079
+ const titleStyle = {
2080
+ fontSize: "16px",
2081
+ fontWeight: 600,
2082
+ color: "#1f2937",
2083
+ margin: 0
2084
+ };
2085
+ const previewContainerStyle = {
2086
+ height: "600px",
2087
+ display: "flex",
2088
+ flexDirection: "column",
2089
+ background: "#f3f4f6"
2090
+ };
2091
+ const errorStyle = {
2092
+ padding: "2rem",
2093
+ textAlign: "center"
2094
+ };
2095
+ const toggleButtonStyle = {
2096
+ padding: "0.5rem 1rem",
2097
+ background: showPreview ? "#ef4444" : "#3b82f6",
2098
+ color: "white",
2099
+ border: "none",
2100
+ borderRadius: "4px",
2101
+ cursor: "pointer",
2102
+ fontSize: "14px",
2103
+ fontWeight: 500
2104
+ };
2105
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: containerStyle, children: [
2106
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: headerStyle, children: [
2107
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h3", { style: titleStyle, children: "Email Preview" }),
2108
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2109
+ "button",
2110
+ {
2111
+ onClick: () => showPreview ? setShowPreview(false) : updatePreview(),
2112
+ style: toggleButtonStyle,
2113
+ disabled: isLoading,
2114
+ children: isLoading ? "Loading..." : showPreview ? "Hide Preview" : "Show Preview"
2115
+ }
2116
+ )
2117
+ ] }),
2118
+ showPreview && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: previewContainerStyle, children: error ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: errorStyle, children: [
2119
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: { color: "#ef4444", margin: "0 0 1rem" }, children: error.message }),
2120
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2121
+ "button",
2122
+ {
2123
+ onClick: updatePreview,
2124
+ style: {
2125
+ padding: "0.5rem 1rem",
2126
+ background: "#3b82f6",
2127
+ color: "white",
2128
+ border: "none",
2129
+ borderRadius: "4px",
2130
+ cursor: "pointer"
2131
+ },
2132
+ children: "Retry"
2133
+ }
2134
+ )
2135
+ ] }) : previewData ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
2136
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2137
+ PreviewControls,
2138
+ {
2139
+ onUpdate: updatePreview,
2140
+ device,
2141
+ onDeviceChange: setDevice,
2142
+ isLoading
2143
+ }
2144
+ ),
2145
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: "2rem" }, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2146
+ EmailRenderer,
2147
+ {
2148
+ template: previewData.template,
2149
+ data: previewData.data,
2150
+ device
2151
+ }
2152
+ ) })
2153
+ ] }) : null })
2154
+ ] });
2155
+ };
2156
+
2157
+ // src/components/Broadcasts/BroadcastPreviewField.tsx
2158
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2159
+ var BroadcastPreviewField = () => {
2160
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: {
2161
+ padding: "1rem",
2162
+ background: "#f9fafb",
2163
+ borderRadius: "4px",
2164
+ fontSize: "14px",
2165
+ color: "#6b7280"
2166
+ }, children: "Email preview is available inline below the content editor." });
2167
+ };
1729
2168
  // Annotate the CommonJS export names for ESM import in node:
1730
2169
  0 && (module.exports = {
1731
2170
  BroadcastEditor,
2171
+ BroadcastInlinePreview,
2172
+ BroadcastPreviewField,
2173
+ DefaultBroadcastTemplate,
1732
2174
  EmailPreview,
1733
2175
  EmailPreviewField,
2176
+ EmailRenderer,
1734
2177
  MagicLinkVerify,
1735
2178
  NewsletterForm,
1736
2179
  PreferencesForm,
2180
+ PreviewControls,
1737
2181
  createMagicLinkVerify,
1738
2182
  createNewsletterForm,
1739
2183
  createPreferencesForm,