@jhits/plugin-newsletter 0.0.16 → 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 (76) 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/send-newsletter.d.ts.map +1 -1
  4. package/dist/api/handlers/send-newsletter.js +54 -6
  5. package/dist/api/handlers/settings.d.ts.map +1 -1
  6. package/dist/api/handlers/settings.js +51 -1
  7. package/dist/index.d.ts +27 -10
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +15 -122
  10. package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
  11. package/dist/lib/blocks/BlockRenderer.js +14 -2
  12. package/dist/lib/email/EmailRenderer.d.ts +1 -0
  13. package/dist/lib/email/EmailRenderer.d.ts.map +1 -1
  14. package/dist/lib/email/EmailRenderer.js +31 -19
  15. package/dist/lib/utils/config-resolver.d.ts +33 -0
  16. package/dist/lib/utils/config-resolver.d.ts.map +1 -0
  17. package/dist/lib/utils/config-resolver.js +47 -0
  18. package/dist/registry/BlockRegistry.d.ts +9 -1
  19. package/dist/registry/BlockRegistry.d.ts.map +1 -1
  20. package/dist/registry/BlockRegistry.js +126 -8
  21. package/dist/state/EditorContext.d.ts +11 -1
  22. package/dist/state/EditorContext.d.ts.map +1 -1
  23. package/dist/state/EditorContext.js +23 -5
  24. package/dist/state/types.d.ts +12 -0
  25. package/dist/state/types.d.ts.map +1 -1
  26. package/dist/types/block.d.ts +9 -0
  27. package/dist/types/block.d.ts.map +1 -1
  28. package/dist/types/newsletter.d.ts +2 -0
  29. package/dist/types/newsletter.d.ts.map +1 -1
  30. package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
  31. package/dist/views/CanvasEditor/BlockWrapper.js +24 -3
  32. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  33. package/dist/views/CanvasEditor/CanvasEditorView.js +77 -17
  34. package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
  35. package/dist/views/CanvasEditor/EditorBody.js +1 -1
  36. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
  37. package/dist/views/CanvasEditor/components/EditorCanvas.js +158 -100
  38. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +3 -1
  39. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
  40. package/dist/views/CanvasEditor/components/EditorSidebar.js +3 -3
  41. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +1 -1
  42. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -1
  43. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.js +6 -40
  44. package/dist/views/components/DomainPromptModal.d.ts +13 -0
  45. package/dist/views/components/DomainPromptModal.d.ts.map +1 -0
  46. package/dist/views/components/DomainPromptModal.js +58 -0
  47. package/dist/views/components/SendNewsletterModal.d.ts.map +1 -1
  48. package/dist/views/components/SendNewsletterModal.js +91 -22
  49. package/dist/views/components/SmtpSettingsModal.d.ts.map +1 -1
  50. package/dist/views/components/SmtpSettingsModal.js +10 -0
  51. package/dist/views/components/TestEmailModal.d.ts.map +1 -1
  52. package/dist/views/components/TestEmailModal.js +86 -17
  53. package/package.json +53 -9
  54. package/src/api/email-utils.ts +53 -4
  55. package/src/api/handlers/send-newsletter.ts +65 -6
  56. package/src/api/handlers/settings.ts +60 -2
  57. package/src/index.tsx +49 -155
  58. package/src/lib/blocks/BlockRenderer.tsx +16 -2
  59. package/src/lib/email/EmailRenderer.tsx +31 -20
  60. package/src/lib/utils/config-resolver.ts +71 -0
  61. package/src/registry/BlockRegistry.tsx +255 -0
  62. package/src/state/EditorContext.tsx +43 -8
  63. package/src/state/types.ts +16 -0
  64. package/src/types/block.ts +10 -0
  65. package/src/types/newsletter.ts +3 -0
  66. package/src/views/CanvasEditor/BlockWrapper.tsx +27 -2
  67. package/src/views/CanvasEditor/CanvasEditorView.tsx +142 -61
  68. package/src/views/CanvasEditor/EditorBody.tsx +17 -13
  69. package/src/views/CanvasEditor/components/EditorCanvas.tsx +178 -115
  70. package/src/views/CanvasEditor/components/EditorSidebar.tsx +57 -2
  71. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +6 -45
  72. package/src/views/components/DomainPromptModal.tsx +160 -0
  73. package/src/views/components/SendNewsletterModal.tsx +270 -184
  74. package/src/views/components/SmtpSettingsModal.tsx +11 -0
  75. package/src/views/components/TestEmailModal.tsx +235 -149
  76. package/src/registry/BlockRegistry.ts +0 -53
