@jhits/plugin-newsletter 0.0.9 → 0.0.11

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 (57) hide show
  1. package/package.json +3 -2
  2. package/src/api/email-utils.ts +165 -0
  3. package/src/api/handler.ts +28 -0
  4. package/src/api/handlers/index.ts +44 -0
  5. package/src/api/handlers/newsletters.ts +332 -0
  6. package/src/api/handlers/send-newsletter.ts +288 -0
  7. package/src/api/handlers/settings.ts +403 -0
  8. package/src/api/handlers/subscribers.ts +152 -0
  9. package/src/api/handlers/upload.ts +47 -0
  10. package/src/api/handlers/welcome-email.ts +210 -0
  11. package/src/api/router.ts +166 -0
  12. package/src/index.server.ts +12 -0
  13. package/src/index.tsx +353 -0
  14. package/src/index.tsx.patch +98 -0
  15. package/src/init.tsx +72 -0
  16. package/src/lib/blocks/BlockRenderer.tsx +125 -0
  17. package/src/lib/email/EmailRenderer.tsx +420 -0
  18. package/src/lib/email/index.ts +6 -0
  19. package/src/lib/i18n.ts +82 -0
  20. package/src/lib/mappers/apiMapper.ts +57 -0
  21. package/src/lib/utils/blockHelpers.ts +71 -0
  22. package/src/lib/utils/slugify.ts +43 -0
  23. package/src/registry/BlockRegistry.ts +53 -0
  24. package/src/registry/index.ts +5 -0
  25. package/src/state/EditorContext.tsx +278 -0
  26. package/src/state/index.ts +10 -0
  27. package/src/state/reducer.ts +561 -0
  28. package/src/state/types.ts +154 -0
  29. package/src/types/block.ts +275 -0
  30. package/src/types/newsletter.ts +152 -0
  31. package/src/types/registry.ts +14 -0
  32. package/src/views/CanvasEditor/BlockWrapper.tsx +143 -0
  33. package/src/views/CanvasEditor/CanvasEditorView.tsx +343 -0
  34. package/src/views/CanvasEditor/EditorBody.tsx +95 -0
  35. package/src/views/CanvasEditor/EditorHeader.tsx +255 -0
  36. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +83 -0
  37. package/src/views/CanvasEditor/components/EditorCanvas.tsx +674 -0
  38. package/src/views/CanvasEditor/components/EditorLibrary.tsx +120 -0
  39. package/src/views/CanvasEditor/components/EditorSidebar.tsx +139 -0
  40. package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
  41. package/src/views/CanvasEditor/components/LibraryItem.tsx +71 -0
  42. package/src/views/CanvasEditor/components/SlashCommandDetector.tsx +196 -0
  43. package/src/views/CanvasEditor/components/SlashCommandMenu.tsx +131 -0
  44. package/src/views/CanvasEditor/components/index.ts +16 -0
  45. package/src/views/CanvasEditor/hooks/index.ts +7 -0
  46. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +136 -0
  47. package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +73 -0
  48. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +54 -0
  49. package/src/views/CanvasEditor/hooks/useSlashCommand.ts +106 -0
  50. package/src/views/CanvasEditor/index.ts +12 -0
  51. package/src/views/NewsletterEditor.tsx +42 -0
  52. package/src/views/NewsletterManager.tsx +483 -0
  53. package/src/views/SettingsView.tsx +216 -0
  54. package/src/views/SubscribersView.tsx +269 -0
  55. package/src/views/components/SendNewsletterModal.tsx +322 -0
  56. package/src/views/components/SmtpSettingsModal.tsx +433 -0
  57. package/src/views/components/TestEmailModal.tsx +268 -0
