@jhits/plugin-newsletter 0.0.15 → 0.0.17

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 (90) hide show
  1. package/dist/api/email-utils.d.ts.map +1 -1
  2. package/dist/api/email-utils.js +45 -4
  3. package/dist/api/handlers/newsletters.d.ts.map +1 -1
  4. package/dist/api/handlers/newsletters.js +33 -16
  5. package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
  6. package/dist/api/handlers/send-newsletter.js +54 -6
  7. package/dist/api/handlers/settings.d.ts.map +1 -1
  8. package/dist/api/handlers/settings.js +51 -1
  9. package/dist/index.d.ts +27 -10
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +15 -122
  12. package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
  13. package/dist/lib/blocks/BlockRenderer.js +14 -2
  14. package/dist/lib/email/EmailRenderer.d.ts +1 -0
  15. package/dist/lib/email/EmailRenderer.d.ts.map +1 -1
  16. package/dist/lib/email/EmailRenderer.js +31 -19
  17. package/dist/lib/utils/config-resolver.d.ts +33 -0
  18. package/dist/lib/utils/config-resolver.d.ts.map +1 -0
  19. package/dist/lib/utils/config-resolver.js +47 -0
  20. package/dist/registry/BlockRegistry.d.ts +9 -1
  21. package/dist/registry/BlockRegistry.d.ts.map +1 -1
  22. package/dist/registry/BlockRegistry.js +126 -8
  23. package/dist/state/EditorContext.d.ts +11 -1
  24. package/dist/state/EditorContext.d.ts.map +1 -1
  25. package/dist/state/EditorContext.js +23 -5
  26. package/dist/state/types.d.ts +12 -0
  27. package/dist/state/types.d.ts.map +1 -1
  28. package/dist/types/block.d.ts +9 -0
  29. package/dist/types/block.d.ts.map +1 -1
  30. package/dist/types/newsletter.d.ts +4 -0
  31. package/dist/types/newsletter.d.ts.map +1 -1
  32. package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
  33. package/dist/views/CanvasEditor/BlockWrapper.js +24 -3
  34. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  35. package/dist/views/CanvasEditor/CanvasEditorView.js +77 -17
  36. package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
  37. package/dist/views/CanvasEditor/EditorBody.js +1 -1
  38. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
  39. package/dist/views/CanvasEditor/components/EditorCanvas.js +158 -100
  40. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +3 -1
  41. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
  42. package/dist/views/CanvasEditor/components/EditorSidebar.js +3 -3
  43. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +1 -1
  44. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -1
  45. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.js +6 -40
  46. package/dist/views/NewsletterManager.d.ts.map +1 -1
  47. package/dist/views/NewsletterManager.js +87 -5
  48. package/dist/views/components/DomainPromptModal.d.ts +13 -0
  49. package/dist/views/components/DomainPromptModal.d.ts.map +1 -0
  50. package/dist/views/components/DomainPromptModal.js +58 -0
  51. package/dist/views/components/NewsletterCard.d.ts +16 -0
  52. package/dist/views/components/NewsletterCard.d.ts.map +1 -0
  53. package/dist/views/components/NewsletterCard.js +94 -0
  54. package/dist/views/components/NewsletterGrid.d.ts +16 -0
  55. package/dist/views/components/NewsletterGrid.d.ts.map +1 -0
  56. package/dist/views/components/NewsletterGrid.js +13 -0
  57. package/dist/views/components/SendNewsletterModal.d.ts.map +1 -1
  58. package/dist/views/components/SendNewsletterModal.js +91 -22
  59. package/dist/views/components/SmtpSettingsModal.d.ts.map +1 -1
  60. package/dist/views/components/SmtpSettingsModal.js +10 -0
  61. package/dist/views/components/TestEmailModal.d.ts.map +1 -1
  62. package/dist/views/components/TestEmailModal.js +86 -17
  63. package/package.json +53 -9
  64. package/src/api/email-utils.ts +53 -4
  65. package/src/api/handlers/newsletters.ts +40 -20
  66. package/src/api/handlers/send-newsletter.ts +65 -6
  67. package/src/api/handlers/settings.ts +60 -2
  68. package/src/index.tsx +49 -155
  69. package/src/lib/blocks/BlockRenderer.tsx +16 -2
  70. package/src/lib/email/EmailRenderer.tsx +31 -20
  71. package/src/lib/utils/config-resolver.ts +71 -0
  72. package/src/registry/BlockRegistry.tsx +255 -0
  73. package/src/state/EditorContext.tsx +43 -8
  74. package/src/state/types.ts +16 -0
  75. package/src/types/block.ts +10 -0
  76. package/src/types/newsletter.ts +5 -0
  77. package/src/views/CanvasEditor/BlockWrapper.tsx +27 -2
  78. package/src/views/CanvasEditor/CanvasEditorView.tsx +142 -61
  79. package/src/views/CanvasEditor/EditorBody.tsx +17 -13
  80. package/src/views/CanvasEditor/components/EditorCanvas.tsx +178 -115
  81. package/src/views/CanvasEditor/components/EditorSidebar.tsx +57 -2
  82. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +6 -45
  83. package/src/views/NewsletterManager.tsx +164 -6
  84. package/src/views/components/DomainPromptModal.tsx +160 -0
  85. package/src/views/components/NewsletterCard.tsx +212 -0
  86. package/src/views/components/NewsletterGrid.tsx +48 -0
  87. package/src/views/components/SendNewsletterModal.tsx +270 -184
  88. package/src/views/components/SmtpSettingsModal.tsx +11 -0
  89. package/src/views/components/TestEmailModal.tsx +235 -149
  90. package/src/registry/BlockRegistry.ts +0 -53
