payload-plugin-newsletter 0.11.0 → 0.12.1

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,21 @@
1
+ ## [0.12.1] - 2025-07-20
2
+
3
+ ### Fixed
4
+ - Resolved ESLint errors in email preview components
5
+ - Removed unused imports and variables
6
+ - Fixed console.info statements in template loader
7
+
8
+ ## [0.12.0] - 2025-07-20
9
+
10
+ ### Added
11
+ - Email preview feature for broadcasts with inline preview component
12
+ - React Email integration for reliable email template rendering
13
+ - Custom email template support
14
+ - Desktop and mobile preview modes
15
+
16
+ ### Changed
17
+ - Broadcast collection now includes inline email preview below content editor
18
+
1
19
  ## [0.11.0] - 2025-07-20
2
20
 
3
21
  ### Added
@@ -7,12 +25,25 @@
7
25
  - Image upload support with Media collection integration
8
26
  - Custom email blocks (Button and Divider)
9
27
  - Enhanced link feature with "open in new tab" option
28
+ - Email preview feature for broadcasts:
29
+ - Live preview with manual update button
30
+ - Desktop and mobile responsive views
31
+ - React Email template rendering
32
+ - Custom template support via `email-templates/broadcast-template.tsx`
33
+ - Inline preview below content editor
10
34
  - Comprehensive image handling in email HTML conversion:
11
35
  - Responsive images with proper email-safe HTML
12
36
  - Support for captions and alt text
13
37
  - 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
38
+ - New utilities and components:
39
+ - `contentTransformer` for preview content processing
40
+ - `templateLoader` for custom template discovery
41
+ - `DefaultBroadcastTemplate` bundled email template
42
+ - `BroadcastInlinePreview` component
43
+ - Documentation:
44
+ - Media collection setup guide (`docs/guides/media-collection-setup.md`)
45
+ - Email preview feature guide (`docs/features/email-preview.md`)
46
+ - Prerequisites section in README mentioning Media collection requirement
16
47
 
17
48
  ### Changed
18
49
  - 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,451 @@ 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