@@ -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, Users } 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,15 +19,19 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
18
19
  const [sendSuccess, setSendSuccess] = useState(false);
19
20
  const [sendError, setSendError] = useState(null);
20
21
  const [resultDetails, setResultDetails] = 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
  setSendMode('subscribers');
24
28
  setSendError(null);
25
29
  setResultDetails(null);
26
30
  setSendSuccess(false);
31
+ setShowDomainPrompt(false);
27
32
  }
28
33
  }, [isOpen]);
29
- const handleSend = async () => {
34
+ const handleSend = async (skipDomainCheck = false) => {
30
35
  if (sendMode === 'test' && !testEmail.trim()) {
31
36
  setSendError('Please enter an email address');
32
37
  return;
@@ -38,6 +43,30 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
38
43
  return;
39
44
  }
40
45
  }
46
+ // Check if we need to prompt for domain first (usually helpful in dev mode)
47
+ if (!skipDomainCheck) {
48
+ try {
49
+ setIsCheckingDomain(true);
50
+ // Check if this language has a domain configured
51
+ const response = await fetch('/api/plugin-website/settings');
52
+ const siteConfig = await response.json();
53
+ const hasDomain = siteConfig.domainLocaleConfig?.some((c) => c.locale === language &&
54
+ c.domain &&
55
+ c.domain !== 'undefined' &&
56
+ c.domain.trim() !== '');
57
+ if (!hasDomain) {
58
+ setShowDomainPrompt(true);
59
+ setIsCheckingDomain(false);
60
+ return; // Stop here, wait for domain prompt
61
+ }
62
+ }
63
+ catch (error) {
64
+ console.warn('Failed to check domain configuration:', error);
65
+ }
66
+ finally {
67
+ setIsCheckingDomain(false);
68
+ }
69
+ }
41
70
  try {
42
71
  setIsSending(true);
43
72
  setSendError(null);
@@ -52,6 +81,12 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
52
81
  });
53
82
  const data = await response.json();
54
83
  if (!response.ok) {
84
+ // If server detects missing domain, trigger prompt
85
+ if (data.code === 'DOMAIN_MISSING') {
86
+ setShowDomainPrompt(true);
87
+ setIsSending(false);
88
+ return;
89
+ }
55
90
  throw new Error(data.error || 'Failed to send newsletter');
56
91
  }
57
92
  setResultDetails(data.details);
@@ -77,31 +112,65 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
77
112
  setIsSending(false);
78
113
  }
79
114
  };
115
+ const handleSaveDomain = async (domain) => {
116
+ try {
117
+ // Get current site config
118
+ const response = await fetch('/api/plugin-website/settings');
119
+ const siteConfig = await response.json();
120
+ const currentConfigs = siteConfig.domainLocaleConfig || [];
121
+ const newConfigs = [...currentConfigs];
122
+ const existingIndex = newConfigs.findIndex((c) => c.locale === language);
123
+ if (existingIndex >= 0) {
124
+ newConfigs[existingIndex] = { ...newConfigs[existingIndex], domain };
125
+ }
126
+ else {
127
+ newConfigs.push({ locale: language, domain });
128
+ }
129
+ // Save updated site config
130
+ const saveResponse = await fetch('/api/plugin-website/settings', {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({
134
+ ...siteConfig,
135
+ domainLocaleConfig: newConfigs
136
+ }),
137
+ });
138
+ if (!saveResponse.ok)
139
+ throw new Error('Failed to save website settings');
140
+ // Re-trigger send with domain check skipped
141
+ setShowDomainPrompt(false);
142
+ handleSend(true);
143
+ }
144
+ catch (error) {
145
+ console.error('Failed to save domain:', error);
146
+ throw error;
147
+ }
148
+ };
80
149
  if (!isOpen)