@@ -120,6 +120,11 @@ export async function GET_SMTP_SETTINGS(
120
120
  fromName: '',
121
121
  primaryLanguage: 'en',
122
122
  logoUrl: '',
123
+ unsubscribeTranslations: {
124
+ en: 'Unsubscribe',
125
+ nl: 'Afmelden',
126
+ sv: 'Avanmälan',
127
+ },
123
128
  });
124
129
  }
125
130
 
@@ -132,6 +137,11 @@ export async function GET_SMTP_SETTINGS(
132
137
  fromName: smtpConfig.fromName || '',
133
138
  primaryLanguage: smtpConfig.primaryLanguage || 'en',
134
139
  logoUrl: smtpConfig.logoUrl || '',
140
+ unsubscribeTranslations: smtpConfig.unsubscribeTranslations || {
141
+ en: 'Unsubscribe',
142
+ nl: 'Afmelden',
143
+ sv: 'Avanmälan',
144
+ },
135
145
  });
136
146
  } catch (error: any) {
137
147
  console.error('[NewsletterAPI] GET_SMTP_SETTINGS error:', error);
@@ -178,6 +188,7 @@ export async function POST_SMTP_SETTINGS(
178
188
  fromName: body.fromName || '',
179
189
  primaryLanguage: body.primaryLanguage || 'en',
180
190
  logoUrl: body.logoUrl || '',
191
+ unsubscribeTranslations: body.unsubscribeTranslations || {},
181
192
  updatedAt: new Date(),
182
193
  updatedBy: userId,
183
194
  },
@@ -217,6 +228,41 @@ function getSmtpConfigFromDb(config: NewsletterApiConfig): Promise<{ host: strin
217
228
  })();
218
229
  }
219
230
 
231
+ async function resolveBaseUrl(config: NewsletterApiConfig, language: string): Promise<string> {
232
+ try {
233
+ const dbConnection = await config.getDb();
234
+ const db = dbConnection.db();
235
+ const settings = db.collection('settings');
236
+
237
+ // Try to get site config from plugin-website (stored in 'settings' collection with identifier 'site_config')
238
+ const siteConfig = await settings.findOne({ identifier: 'site_config' });
239
+
240
+ if (siteConfig && siteConfig.domainLocaleConfig && Array.isArray(siteConfig.domainLocaleConfig)) {
241
+ // Find domain for this locale
242
+ const localeConfig = siteConfig.domainLocaleConfig.find((c: any) => c.locale === language);
243
+ if (localeConfig && localeConfig.domain && localeConfig.domain !== 'undefined' && localeConfig.domain.trim() !== '') {
244
+ const domain = localeConfig.domain.trim();
245
+ // Add protocol if missing
246
+ if (!domain.startsWith('http')) {
247
+ const protocol = domain.includes('localhost') ? 'http' : 'https';
248
+ return `${protocol}://${domain}`;
249
+ }
250
+ return domain;
251
+ }
252
+ }
253
+ } catch (error) {
254
+ console.warn('[NewsletterAPI] Failed to resolve language-specific base URL:', error);
255
+ }
256
+
257
+ // Fallback to default, carefully checking for 'undefined' string
258
+ const fallback = process.env.NEXT_PUBLIC_SITE_URL;
259
+ if (fallback && fallback !== 'undefined' && fallback.trim() !== '') {
260
+ return fallback;
261
+ }
262
+
263
+ return 'https://botanicsandyou.com';
264
+ }
265
+
220
266
  export async function POST_TEST_EMAIL(
221
267
  req: NextRequest,
222
268
  config: NewsletterApiConfig
@@ -267,8 +313,20 @@ export async function POST_TEST_EMAIL(
267
313
  logoExists = fs.existsSync(altPath);
268
314
  }
269
315
 
270
- const baseUrl = config.baseUrl || 'http://localhost:3001';
271
-
316
+ // Resolve base URL based on language settings from plugin-website
317
+ const baseUrl = await resolveBaseUrl(config, language);
318
+
319
+ // Final sanity check - if domain is STILL undefined, stop sending and ask user for domain
320
+ if (baseUrl.includes('undefined')) {
321
+ return NextResponse.json(
322
+ {
323
+ error: 'Domain not configured for this language. Please define your website domain first.',
324
+ code: 'DOMAIN_MISSING'
325
+ },
326
+ { status: 400 }
327
+ );
328
+ }
329
+
272
330
  let logoAttachment: any = undefined;
273
331
  let logoSrc = smtpConfig.logoUrl || `${baseUrl}/logo_black.svg`;
274
332
 
package/src/index.tsx CHANGED
@@ -15,6 +15,7 @@ import { SettingsView } from './views/SettingsView';
15
15
  import { NewsletterManagerView } from './views/NewsletterManager';
16
16
  import { NewsletterEditorView } from './views/NewsletterEditor';
17
17
  import { editorStateToAPI } from './lib/mappers/apiMapper';
18
+ import { resolvePluginConfig } from './lib/utils/config-resolver';
18
19
 
19
20
  /**
20
21
  * Plugin Props Interface
@@ -35,109 +36,48 @@ export interface PluginProps {
35
36
  /** Background color for dark mode (optional) */
36
37
  dark?: string;
37
38
  };