+ }
1828
+ } catch {
1829
+ }
1830
+ }
1831
+ // Reset for testing
1832
+ reset() {
1833
+ this.customTemplate = void 0;
1834
+ this.loadAttempted = false;
1835
+ }
1836
+ };
1837
+ var templateLoader = new TemplateLoader();
1838
+ async function loadTemplate() {
1839
+ return templateLoader.loadTemplate();
1840
+ }
1841
+
1842
+ // src/components/Broadcasts/EmailRenderer.tsx
1843
+ var import_react8 = require("react");
1844
+ var import_render = require("@react-email/render");
1845
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1846
+ var EmailRenderer = ({
1847
+ template,
1848
+ data,
1849
+ device = "desktop",
1850
+ onRender
1851
+ }) => {
1852
+ const [renderedHtml, setRenderedHtml] = (0, import_react8.useState)("");
1853
+ const [error, setError] = (0, import_react8.useState)(null);
1854
+ const iframeRef = (0, import_react8.useRef)(null);
1855
+ const renderEmail = (0, import_react8.useCallback)(async () => {
1856
+ try {
1857
+ const TemplateComponent = template;
1858
+ const element = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TemplateComponent, { ...data });
1859
+ const html = await (0, import_render.render)(element, {
1860
+ pretty: true
1861
+ });
1862
+ setRenderedHtml(html);
1863
+ onRender?.(html);
1864
+ setError(null);
1865
+ } catch (err) {
1866
+ setError(err);
1867
+ console.error("Failed to render email template:", err);
1868
+ }
1869
+ }, [template, data, onRender]);
1870
+ (0, import_react8.useEffect)(() => {
1871
+ renderEmail();
1872
+ }, [renderEmail]);
1873
+ (0, import_react8.useEffect)(() => {
1874
+ if (iframeRef.current && renderedHtml) {
1875
+ const iframe = iframeRef.current;
1876
+ const doc = iframe.contentDocument || iframe.contentWindow?.document;
1877
+ if (doc) {
1878
+ doc.open();
1879
+ doc.write(renderedHtml);
1880
+ doc.close();
1881
+ }
1882
+ }
1883
+ }, [renderedHtml]);
1884
+ const containerStyle = {
1885
+ width: "100%",
1886
+ height: "100%",
1887
+ display: "flex",
1888
+ alignItems: "center",
1889
+ justifyContent: "center"
1890
+ };
1891
+ const iframeStyle = {
1892
+ width: device === "mobile" ? "375px" : "600px",
1893
+ maxWidth: "100%",
1894
+ height: "100%",
1895
+ background: "white",
1896
+ boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
1897
+ borderRadius: device === "mobile" ? "20px" : "8px",
1898
+ border: "none",
1899
+ display: "block"
1900
+ };
1901
+ const errorStyle = {
1902
+ background: "white",
1903
+ border: "1px solid #ef4444",
1904
+ borderRadius: "4px",
1905
+ padding: "2rem",
1906
+ maxWidth: "500px"
1907
+ };
1908
+ if (error) {
1909
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: errorStyle, children: [
1910
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { style: { color: "#ef4444", margin: "0 0 1rem" }, children: "Template Render Error" }),
1911
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("pre", { style: {
1912
+ background: "#f9fafb",
1913
+ padding: "1rem",
1914
+ borderRadius: "4px",
1915
+ overflowX: "auto",
1916
+ fontSize: "12px",
1917
+ color: "#374151",
1918
+ margin: 0
1919
+ }, children: error.message })
1920
+ ] });
1921
+ }
1922
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: containerStyle, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1923
+ "iframe",
1924
+ {
1925
+ ref: iframeRef,
1926
+ style: iframeStyle,
1927
+ sandbox: "allow-same-origin",
1928
+ title: "Email Preview"
1929
+ }
1930
+ ) });
1931
+ };
1932
+
1933
+ // src/components/Broadcasts/PreviewControls.tsx
1934
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1935
+ var PreviewControls = ({
1936
+ onUpdate,
1937
+ device,
1938
+ onDeviceChange,
1939
+ isLoading = false
1940
+ }) => {
1941
+ const controlsStyle = {
1942
+ display: "flex",
1943
+ alignItems: "center",
1944
+ justifyContent: "space-between",
1945
+ padding: "1rem",
1946
+ background: "white",
1947
+ borderBottom: "1px solid #e5e7eb"
1948
+ };
1949
+ const updateButtonStyle = {
1950
+ padding: "0.5rem 1rem",
1951
+ background: "#10b981",
1952
+ color: "white",
1953
+ border: "none",
1954
+ borderRadius: "4px",
1955
+ cursor: isLoading ? "not-allowed" : "pointer",
1956
+ fontSize: "14px",
1957
+ fontWeight: 500,
1958
+ opacity: isLoading ? 0.6 : 1
1959
+ };
1960
+ const deviceSelectorStyle = {
1961
+ display: "flex",
1962
+ gap: "0.5rem"
1963
+ };
1964
+ const deviceButtonStyle = (isActive) => ({
1965
+ display: "flex",
1966
+ alignItems: "center",
1967
+ gap: "0.5rem",
1968
+ padding: "0.5rem 0.75rem",
1969
+ background: isActive ? "#1f2937" : "white",
1970
+ color: isActive ? "white" : "#374151",
1971
+ border: `1px solid ${isActive ? "#1f2937" : "#e5e7eb"}`,
1972
+ borderRadius: "4px",
1973
+ cursor: "pointer",
1974
+ fontSize: "14px"
1975
+ });
1976
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: controlsStyle, children: [
1977
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1978
+ "button",
1979
+ {
1980
+ style: updateButtonStyle,
1981
+ onClick: onUpdate,
1982
+ disabled: isLoading,
1983
+ children: isLoading ? "Updating..." : "Update Preview"
1984
+ }
1985
+ ),
1986
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: deviceSelectorStyle, children: [
1987
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1988
+ "button",
1989
+ {
1990
+ style: deviceButtonStyle(device === "desktop"),
1991
+ onClick: () => onDeviceChange("desktop"),
1992
+ "aria-label": "Desktop view",
1993
+ children: [
1994
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1995
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2", ry: "2" }),
1996
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "8", y1: "21", x2: "16", y2: "21" }),
1997
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "12", y1: "17", x2: "12", y2: "21" })
1998
+ ] }),
1999
+ "Desktop"
2000
+ ]
2001
+ }
2002
+ ),
2003
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2004
+ "button",
2005
+ {
2006
+ style: deviceButtonStyle(device === "mobile"),
2007
+ onClick: () => onDeviceChange("mobile"),
2008
+ "aria-label": "Mobile view",
2009
+ children: [
2010
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
2011
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("rect", { x: "5", y: "2", width: "14", height: "20", rx: "2", ry: "2" }),
2012
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "12", y1: "18", x2: "12", y2: "18" })
2013
+ ] }),
2014
+ "Mobile"
2015
+ ]
2016
+ }
2017
+ )
2018
+ ] })
2019
+ ] });
2020
+ };
2021
+
2022
+ // src/components/Broadcasts/BroadcastInlinePreview.tsx
2023
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2024
+ var BroadcastInlinePreview = () => {
2025
+ const [device, setDevice] = (0, import_react9.useState)("desktop");
2026
+ const [isLoading, setIsLoading] = (0, import_react9.useState)(false);
2027
+ const [showPreview, setShowPreview] = (0, import_react9.useState)(false);
2028
+ const [previewData, setPreviewData] = (0, import_react9.useState)(null);
2029
+ const [error, setError] = (0, import_react9.useState)(null);
2030
+ const fields = (0, import_ui3.useFormFields)(([fields2]) => ({
2031
+ subject: fields2.subject?.value,
2032
+ preheader: fields2.preheader?.value,
2033
+ content: fields2.content?.value
2034
+ }));
2035
+ const updatePreview = (0, import_react9.useCallback)(async () => {
2036
+ if (!fields.content) {
2037
+ setError(new Error("Please add some content before previewing"));
2038
+ return;
2039
+ }
2040
+ setIsLoading(true);
2041
+ setError(null);
2042
+ try {
2043
+ const htmlContent = await transformContentForPreview(fields.content, {
2044
+ mediaUrl: "/api/media"
2045
+ });
2046
+ const template = await loadTemplate();
2047
+ setPreviewData({
2048
+ template,
2049
+ data: {
2050
+ subject: fields.subject || "",
2051
+ preheader: fields.preheader || "",
2052
+ content: htmlContent
2053
+ }
2054
+ });
2055
+ setShowPreview(true);
2056
+ } catch (err) {
2057
+ setError(err);
2058
+ console.error("Failed to update preview:", err);
2059
+ } finally {
2060
+ setIsLoading(false);
2061
+ }
2062
+ }, [fields]);
2063
+ const containerStyle = {
2064
+ marginTop: "2rem",
2065
+ border: "1px solid #e5e7eb",
2066
+ borderRadius: "8px",
2067
+ overflow: "hidden"
2068
+ };
2069
+ const headerStyle = {
2070
+ display: "flex",
2071
+ alignItems: "center",
2072
+ justifyContent: "space-between",
2073
+ padding: "1rem",
2074
+ background: "#f9fafb",
2075
+ borderBottom: "1px solid #e5e7eb"
2076
+ };
2077
+ const titleStyle = {
2078
+ fontSize: "16px",
2079
+ fontWeight: 600,
2080
+ color: "#1f2937",
2081
+ margin: 0
2082
+ };
2083
+ const previewContainerStyle = {
2084
+ height: "600px",
2085
+ display: "flex",
2086
+ flexDirection: "column",
2087
+ background: "#f3f4f6"
2088
+ };
2089
+ const errorStyle = {
2090
+ padding: "2rem",
2091
+ textAlign: "center"
2092
+ };
2093
+ const toggleButtonStyle = {
2094
+ padding: "0.5rem 1rem",
2095
+ background: showPreview ? "#ef4444" : "#3b82f6",
2096
+ color: "white",
2097
+ border: "none",
2098
+ borderRadius: "4px",
2099
+ cursor: "pointer",
2100
+ fontSize: "14px",
2101
+ fontWeight: 500
2102
+ };
2103
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: containerStyle, children: [
2104
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: headerStyle, children: [
2105
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h3", { style: titleStyle, children: "Email Preview" }),
2106
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2107
+ "button",
2108
+ {
2109
+ onClick: () => showPreview ? setShowPreview(false) : updatePreview(),
2110
+ style: toggleButtonStyle,
2111
+ disabled: isLoading,
2112
+ children: isLoading ? "Loading..." : showPreview ? "Hide Preview" : "Show Preview"
2113
+ }
2114
+ )
2115
+ ] }),
2116
+ showPreview && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: previewContainerStyle, children: error ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: errorStyle, children: [
2117
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: { color: "#ef4444", margin: "0 0 1rem" }, children: error.message }),
2118
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2119
+ "button",
2120
+ {
2121
+ onClick: updatePreview,
2122
+ style: {
2123
+ padding: "0.5rem 1rem",
2124
+ background: "#3b82f6",
2125
+ color: "white",
2126
+ border: "none",
2127
+ borderRadius: "4px",
2128
+ cursor: "pointer"
2129
+ },
2130
+ children: "Retry"
2131
+ }
2132
+ )
2133
+ ] }) : previewData ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
2134
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2135
+ PreviewControls,
2136
+ {
2137
+ onUpdate: updatePreview,
2138
+ device,
2139
+ onDeviceChange: setDevice,
2140
+ isLoading
2141
+ }
2142
+ ),
2143
+ /* @__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)(
2144
+ EmailRenderer,
2145
+ {
2146
+ template: previewData.template,
2147
+ data: previewData.data,
2148
+ device
2149
+ }
2150
+ ) })
2151
+ ] }) : null })
2152
+ ] });
2153
+ };
2154
+
2155
+ // src/components/Broadcasts/BroadcastPreviewField.tsx
2156
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2157
+ var BroadcastPreviewField = () => {
2158
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: {
2159
+ padding: "1rem",
2160
+ background: "#f9fafb",
2161
+ borderRadius: "4px",
2162
+ fontSize: "14px",
2163
+ color: "#6b7280"
2164
+ }, children: "Email preview is available inline below the content editor." });
2165
+ };
1729
2166
  // Annotate the CommonJS export names for ESM import in node:
1730
2167
  0 && (module.exports = {
1731
2168
  BroadcastEditor,
2169
+ BroadcastInlinePreview,
2170
+ BroadcastPreviewField,
2171
+ DefaultBroadcastTemplate,
1732
2172
  EmailPreview,
1733
2173
  EmailPreviewField,
2174
+ EmailRenderer,
1734
2175
  MagicLinkVerify,
1735
2176
  NewsletterForm,
1736
2177
  PreferencesForm,
2178
+ PreviewControls,
1737
2179
  createMagicLinkVerify,
1738
2180
  createNewsletterForm,
1739
2181
  createPreferencesForm,