81
150
  return null;
82
151
  const canSend = newsletter.hasContent && (sendMode === 'test' || subscriberCount > 0);
83
152
  const isAlreadySent = newsletter.status === 'sent';
84
153
  const noSubscribersError = sendMode === 'subscribers' && subscriberCount === 0;
85
- 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(Send, { className: "w-5 h-5 text-primary" }) }), _jsxs("div", { children: [_jsxs("h2", { className: "text-xl font-black text-dashboard-text uppercase tracking-tight", children: [isAlreadySent ? 'Resend' : 'Send', " Newsletter"] }), _jsx("p", { className: "text-xs text-dashboard-text-secondary truncate max-w-[200px]", children: newsletter.title })] })] }), _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" }) })] }), _jsxs("div", { className: "p-8", children: [isAlreadySent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800", children: _jsxs("div", { className: "flex items-center gap-2 text-blue-700 dark:text-blue-400", children: [_jsx(RefreshCw, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter was already sent - sending again" })] }) })), !newsletter.hasContent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800", children: _jsxs("div", { className: "flex items-center gap-2 text-red-700 dark:text-red-400", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter has no content" })] }) })), _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: "Send To" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: () => setSendMode('subscribers'), 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 ${sendMode === 'subscribers'
86
- ? 'bg-primary text-white'
87
- : subscriberCount > 0
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'}`, children: [_jsx(Users, { size: 14 }), "Subscribers", subscriberCount > 0 && (_jsxs("span", { className: "text-[10px] opacity-70", children: ["(", subscriberCount, ")"] }))] }), _jsxs("button", { onClick: () => setSendMode('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 ${sendMode === 'test'
90
- ? 'bg-primary text-white'
91
- : 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'}`, children: [_jsx(Mail, { size: 14 }), "Test"] })] })] }), sendMode === 'test' && (_jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Test Email Address" }), _jsx("input", { type: "email", value: testEmail, onChange: (e) => {
92
- setTestEmail(e.target.value);
93
- setSendError(null);
94
- }, 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
95
- ? 'bg-primary text-white'
96
- : '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] })), noSubscribersError && (_jsxs("div", { className: "flex items-center gap-2 text-amber-600 dark:text-amber-400 text-sm", children: [_jsx(AlertCircle, { size: 16 }), "No subscribers found. Add subscribers or use Test mode."] })), sendSuccess && resultDetails && (_jsxs("div", { className: "flex items-center gap-2 text-green-600 dark:text-green-400 text-sm", children: [_jsx(CheckCircle2, { size: 16 }), sendMode === 'test'
97
- ? `Test newsletter sent successfully!`
98
- : `Sent to ${resultDetails.successCount} subscriber${resultDetails.successCount !== 1 ? 's' : ''}${resultDetails.failedCount > 0 ? ` (${resultDetails.failedCount} failed)` : ''}`] })), !sendSuccess && (_jsx("p", { className: "text-xs text-dashboard-text-secondary", children: sendMode === 'subscribers'
99
- ? `This will send the newsletter to all ${subscriberCount} active subscriber${subscriberCount !== 1 ? 's' : ''}.`
100
- : 'Send a test newsletter to verify the email looks correct.' }))] })] }), _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: sendSuccess ? 'Close' : 'Cancel' }), _jsx("button", { onClick: handleSend, disabled: isSending || sendSuccess || !canSend, 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
101
- ? 'bg-neutral-400 text-white cursor-not-allowed'
102
- : sendSuccess
103
- ? 'bg-green-600 text-white'
104
- : !canSend
154
+ 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(Send, { className: "w-5 h-5 text-primary" }) }), _jsxs("div", { children: [_jsxs("h2", { className: "text-xl font-black text-dashboard-text uppercase tracking-tight", children: [isAlreadySent ? 'Resend' : 'Send', " Newsletter"] }), _jsx("p", { className: "text-xs text-dashboard-text-secondary truncate max-w-[200px]", children: newsletter.title })] })] }), _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" }) })] }), _jsxs("div", { className: "p-8", children: [isAlreadySent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800", children: _jsxs("div", { className: "flex items-center gap-2 text-blue-700 dark:text-blue-400", children: [_jsx(RefreshCw, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter was already sent - sending again" })] }) })), !newsletter.hasContent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800", children: _jsxs("div", { className: "flex items-center gap-2 text-red-700 dark:text-red-400", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter has no content" })] }) })), _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: "Send To" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: () => setSendMode('subscribers'), 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 ${sendMode === 'subscribers'
155
+ ? 'bg-primary text-white'
156
+ : subscriberCount > 0
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'}`, children: [_jsx(Users, { size: 14 }), "Subscribers", subscriberCount > 0 && (_jsxs("span", { className: "text-[10px] opacity-70", children: ["(", subscriberCount, ")"] }))] }), _jsxs("button", { onClick: () => setSendMode('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 ${sendMode === 'test'
159
+ ? 'bg-primary text-white'
160
+ : 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'}`, children: [_jsx(Mail, { size: 14 }), "Test"] })] })] }), sendMode === 'test' && (_jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Test Email Address" }), _jsx("input", { type: "email", value: testEmail, onChange: (e) => {
161
+ setTestEmail(e.target.value);
162
+ setSendError(null);
163
+ }, 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
164
+ ? 'bg-primary text-white'
165
+ : '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] })), noSubscribersError && (_jsxs("div", { className: "flex items-center gap-2 text-amber-600 dark:text-amber-400 text-sm", children: [_jsx(AlertCircle, { size: 16 }), "No subscribers found. Add subscribers or use Test mode."] })), sendSuccess && resultDetails && (_jsxs("div", { className: "flex items-center gap-2 text-green-600 dark:text-green-400 text-sm", children: [_jsx(CheckCircle2, { size: 16 }), sendMode === 'test'
166
+ ? `Test newsletter sent successfully!`
167
+ : `Sent to ${resultDetails.successCount} subscriber${resultDetails.successCount !== 1 ? 's' : ''}${resultDetails.failedCount > 0 ? ` (${resultDetails.failedCount} failed)` : ''}`] })), !sendSuccess && (_jsx("p", { className: "text-xs text-dashboard-text-secondary", children: sendMode === 'subscribers'
168
+ ? `This will send the newsletter to all ${subscriberCount} active subscriber${subscriberCount !== 1 ? 's' : ''}.`
169
+ : 'Send a test newsletter to verify the email looks correct.' }))] })] }), _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: sendSuccess ? 'Close' : 'Cancel' }), _jsx("button", { onClick: () => handleSend(), disabled: isSending || sendSuccess || !canSend || 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
105
170
  ? 'bg-neutral-400 text-white cursor-not-allowed'
106
- : '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" }), sendMode === 'subscribers' ? 'Send to Subscribers' : 'Send Test'] })) })] })] })] }));
171
+ : sendSuccess
172
+ ? 'bg-green-600 text-white'
173
+ : !canSend
174
+ ? 'bg-neutral-400 text-white cursor-not-allowed'
175
+ : '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" }), sendMode === 'subscribers' ? 'Send to Subscribers' : 'Send Test'] })) })] })] })] }), _jsx(DomainPromptModal, { isOpen: showDomainPrompt, onClose: () => setShowDomainPrompt(false), onSave: handleSaveDomain, language: language })] }));
107
176
  }
@@ -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.16",
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
  });