package/src/index.tsx ADDED
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Plugin Newsletter - Client Entry Point
3
+ * Main newsletter management interface for the dashboard
4
+ * Block-Based Newsletter Management System
5
+ * Multi-Tenant Architecture: Accepts custom blocks from client applications
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React, { useMemo, useEffect } from 'react';
11
+ import { EditorProvider } from './state/EditorContext';
12
+ import { ClientBlockDefinition } from './types/block';
13
+ import { SubscribersView } from './views/SubscribersView';
14
+ import { SettingsView } from './views/SettingsView';
15
+ import { NewsletterManagerView } from './views/NewsletterManager';
16
+ import { NewsletterEditorView } from './views/NewsletterEditor';
17
+ import { editorStateToAPI } from './lib/mappers/apiMapper';
18
+
19
+ /**
20
+ * Plugin Props Interface
21
+ * Matches the PluginProps from @jhits/jhits-dashboard
22
+ */
23
+ export interface PluginProps {
24
+ subPath: string[];
25
+ siteId: string;
26
+ locale: string;
27
+ /** Custom blocks from client application (optional, can also come from window.__JHITS_PLUGIN_PROPS__) */
28
+ customBlocks?: ClientBlockDefinition[];
29
+ /** Enable dark mode for content area and wrappers (default: true) */
30
+ darkMode?: boolean;
31
+ /** Background colors for the editor */
32
+ backgroundColors?: {
33
+ /** Background color for light mode (REQUIRED) */
34
+ light: string;
35
+ /** Background color for dark mode (optional) */
36
+ dark?: string;
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Main Router Component
42
+ * 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
+ */
49
+ 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]);
137
+
138
+ const route = subPath[0] || 'newsletters';
139
+
140
+ // Listen for config updates from settings screen
141
+ useEffect(() => {
142
+ if (typeof window === 'undefined') return;
143
+
144
+ const handleConfigUpdate = () => {
145
+ // Reload page to apply changes (simplest way to ensure all components pick up new values)
146
+ window.location.reload();
147
+ };
148
+
149
+ window.addEventListener('newsletter-plugin-config-updated', handleConfigUpdate as EventListener);
150
+ return () => {
151
+ window.removeEventListener('newsletter-plugin-config-updated', handleConfigUpdate as EventListener);
152
+ };
153
+ }, []);
154
+
155
+ // Route to appropriate view
156
+ switch (route) {
157
+ case 'newsletters':
158
+ return <NewsletterManagerView siteId={siteId} locale={locale} />;
159
+
160
+ case 'editor':
161
+ const newsletterId = subPath[1];
162
+ return (
163
+ <EditorProvider
164
+ customBlocks={customBlocks}
165
+ darkMode={darkMode}
166
+ backgroundColors={backgroundColors}
167
+ onSave={async (state, extraData?: { language?: string }) => {
168
+ // Save to API - create new or update existing newsletter
169
+ const originalId = newsletterId || state.slug;
170
+ const apiData = editorStateToAPI(state);
171
+
172
+ // Include language in metadata if provided
173
+ if (extraData?.language) {
174
+ apiData.metadata = apiData.metadata || {};
175
+ apiData.metadata.lang = extraData.language;
176
+ }
177
+
178
+ // If we have an id, try to update first
179
+ if (originalId) {
180
+ console.log('[NewsletterPlugin] Attempting to update newsletter with id:', originalId);
181
+ const updateResponse = await fetch(`/api/plugin-newsletter/newsletters/${originalId}`, {
182
+ method: 'PUT',
183
+ headers: { 'Content-Type': 'application/json' },
184
+ credentials: 'include',
185
+ body: JSON.stringify(apiData),
186
+ });
187
+
188
+ if (updateResponse.ok) {
189
+ const result = await updateResponse.json();
190
+ // If the id changed, update the URL
191
+ if (result.id && result.id !== originalId) {
192
+ window.history.replaceState(null, '', `/dashboard/newsletter/editor/${result.id}`);
193
+ }
194
+ return result;
195
+ }
196
+
197
+ // If 404, newsletter doesn't exist, create a new one
198
+ if (updateResponse.status === 404) {
199
+ console.log('[NewsletterPlugin] Newsletter not found, creating new newsletter');
200
+ } else {
201
+ // Other error, throw it
202
+ const error = await updateResponse.json();
203
+ console.error('[NewsletterPlugin] Save failed:', {
204
+ status: updateResponse.status,
205
+ statusText: updateResponse.statusText,
206
+ error,
207
+ });
208
+ const errorMessage = error.message || error.error || 'Failed to save newsletter';
209
+ throw new Error(errorMessage);
210
+ }
211
+ }
212
+
213
+ // Create new newsletter (either no id or update returned 404)
214
+ console.log('[NewsletterPlugin] Creating new newsletter');
215
+ const createResponse = await fetch('/api/plugin-newsletter/newsletters/new', {
216
+ method: 'POST',
217
+ headers: { 'Content-Type': 'application/json' },
218
+ credentials: 'include',
219
+ body: JSON.stringify(apiData),
220
+ });
221
+
222
+ if (!createResponse.ok) {
223
+ const error = await createResponse.json();
224
+ console.error('[NewsletterPlugin] Create failed:', {
225
+ status: createResponse.status,
226
+ statusText: createResponse.statusText,
227
+ error,
228
+ });
229
+ const errorMessage = error.message || error.error || 'Failed to create newsletter';
230
+ throw new Error(errorMessage);
231
+ }
232
+
233
+ const result = await createResponse.json();
234
+ // Update the URL to the new newsletter's id
235
+ if (result.id) {
236
+ window.history.replaceState(null, '', `/dashboard/newsletter/editor/${result.id}`);
237
+ }
238
+ return result;
239
+ }}
240
+ >
241
+ <NewsletterEditorView newsletterId={newsletterId} siteId={siteId} locale={locale} darkMode={darkMode} backgroundColors={backgroundColors} />
242
+ </EditorProvider>
243
+ );
244
+
245
+ case 'new':
246
+ return (
247
+ <EditorProvider
248
+ customBlocks={customBlocks}
249
+ darkMode={darkMode}
250
+ backgroundColors={backgroundColors}
251
+ onSave={async (state, extraData?: { language?: string }) => {
252
+ // Save to API - create new newsletter
253
+ const apiData = editorStateToAPI(state);
254
+
255
+ // Include language in metadata if provided
256
+ if (extraData?.language) {
257
+ apiData.metadata = apiData.metadata || {};
258
+ apiData.metadata.lang = extraData.language;
259
+ }
260
+
261
+ const response = await fetch('/api/plugin-newsletter/newsletters/new', {
262
+ method: 'POST',
263
+ headers: { 'Content-Type': 'application/json' },
264
+ credentials: 'include',
265
+ body: JSON.stringify(apiData),
266
+ });
267
+ if (!response.ok) {
268
+ const error = await response.json();
269
+ throw new Error(error.message || 'Failed to create newsletter');
270
+ }
271
+ const result = await response.json();
272
+ // Update the URL to the new newsletter's slug
273
+ if (result.slug) {
274
+ window.history.replaceState(null, '', `/dashboard/newsletter/editor/${result.slug}`);
275
+ }
276
+ return result;
277
+ }}
278
+ >
279
+ <NewsletterEditorView siteId={siteId} locale={locale} darkMode={darkMode} backgroundColors={backgroundColors} />
280
+ </EditorProvider>
281
+ );
282
+
283
+ case 'subscribers':
284
+ return <SubscribersView siteId={siteId} locale={locale} />;
285
+
286
+ case 'welcome':
287
+ return (
288
+ <EditorProvider
289
+ customBlocks={customBlocks}
290
+ darkMode={darkMode}
291
+ backgroundColors={backgroundColors}
292
+ isWelcomeEmail={true}
293
+ onSave={async (state, extraData?: { language?: string }) => {
294
+ const apiData = editorStateToAPI(state);
295
+ const language = extraData?.language || locale || 'en';
296
+ const response = await fetch(`/api/plugin-newsletter/welcome-email?language=${language}`, {
297
+ method: 'POST',
298
+ headers: { 'Content-Type': 'application/json' },
299
+ credentials: 'include',
300
+ body: JSON.stringify(apiData),
301
+ });
302
+ if (!response.ok) {
303
+ const error = await response.json();
304
+ throw new Error(error.message || 'Failed to save welcome email');
305
+ }
306
+ return await response.json();
307
+ }}
308
+ >
309
+ <NewsletterEditorView siteId={siteId} locale={locale} darkMode={darkMode} backgroundColors={backgroundColors} isWelcomeEmail={true} />
310
+ </EditorProvider>
311
+ );
312
+
313
+ case 'settings':
314
+ return <SettingsView siteId={siteId} locale={locale} />;
315
+
316
+ default:
317
+ return <NewsletterManagerView siteId={siteId} locale={locale} />;
318
+ }
319
+ }
320
+
321
+ // Export for use as default
322
+ export { NewsletterPlugin as Index };
323
+
324
+ // Export types for client applications
325
+ export type {
326
+ Block,
327
+ BlockTypeDefinition,
328
+ ClientBlockDefinition,
329
+ RichTextFormattingConfig,
330
+ BlockEditProps,
331
+ BlockPreviewProps,
332
+ IBlockComponent,
333
+ } from './types/block';
334
+
335
+ // Export newsletter types
336
+ export type {
337
+ Newsletter,
338
+ NewsletterStatus,
339
+ NewsletterMetadata,
340
+ NewsletterListItem,
341
+ NewsletterFilterOptions,
342
+ } from './types/newsletter';
343
+
344
+ // Export initialization utility for easy setup
345
+ export { initNewsletterPlugin } from './init';
346
+ export type { NewsletterPluginConfig } from './init';
347
+
348
+ // Export editor state management
349
+ export { EditorProvider, useEditor } from './state/EditorContext';
350
+ export type { EditorProviderProps, EditorState, EditorContextValue } from './state';
351
+
352
+ // Export block registry
353
+ export { blockRegistry } from './registry';
@@ -0,0 +1,98 @@
1
+ --- a/packages/plugin-newsletter/src/index.tsx
2
+ +++ b/packages/plugin-newsletter/src/index.tsx
3
+ @@ -165,7 +165,50 @@ export default function NewsletterPlugin(props: PluginProps) {
4
+ onSave={async (state) => {
5
+ - // Save to API - update existing newsletter
6
+ + // Save to API - create new or update existing newsletter
7
+ const originalSlug = newsletterSlug || state.slug;
8
+ - if (!originalSlug) {
9
+ - throw new Error('Cannot save: no newsletter identifier available. Please reload the page.');
10
+ - }
11
+ - console.log('[NewsletterPlugin] Saving newsletter with slug:', originalSlug);
12
+ const apiData = editorStateToAPI(state);
13
+ - const response = await fetch(`/api/plugin-newsletter/newsletters/${originalSlug}`, {
14
+ - method: 'PUT',
15
+ - headers: { 'Content-Type': 'application/json' },
16
+ - credentials: 'include',
17
+ - body: JSON.stringify(apiData),
18
+ - });
19
+ - if (!response.ok) {
20
+ - const error = await response.json();
21
+ - console.error('[NewsletterPlugin] Save failed:', {
22
+ - status: response.status,
23
+ - statusText: response.statusText,
24
+ - error,
25
+ - });
26
+ - const errorMessage = error.message || error.error || 'Failed to save newsletter';
27
+ - throw new Error(errorMessage);
28
+ - }
29
+ - const result = await response.json();
30
+ - // If the slug changed, update the URL
31
+ - if (result.slug && result.slug !== originalSlug) {
32
+ - window.history.replaceState(null, '', `/dashboard/newsletter/editor/${result.slug}`);
33
+ - }
34
+ - return result;
35
+ +
36
+ + // If we have a slug, try to update first
37
+ + if (originalSlug) {
38
+ + console.log('[NewsletterPlugin] Attempting to update newsletter with slug:', originalSlug);
39
+ + const updateResponse = await fetch(`/api/plugin-newsletter/newsletters/${originalSlug}`, {
40
+ + method: 'PUT',
41
+ + headers: { 'Content-Type': 'application/json' },
42
+ + credentials: 'include',
43
+ + body: JSON.stringify(apiData),
44
+ + });
45
+ +
46
+ + if (updateResponse.ok) {
47
+ + const result = await updateResponse.json();
48
+ + // If the slug changed, update the URL
49
+ + if (result.slug && result.slug !== originalSlug) {
50
+ + window.history.replaceState(null, '', `/dashboard/newsletter/editor/${result.slug}`);
51
+ + }
52
+ + return result;
53
+ + }
54
+ +
55
+ + // If 404, newsletter doesn't exist, create a new one
56
+ + if (updateResponse.status === 404) {
57
+ + console.log('[NewsletterPlugin] Newsletter not found, creating new newsletter');
58
+ + } else {
59
+ + // Other error, throw it
60
+ + const error = await updateResponse.json();
61
+ + console.error('[NewsletterPlugin] Save failed:', {
62
+ + status: updateResponse.status,
63
+ + statusText: updateResponse.statusText,
64
+ + error,
65
+ + });
66
+ + const errorMessage = error.message || error.error || 'Failed to save newsletter';
67
+ + throw new Error(errorMessage);
68
+ + }
69
+ + }
70
+ +
71
+ + // Create new newsletter (either no slug or update returned 404)
72
+ + console.log('[NewsletterPlugin] Creating new newsletter');
73
+ + const createResponse = await fetch('/api/plugin-newsletter/newsletters/new', {
74
+ + method: 'POST',
75
+ + headers: { 'Content-Type': 'application/json' },
76
+ + credentials: 'include',
77
+ + body: JSON.stringify(apiData),
78
+ + });
79
+ +
80
+ + if (!createResponse.ok) {
81
+ + const error = await createResponse.json();
82
+ + console.error('[NewsletterPlugin] Create failed:', {
83
+ + status: createResponse.status,
84
+ + statusText: createResponse.statusText,
85
+ + error,
86
+ + });
87
+ + const errorMessage = error.message || error.error || 'Failed to create newsletter';
88
+ + throw new Error(errorMessage);
89
+ + }
90
+ +
91
+ + const result = await createResponse.json();
92
+ + // Update the URL to the new newsletter's slug
93
+ + if (result.slug) {
94
+ + window.history.replaceState(null, '', `/dashboard/newsletter/editor/${result.slug}`);
95
+ + }
96
+ + return result;
97
+ }}
98
+ >
package/src/init.tsx ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Newsletter Plugin Initialization Utility
3
+ *
4
+ * Simple function to initialize the newsletter plugin with client configuration.
5
+ * Call this once in your app (e.g., in a script tag or root layout) to configure
6
+ * the newsletter plugin without needing a React component.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { initNewsletterPlugin } from '@jhits/plugin-newsletter/init';
11
+ * import { newsletterConfig } from '@/plugins/newsletter-config';
12
+ *
13
+ * // Call once when your app loads
14
+ * initNewsletterPlugin(newsletterConfig);
15
+ * ```
16
+ */
17
+
18
+ 'use client';
19
+
20
+ import React from 'react';
21
+ import type { ClientBlockDefinition } from './types/block';
22
+
23
+ export interface NewsletterPluginConfig {
24
+ /** Custom blocks available in the editor */
25
+ customBlocks?: ClientBlockDefinition[];
26
+ /** Dark mode setting for the editor content area and wrappers (default: true) */
27
+ darkMode?: boolean;
28
+ /** Background colors for the editor */
29
+ backgroundColors?: {
30
+ /** Background color for light mode (REQUIRED) - CSS color value (hex, rgb, or named color) */
31
+ light: string;
32
+ /** Background color for dark mode (optional) - CSS color value (hex, rgb, or named color) */
33
+ dark?: string;
34
+ };
35
+ /** Email configuration */
36
+ emailConfig?: {
37
+ /** Logo URL to display in email header (absolute URL) */
38
+ logoUrl?: string;
39
+ /** Logo alt text */
40
+ logoAlt?: string;
41
+ /** Footer text for emails */
42
+ footerText?: string;
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Initialize the newsletter plugin with client configuration
48
+ *
49
+ * This function sets up the window global that the plugin reads from automatically.
50
+ * Call this once when your app loads, before the plugin is rendered.
51
+ *
52
+ * @param config - Newsletter plugin configuration (customBlocks, darkMode, etc.)
53
+ */
54
+ export function initNewsletterPlugin(config: NewsletterPluginConfig): void {
55
+ if (typeof window === 'undefined') {
56
+ // Server-side: no-op
57
+ return;
58
+ }
59
+
60
+ // Initialize the global plugin props object if it doesn't exist
61
+ if (!(window as any).__JHITS_PLUGIN_PROPS__) {
62
+ (window as any).__JHITS_PLUGIN_PROPS__ = {};
63
+ }
64
+
65
+ // Set newsletter plugin configuration
66
+ (window as any).__JHITS_PLUGIN_PROPS__['plugin-newsletter'] = {
67
+ customBlocks: config.customBlocks || [],
68
+ darkMode: config.darkMode !== undefined ? config.darkMode : true, // Default to true
69
+ backgroundColors: config.backgroundColors || undefined,
70
+ emailConfig: config.emailConfig || undefined,
71
+ };
72
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Block Renderer
3
+ * Library component for rendering blocks (decoupled from editor)
4
+ * This is the "headless" rendering layer
5
+ *
6
+ * Multi-Tenant: Uses Preview components from client-provided blocks
7
+ */
8
+
9
+ 'use client';
10
+
11
+ import React from 'react';
12
+ import { Block, BlockPreviewProps } from '../../types/block';
13
+ import { blockRegistry } from '../../registry/BlockRegistry';
14
+ import { getChildBlocks } from '../utils/blockHelpers';
15
+
16
+ /**
17
+ * Block Renderer Props
18
+ */
19
+ export interface BlockRendererProps {
20
+ /** Block to render */
21
+ block: Block;
22
+
23
+ /** Custom renderers for specific block types (optional override) */
24
+ customRenderers?: Map<string, React.ComponentType<BlockPreviewProps>>;
25
+
26
+ /** Additional context for rendering */
27
+ context?: {
28
+ siteId?: string;
29
+ locale?: string;
30
+ [key: string]: unknown;
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Block Renderer Component
36
+ * Renders a single block using its Preview component from the registry
37
+ */
38
+ export function BlockRenderer({
39
+ block,
40
+ customRenderers,
41
+ context = {}
42
+ }: BlockRendererProps) {
43
+ // Check for custom renderer override first
44
+ if (customRenderers?.has(block.type)) {
45
+ const CustomRenderer = customRenderers.get(block.type)!;
46
+ return <CustomRenderer block={block} context={context} />;
47
+ }
48
+
49
+ // Get block definition from registry
50
+ const definition = blockRegistry.get(block.type);
51
+ if (!definition) {
52
+ console.warn(`Block type "${block.type}" not found in registry. Available types:`,
53
+ blockRegistry.getAll().map(b => b.type).join(', '));
54
+ return (
55
+ <div className="p-4 border border-red-300 bg-red-50 rounded">
56
+ <p className="text-red-600">Unknown block type: {block.type}</p>
57
+ <p className="text-xs text-red-500 mt-1">
58
+ Make sure this block type is registered via customBlocks prop
59
+ </p>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ // Use the Preview component from the block definition
65
+ const PreviewComponent = definition.components.Preview;
66
+
67
+ // Check if this is a container block with children
68
+ const isContainer = definition.isContainer === true;
69
+ const childBlocks = isContainer && block.children && Array.isArray(block.children) && block.children.length > 0
70
+ ? (typeof block.children[0] === 'object'
71
+ ? block.children as Block[]
72
+ : [])
73
+ : [];
74
+
75
+ // If container block, pass child blocks and render function
76
+ if (isContainer) {
77
+ return (
78
+ <PreviewComponent
79
+ block={block}
80
+ context={context}
81
+ childBlocks={childBlocks}
82
+ renderChild={(childBlock: Block) => (
83
+ <BlockRenderer
84
+ block={childBlock}
85
+ customRenderers={customRenderers}
86
+ context={context}
87
+ />
88
+ )}
89
+ />
90
+ );
91
+ }
92
+
93
+ return <PreviewComponent block={block} context={context} />;
94
+ }
95
+
96
+ /**
97
+ * Blocks Renderer Component
98
+ * Renders multiple blocks in sequence
99
+ */
100
+ export function BlocksRenderer({
101
+ blocks,
102
+ customRenderers,
103
+ context = {}
104
+ }: {
105
+ blocks: Block[];
106
+ customRenderers?: Map<string, React.ComponentType<BlockPreviewProps>>;
107
+ context?: {
108
+ siteId?: string;
109
+ locale?: string;
110
+ [key: string]: unknown;
111
+ };
112
+ }) {
113
+ return (
114
+ <>
115
+ {blocks.map((block) => (
116
+ <BlockRenderer
117
+ key={block.id}
118
+ block={block}
119
+ customRenderers={customRenderers}
120
+ context={context}
121
+ />
122
+ ))}
123
+ </>
124
+ );
125
+ }