@jhits/plugin-newsletter 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/api/email-utils.d.ts.map +1 -1
  2. package/dist/api/email-utils.js +45 -4
  3. package/dist/api/handlers/newsletters.d.ts.map +1 -1
  4. package/dist/api/handlers/newsletters.js +33 -16
  5. package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
  6. package/dist/api/handlers/send-newsletter.js +54 -6
  7. package/dist/api/handlers/settings.d.ts.map +1 -1
  8. package/dist/api/handlers/settings.js +51 -1
  9. package/dist/index.d.ts +27 -10
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +15 -122
  12. package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
  13. package/dist/lib/blocks/BlockRenderer.js +14 -2
  14. package/dist/lib/email/EmailRenderer.d.ts +1 -0
  15. package/dist/lib/email/EmailRenderer.d.ts.map +1 -1
  16. package/dist/lib/email/EmailRenderer.js +31 -19
  17. package/dist/lib/utils/config-resolver.d.ts +33 -0
  18. package/dist/lib/utils/config-resolver.d.ts.map +1 -0
  19. package/dist/lib/utils/config-resolver.js +47 -0
  20. package/dist/registry/BlockRegistry.d.ts +9 -1
  21. package/dist/registry/BlockRegistry.d.ts.map +1 -1
  22. package/dist/registry/BlockRegistry.js +126 -8
  23. package/dist/state/EditorContext.d.ts +11 -1
  24. package/dist/state/EditorContext.d.ts.map +1 -1
  25. package/dist/state/EditorContext.js +23 -5
  26. package/dist/state/types.d.ts +12 -0
  27. package/dist/state/types.d.ts.map +1 -1
  28. package/dist/types/block.d.ts +9 -0
  29. package/dist/types/block.d.ts.map +1 -1
  30. package/dist/types/newsletter.d.ts +4 -0
  31. package/dist/types/newsletter.d.ts.map +1 -1
  32. package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
  33. package/dist/views/CanvasEditor/BlockWrapper.js +24 -3
  34. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  35. package/dist/views/CanvasEditor/CanvasEditorView.js +77 -17
  36. package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
  37. package/dist/views/CanvasEditor/EditorBody.js +1 -1
  38. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
  39. package/dist/views/CanvasEditor/components/EditorCanvas.js +158 -100
  40. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +3 -1
  41. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
  42. package/dist/views/CanvasEditor/components/EditorSidebar.js +3 -3
  43. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +1 -1
  44. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -1
  45. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.js +6 -40
  46. package/dist/views/NewsletterManager.d.ts.map +1 -1
  47. package/dist/views/NewsletterManager.js +87 -5
  48. package/dist/views/components/DomainPromptModal.d.ts +13 -0
  49. package/dist/views/components/DomainPromptModal.d.ts.map +1 -0
  50. package/dist/views/components/DomainPromptModal.js +58 -0
  51. package/dist/views/components/NewsletterCard.d.ts +16 -0
  52. package/dist/views/components/NewsletterCard.d.ts.map +1 -0
  53. package/dist/views/components/NewsletterCard.js +94 -0
  54. package/dist/views/components/NewsletterGrid.d.ts +16 -0
  55. package/dist/views/components/NewsletterGrid.d.ts.map +1 -0
  56. package/dist/views/components/NewsletterGrid.js +13 -0
  57. package/dist/views/components/SendNewsletterModal.d.ts.map +1 -1
  58. package/dist/views/components/SendNewsletterModal.js +91 -22
  59. package/dist/views/components/SmtpSettingsModal.d.ts.map +1 -1
  60. package/dist/views/components/SmtpSettingsModal.js +10 -0
  61. package/dist/views/components/TestEmailModal.d.ts.map +1 -1
  62. package/dist/views/components/TestEmailModal.js +86 -17
  63. package/package.json +53 -9
  64. package/src/api/email-utils.ts +53 -4
  65. package/src/api/handlers/newsletters.ts +40 -20
  66. package/src/api/handlers/send-newsletter.ts +65 -6
  67. package/src/api/handlers/settings.ts +60 -2
  68. package/src/index.tsx +49 -155
  69. package/src/lib/blocks/BlockRenderer.tsx +16 -2
  70. package/src/lib/email/EmailRenderer.tsx +31 -20
  71. package/src/lib/utils/config-resolver.ts +71 -0
  72. package/src/registry/BlockRegistry.tsx +255 -0
  73. package/src/state/EditorContext.tsx +43 -8
  74. package/src/state/types.ts +16 -0
  75. package/src/types/block.ts +10 -0
  76. package/src/types/newsletter.ts +5 -0
  77. package/src/views/CanvasEditor/BlockWrapper.tsx +27 -2
  78. package/src/views/CanvasEditor/CanvasEditorView.tsx +142 -61
  79. package/src/views/CanvasEditor/EditorBody.tsx +17 -13
  80. package/src/views/CanvasEditor/components/EditorCanvas.tsx +178 -115
  81. package/src/views/CanvasEditor/components/EditorSidebar.tsx +57 -2
  82. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +6 -45
  83. package/src/views/NewsletterManager.tsx +164 -6
  84. package/src/views/components/DomainPromptModal.tsx +160 -0
  85. package/src/views/components/NewsletterCard.tsx +212 -0
  86. package/src/views/components/NewsletterGrid.tsx +48 -0
  87. package/src/views/components/SendNewsletterModal.tsx +270 -184
  88. package/src/views/components/SmtpSettingsModal.tsx +11 -0
  89. package/src/views/components/TestEmailModal.tsx +235 -149
  90. package/src/registry/BlockRegistry.ts +0 -53
