@jhits/plugin-newsletter 0.0.4 → 0.0.6

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.
Files changed (173) hide show
  1. package/dist/api/handler.d.ts +51 -0
  2. package/dist/api/handler.d.ts.map +1 -0
  3. package/dist/api/handler.js +526 -0
  4. package/dist/api/router.d.ts +11 -0
  5. package/dist/api/router.d.ts.map +1 -0
  6. package/dist/api/router.js +82 -0
  7. package/dist/index.d.ts +46 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +222 -0
  10. package/dist/index.server.d.ts +10 -0
  11. package/dist/index.server.d.ts.map +1 -0
  12. package/dist/index.server.js +8 -0
  13. package/dist/init.d.ts +49 -0
  14. package/dist/init.d.ts.map +1 -0
  15. package/dist/init.js +42 -0
  16. package/dist/lib/blocks/BlockRenderer.d.ts +43 -0
  17. package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -0
  18. package/dist/lib/blocks/BlockRenderer.js +48 -0
  19. package/dist/lib/email/EmailRenderer.d.ts +47 -0
  20. package/dist/lib/email/EmailRenderer.d.ts.map +1 -0
  21. package/dist/lib/email/EmailRenderer.js +359 -0
  22. package/dist/lib/email/index.d.ts +6 -0
  23. package/dist/lib/email/index.d.ts.map +1 -0
  24. package/dist/lib/email/index.js +4 -0
  25. package/dist/lib/mappers/apiMapper.d.ts +30 -0
  26. package/dist/lib/mappers/apiMapper.d.ts.map +1 -0
  27. package/dist/lib/mappers/apiMapper.js +36 -0
  28. package/dist/lib/utils/blockHelpers.d.ts +23 -0
  29. package/dist/lib/utils/blockHelpers.d.ts.map +1 -0
  30. package/dist/lib/utils/blockHelpers.js +65 -0
  31. package/dist/lib/utils/slugify.d.ts +14 -0
  32. package/dist/lib/utils/slugify.d.ts.map +1 -0
  33. package/dist/lib/utils/slugify.js +37 -0
  34. package/dist/registry/BlockRegistry.d.ts +31 -0
  35. package/dist/registry/BlockRegistry.d.ts.map +1 -0
  36. package/dist/registry/BlockRegistry.js +34 -0
  37. package/dist/registry/index.d.ts +5 -0
  38. package/dist/registry/index.d.ts.map +1 -0
  39. package/dist/registry/index.js +4 -0
  40. package/dist/state/EditorContext.d.ts +44 -0
  41. package/dist/state/EditorContext.d.ts.map +1 -0
  42. package/dist/state/EditorContext.js +212 -0
  43. package/dist/state/index.d.ts +10 -0
  44. package/dist/state/index.d.ts.map +1 -0
  45. package/dist/state/index.js +6 -0
  46. package/dist/state/reducer.d.ts +11 -0
  47. package/dist/state/reducer.d.ts.map +1 -0
  48. package/dist/state/reducer.js +488 -0
  49. package/dist/state/types.d.ts +157 -0
  50. package/dist/state/types.d.ts.map +1 -0
  51. package/dist/state/types.js +26 -0
  52. package/dist/types/block.d.ts +230 -0
  53. package/dist/types/block.d.ts.map +1 -0
  54. package/dist/types/block.js +8 -0
  55. package/dist/types/newsletter.d.ts +129 -0
  56. package/dist/types/newsletter.d.ts.map +1 -0
  57. package/dist/types/newsletter.js +4 -0
  58. package/dist/types/registry.d.ts +13 -0
  59. package/dist/types/registry.d.ts.map +1 -0
  60. package/dist/types/registry.js +4 -0
  61. package/dist/views/CanvasEditor/BlockWrapper.d.ts +23 -0
  62. package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -0
  63. package/dist/views/CanvasEditor/BlockWrapper.js +44 -0
  64. package/dist/views/CanvasEditor/CanvasEditorView.d.ts +14 -0
  65. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -0
  66. package/dist/views/CanvasEditor/CanvasEditorView.js +139 -0
  67. package/dist/views/CanvasEditor/EditorBody.d.ts +24 -0
  68. package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -0
  69. package/dist/views/CanvasEditor/EditorBody.js +21 -0
  70. package/dist/views/CanvasEditor/EditorHeader.d.ts +12 -0
  71. package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -0
  72. package/dist/views/CanvasEditor/EditorHeader.js +47 -0
  73. package/dist/views/CanvasEditor/components/CustomBlockItem.d.ts +10 -0
  74. package/dist/views/CanvasEditor/components/CustomBlockItem.d.ts.map +1 -0
  75. package/dist/views/CanvasEditor/components/CustomBlockItem.js +36 -0
  76. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts +25 -0
  77. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -0
  78. package/dist/views/CanvasEditor/components/EditorCanvas.js +397 -0
  79. package/dist/views/CanvasEditor/components/EditorLibrary.d.ts +7 -0
  80. package/dist/views/CanvasEditor/components/EditorLibrary.d.ts.map +1 -0
  81. package/dist/views/CanvasEditor/components/EditorLibrary.js +25 -0
  82. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +9 -0
  83. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -0
  84. package/dist/views/CanvasEditor/components/EditorSidebar.js +16 -0
  85. package/dist/views/CanvasEditor/components/ErrorBanner.d.ts +6 -0
  86. package/dist/views/CanvasEditor/components/ErrorBanner.d.ts.map +1 -0
  87. package/dist/views/CanvasEditor/components/ErrorBanner.js +8 -0
  88. package/dist/views/CanvasEditor/components/LibraryItem.d.ts +10 -0
  89. package/dist/views/CanvasEditor/components/LibraryItem.d.ts.map +1 -0
  90. package/dist/views/CanvasEditor/components/LibraryItem.js +35 -0
  91. package/dist/views/CanvasEditor/components/SlashCommandDetector.d.ts +18 -0
  92. package/dist/views/CanvasEditor/components/SlashCommandDetector.d.ts.map +1 -0
  93. package/dist/views/CanvasEditor/components/SlashCommandDetector.js +164 -0
  94. package/dist/views/CanvasEditor/components/SlashCommandMenu.d.ts +22 -0
  95. package/dist/views/CanvasEditor/components/SlashCommandMenu.d.ts.map +1 -0
  96. package/dist/views/CanvasEditor/components/SlashCommandMenu.js +57 -0
  97. package/dist/views/CanvasEditor/components/index.d.ts +16 -0
  98. package/dist/views/CanvasEditor/components/index.d.ts.map +1 -0
  99. package/dist/views/CanvasEditor/components/index.js +9 -0
  100. package/dist/views/CanvasEditor/hooks/index.d.ts +7 -0
  101. package/dist/views/CanvasEditor/hooks/index.d.ts.map +1 -0
  102. package/dist/views/CanvasEditor/hooks/index.js +6 -0
  103. package/dist/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts +3 -0
  104. package/dist/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts.map +1 -0
  105. package/dist/views/CanvasEditor/hooks/useKeyboardShortcuts.js +114 -0
  106. package/dist/views/CanvasEditor/hooks/useNewsletterLoader.d.ts +5 -0
  107. package/dist/views/CanvasEditor/hooks/useNewsletterLoader.d.ts.map +1 -0
  108. package/dist/views/CanvasEditor/hooks/useNewsletterLoader.js +28 -0
  109. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +2 -0
  110. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -0
  111. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.js +46 -0
  112. package/dist/views/CanvasEditor/hooks/useSlashCommand.d.ts +31 -0
  113. package/dist/views/CanvasEditor/hooks/useSlashCommand.d.ts.map +1 -0
  114. package/dist/views/CanvasEditor/hooks/useSlashCommand.js +87 -0
  115. package/dist/views/CanvasEditor/index.d.ts +12 -0
  116. package/dist/views/CanvasEditor/index.d.ts.map +1 -0
  117. package/dist/views/CanvasEditor/index.js +7 -0
  118. package/dist/views/NewsletterEditor.d.ts +16 -0
  119. package/dist/views/NewsletterEditor.d.ts.map +1 -0
  120. package/dist/views/NewsletterEditor.js +10 -0
  121. package/dist/views/NewsletterManager.d.ts +10 -0
  122. package/dist/views/NewsletterManager.d.ts.map +1 -0
  123. package/dist/views/NewsletterManager.js +95 -0
  124. package/dist/views/SettingsView.d.ts +10 -0
  125. package/dist/views/SettingsView.d.ts.map +1 -0
  126. package/dist/views/SettingsView.js +103 -0
  127. package/dist/views/SubscribersView.d.ts +10 -0
  128. package/dist/views/SubscribersView.d.ts.map +1 -0
  129. package/dist/views/SubscribersView.js +94 -0
  130. package/package.json +24 -23
  131. package/src/api/handler.ts +340 -1
  132. package/src/api/router.ts +35 -0
  133. package/src/index.tsx +284 -4
  134. package/src/index.tsx.patch +98 -0
  135. package/src/init.tsx +72 -0
  136. package/src/lib/blocks/BlockRenderer.tsx +125 -0
  137. package/src/lib/email/EmailRenderer.tsx +425 -0
  138. package/src/lib/email/index.ts +6 -0
  139. package/src/lib/mappers/apiMapper.ts +57 -0
  140. package/src/lib/utils/blockHelpers.ts +71 -0
  141. package/src/lib/utils/slugify.ts +43 -0
  142. package/src/registry/BlockRegistry.ts +53 -0
  143. package/src/registry/index.ts +5 -0
  144. package/src/state/EditorContext.tsx +279 -0
  145. package/src/state/index.ts +10 -0
  146. package/src/state/reducer.ts +561 -0
  147. package/src/state/types.ts +154 -0
  148. package/src/types/block.ts +275 -0
  149. package/src/types/newsletter.ts +114 -1
  150. package/src/types/registry.ts +14 -0
  151. package/src/views/CanvasEditor/BlockWrapper.tsx +143 -0
  152. package/src/views/CanvasEditor/CanvasEditorView.tsx +249 -0
  153. package/src/views/CanvasEditor/EditorBody.tsx +95 -0
  154. package/src/views/CanvasEditor/EditorHeader.tsx +139 -0
  155. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +83 -0
  156. package/src/views/CanvasEditor/components/EditorCanvas.tsx +674 -0
  157. package/src/views/CanvasEditor/components/EditorLibrary.tsx +120 -0
  158. package/src/views/CanvasEditor/components/EditorSidebar.tsx +156 -0
  159. package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
  160. package/src/views/CanvasEditor/components/LibraryItem.tsx +71 -0
  161. package/src/views/CanvasEditor/components/SlashCommandDetector.tsx +196 -0
  162. package/src/views/CanvasEditor/components/SlashCommandMenu.tsx +131 -0
  163. package/src/views/CanvasEditor/components/index.ts +16 -0
  164. package/src/views/CanvasEditor/hooks/index.ts +7 -0
  165. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +136 -0
  166. package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +34 -0
  167. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +54 -0
  168. package/src/views/CanvasEditor/hooks/useSlashCommand.ts +106 -0
  169. package/src/views/CanvasEditor/index.ts +12 -0
  170. package/src/views/NewsletterEditor.tsx +38 -0
  171. package/src/views/NewsletterManager.tsx +240 -0
  172. package/src/views/SettingsView.tsx +14 -14
  173. package/src/views/SubscribersView.tsx +20 -20
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Email Renderer
3
+ * Converts blocks to email-safe HTML
4
+ *
5
+ * Email constraints:
6
+ * - Inline styles only (no CSS classes)
7
+ * - Table-based layouts (no flexbox/grid)
8
+ * - Max width ~600px
9
+ * - Limited CSS support
10
+ * - Images need absolute URLs
11
+ * - No JavaScript
12
+ */
13
+ import { blockRegistry } from '../../registry/BlockRegistry';
14
+ /**
15
+ * Convert a block to email-safe HTML
16
+ */
17
+ export function renderBlockToEmail(block, context = {}) {
18
+ const definition = blockRegistry.get(block.type);
19
+ if (!definition) {
20
+ return `<p style="color: #666; font-size: 14px; padding: 10px;">Unknown block type: ${block.type}</p>`;
21
+ }
22
+ // Check if block has email-specific renderer (preferred)
23
+ if (definition.components.Email) {
24
+ const childBlocks = block.children && Array.isArray(block.children) && block.children.length > 0
25
+ ? (typeof block.children[0] === 'object' ? block.children : [])
26
+ : [];
27
+ return definition.components.Email({
28
+ block,
29
+ context,
30
+ childBlocks,
31
+ renderChild: (childBlock) => renderBlockToEmail(childBlock, context),
32
+ });
33
+ }
34
+ // Fallback to converting Preview component output to email HTML
35
+ // For now, we'll create basic email-safe HTML based on block type
36
+ return renderBlockByType(block, context);
37
+ }
38
+ /**
39
+ * Render block by type with email-safe HTML
40
+ */
41
+ function renderBlockByType(block, context) {
42
+ const { baseUrl = '' } = context;
43
+ switch (block.type) {
44
+ case 'heading': {
45
+ const level = block.data.level || 1;
46
+ const text = block.data.text || '';
47
+ const tag = `h${Math.min(Math.max(level, 1), 6)}`;
48
+ const fontSize = level === 1 ? '32px' : level === 2 ? '24px' : level === 3 ? '20px' : '18px';
49
+ return `<${tag} style="font-size: ${fontSize}; font-weight: bold; color: #1a2e26; margin: 20px 0 10px 0; line-height: 1.3;">${escapeHtml(text)}</${tag}>`;
50
+ }
51
+ case 'paragraph': {
52
+ const html = block.data.html || block.data.text || '';
53
+ // Convert basic HTML tags to email-safe inline styles
54
+ const emailHtml = convertHtmlToEmailSafe(html);
55
+ return `<p style="font-size: 15px; line-height: 1.8; color: #1a2e26; margin: 0 0 15px 0;">${emailHtml}</p>`;
56
+ }
57
+ case 'image': {
58
+ const imageId = block.data.imageId;
59
+ const alt = block.data.alt || '';
60
+ const caption = block.data.caption || '';
61
+ const height = block.data.height || undefined;
62
+ const widthPercent = block.data.widthPercent || 100;
63
+ const brightness = block.data.brightness ?? 100;
64
+ const blur = block.data.blur ?? 0;
65
+ const borderRadius = block.data.borderRadius || 'none';
66
+ if (!imageId) {
67
+ return '';
68
+ }
69
+ // Check if imageId is already a filename (has extension or starts with timestamp)
70
+ const hasFileExtension = /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(imageId);
71
+ const looksLikeTimestamp = /^\d+-/.test(imageId);
72
+ const isFilename = hasFileExtension || looksLikeTimestamp;
73
+ // Generate image URL
74
+ const imageUrl = isFilename
75
+ ? `${baseUrl}/api/uploads/${encodeURIComponent(imageId)}`
76
+ : `${baseUrl}/api/uploads/${encodeURIComponent(imageId)}`; // Will be resolved in preview component
77
+ // Build image styles
78
+ const imageStyles = [
79
+ 'display: block',
80
+ 'max-width: 100%',
81
+ ];
82
+ // Apply width percentage
83
+ if (widthPercent !== 100) {
84
+ imageStyles.push(`width: ${widthPercent}%`);
85
+ }
86
+ // Apply height if specified
87
+ if (height) {
88
+ imageStyles.push(`height: ${height}px`);
89
+ imageStyles.push('object-fit: cover');
90
+ }
91
+ else {
92
+ imageStyles.push('height: auto');
93
+ }
94
+ // Apply filters (brightness and blur) - supported in most email clients
95
+ const filters = [];
96
+ if (brightness !== 100) {
97
+ filters.push(`brightness(${brightness}%)`);
98
+ }
99
+ if (blur > 0) {
100
+ filters.push(`blur(${blur}px)`);
101
+ }
102
+ if (filters.length > 0) {
103
+ imageStyles.push(`filter: ${filters.join(' ')}`);
104
+ }
105
+ // Apply border radius
106
+ const borderRadiusMap = {
107
+ 'none': '0',
108
+ 'sm': '2px',
109
+ 'md': '4px',
110
+ 'lg': '8px',
111
+ 'xl': '12px',
112
+ '2xl': '16px',
113
+ '3xl': '24px',
114
+ 'full': '9999px',
115
+ };
116
+ const borderRadiusValue = borderRadiusMap[borderRadius] || '0';
117
+ if (borderRadiusValue !== '0') {
118
+ imageStyles.push(`border-radius: ${borderRadiusValue}`);
119
+ }
120
+ const imageStyleString = imageStyles.join('; ');
121
+ let html = `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 20px 0;">
122
+ <tr>
123
+ <td align="center">
124
+ <img src="${imageUrl}" alt="${escapeHtml(alt)}" style="${imageStyleString}" />
125
+ </td>
126
+ </tr>`;
127
+ if (caption) {
128
+ html += `<tr>
129
+ <td align="center" style="padding-top: 10px; font-size: 12px; color: #666; font-style: italic;">
130
+ ${escapeHtml(caption)}
131
+ </td>
132
+ </tr>`;
133
+ }
134
+ html += `</table>`;
135
+ return html;
136
+ }
137
+ case 'list': {
138
+ const items = block.data.items || [];
139
+ const type = block.data.type || 'ul';
140
+ const isOrdered = type === 'ol';
141
+ const tag = isOrdered ? 'ol' : 'ul';
142
+ const itemsHtml = items.map((item, idx) => {
143
+ const text = typeof item === 'string' ? item : (item.html || item.text || '');
144
+ const emailHtml = convertHtmlToEmailSafe(text);
145
+ return `<li style="margin: 8px 0; padding-left: 5px; line-height: 1.8; color: #1a2e26;">${emailHtml}</li>`;
146
+ }).join('');
147
+ return `<${tag} style="margin: 15px 0; padding-left: 25px; color: #1a2e26; font-size: 15px; line-height: 1.8;">${itemsHtml}</${tag}>`;
148
+ }
149
+ case 'table': {
150
+ const rows = (Array.isArray(block.data?.rows) ? block.data.rows : []);
151
+ const useHeader = block.data.useHeader ?? true;
152
+ if (!rows.length) {
153
+ return '';
154
+ }
155
+ // Build table HTML using email-safe table structure
156
+ let tableHtml = `<table width="100%" cellpadding="12" cellspacing="0" border="0" style="margin: 20px 0; border-collapse: collapse; width: 100%;">`;
157
+ rows.forEach((row, rIdx) => {
158
+ const isHeaderRow = rIdx === 0 && useHeader;
159
+ const tag = isHeaderRow ? 'th' : 'td';
160
+ const bgColor = isHeaderRow ? '#1a2e26' : 'transparent';
161
+ const textColor = isHeaderRow ? '#ffffff' : '#1a2e26';
162
+ const fontWeight = isHeaderRow ? 'bold' : 'normal';
163
+ tableHtml += `<tr>`;
164
+ row.forEach((cell) => {
165
+ const cellHtml = convertHtmlToEmailSafe(cell.html || cell.text || '');
166
+ tableHtml += `<${tag} style="padding: 12px; border: 1px solid #e5e7eb; text-align: left; color: ${textColor}; font-size: 15px; line-height: 1.8; background-color: ${bgColor}; font-weight: ${fontWeight};">${cellHtml}</${tag}>`;
167
+ });
168
+ tableHtml += `</tr>`;
169
+ });
170
+ tableHtml += `</table>`;
171
+ return tableHtml;
172
+ }
173
+ case 'divider': {
174
+ return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0;">
175
+ <tr>
176
+ <td align="center">
177
+ <table width="40" cellpadding="0" cellspacing="0" border="0">
178
+ <tr>
179
+ <td height="1" bgcolor="#1a2e26" style="background-color: #1a2e2620; height: 1px;"></td>
180
+ </tr>
181
+ </table>
182
+ </td>
183
+ </tr>
184
+ </table>`;
185
+ }
186
+ default:
187
+ // Generic fallback - try to extract text content
188
+ const text = extractTextFromBlock(block);
189
+ if (text) {
190
+ return `<p style="font-size: 15px; line-height: 1.8; color: #1a2e26; margin: 0 0 15px 0;">${escapeHtml(text)}</p>`;
191
+ }
192
+ return '';
193
+ }
194
+ }
195
+ /**
196
+ * Convert HTML to email-safe HTML (inline styles)
197
+ */
198
+ function convertHtmlToEmailSafe(html) {
199
+ if (!html)
200
+ return '';
201
+ // Convert common HTML tags to email-safe versions with inline styles
202
+ let result = html
203
+ // Bold
204
+ .replace(/<strong\b[^>]*>(.*?)<\/strong>/gi, '<strong style="font-weight: bold;">$1</strong>')
205
+ .replace(/<b\b[^>]*>(.*?)<\/b>/gi, '<strong style="font-weight: bold;">$1</strong>')
206
+ // Italic
207
+ .replace(/<em\b[^>]*>(.*?)<\/em>/gi, '<em style="font-style: italic;">$1</em>')
208
+ .replace(/<i\b[^>]*>(.*?)<\/i>/gi, '<em style="font-style: italic;">$1</em>')
209
+ // Links
210
+ .replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, '<a href="$1" style="color: #1a2e26; text-decoration: underline;">$2</a>')
211
+ // Line breaks
212
+ .replace(/<br\s*\/?>/gi, '<br />');
213
+ return result;
214
+ }
215
+ /**
216
+ * Extract text content from block data
217
+ */
218
+ function extractTextFromBlock(block) {
219
+ if (block.data.text)
220
+ return String(block.data.text);
221
+ if (block.data.html) {
222
+ const html = String(block.data.html);
223
+ return html.replace(/<[^>]*>/g, '').trim();
224
+ }
225
+ if (block.data.title)
226
+ return String(block.data.title);
227
+ return '';
228
+ }
229
+ /**
230
+ * Escape HTML entities
231
+ */
232
+ function escapeHtml(text) {
233
+ const map = {
234
+ '&': '&amp;',
235
+ '<': '&lt;',
236
+ '>': '&gt;',
237
+ '"': '&quot;',
238
+ "'": '&#039;',
239
+ };
240
+ return text.replace(/[&<>"']/g, (m) => map[m]);
241
+ }
242
+ /**
243
+ * Render all blocks to email HTML
244
+ */
245
+ export function renderBlocksToEmail(blocks, context = {}) {
246
+ return blocks.map(block => renderBlockToEmail(block, context)).join('\n');
247
+ }
248
+ /**
249
+ * Generate complete email HTML from newsletter
250
+ * Uses the same styling as the welcome email template
251
+ */
252
+ export function generateNewsletterEmailHtml(blocks, metadata, context) {
253
+ const { baseUrl = '', unsubscribeUrl, footerText, logoUrl, logoAlt = 'Logo', locale = 'en' } = context;
254
+ const contentHtml = renderBlocksToEmail(blocks, context);
255
+ // Get unsubscribe text based on locale (matching welcome email)
256
+ const isDutch = locale === 'nl';
257
+ const unsubscribeText = isDutch ? 'Afmelden' : 'Unsubscribe';
258
+ // Build header HTML if logo is provided
259
+ const headerHtml = logoUrl ? `
260
+ <div class="header">
261
+ <img src="${logoUrl}" alt="${escapeHtml(logoAlt)}" class="logo">
262
+ </div>` : '';
263
+ return `<!DOCTYPE html>
264
+ <html>
265
+ <head>
266
+ <meta charset="utf-8">
267
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
268
+ <meta name="color-scheme" content="light">
269
+ <style>
270
+ body {
271
+ background-color: #faf9f6;
272
+ margin: 0;
273
+ padding: 0;
274
+ font-family: 'Georgia', serif;
275
+ -webkit-font-smoothing: antialiased;
276
+ -moz-osx-font-smoothing: grayscale;
277
+ }
278
+ .container {
279
+ max-width: 600px;
280
+ margin: 20px auto;
281
+ background-color: #ffffff;
282
+ border-radius: 40px;
283
+ border: 1px solid #1a2e260d;
284
+ overflow: hidden;
285
+ }
286
+ .header {
287
+ padding: 40px 0 20px 0;
288
+ text-align: center;
289
+ }
290
+ .logo {
291
+ width: 180px;
292
+ height: auto;
293
+ }
294
+ .content {
295
+ padding: 0 50px 40px 50px;
296
+ color: #1a2e26;
297
+ line-height: 1.8;
298
+ font-size: 15px;
299
+ }
300
+ .footer {
301
+ padding: 40px 50px;
302
+ text-align: center;
303
+ font-family: sans-serif;
304
+ font-size: 10px;
305
+ color: #a1a1aa;
306
+ letter-spacing: 1px;
307
+ border-top: 1px solid #faf9f6;
308
+ }
309
+ h1 {
310
+ font-weight: normal;
311
+ font-style: italic;
312
+ font-size: 30px;
313
+ margin-bottom: 30px;
314
+ color: #1a2e26;
315
+ text-align: center;
316
+ }
317
+ .divider {
318
+ height: 1px;
319
+ width: 40px;
320
+ background-color: #1a2e2620;
321
+ margin: 30px auto;
322
+ }
323
+ @media only screen and (max-width: 600px) {
324
+ .container {
325
+ width: 100% !important;
326
+ border-radius: 0 !important;
327
+ }
328
+ .content {
329
+ padding: 0 30px 30px 30px !important;
330
+ }
331
+ .footer {
332
+ padding: 30px !important;
333
+ }
334
+ }
335
+ </style>
336
+ </head>
337
+ <body>
338
+ <div class="container">
339
+ ${headerHtml}
340
+ <div class="content">
341
+ ${contentHtml}
342
+ ${unsubscribeUrl ? `
343
+ <div class="divider"></div>
344
+ <p style="text-align: center; font-size: 12px; color: #a1a1aa;">
345
+ <a href="${unsubscribeUrl}" style="color: #a1a1aa; text-decoration: none;">
346
+ ${unsubscribeText}
347
+ </a>
348
+ </p>
349
+ ` : ''}
350
+ </div>
351
+ ${footerText ? `
352
+ <div class="footer">
353
+ ${footerText}
354
+ </div>
355
+ ` : ''}
356
+ </div>
357
+ </body>
358
+ </html>`;
359
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Email Rendering Exports
3
+ */
4
+ export { renderBlockToEmail, renderBlocksToEmail, generateNewsletterEmailHtml } from './EmailRenderer';
5
+ export type { EmailRenderContext } from './EmailRenderer';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/email/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AACvG,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Email Rendering Exports
3
+ */
4
+ export { renderBlockToEmail, renderBlocksToEmail, generateNewsletterEmailHtml } from './EmailRenderer';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * API Mapper
3
+ * Converts editor state to API format and vice versa
4
+ */
5
+ import { EditorState } from '../../state/types';
6
+ import { Newsletter, NewsletterPublicationData } from '../../types/newsletter';
7
+ import { Block } from '../../types/block';
8
+ /**
9
+ * Convert editor state to API format
10
+ */
11
+ export declare function editorStateToAPI(state: EditorState): {
12
+ title: string;
13
+ slug: string;
14
+ blocks: Block[];
15
+ metadata: {
16
+ subject: string;
17
+ previewText?: string;
18
+ lang?: string;
19
+ recipientFilter?: {
20
+ type: 'all' | 'language' | 'custom';
21
+ value?: string;
22
+ };
23
+ };
24
+ publication: NewsletterPublicationData;
25
+ };
26
+ /**
27
+ * Convert API format to editor state
28
+ */
29
+ export declare function apiToEditorState(newsletter: Newsletter): Partial<EditorState>;
30
+ //# sourceMappingURL=apiMapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiMapper.d.ts","sourceRoot":"","sources":["../../../src/lib/mappers/apiMapper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,QAAQ,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,eAAe,CAAC,EAAE;YACd,IAAI,EAAE,KAAK,GAAG,UAAU,GAAG,QAAQ,CAAC;YACpC,KAAK,CAAC,EAAE,MAAM,CAAC;SAClB,CAAC;KACL,CAAC;IACF,WAAW,EAAE,yBAAyB,CAAC;CAC1C,CAgBA;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAS7E"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * API Mapper
3
+ * Converts editor state to API format and vice versa
4
+ */
5
+ /**
6
+ * Convert editor state to API format
7
+ */
8
+ export function editorStateToAPI(state) {
9
+ // For newsletters, use subject as title if title is empty
10
+ // This makes sense since email subject is the primary identifier
11
+ const newsletterTitle = state.title?.trim() || state.metadata?.subject?.trim() || '';
12
+ return {
13
+ title: newsletterTitle,
14
+ slug: state.slug,
15
+ blocks: state.blocks,
16
+ metadata: state.metadata,
17
+ publication: {
18
+ status: state.status,
19
+ authorId: undefined, // Will be set by API handler
20
+ updatedAt: new Date().toISOString(),
21
+ },
22
+ };
23
+ }
24
+ /**
25
+ * Convert API format to editor state
26
+ */
27
+ export function apiToEditorState(newsletter) {
28
+ return {
29
+ blocks: newsletter.blocks,
30
+ title: newsletter.title,
31
+ slug: newsletter.slug,
32
+ metadata: newsletter.metadata,
33
+ status: newsletter.publication.status,
34
+ newsletterId: newsletter.id,
35
+ };
36
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Block Helper Utilities
3
+ * Functions for working with blocks, including nested structures
4
+ */
5
+ import { Block } from '../../types/block';
6
+ /**
7
+ * Get child blocks from a container block
8
+ * Handles both Block[] and string[] formats
9
+ */
10
+ export declare function getChildBlocks(block: Block, allBlocks?: Block[]): Block[];
11
+ /**
12
+ * Check if a block is a container (has children or is marked as container)
13
+ */
14
+ export declare function isContainerBlock(block: Block, blockRegistry: {
15
+ get: (type: string) => {
16
+ isContainer?: boolean;
17
+ } | undefined;
18
+ }): boolean;
19
+ /**
20
+ * Find a block by ID recursively (including nested blocks)
21
+ */
22
+ export declare function findBlockById(blocks: Block[], id: string): Block | null;
23
+ //# sourceMappingURL=blockHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blockHelpers.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/blockHelpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,GAAE,KAAK,EAAO,GAAG,KAAK,EAAE,CAiC7E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE;IAAE,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAA;CAAE,GAAG,OAAO,CAGvI;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAavE"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Block Helper Utilities
3
+ * Functions for working with blocks, including nested structures
4
+ */
5
+ /**
6
+ * Get child blocks from a container block
7
+ * Handles both Block[] and string[] formats
8
+ */
9
+ export function getChildBlocks(block, allBlocks = []) {
10
+ if (!block.children || block.children.length === 0) {
11
+ return [];
12
+ }
13
+ // If children are Block objects, return them directly
14
+ if (block.children.length > 0 && typeof block.children[0] === 'object' && 'id' in block.children[0]) {
15
+ return block.children;
16
+ }
17
+ // If children are IDs, resolve them from allBlocks (recursively search nested blocks)
18
+ const childIds = block.children;
19
+ const resolvedBlocks = [];
20
+ function findBlockRecursive(blocks, id) {
21
+ for (const b of blocks) {
22
+ if (b.id === id)
23
+ return b;
24
+ if (b.children && Array.isArray(b.children) && b.children.length > 0) {
25
+ if (typeof b.children[0] === 'object') {
26
+ const found = findBlockRecursive(b.children, id);
27
+ if (found)
28
+ return found;
29
+ }
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+ childIds.forEach(id => {
35
+ const found = findBlockRecursive(allBlocks, id);
36
+ if (found)
37
+ resolvedBlocks.push(found);
38
+ });
39
+ return resolvedBlocks;
40
+ }
41
+ /**
42
+ * Check if a block is a container (has children or is marked as container)
43
+ */
44
+ export function isContainerBlock(block, blockRegistry) {
45
+ const definition = blockRegistry.get(block.type);
46
+ return definition?.isContainer === true || (block.children !== undefined && block.children.length > 0);
47
+ }
48
+ /**
49
+ * Find a block by ID recursively (including nested blocks)
50
+ */
51
+ export function findBlockById(blocks, id) {
52
+ for (const block of blocks) {
53
+ if (block.id === id) {
54
+ return block;
55
+ }
56
+ if (block.children && Array.isArray(block.children) && block.children.length > 0) {
57
+ if (typeof block.children[0] === 'object') {
58
+ const found = findBlockById(block.children, id);
59
+ if (found)
60
+ return found;
61
+ }
62
+ }
63
+ }
64
+ return null;
65
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Slug Utilities
3
+ * Functions for generating and validating URL slugs
4
+ */
5
+ /**
6
+ * Convert a string to a URL-friendly slug
7
+ */
8
+ export declare function slugify(text: string): string;
9
+ /**
10
+ * Generate a slug from a title
11
+ * Automatically handles edge cases and ensures uniqueness
12
+ */
13
+ export declare function generateSlugFromTitle(title: string, existingSlugs?: string[]): string;
14
+ //# sourceMappingURL=slugify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slugify.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/slugify.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAU5C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,GAAE,MAAM,EAAO,GAAG,MAAM,CAkBzF"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Slug Utilities
3
+ * Functions for generating and validating URL slugs
4
+ */
5
+ /**
6
+ * Convert a string to a URL-friendly slug
7
+ */
8
+ export function slugify(text) {
9
+ return text
10
+ .toString()
11
+ .toLowerCase()
12
+ .trim()
13
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
14
+ .replace(/[^\w\-]+/g, '') // Remove all non-word chars
15
+ .replace(/\-\-+/g, '-') // Replace multiple hyphens with single hyphen
16
+ .replace(/^-+/, '') // Trim hyphens from start
17
+ .replace(/-+$/, ''); // Trim hyphens from end
18
+ }
19
+ /**
20
+ * Generate a slug from a title
21
+ * Automatically handles edge cases and ensures uniqueness
22
+ */
23
+ export function generateSlugFromTitle(title, existingSlugs = []) {
24
+ let baseSlug = slugify(title);
25
+ // If slug is empty after processing, use a fallback
26
+ if (!baseSlug) {
27
+ baseSlug = 'untitled-newsletter';
28
+ }
29
+ // Check for collisions and append number if needed
30
+ let finalSlug = baseSlug;
31
+ let counter = 1;
32
+ while (existingSlugs.includes(finalSlug)) {
33
+ finalSlug = `${baseSlug}-${counter}`;
34
+ counter++;
35
+ }
36
+ return finalSlug;
37
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Block Registry for Newsletter Plugin
3
+ * Dynamic registry for all block types in system
4
+ * Multi-Tenant Architecture: Blocks are provided by client applications
5
+ *
6
+ * The registry is a singleton that starts empty and is populated by client apps
7
+ */
8
+ import { BlockTypeDefinition } from '../types/block';
9
+ interface IBlockRegistry {
10
+ register(definition: BlockTypeDefinition): void;
11
+ get(type: string): BlockTypeDefinition | undefined;
12
+ getAll(): BlockTypeDefinition[];
13
+ has(type: string): boolean;
14
+ clear(): void;
15
+ }
16
+ /**
17
+ * Block Registry Implementation
18
+ * Singleton that manages all block types in the system
19
+ */
20
+ declare class BlockRegistryImpl implements IBlockRegistry {
21
+ private blocks;
22
+ register(definition: BlockTypeDefinition): void;
23
+ get(type: string): BlockTypeDefinition | undefined;
24
+ getAll(): BlockTypeDefinition[];
25
+ has(type: string): boolean;
26
+ clear(): void;
27
+ }
28
+ export declare const BlockRegistry: BlockRegistryImpl;
29
+ export declare const blockRegistry: BlockRegistryImpl;
30
+ export {};
31
+ //# sourceMappingURL=BlockRegistry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlockRegistry.d.ts","sourceRoot":"","sources":["../../src/registry/BlockRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACH,mBAAmB,EAEtB,MAAM,gBAAgB,CAAC;AAGxB,UAAU,cAAc;IACpB,QAAQ,CAAC,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAChD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAAC;IACnD,MAAM,IAAI,mBAAmB,EAAE,CAAC;IAChC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,KAAK,IAAI,IAAI,CAAC;CACjB;AAED;;;GAGG;AACH,cAAM,iBAAkB,YAAW,cAAc;IAC7C,OAAO,CAAC,MAAM,CAA+C;IAE7D,QAAQ,CAAC,UAAU,EAAE,mBAAmB,GAAG,IAAI;IAI/C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAIlD,MAAM,IAAI,mBAAmB,EAAE;IAI/B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,KAAK,IAAI,IAAI;CAGhB;AAGD,eAAO,MAAM,aAAa,mBAA0B,CAAC;AACrD,eAAO,MAAM,aAAa,mBAAgB,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Block Registry for Newsletter Plugin
3
+ * Dynamic registry for all block types in system
4
+ * Multi-Tenant Architecture: Blocks are provided by client applications
5
+ *
6
+ * The registry is a singleton that starts empty and is populated by client apps
7
+ */
8
+ /**
9
+ * Block Registry Implementation
10
+ * Singleton that manages all block types in the system
11
+ */
12
+ class BlockRegistryImpl {
13
+ constructor() {
14
+ this.blocks = new Map();
15
+ }
16
+ register(definition) {
17
+ this.blocks.set(definition.type, definition);
18
+ }
19
+ get(type) {
20
+ return this.blocks.get(type);
21
+ }
22
+ getAll() {
23
+ return Array.from(this.blocks.values());
24
+ }
25
+ has(type) {
26
+ return this.blocks.has(type);
27
+ }
28
+ clear() {
29
+ this.blocks.clear();
30
+ }
31
+ }
32
+ // Export singleton instance
33
+ export const BlockRegistry = new BlockRegistryImpl();
34
+ export const blockRegistry = BlockRegistry;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Registry Exports
3
+ */
4
+ export { blockRegistry } from './BlockRegistry';
5
+ //# sourceMappingURL=index.d.ts.map