@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SmtpSettingsModal.d.ts","sourceRoot":"","sources":["../../../src/views/components/SmtpSettingsModal.tsx"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"SmtpSettingsModal.d.ts","sourceRoot":"","sources":["../../../src/views/components/SmtpSettingsModal.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAmBH,UAAU,sBAAsB;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,sBAAsB,kDAia5E"}
|
|
@@ -15,6 +15,11 @@ export function SmtpSettingsModal({ isOpen, onClose }) {
|
|
|
15
15
|
fromName: '',
|
|
16
16
|
primaryLanguage: 'en',
|
|
17
17
|
logoUrl: '',
|
|
18
|
+
unsubscribeTranslations: {
|
|
19
|
+
en: 'Unsubscribe',
|
|
20
|
+
nl: 'Afmelden',
|
|
21
|
+
sv: 'Avanmälan',
|
|
22
|
+
},
|
|
18
23
|
});
|
|
19
24
|
const [smtpLoading, setSmtpLoading] = useState(true);
|
|
20
25
|
const [smtpSaving, setSmtpSaving] = useState(false);
|
|
@@ -47,6 +52,11 @@ export function SmtpSettingsModal({ isOpen, onClose }) {
|
|
|
47
52
|
fromName: data.fromName || '',
|
|
48
53
|
primaryLanguage: data.primaryLanguage || 'en',
|
|
49
54
|
logoUrl: data.logoUrl || '',
|
|
55
|
+
unsubscribeTranslations: data.unsubscribeTranslations || {
|
|
56
|
+
en: 'Unsubscribe',
|
|
57
|
+
nl: 'Afmelden',
|
|
58
|
+
sv: 'Avanmälan',
|
|
59
|
+
},
|
|
50
60
|
});
|
|
51
61
|
setFromEmailEditable(!!data.from && data.from !== data.user);
|
|
52
62
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TestEmailModal.d.ts","sourceRoot":"","sources":["../../../src/views/components/TestEmailModal.tsx"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"TestEmailModal.d.ts","sourceRoot":"","sources":["../../../src/views/components/TestEmailModal.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAQH,UAAU,mBAAmB;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;CACvB;AAWD,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,mBAAmB,kDAyUtE"}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
6
6
|
import { useState, useEffect } from 'react';
|
|
7
7
|
import { X, Send, RefreshCw, CheckCircle2, AlertCircle, Mail, Globe, Sparkles } from 'lucide-react';
|
|
8
|
+
import { DomainPromptModal } from './DomainPromptModal';
|
|
8
9
|
const languages = [
|
|
9
10
|
{ code: 'en', label: 'EN' },
|
|
10
11
|
{ code: 'nl', label: 'NL' },
|
|
@@ -18,9 +19,13 @@ export function TestEmailModal({ isOpen, onClose }) {
|
|
|
18
19
|
const [isSending, setIsSending] = useState(false);
|
|
19
20
|
const [sendSuccess, setSendSuccess] = useState(false);
|
|
20
21
|
const [sendError, setSendError] = useState(null);
|
|
22
|
+
// Domain prompt state
|
|
23
|
+
const [showDomainPrompt, setShowDomainPrompt] = useState(false);
|
|
24
|
+
const [isCheckingDomain, setIsCheckingDomain] = useState(false);
|
|
21
25
|
useEffect(() => {
|
|
22
26
|
if (isOpen) {
|
|
23
27
|
fetchWelcomeEmailStatus();
|
|
28
|
+
setShowDomainPrompt(false);
|
|
24
29
|
}
|
|
25
30
|
}, [isOpen]);
|
|
26
31
|
const fetchWelcomeEmailStatus = async () => {
|
|
@@ -40,7 +45,7 @@ export function TestEmailModal({ isOpen, onClose }) {
|
|
|
40
45
|
console.error('Failed to load welcome email status:', error);
|
|
41
46
|
}
|
|
42
47
|
};
|
|
43
|
-
const handleSend = async () => {
|
|
48
|
+
const handleSend = async (skipDomainCheck = false) => {
|
|
44
49
|
if (!email.trim()) {
|
|
45
50
|
setSendError('Please enter an email address');
|
|
46
51
|
return;
|
|
@@ -50,6 +55,30 @@ export function TestEmailModal({ isOpen, onClose }) {
|
|
|
50
55
|
setSendError('Please enter a valid email address');
|
|
51
56
|
return;
|
|
52
57
|
}
|
|
58
|
+
// Check if we need to prompt for domain first (usually helpful in dev mode)
|
|
59
|
+
if (!skipDomainCheck) {
|
|
60
|
+
try {
|
|
61
|
+
setIsCheckingDomain(true);
|
|
62
|
+
// Check if this language has a domain configured
|
|
63
|
+
const response = await fetch('/api/plugin-website/settings');
|
|
64
|
+
const siteConfig = await response.json();
|
|
65
|
+
const hasDomain = siteConfig.domainLocaleConfig?.some((c) => c.locale === language &&
|
|
66
|
+
c.domain &&
|
|
67
|
+
c.domain !== 'undefined' &&
|
|
68
|
+
c.domain.trim() !== '');
|
|
69
|
+
if (!hasDomain) {
|
|
70
|
+
setShowDomainPrompt(true);
|
|
71
|
+
setIsCheckingDomain(false);
|
|
72
|
+
return; // Stop here, wait for domain prompt
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.warn('Failed to check domain configuration:', error);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
setIsCheckingDomain(false);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
53
82
|
try {
|
|
54
83
|
setIsSending(true);
|
|
55
84
|
setSendError(null);
|
|
@@ -61,6 +90,12 @@ export function TestEmailModal({ isOpen, onClose }) {
|
|
|
61
90
|
});
|
|
62
91
|
const data = await response.json();
|
|
63
92
|
if (!response.ok) {
|
|
93
|
+
// If server detects missing domain, trigger prompt
|
|
94
|
+
if (data.code === 'DOMAIN_MISSING') {
|
|
95
|
+
setShowDomainPrompt(true);
|
|
96
|
+
setIsSending(false);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
64
99
|
throw new Error(data.error || 'Failed to send test email');
|
|
65
100
|
}
|
|
66
101
|
setSendSuccess(true);
|
|
@@ -78,22 +113,56 @@ export function TestEmailModal({ isOpen, onClose }) {
|
|
|
78
113
|
setIsSending(false);
|
|
79
114
|
}
|
|
80
115
|
};
|
|
116
|
+
const handleSaveDomain = async (domain) => {
|
|
117
|
+
try {
|
|
118
|
+
// Get current site config
|
|
119
|
+
const response = await fetch('/api/plugin-website/settings');
|
|
120
|
+
const siteConfig = await response.json();
|
|
121
|
+
const currentConfigs = siteConfig.domainLocaleConfig || [];
|
|
122
|
+
const newConfigs = [...currentConfigs];
|
|
123
|
+
const existingIndex = newConfigs.findIndex((c) => c.locale === language);
|
|
124
|
+
if (existingIndex >= 0) {
|
|
125
|
+
newConfigs[existingIndex] = { ...newConfigs[existingIndex], domain };
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
newConfigs.push({ locale: language, domain });
|
|
129
|
+
}
|
|
130
|
+
// Save updated site config
|
|
131
|
+
const saveResponse = await fetch('/api/plugin-website/settings', {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
...siteConfig,
|
|
136
|
+
domainLocaleConfig: newConfigs
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
if (!saveResponse.ok)
|
|
140
|
+
throw new Error('Failed to save website settings');
|
|
141
|
+
// Re-trigger send with domain check skipped
|
|
142
|
+
setShowDomainPrompt(false);
|
|
143
|
+
handleSend(true);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error('Failed to save domain:', error);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
81
150
|
if (!isOpen)
|
|
82
151
|
return null;
|
|
83
|
-
return (_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [_jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-sm", onClick: () => !isSending && onClose() }), _jsxs("div", { className: "relative w-full max-w-md mx-4 bg-white dark:bg-neutral-900 rounded-3xl border border-dashboard-border shadow-2xl overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-8 py-6 border-b border-dashboard-border", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-10 h-10 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Mail, { className: "w-5 h-5 text-primary" }) }), _jsxs("div", { children: [_jsx("h2", { className: "text-xl font-black text-dashboard-text uppercase tracking-tight", children: "Test Email" }), _jsx("p", { className: "text-xs text-dashboard-text-secondary", children: "Send a test email to verify SMTP" })] })] }), _jsx("button", { onClick: () => !isSending && onClose(), disabled: isSending, className: "p-2 rounded-full hover:bg-dashboard-border transition-colors", children: _jsx(X, { size: 20, className: "text-dashboard-text-secondary" }) })] }), _jsx("div", { className: "p-8", children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Email Template" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: () => setEmailType('test'), className: `flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${emailType === 'test'
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
152
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [_jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-sm", onClick: () => !isSending && onClose() }), _jsxs("div", { className: "relative w-full max-w-md mx-4 bg-white dark:bg-neutral-900 rounded-3xl border border-dashboard-border shadow-2xl overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-8 py-6 border-b border-dashboard-border", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-10 h-10 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Mail, { className: "w-5 h-5 text-primary" }) }), _jsxs("div", { children: [_jsx("h2", { className: "text-xl font-black text-dashboard-text uppercase tracking-tight", children: "Test Email" }), _jsx("p", { className: "text-xs text-dashboard-text-secondary", children: "Send a test email to verify SMTP" })] })] }), _jsx("button", { onClick: () => !isSending && onClose(), disabled: isSending, className: "p-2 rounded-full hover:bg-dashboard-border transition-colors", children: _jsx(X, { size: 20, className: "text-dashboard-text-secondary" }) })] }), _jsx("div", { className: "p-8", children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Email Template" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: () => setEmailType('test'), className: `flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${emailType === 'test'
|
|
153
|
+
? 'bg-primary text-white'
|
|
154
|
+
: 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'}`, children: [_jsx(Mail, { size: 14 }), "Test"] }), _jsxs("button", { onClick: () => setEmailType('welcome'), disabled: !welcomeEmailConfigured, className: `flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${emailType === 'welcome'
|
|
155
|
+
? 'bg-primary text-white'
|
|
156
|
+
: welcomeEmailConfigured
|
|
157
|
+
? 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
|
|
158
|
+
: 'bg-dashboard-bg border border-dashboard-border text-dashboard-text-secondary opacity-50 cursor-not-allowed'}`, children: [_jsx(Sparkles, { size: 14 }), "Welcome", !welcomeEmailConfigured && (_jsx("span", { className: "text-[9px] normal-case opacity-70", children: "(Not configured)" }))] })] })] }), _jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Recipient Email" }), _jsx("input", { type: "email", value: email, onChange: (e) => {
|
|
159
|
+
setEmail(e.target.value);
|
|
160
|
+
setSendError(null);
|
|
161
|
+
}, onKeyDown: (e) => e.key === 'Enter' && handleSend(), placeholder: "your@email.com", className: "w-full px-4 py-3 bg-dashboard-bg border border-dashboard-border rounded-xl outline-none focus:ring-2 focus:ring-primary transition-colors text-dashboard-text" })] }), _jsxs("div", { children: [_jsxs("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: [_jsx(Globe, { size: 12, className: "inline mr-1" }), "Language"] }), _jsx("div", { className: "flex gap-2", children: languages.map((lang) => (_jsx("button", { onClick: () => setLanguage(lang.code), className: `px-4 py-2 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${language === lang.code
|
|
162
|
+
? 'bg-primary text-white'
|
|
163
|
+
: 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'}`, children: lang.label }, lang.code))) })] }), sendError && (_jsxs("div", { className: "flex items-center gap-2 text-red-500 text-sm", children: [_jsx(AlertCircle, { size: 16 }), sendError] })), _jsx("p", { className: "text-xs text-dashboard-text-secondary", children: "A test email will be sent to this address using your configured SMTP settings." })] }) }), _jsxs("div", { className: "flex items-center justify-end gap-3 px-8 py-6 border-t border-dashboard-border bg-dashboard-bg/50", children: [_jsx("button", { onClick: onClose, disabled: isSending, className: "px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border", children: "Cancel" }), _jsx("button", { onClick: () => handleSend(), disabled: isSending || sendSuccess || isCheckingDomain, className: `inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg ${isSending || isCheckingDomain
|
|
164
|
+
? 'bg-neutral-400 text-white cursor-not-allowed'
|
|
165
|
+
: sendSuccess
|
|
166
|
+
? 'bg-green-600 text-white'
|
|
167
|
+
: 'bg-primary text-white hover:bg-primary/90'}`, children: isSending || isCheckingDomain ? (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { className: "w-4 h-4 animate-spin" }), isCheckingDomain ? 'Checking...' : 'Sending...'] })) : sendSuccess ? (_jsxs(_Fragment, { children: [_jsx(CheckCircle2, { className: "w-4 h-4" }), "Sent!"] })) : (_jsxs(_Fragment, { children: [_jsx(Send, { className: "w-4 h-4" }), "Send Test"] })) })] })] })] }), _jsx(DomainPromptModal, { isOpen: showDomainPrompt, onClose: () => setShowDomainPrompt(false), onSave: handleSaveDomain, language: language })] }));
|
|
99
168
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhits/plugin-newsletter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "Newsletter management and email delivery plugin for the JHITS ecosystem",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
|
-
"main": "./
|
|
9
|
-
"types": "./
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
-
"types": "./
|
|
13
|
-
"import": "./
|
|
14
|
-
"default": "./
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
15
|
},
|
|
16
16
|
"./src": {
|
|
17
17
|
"types": "./src/index.tsx",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"default": "./src/index.tsx"
|
|
20
20
|
},
|
|
21
21
|
"./server": {
|
|
22
|
-
"types": "./
|
|
23
|
-
"import": "./
|
|
24
|
-
"default": "./
|
|
22
|
+
"types": "./dist/index.server.d.ts",
|
|
23
|
+
"import": "./dist/index.server.js",
|
|
24
|
+
"default": "./dist/index.server.js"
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
@@ -48,6 +48,50 @@
|
|
|
48
48
|
"react-dom": "19.2.4",
|
|
49
49
|
"typescript": "^5.9.3"
|
|
50
50
|
},
|
|
51
|
+
"jhits": {
|
|
52
|
+
"views": [
|
|
53
|
+
{
|
|
54
|
+
"id": "newsletters",
|
|
55
|
+
"labels": {
|
|
56
|
+
"en": "Campaigns",
|
|
57
|
+
"nl": "Campagnes",
|
|
58
|
+
"sv": "Kampanjer"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "new",
|
|
63
|
+
"labels": {
|
|
64
|
+
"en": "New Campaign",
|
|
65
|
+
"nl": "Nieuwe Campagne",
|
|
66
|
+
"sv": "Ny Kampanj"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "welcome",
|
|
71
|
+
"labels": {
|
|
72
|
+
"en": "Welcome Email",
|
|
73
|
+
"nl": "Welkomstmail",
|
|
74
|
+
"sv": "Välkomstbrev"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "subscribers",
|
|
79
|
+
"labels": {
|
|
80
|
+
"en": "Subscribers",
|
|
81
|
+
"nl": "Abonnees",
|
|
82
|
+
"sv": "Prenumeranter"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "settings",
|
|
87
|
+
"labels": {
|
|
88
|
+
"en": "Settings",
|
|
89
|
+
"nl": "Instellingen",
|
|
90
|
+
"sv": "Inställningar"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
51
95
|
"files": [
|
|
52
96
|
"src",
|
|
53
97
|
"dist",
|
package/src/api/email-utils.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface SmtpConfig {
|
|
|
19
19
|
from: string;
|
|
20
20
|
fromName: string;
|
|
21
21
|
logoUrl?: string;
|
|
22
|
+
unsubscribeTranslations?: Record<string, string>;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async function getSmtpConfig(config: NewsletterApiConfig): Promise<SmtpConfig | null> {
|
|
@@ -36,11 +37,47 @@ async function getSmtpConfig(config: NewsletterApiConfig): Promise<SmtpConfig |
|
|
|
36
37
|
from: smtpConfig.from,
|
|
37
38
|
fromName: smtpConfig.fromName || '',
|
|
38
39
|
logoUrl: smtpConfig.logoUrl || '',
|
|
40
|
+
unsubscribeTranslations: smtpConfig.unsubscribeTranslations || {},
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
43
|
return null;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
async function resolveBaseUrl(config: NewsletterApiConfig, language: string): Promise<string> {
|
|
47
|
+
try {
|
|
48
|
+
const dbConnection = await config.getDb();
|
|
49
|
+
const db = dbConnection.db();
|
|
50
|
+
const settings = db.collection('settings');
|
|
51
|
+
|
|
52
|
+
// Try to get site config from plugin-website (stored in 'settings' collection with identifier 'site_config')
|
|
53
|
+
const siteConfig = await settings.findOne({ identifier: 'site_config' });
|
|
54
|
+
|
|
55
|
+
if (siteConfig && siteConfig.domainLocaleConfig && Array.isArray(siteConfig.domainLocaleConfig)) {
|
|
56
|
+
// Find domain for this locale
|
|
57
|
+
const localeConfig = siteConfig.domainLocaleConfig.find((c: any) => c.locale === language);
|
|
58
|
+
if (localeConfig && localeConfig.domain && localeConfig.domain !== 'undefined' && localeConfig.domain.trim() !== '') {
|
|
59
|
+
const domain = localeConfig.domain.trim();
|
|
60
|
+
// Add protocol if missing
|
|
61
|
+
if (!domain.startsWith('http')) {
|
|
62
|
+
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
|
63
|
+
return `${protocol}://${domain}`;
|
|
64
|
+
}
|
|
65
|
+
return domain;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn('[NewsletterAPI] Failed to resolve language-specific base URL:', error);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fallback to default, carefully checking for 'undefined' string
|
|
73
|
+
const fallback = process.env.NEXT_PUBLIC_SITE_URL;
|
|
74
|
+
if (fallback && fallback !== 'undefined' && fallback.trim() !== '') {
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return 'https://botanicsandyou.com';
|
|
79
|
+
}
|
|
80
|
+
|
|
44
81
|
export async function sendWelcomeEmail(
|
|
45
82
|
config: NewsletterApiConfig,
|
|
46
83
|
email: string,
|
|
@@ -54,9 +91,9 @@ export async function sendWelcomeEmail(
|
|
|
54
91
|
|
|
55
92
|
const isDutch = language === 'nl';
|
|
56
93
|
const isSwedish = language === 'sv';
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
94
|
+
|
|
95
|
+
// Resolve base URL based on language settings from plugin-website
|
|
96
|
+
const baseUrl = await resolveBaseUrl(config, language);
|
|
60
97
|
|
|
61
98
|
const slugs: Record<string, string> = {
|
|
62
99
|
sv: '/avmälla',
|
|
@@ -103,6 +140,12 @@ export async function sendWelcomeEmail(
|
|
|
103
140
|
if (blocks && blocks.length > 0) {
|
|
104
141
|
const { generateNewsletterEmailHtml } = await import('../lib/email/EmailRenderer');
|
|
105
142
|
|
|
143
|
+
// Get unsubscribe text (Priority: metadata manual override > global SMTP translation > hardcoded localized default)
|
|
144
|
+
const globalTranslations = smtpConfig.unsubscribeTranslations || {};
|
|
145
|
+
const hardcodedDefaults: Record<string, string> = { en: 'Unsubscribe', nl: 'Afmelden', sv: 'Avanmälan' };
|
|
146
|
+
const defaultText = globalTranslations[language] || hardcodedDefaults[language] || hardcodedDefaults.en;
|
|
147
|
+
const unsubscribeText = metadata?.unsubscribeText || defaultText;
|
|
148
|
+
|
|
106
149
|
html = generateNewsletterEmailHtml(
|
|
107
150
|
blocks,
|
|
108
151
|
{ subject: metadata?.subject || '', previewText: metadata?.previewText || '' },
|
|
@@ -111,6 +154,7 @@ export async function sendWelcomeEmail(
|
|
|
111
154
|
locale: language,
|
|
112
155
|
logoUrl: logoSrc,
|
|
113
156
|
unsubscribeUrl,
|
|
157
|
+
unsubscribeText,
|
|
114
158
|
footerText: `© ${new Date().getFullYear()} ${smtpConfig.fromName || 'Botanics & You'}`,
|
|
115
159
|
}
|
|
116
160
|
);
|
|
@@ -131,6 +175,11 @@ export async function sendWelcomeEmail(
|
|
|
131
175
|
}
|
|
132
176
|
}
|
|
133
177
|
|
|
178
|
+
// Get unsubscribe text for legacy template too
|
|
179
|
+
const globalTranslations = smtpConfig.unsubscribeTranslations || {};
|
|
180
|
+
const hardcodedDefaults: Record<string, string> = { en: 'Unsubscribe', nl: 'Afmelden', sv: 'Avanmälan' };
|
|
181
|
+
const unsubscribeText = globalTranslations[language] || hardcodedDefaults[language] || hardcodedDefaults.en;
|
|
182
|
+
|
|
134
183
|
const template = handlebars.compile(templateContent);
|
|
135
184
|
html = template({
|
|
136
185
|
fromName: smtpConfig.fromName || 'Botanics & You',
|
|
@@ -141,7 +190,7 @@ export async function sendWelcomeEmail(
|
|
|
141
190
|
? 'Tack för att du är en del av vår community. Vi tror att naturen ger oss allt vi verkligen behöver.'
|
|
142
191
|
: 'Thank you for joining our community. We believe that nature provides everything we truly need.',
|
|
143
192
|
unsubscribeUrl,
|
|
144
|
-
unsubscribeText
|
|
193
|
+
unsubscribeText,
|
|
145
194
|
tagline: isDutch ? 'Natuurlijk verbonden' : isSwedish ? 'Naturligt ansluten' : 'Naturally connected',
|
|
146
195
|
currentYear: new Date().getFullYear(),
|
|
147
196
|
});
|
|
@@ -40,8 +40,9 @@ export async function GET_NEWSLETTERS(
|
|
|
40
40
|
const skip = parseInt(searchParams.get('skip') || '0', 10);
|
|
41
41
|
const sortBy = searchParams.get('sortBy') || 'updatedAt';
|
|
42
42
|
const sortOrder = searchParams.get('sortOrder') || 'desc';
|
|
43
|
+
const language = searchParams.get('language') || 'en';
|
|
43
44
|
|
|
44
|
-
const query:
|
|
45
|
+
const query: Record<string, unknown> = {};
|
|
45
46
|
if (status) {
|
|
46
47
|
query['publication.status'] = status;
|
|
47
48
|
}
|
|
@@ -55,7 +56,7 @@ export async function GET_NEWSLETTERS(
|
|
|
55
56
|
]
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
const sort:
|
|
59
|
+
const sort: Record<string, 1 | -1> = {};
|
|
59
60
|
sort[sortBy] = sortOrder === 'asc' ? 1 : -1;
|
|
60
61
|
|
|
61
62
|
const newsletterList = await newsletters
|
|
@@ -65,20 +66,39 @@ export async function GET_NEWSLETTERS(
|
|
|
65
66
|
.skip(skip)
|
|
66
67
|
.toArray();
|
|
67
68
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
69
|
+
const primaryLanguage = 'en'; // Default primary
|
|
70
|
+
|
|
71
|
+
const listItems: NewsletterListItem[] = newsletterList.map((newsletter: Record<string, any>) => {
|
|
72
|
+
const languages = newsletter.languages || {};
|
|
73
|
+
const newsletterPrimaryLang = newsletter.metadata?.lang || primaryLanguage;
|
|
74
|
+
|
|
75
|
+
// Get title (from subject) and subject from language-specific content
|
|
76
|
+
let title = newsletter.metadata?.subject || '';
|
|
77
|
+
|
|
78
|
+
if (languages[language]) {
|
|
79
|
+
title = languages[language].metadata?.subject || title;
|
|
80
|
+
} else if (language !== newsletterPrimaryLang && languages[newsletterPrimaryLang]) {
|
|
81
|
+
// Fall back to primary language
|
|
82
|
+
title = languages[newsletterPrimaryLang].metadata?.subject || title;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
id: newsletter._id?.toString() || newsletter.id,
|
|
87
|
+
title: title || newsletter.title || 'Untitled',
|
|
88
|
+
slug: newsletter.slug,
|
|
89
|
+
status: newsletter.publication?.status || 'draft',
|
|
90
|
+
subject: title,
|
|
91
|
+
scheduledDate: newsletter.publication?.scheduledDate,
|
|
92
|
+
sentDate: newsletter.publication?.sentDate,
|
|
93
|
+
authorId: newsletter.publication?.authorId,
|
|
94
|
+
updatedAt: newsletter.updatedAt || newsletter.createdAt,
|
|
95
|
+
recipientCount: newsletter.recipientCount,
|
|
96
|
+
hidden: newsletter.hidden,
|
|
97
|
+
sendHistory: newsletter.sendHistory || [],
|
|
98
|
+
availableLanguages: Object.keys(languages),
|
|
99
|
+
languages,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
82
102
|
|
|
83
103
|
return NextResponse.json(listItems);
|
|
84
104
|
} catch (error: any) {
|
|
@@ -142,7 +162,7 @@ export async function GET_NEWSLETTER(
|
|
|
142
162
|
|
|
143
163
|
const result: Newsletter = {
|
|
144
164
|
id: newsletter._id?.toString() || newsletter.id,
|
|
145
|
-
title:
|
|
165
|
+
title: metadata.subject || 'Untitled',
|
|
146
166
|
slug: newsletter.slug,
|
|
147
167
|
blocks,
|
|
148
168
|
metadata,
|
|
@@ -201,7 +221,7 @@ export async function POST_NEWSLETTER(
|
|
|
201
221
|
|
|
202
222
|
// Generate slug for backwards compatibility, but id is primary
|
|
203
223
|
const existingNewsletters = await newsletters.find({}, { projection: { slug: 1 } }).toArray();
|
|
204
|
-
const existingSlugs = existingNewsletters.map((n: any) => n.slug).filter(Boolean);
|
|
224
|
+
const existingSlugs = existingNewsletters.map((n: Record<string, any>) => n.slug).filter(Boolean);
|
|
205
225
|
const slug = generateSlugFromTitle(finalTitle, existingSlugs);
|
|
206
226
|
|
|
207
227
|
const newsletterDocument = {
|
|
@@ -306,8 +326,8 @@ export async function PUT_NEWSLETTER(
|
|
|
306
326
|
// Set primary language if not set
|
|
307
327
|
const primaryLanguage = existing.metadata?.lang || language;
|
|
308
328
|
|
|
309
|
-
const updateData:
|
|
310
|
-
title: finalTitle,
|
|
329
|
+
const updateData: Record<string, unknown> = {
|
|
330
|
+
title: finalTitle, // Keep root title for backwards compatibility
|
|
311
331
|
blocks: blocks || [], // Keep blocks at root for backwards compatibility
|
|
312
332
|
metadata: {
|
|
313
333
|
subject: metadata.subject.trim(),
|
|
@@ -20,6 +20,7 @@ interface SmtpConfig {
|
|
|
20
20
|
from: string;
|
|
21
21
|
fromName: string;
|
|
22
22
|
logoUrl?: string;
|
|
23
|
+
unsubscribeTranslations?: Record<string, string>;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
async function getSmtpConfig(config: NewsletterApiConfig): Promise<SmtpConfig | null> {
|
|
@@ -37,6 +38,7 @@ async function getSmtpConfig(config: NewsletterApiConfig): Promise<SmtpConfig |
|
|
|
37
38
|
from: smtpConfig.from,
|
|
38
39
|
fromName: smtpConfig.fromName || '',
|
|
39
40
|
logoUrl: smtpConfig.logoUrl || '',
|
|
41
|
+
unsubscribeTranslations: smtpConfig.unsubscribeTranslations || {},
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
return null;
|
|
@@ -49,6 +51,41 @@ function getNewsletterFilter(idOrSlug: string) {
|
|
|
49
51
|
return { slug: idOrSlug };
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
async function resolveBaseUrl(config: NewsletterApiConfig, language: string): Promise<string> {
|
|
55
|
+
try {
|
|
56
|
+
const dbConnection = await config.getDb();
|
|
57
|
+
const db = dbConnection.db();
|
|
58
|
+
const settings = db.collection('settings');
|
|
59
|
+
|
|
60
|
+
// Try to get site config from plugin-website (stored in 'settings' collection with identifier 'site_config')
|
|
61
|
+
const siteConfig = await settings.findOne({ identifier: 'site_config' });
|
|
62
|
+
|
|
63
|
+
if (siteConfig && siteConfig.domainLocaleConfig && Array.isArray(siteConfig.domainLocaleConfig)) {
|
|
64
|
+
// Find domain for this locale
|
|
65
|
+
const localeConfig = siteConfig.domainLocaleConfig.find((c: any) => c.locale === language);
|
|
66
|
+
if (localeConfig && localeConfig.domain && localeConfig.domain !== 'undefined' && localeConfig.domain.trim() !== '') {
|
|
67
|
+
const domain = localeConfig.domain.trim();
|
|
68
|
+
// Add protocol if missing
|
|
69
|
+
if (!domain.startsWith('http')) {
|
|
70
|
+
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
|
71
|
+
return `${protocol}://${domain}`;
|
|
72
|
+
}
|
|
73
|
+
return domain;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.warn('[NewsletterAPI] Failed to resolve language-specific base URL:', error);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback to default, carefully checking for 'undefined' string
|
|
81
|
+
const fallback = process.env.NEXT_PUBLIC_SITE_URL;
|
|
82
|
+
if (fallback && fallback !== 'undefined' && fallback.trim() !== '') {
|
|
83
|
+
return fallback;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return 'https://botanicsandyou.com';
|
|
87
|
+
}
|
|
88
|
+
|
|
52
89
|
export async function POST_SEND_NEWSLETTER(
|
|
53
90
|
req: NextRequest,
|
|
54
91
|
idOrSlug: string,
|
|
@@ -123,7 +160,19 @@ export async function POST_SEND_NEWSLETTER(
|
|
|
123
160
|
connectionTimeout: 30000,
|
|
124
161
|
});
|
|
125
162
|
|
|
126
|
-
|
|
163
|
+
// Resolve base URL based on language settings from plugin-website
|
|
164
|
+
const baseUrl = await resolveBaseUrl(config, language);
|
|
165
|
+
|
|
166
|
+
// Final sanity check - if domain is STILL undefined, stop sending and ask user for domain
|
|
167
|
+
if (baseUrl.includes('undefined')) {
|
|
168
|
+
return NextResponse.json(
|
|
169
|
+
{
|
|
170
|
+
error: 'Domain not configured for this language. Please define your website domain first.',
|
|
171
|
+
code: 'DOMAIN_MISSING'
|
|
172
|
+
},
|
|
173
|
+
{ status: 400 }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
127
176
|
|
|
128
177
|
let logoAttachment: any = undefined;
|
|
129
178
|
let logoSrc = smtpConfig.logoUrl || `${baseUrl}/logo_black.svg`;
|
|
@@ -188,25 +237,35 @@ export async function POST_SEND_NEWSLETTER(
|
|
|
188
237
|
const subscriber = isTest ? null : await subscribers.findOne({ email });
|
|
189
238
|
const subscriberLang = subscriber?.language || language;
|
|
190
239
|
const subscriberSlug = slugs[subscriberLang] || slugs.en;
|
|
191
|
-
const unsubscribeUrl = `${baseUrl}${subscriberSlug}?email=${encodeURIComponent(email)}`;
|
|
192
240
|
|
|
193
|
-
|
|
241
|
+
// Resolve correct base URL for this specific subscriber's language
|
|
242
|
+
const subscriberBaseUrl = await resolveBaseUrl(config, subscriberLang);
|
|
243
|
+
const unsubscribeUrl = `${subscriberBaseUrl}${subscriberSlug}?email=${encodeURIComponent(email)}`;
|
|
244
|
+
|
|
245
|
+
return { email, unsubscribeUrl, subscriberLang, subscriberBaseUrl };
|
|
194
246
|
})
|
|
195
247
|
);
|
|
196
248
|
|
|
197
249
|
let successCount = 0;
|
|
198
250
|
let failedCount = 0;
|
|
199
251
|
|
|
200
|
-
for (const { email, unsubscribeUrl } of recipientsWithUrls) {
|
|
252
|
+
for (const { email, unsubscribeUrl, subscriberLang, subscriberBaseUrl } of recipientsWithUrls) {
|
|
201
253
|
try {
|
|
254
|
+
// Get unsubscribe text (Priority: metadata manual override > global SMTP translation > hardcoded localized default)
|
|
255
|
+
const globalTranslations = smtpConfig.unsubscribeTranslations || {};
|
|
256
|
+
const hardcodedDefaults: Record<string, string> = { en: 'Unsubscribe', nl: 'Afmelden', sv: 'Avanmälan' };
|
|
257
|
+
const defaultText = globalTranslations[subscriberLang] || hardcodedDefaults[subscriberLang] || hardcodedDefaults.en;
|
|
258
|
+
const unsubscribeText = metadata?.unsubscribeText || defaultText;
|
|
259
|
+
|
|
202
260
|
const html = generateNewsletterEmailHtml(
|
|
203
261
|
blocks,
|
|
204
262
|
{ subject: metadata.subject || '', previewText: metadata.previewText || '' },
|
|
205
263
|
{
|
|
206
|
-
baseUrl,
|
|
207
|
-
locale:
|
|
264
|
+
baseUrl: subscriberBaseUrl,
|
|
265
|
+
locale: subscriberLang,
|
|
208
266
|
logoUrl: logoSrc,
|
|
209
267
|
unsubscribeUrl,
|
|
268
|
+
unsubscribeText,
|
|
210
269
|
footerText: `© ${new Date().getFullYear()} ${smtpConfig.fromName || 'Botanics & You'}`,
|
|
211
270
|
}
|
|
212
271
|
);
|