@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.
- package/dist/api/email-utils.d.ts.map +1 -1
- package/dist/api/email-utils.js +45 -4
- package/dist/api/handlers/newsletters.d.ts.map +1 -1
- package/dist/api/handlers/newsletters.js +33 -16
- package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
- package/dist/api/handlers/send-newsletter.js +54 -6
- package/dist/api/handlers/settings.d.ts.map +1 -1
- package/dist/api/handlers/settings.js +51 -1
- package/dist/index.d.ts +27 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -122
- package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
- package/dist/lib/blocks/BlockRenderer.js +14 -2
- package/dist/lib/email/EmailRenderer.d.ts +1 -0
- package/dist/lib/email/EmailRenderer.d.ts.map +1 -1
- package/dist/lib/email/EmailRenderer.js +31 -19
- package/dist/lib/utils/config-resolver.d.ts +33 -0
- package/dist/lib/utils/config-resolver.d.ts.map +1 -0
- package/dist/lib/utils/config-resolver.js +47 -0
- package/dist/registry/BlockRegistry.d.ts +9 -1
- package/dist/registry/BlockRegistry.d.ts.map +1 -1
- package/dist/registry/BlockRegistry.js +126 -8
- package/dist/state/EditorContext.d.ts +11 -1
- package/dist/state/EditorContext.d.ts.map +1 -1
- package/dist/state/EditorContext.js +23 -5
- package/dist/state/types.d.ts +12 -0
- package/dist/state/types.d.ts.map +1 -1
- package/dist/types/block.d.ts +9 -0
- package/dist/types/block.d.ts.map +1 -1
- package/dist/types/newsletter.d.ts +4 -0
- package/dist/types/newsletter.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.js +24 -3
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.js +77 -17
- package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
- package/dist/views/CanvasEditor/EditorBody.js +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.js +158 -100
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +3 -1
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorSidebar.js +3 -3
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +1 -1
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -1
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.js +6 -40
- package/dist/views/NewsletterManager.d.ts.map +1 -1
- package/dist/views/NewsletterManager.js +87 -5
- package/dist/views/components/DomainPromptModal.d.ts +13 -0
- package/dist/views/components/DomainPromptModal.d.ts.map +1 -0
- package/dist/views/components/DomainPromptModal.js +58 -0
- package/dist/views/components/NewsletterCard.d.ts +16 -0
- package/dist/views/components/NewsletterCard.d.ts.map +1 -0
- package/dist/views/components/NewsletterCard.js +94 -0
- package/dist/views/components/NewsletterGrid.d.ts +16 -0
- package/dist/views/components/NewsletterGrid.d.ts.map +1 -0
- package/dist/views/components/NewsletterGrid.js +13 -0
- package/dist/views/components/SendNewsletterModal.d.ts.map +1 -1
- package/dist/views/components/SendNewsletterModal.js +91 -22
- package/dist/views/components/SmtpSettingsModal.d.ts.map +1 -1
- package/dist/views/components/SmtpSettingsModal.js +10 -0
- package/dist/views/components/TestEmailModal.d.ts.map +1 -1
- package/dist/views/components/TestEmailModal.js +86 -17
- package/package.json +53 -9
- package/src/api/email-utils.ts +53 -4
- package/src/api/handlers/newsletters.ts +40 -20
- package/src/api/handlers/send-newsletter.ts +65 -6
- package/src/api/handlers/settings.ts +60 -2
- package/src/index.tsx +49 -155
- package/src/lib/blocks/BlockRenderer.tsx +16 -2
- package/src/lib/email/EmailRenderer.tsx +31 -20
- package/src/lib/utils/config-resolver.ts +71 -0
- package/src/registry/BlockRegistry.tsx +255 -0
- package/src/state/EditorContext.tsx +43 -8
- package/src/state/types.ts +16 -0
- package/src/types/block.ts +10 -0
- package/src/types/newsletter.ts +5 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +27 -2
- package/src/views/CanvasEditor/CanvasEditorView.tsx +142 -61
- package/src/views/CanvasEditor/EditorBody.tsx +17 -13
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +178 -115
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +57 -2
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +6 -45
- package/src/views/NewsletterManager.tsx +164 -6
- package/src/views/components/DomainPromptModal.tsx +160 -0
- package/src/views/components/NewsletterCard.tsx +212 -0
- package/src/views/components/NewsletterGrid.tsx +48 -0
- package/src/views/components/SendNewsletterModal.tsx +270 -184
- package/src/views/components/SmtpSettingsModal.tsx +11 -0
- package/src/views/components/TestEmailModal.tsx +235 -149
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
217
|
-
|
|
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
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
359
|
-
font-size:
|
|
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:
|
|
372
|
-
font-
|
|
373
|
-
font-size: 30px;
|
|
382
|
+
font-weight: bold;
|
|
383
|
+
font-size: 48px;
|
|
374
384
|
margin-bottom: 30px;
|
|
375
385
|
color: #1a2e26;
|
|
376
|
-
text-align:
|
|
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
|
+
}
|