39
+ /** Localized strings for modular blocks in the editor */
40
+ translations?: Record<string, any>;
41
+ /** Global translations for unsubscribe text */
42
+ unsubscribeTranslations?: Record<string, string>;
38
43
  }
39
44
 
45
+ /**
46
+ * Client-facing configuration type
47
+ * Allows partial overrides of plugin behavior
48
+ */
49
+ export type NewsletterPluginConfig = Partial<Omit<PluginProps, 'subPath' | 'siteId' | 'locale'>> & {
50
+ customBlocks?: ClientBlockDefinition[];
51
+ darkMode?: boolean;
52
+ backgroundColors?: {
53
+ light: string;
54
+ dark?: string;
55
+ };
56
+ translations?: Record<string, any>;
57
+ unsubscribeTranslations?: Record<string, string>;
58
+ emailConfig?: {
59
+ logoUrl?: string;
60
+ logoAlt?: string;
61
+ footerText?: string;
62
+ primaryColor?: string;
63
+ };
64
+ };
65
+
40
66
  /**
41
67
  * Main Router Component
42
68
  * Handles routing within the newsletter plugin
43
- *
44
- * Client Handshake:
45
- * - Client apps can pass customBlocks via props
46
- * - Or via window.__JHITS_PLUGIN_PROPS__['plugin-newsletter'].customBlocks
47
- * - The EditorProvider will automatically register these blocks
48
69
  */