@@ -1 +1 @@
1
- {"version":3,"file":"SmtpSettingsModal.d.ts","sourceRoot":"","sources":["../../../src/views/components/SmtpSettingsModal.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAkBH,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,kDAuZ5E"}
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;AAOH,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,kDAoPtE"}
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
- ? 'bg-primary text-white'
85
- : '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'
86
- ? 'bg-primary text-white'
87
- : welcomeEmailConfigured
88
- ? 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
89
- : '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) => {
90
- setEmail(e.target.value);
91
- setSendError(null);
92
- }, 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
93
- ? 'bg-primary text-white'
94
- : '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, 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
95
- ? 'bg-neutral-400 text-white cursor-not-allowed'
96
- : sendSuccess
97
- ? 'bg-green-600 text-white'
98
- : 'bg-primary text-white hover:bg-primary/90'}`, children: isSending ? (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { className: "w-4 h-4 animate-spin" }), "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"] })) })] })] })] }));
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.15",
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": "./src/index.ts",
9
- "types": "./src/index.ts",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "types": "./src/index.tsx",
13
- "import": "./src/index.tsx",
14
- "default": "./src/index.tsx"
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": "./src/index.server.ts",
23
- "import": "./src/index.server.ts",
24
- "default": "./src/index.server.ts"
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",
@@ -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
- const baseUrl = host
58
- ? (host.includes('localhost') ? 'http' : 'https') + '://' + host
59
- : config.baseUrl || 'https://bya.jorishummel.com';
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: isDutch ? 'Afmelden' : isSwedish ? 'Avanmälan' : 'Unsubscribe',
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: any = {};
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: any = {};
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 listItems: NewsletterListItem[] = newsletterList.map((newsletter: any) => ({
69
- id: newsletter._id?.toString() || newsletter.id,
70
- title: newsletter.title,
71
- slug: newsletter.slug,
72
- status: newsletter.publication?.status || 'draft',
73
- subject: newsletter.metadata?.subject || '',
74
- scheduledDate: newsletter.publication?.scheduledDate,
75
- sentDate: newsletter.publication?.sentDate,
76
- authorId: newsletter.publication?.authorId,
77
- updatedAt: newsletter.updatedAt || newsletter.createdAt,
78
- recipientCount: newsletter.recipientCount,
79
- hidden: newsletter.hidden,
80
- sendHistory: newsletter.sendHistory || [],
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: newsletter.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: any = {
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
- const baseUrl = config.baseUrl || 'http://localhost:3001';
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
- return { email, unsubscribeUrl };
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: language,
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
  );