49
70
  export default function NewsletterPlugin(props: PluginProps) {
50
- const { subPath, siteId, locale, customBlocks: propsCustomBlocks, darkMode: propsDarkMode, backgroundColors: propsBackgroundColors } = props;
51
-
52
- // Get custom blocks from props or window global (client app injection point)
53
- const customBlocks = useMemo(() => {
54
- // First, try props
55
- if (propsCustomBlocks && propsCustomBlocks.length > 0) {
56
- return propsCustomBlocks;
57
- }
58
-
59
- // Fallback to window global (for client app injection)
60
- if (typeof window !== 'undefined' && (window as any).__JHITS_PLUGIN_PROPS__) {
61
- const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__['plugin-newsletter'];
62
- if (pluginProps?.customBlocks) {
63
- return pluginProps.customBlocks as ClientBlockDefinition[];
64
- }
65
- }
66
-
67
- return [];
68
- }, [propsCustomBlocks]);
69
-
70
- // Get dark mode setting from props, localStorage (dev settings), or window global
71
- // Priority: localStorage (dev) > props > window global > default
72
- const darkMode = useMemo(() => {
73
- // First, check localStorage for dev settings (highest priority for dev)
74
- if (typeof window !== 'undefined') {
75
- try {
76
- const saved = localStorage.getItem('__JHITS_PLUGIN_NEWSLETTER_CONFIG__');
77
- if (saved) {
78
- const config = JSON.parse(saved);
79
- if (config.darkMode !== undefined) {
80
- return config.darkMode as boolean;
81
- }
82
- }
83
- } catch (e) {
84
- // Ignore localStorage errors
85
- }
86
- }
87
-
88
- // Then try props
89
- if (propsDarkMode !== undefined) {
90
- return propsDarkMode;
91
- }
92
-
93
- // Fallback to window global if prop not provided
94
- if (typeof window !== 'undefined' && (window as any).__JHITS_PLUGIN_PROPS__) {
95
- const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__['plugin-newsletter'];
96
- if (pluginProps?.darkMode !== undefined) {
97
- return pluginProps.darkMode as boolean;
98
- }
99
- }
100
-
101
- return true; // Default to dark mode enabled
102
- }, [propsDarkMode]);
103
-
104
- // Get background colors from props, localStorage (dev settings), or window global
105
- // Priority: localStorage (dev) > props > window global
106
- const backgroundColors = useMemo(() => {
107
- // First, check localStorage for dev settings (highest priority for dev)
108
- if (typeof window !== 'undefined') {
109
- try {
110
- const saved = localStorage.getItem('__JHITS_PLUGIN_NEWSLETTER_CONFIG__');
111
- if (saved) {
112
- const config = JSON.parse(saved);
113
- if (config.backgroundColors) {
114
- return config.backgroundColors;
115
- }
116
- }
117
- } catch (e) {
118
- // Ignore localStorage errors
119
- }
120
- }
121
-
122
- // Then try props
123
- if (propsBackgroundColors) {
124
- return propsBackgroundColors;
125
- }
126
-
127
- // Fallback to window global
128
- if (typeof window !== 'undefined' && (window as any).__JHITS_PLUGIN_PROPS__) {
129
- const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__['plugin-newsletter'];
130
- if (pluginProps?.backgroundColors) {
131
- return pluginProps.backgroundColors as { light: string; dark?: string };
132
- }
133
- }
134
-
135
- return undefined;
136
- }, [propsBackgroundColors]);
71
+ const { subPath, siteId, locale } = props;
137
72
 
73
+ // Resolve configuration from multiple sources (Props, Window, Storage)
74
+ const config = useMemo(() => resolvePluginConfig(props), [props]);
75
+ const { customBlocks, darkMode, backgroundColors, translations, emailConfig, unsubscribeTranslations } = config;
76
+
138
77
  const route = subPath[0] || 'newsletters';
139
-
140
- // Listen for config updates from settings screen
78
+ const newsletterId = subPath[1];
79
+
80
+ // Listen for config updates (e.g. from settings screen)
141
81
  useEffect(() => {
142
82
  if (typeof window === 'undefined') return;
143
83
 
@@ -158,12 +98,14 @@ export default function NewsletterPlugin(props: PluginProps) {
158
98
  return <NewsletterManagerView siteId={siteId} locale={locale} />;
159
99
 
160
100
  case 'editor':
161
- const newsletterId = subPath[1];
162
101
  return (
163
102
  <EditorProvider
164
103
  customBlocks={customBlocks}
165
104
  darkMode={darkMode}
166
105
  backgroundColors={backgroundColors}
106
+ translations={translations}
107
+ emailConfig={emailConfig}
108
+ unsubscribeTranslations={unsubscribeTranslations}
167
109
  onSave={async (state, extraData?: { language?: string }) => {
168
110
  // Save to API - create new or update existing newsletter
169
111
  const originalId = newsletterId || state.slug;
@@ -180,7 +122,6 @@ export default function NewsletterPlugin(props: PluginProps) {
180
122
 
181
123
  // If we have an id, try to update first
182
124
  if (originalId) {
183
- console.log('[NewsletterPlugin] Attempting to update newsletter with id:', originalId);
184
125
  const updateResponse = await fetch(`/api/plugin-newsletter/newsletters/${originalId}${langParam}`, {
185
126
  method: 'PUT',
186
127
  headers: { 'Content-Type': 'application/json' },
@@ -190,32 +131,15 @@ export default function NewsletterPlugin(props: PluginProps) {
190
131
 
191
132
  if (updateResponse.ok) {
192
133
  const result = await updateResponse.json();
193
- // If the id changed, update the URL
194
134
  if (result.id && result.id !== originalId) {
195
135
  window.history.replaceState(null, '', `/dashboard/newsletter/editor/${result.id}`);
196
136
  }
197
137
  return result;
198
138
  }
199
-
200
- // If 404, newsletter doesn't exist, create a new one
201
- if (updateResponse.status === 404) {
202
- console.log('[NewsletterPlugin] Newsletter not found, creating new newsletter');
203
- } else {
204
- // Other error, throw it
205
- const error = await updateResponse.json();
206
- console.error('[NewsletterPlugin] Save failed:', {
207
- status: updateResponse.status,
208
- statusText: updateResponse.statusText,
209
- error,
210
- });
211
- const errorMessage = error.message || error.error || 'Failed to save newsletter';
212
- throw new Error(errorMessage);
213
- }
214
139
  }
215
140
 
216
- // Create new newsletter (either no id or update returned 404)
217
- console.log('[NewsletterPlugin] Creating new newsletter');
218
- const createResponse = await fetch('/api/plugin-newsletter/newsletters/new', {
141
+ // Create new newsletter
142
+ const createResponse = await fetch(`/api/plugin-newsletter/newsletters/new${langParam}`, {
219
143
  method: 'POST',
220
144
  headers: { 'Content-Type': 'application/json' },
221
145
  credentials: 'include',
@@ -224,17 +148,11 @@ export default function NewsletterPlugin(props: PluginProps) {
224
148
 
225
149
  if (!createResponse.ok) {
226
150
  const error = await createResponse.json();
227
- console.error('[NewsletterPlugin] Create failed:', {
228
- status: createResponse.status,
229
- statusText: createResponse.statusText,
230
- error,
231
- });
232
151
  const errorMessage = error.message || error.error || 'Failed to create newsletter';
233
152
  throw new Error(errorMessage);
234
153
  }
235
154
 
236
155
  const result = await createResponse.json();
237
- // Update the URL to the new newsletter's id
238
156
  if (result.id) {
239
157
  window.history.replaceState(null, '', `/dashboard/newsletter/editor/${result.id}`);
240
158
  }
@@ -251,11 +169,11 @@ export default function NewsletterPlugin(props: PluginProps) {
251
169
  customBlocks={customBlocks}
252
170
  darkMode={darkMode}
253
171
  backgroundColors={backgroundColors}
172
+ translations={translations}
173
+ emailConfig={emailConfig}
174
+ unsubscribeTranslations={unsubscribeTranslations}
254
175
  onSave={async (state, extraData?: { language?: string }) => {
255
- // Save to API - create new newsletter
256
176
  const apiData = editorStateToAPI(state);
257
-
258
- // Include language in metadata if provided
259
177
  if (extraData?.language) {
260
178
  apiData.metadata = apiData.metadata || {};
261
179
  apiData.metadata.lang = extraData.language;
@@ -272,7 +190,6 @@ export default function NewsletterPlugin(props: PluginProps) {
272
190
  throw new Error(error.message || 'Failed to create newsletter');
273
191
  }
274
192
  const result = await response.json();
275
- // Update the URL to the new newsletter's slug
276
193
  if (result.slug) {
277
194
  window.history.replaceState(null, '', `/dashboard/newsletter/editor/${result.slug}`);
278
195
  }
@@ -292,6 +209,9 @@ export default function NewsletterPlugin(props: PluginProps) {
292
209
  customBlocks={customBlocks}
293
210
  darkMode={darkMode}
294
211
  backgroundColors={backgroundColors}
212
+ translations={translations}
213
+ emailConfig={emailConfig}
214
+ unsubscribeTranslations={unsubscribeTranslations}
295
215
  isWelcomeEmail={true}
296
216
  onSave={async (state, extraData?: { language?: string }) => {
297
217
  const apiData = editorStateToAPI(state);
@@ -321,36 +241,10 @@ export default function NewsletterPlugin(props: PluginProps) {
321
241
  }
322
242
  }
323
243
 
324
- // Export for use as default
244
+ // Export symbols as needed
325
245
  export { NewsletterPlugin as Index };
326
-
327
- // Export types for client applications
328
- export type {
329
- Block,
330
- BlockTypeDefinition,
331
- ClientBlockDefinition,
332
- RichTextFormattingConfig,
333
- BlockEditProps,
334
- BlockPreviewProps,
335
- IBlockComponent,
336
- } from './types/block';
337
-
338
- // Export newsletter types
339
- export type {
340
- Newsletter,
341
- NewsletterStatus,
342
- NewsletterMetadata,
343
- NewsletterListItem,
344
- NewsletterFilterOptions,
345
- } from './types/newsletter';
346
-
347
- // Export initialization utility for easy setup
246
+ export type { Block, ClientBlockDefinition } from './types/block';
348
247
  export { initNewsletterPlugin } from './init';
349
- export type { NewsletterPluginConfig } from './init';
350
-
351
- // Export editor state management
352
- export { EditorProvider, useEditor } from './state/EditorContext';
353
- export type { EditorProviderProps, EditorState, EditorContextValue } from './state';
354
-
355
- // Export block registry
356
248
  export { blockRegistry } from './registry';
249
+ export { EditorProvider, useEditor } from './state/EditorContext';
250
+ export { BlockRenderer, BlocksRenderer } from './lib/blocks/BlockRenderer';
@@ -12,6 +12,7 @@ import React from 'react';
12
12
  import { Block, BlockPreviewProps } from '../../types/block';
13
13
  import { blockRegistry } from '../../registry/BlockRegistry';
14
14
  import { getChildBlocks } from '../utils/blockHelpers';
15
+ import { useEditor } from '../../state/EditorContext';
15
16
 
16
17
  /**
17
18
  * Block Renderer Props
@@ -40,6 +41,13 @@ export function BlockRenderer({
40
41
  customRenderers,
41
42
  context = {}
42
43
  }: BlockRendererProps) {
44
+ // We access the context to ensure re-render when blocks are registered
45
+ try {
46
+ useEditor();
47
+ } catch (e) {
48
+ // Not in editor context (e.g. preview bridge)
49
+ }
50
+
43
51
  // Check for custom renderer override first
44
52
  if (customRenderers?.has(block.type)) {
45
53
  const CustomRenderer = customRenderers.get(block.type)!;
@@ -47,9 +55,15 @@ export function BlockRenderer({
47
55
  }
48
56
 
49
57
  // Get block definition from registry
50
- const definition = blockRegistry.get(block.type);
58
+ let definition = blockRegistry.get(block.type);
59
+
60
+ // If not found, try one last immediate re-check of the global window registry
61
+ if (!definition && typeof window !== 'undefined' && (window as any).__JHITS_NEWSLETTER_REGISTRY__) {
62
+ definition = (window as any).__JHITS_NEWSLETTER_REGISTRY__.get(block.type);
63
+ }
64
+
51
65
  if (!definition) {
52
- console.warn(`Block type "${block.type}" not found in registry. Available types:`,
66
+ console.warn(`[BlockRenderer] Unknown block type: ${block.type}. Registry contains:`,
53
67
  blockRegistry.getAll().map(b => b.type).join(', '));
54
68
  return (
55
69
  <div className="p-4 border border-red-300 bg-red-50 rounded">
@@ -13,7 +13,6 @@
13
13
 
14
14
  import { Block } from '../../types/block';
15
15
  import { blockRegistry } from '../../registry/BlockRegistry';
16
- import { getChildBlocks } from '../utils/blockHelpers';
17
16
 
18
17
  /**
19
18
  * Email rendering context
@@ -62,15 +61,25 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
62
61
  const level = (block.data.level as number) || 1;
63
62
  const text = (block.data.text as string) || '';
64
63
  const tag = `h${Math.min(Math.max(level, 1), 6)}`;
65
- const fontSize = level === 1 ? '32px' : level === 2 ? '24px' : level === 3 ? '20px' : '18px';
66
- return `<${tag} style="font-size: ${fontSize}; font-weight: bold; color: #1a2e26; margin: 20px 0 10px 0; line-height: 1.3;">${escapeHtml(text)}</${tag}>`;
64
+
65
+ // Aligned with BotanicsAndYou heading sizes
66
+ let fontSize = '30px';
67
+ if (level === 1) fontSize = '48px';
68
+ if (level === 2) fontSize = '36px';
69
+ if (level === 3) fontSize = '30px';
70
+ if (level >= 4) fontSize = '20px';
71
+
72
+ const fontWeight = level >= 4 ? '500' : 'bold';
73
+
74
+ return `<${tag} style="font-family: 'Georgia', serif; font-size: ${fontSize}; font-weight: ${fontWeight}; color: #1a2e26; margin: 40px 0 15px 0; line-height: 1.2;">${escapeHtml(text)}</${tag}>`;
67
75
  }
68
76
 
69
77
  case 'paragraph': {
70
78
  const html = (block.data.html as string) || (block.data.text as string) || '';
71
79
  // Convert basic HTML tags to email-safe inline styles
72
80
  const emailHtml = convertHtmlToEmailSafe(html);
73
- return `<p style="font-size: 15px; line-height: 1.8; color: #1a2e26; margin: 0 0 15px 0;">${emailHtml}</p>`;
81
+ // Aligned with BotanicsAndYou paragraph style (text-lg = 18px)
82
+ return `<p style="font-family: 'Georgia', serif; font-size: 18px; line-height: 1.625; color: #1a2e26; margin: 0 0 24px 0;">${emailHtml}</p>`;
74
83
  }
75
84
 
76
85
  case 'image': {
@@ -146,7 +155,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
146
155
 
147
156
  const imageStyleString = imageStyles.join('; ');
148
157
 
149
- let html = `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 20px 0;">
158
+ let html = `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0;">
150
159
  <tr>
151
160
  <td align="center">
152
161
  <img src="${imageUrl}" alt="${escapeHtml(alt)}" style="${imageStyleString}" />
@@ -155,7 +164,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
155
164
 
156
165
  if (caption) {
157
166
  html += `<tr>
158
- <td align="center" style="padding-top: 10px; font-size: 12px; color: #666; font-style: italic;">
167
+ <td align="center" style="padding-top: 10px; font-size: 14px; color: #666; font-style: italic; font-family: 'Georgia', serif;">
159
168
  ${escapeHtml(caption)}
160
169
  </td>
161
170
  </tr>`;
@@ -174,10 +183,10 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
174
183
  const itemsHtml = items.map((item, idx) => {
175
184
  const text = typeof item === 'string' ? item : (item.html || item.text || '');
176
185
  const emailHtml = convertHtmlToEmailSafe(text);
177
- return `<li style="margin: 8px 0; padding-left: 5px; line-height: 1.8; color: #1a2e26;">${emailHtml}</li>`;
186
+ return `<li style="margin: 12px 0; padding-left: 5px; line-height: 1.625; color: #1a2e26;">${emailHtml}</li>`;
178
187
  }).join('');
179
188
 
180
- return `<${tag} style="margin: 15px 0; padding-left: 25px; color: #1a2e26; font-size: 15px; line-height: 1.8;">${itemsHtml}</${tag}>`;
189
+ return `<${tag} style="font-family: 'Georgia', serif; margin: 24px 0; padding-left: 24px; color: #1a2e26; font-size: 18px; line-height: 1.625;">${itemsHtml}</${tag}>`;
181
190
  }
182
191
 
183
192
  case 'table': {
@@ -189,7 +198,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
189
198
  }
190
199
 
191
200
  // Build table HTML using email-safe table structure
192
- let tableHtml = `<table width="100%" cellpadding="12" cellspacing="0" border="0" style="margin: 20px 0; border-collapse: collapse; width: 100%;">`;
201
+ let tableHtml = `<table width="100%" cellpadding="12" cellspacing="0" border="0" style="margin: 30px 0; border-collapse: collapse; width: 100%; font-family: 'Georgia', serif;">`;
193
202
 
194
203
  rows.forEach((row, rIdx) => {
195
204
  const isHeaderRow = rIdx === 0 && useHeader;
@@ -201,7 +210,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
201
210
  tableHtml += `<tr>`;
202
211
  row.forEach((cell) => {
203
212
  const cellHtml = convertHtmlToEmailSafe(cell.html || cell.text || '');
204
- 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}>`;
213
+ tableHtml += `<${tag} style="padding: 15px; border: 1px solid #e5e7eb; text-align: left; color: ${textColor}; font-size: 16px; line-height: 1.6; background-color: ${bgColor}; font-weight: ${fontWeight};">${cellHtml}</${tag}>`;
205
214
  });
206
215
  tableHtml += `</tr>`;
207
216
  });
@@ -211,7 +220,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
211
220
  }
212
221
 
213
222
  case 'divider': {
214
- return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0;">
223
+ return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 40px 0;">
215
224
  <tr>
216
225
  <td align="center">
217
226
  <table width="40" cellpadding="0" cellspacing="0" border="0">
@@ -228,7 +237,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
228
237
  // Generic fallback - try to extract text content
229
238
  const text = extractTextFromBlock(block);
230
239
  if (text) {
231
- return `<p style="font-size: 15px; line-height: 1.8; color: #1a2e26; margin: 0 0 15px 0;">${escapeHtml(text)}</p>`;
240
+ return `<p style="font-family: 'Georgia', serif; font-size: 18px; line-height: 1.625; color: #1a2e26; margin: 0 0 24px 0;">${escapeHtml(text)}</p>`;
232
241
  }
233
242
  return '';
234
243
  }
@@ -302,18 +311,20 @@ export function generateNewsletterEmailHtml(
302
311
  },
303
312
  context: EmailRenderContext & {
304
313
  unsubscribeUrl?: string;
314
+ unsubscribeText?: string;
305
315
  footerText?: string;
306
316
  logoUrl?: string;
307
317
  logoAlt?: string;
308
318
  locale?: string;
309
319
  }
310
320
  ): string {
311
- const { baseUrl = '', unsubscribeUrl, footerText, logoUrl, logoAlt = 'Logo', locale = 'en' } = context;
321
+ const { baseUrl = '', unsubscribeUrl, unsubscribeText: propsUnsubscribeText, footerText, logoUrl, logoAlt = 'Logo', locale = 'en' } = context;
312
322
  const contentHtml = renderBlocksToEmail(blocks, context);
313
323
 
314
324
  // Get unsubscribe text based on locale (matching welcome email)
315
325
  const isDutch = locale === 'nl';
316
- const unsubscribeText = isDutch ? 'Afmelden' : 'Unsubscribe';
326
+ const defaultText = isDutch ? 'Afmelden' : 'Unsubscribe';
327
+ const unsubscribeText = propsUnsubscribeText || defaultText;
317
328
 
318
329
  // Build header HTML if logo is provided
319
330
  const headerHtml = logoUrl ? `
@@ -355,8 +366,8 @@ export function generateNewsletterEmailHtml(
355
366
  .content {
356
367
  padding: 0 50px 40px 50px;
357
368
  color: #1a2e26;
358
- line-height: 1.8;
359
- font-size: 15px;
369
+ line-height: 1.625;
370
+ font-size: 18px;
360
371
  }
361
372
  .footer {
362
373
  padding: 40px 50px;
@@ -368,12 +379,12 @@ export function generateNewsletterEmailHtml(
368
379
  border-top: 1px solid #faf9f6;
369
380
  }
370
381
  h1 {
371
- font-weight: normal;
372
- font-style: italic;
373
- font-size: 30px;
382
+ font-weight: bold;
383
+ font-size: 48px;
374
384
  margin-bottom: 30px;
375
385
  color: #1a2e26;
376
- text-align: center;
386
+ text-align: left;
387
+ line-height: 1.2;
377
388
  }
378
389
  .divider {
379
390
  height: 1px;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Newsletter Plugin Configuration Resolver
3
+ * Centralizes the logic for resolving plugin settings from multiple sources
4
+ */
5
+
6
+ import { ClientBlockDefinition } from '../../types/block';
7
+
8
+ export interface ResolvedConfig {
9
+ customBlocks: ClientBlockDefinition[];
10
+ darkMode: boolean;
11
+ backgroundColors?: {
12
+ light: string;
13
+ dark?: string;
14
+ };
15
+ translations?: Record<string, any>;
16
+ unsubscribeTranslations?: Record<string, string>;
17
+ emailConfig?: {
18
+ logoUrl?: string;
19
+ logoAlt?: string;
20
+ footerText?: string;
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Resolves a specific configuration value from priority sources:
26
+ * 1. LocalStorage (Dev overrides)
27
+ * 2. Component Props
28
+ * 3. Window Global (__JHITS_PLUGIN_PROPS__)
29
+ * 4. Default Value
30
+ */
31
+ export function resolveConfigValue<T>(
32
+ key: string,
33
+ propValue: T | undefined,
34
+ defaultValue: T,
35
+ pluginKey: string = 'plugin-newsletter'
36
+ ): T {
37
+ if (typeof window === 'undefined') return propValue ?? defaultValue;
38
+
39
+ // 1. Check LocalStorage (Dev priority)
40
+ try {
41
+ const saved = localStorage.getItem(`__JHITS_PLUGIN_NEWSLETTER_CONFIG__`);
42
+ if (saved) {
43
+ const config = JSON.parse(saved);
44
+ if (config[key] !== undefined) return config[key];
45
+ }
46
+ } catch (e) {}
47
+
48
+ // 2. Check Props
49
+ if (propValue !== undefined) return propValue;
50
+
51
+ // 3. Check Window Global
52
+ const globalProps = (window as any).__JHITS_PLUGIN_PROPS__?.[pluginKey] || (window as any).__JHITS_PLUGIN_PROPS__?.['newsletter'];
53
+ if (globalProps && globalProps[key] !== undefined) return globalProps[key];
54
+
55
+ // 4. Default
56
+ return defaultValue;
57
+ }
58
+
59
+ /**
60
+ * Resolves all plugin configuration
61
+ */
62
+ export function resolvePluginConfig(props: any): ResolvedConfig {
63
+ return {
64
+ customBlocks: resolveConfigValue('customBlocks', props.customBlocks, []),
65
+ darkMode: resolveConfigValue('darkMode', props.darkMode, true),
66
+ backgroundColors: resolveConfigValue('backgroundColors', props.backgroundColors, undefined),
67
+ translations: resolveConfigValue('translations', props.translations, undefined),
68
+ unsubscribeTranslations: resolveConfigValue('unsubscribeTranslations', props.unsubscribeTranslations, undefined),
69
+ emailConfig: resolveConfigValue('emailConfig', props.emailConfig, undefined),
70
+ };
71
+ }