@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
@@ -6,6 +6,7 @@
6
6
 
7
7
  import React, { useState, useEffect } from 'react';
8
8
  import { X, Send, RefreshCw, CheckCircle2, AlertCircle, Mail, Globe, Users, Radio } from 'lucide-react';
9
+ import { DomainPromptModal } from './DomainPromptModal';
9
10
 
10
11
  interface SendNewsletterModalProps {
11
12
  isOpen: boolean;
@@ -37,6 +38,10 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
37
38
  const [sendSuccess, setSendSuccess] = useState(false);
38
39
  const [sendError, setSendError] = useState<string | null>(null);
39
40
  const [resultDetails, setResultDetails] = useState<{ successCount: number; failedCount: number } | null>(null);
41
+
42
+ // Domain prompt state
43
+ const [showDomainPrompt, setShowDomainPrompt] = useState(false);
44
+ const [isCheckingDomain, setIsCheckingDomain] = useState(false);
40
45
 
41
46
  useEffect(() => {
42
47
  if (isOpen) {
@@ -44,10 +49,11 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
44
49
  setSendError(null);
45
50
  setResultDetails(null);
46
51
  setSendSuccess(false);
52
+ setShowDomainPrompt(false);
47
53
  }
48
54
  }, [isOpen]);
49
55
 
50
- const handleSend = async () => {
56
+ const handleSend = async (skipDomainCheck = false) => {
51
57
  if (sendMode === 'test' && !testEmail.trim()) {
52
58
  setSendError('Please enter an email address');
53
59
  return;
@@ -61,6 +67,33 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
61
67
  }
62
68
  }
63
69
 
70
+ // Check if we need to prompt for domain first (usually helpful in dev mode)
71
+ if (!skipDomainCheck) {
72
+ try {
73
+ setIsCheckingDomain(true);
74
+ // Check if this language has a domain configured
75
+ const response = await fetch('/api/plugin-website/settings');
76
+ const siteConfig = await response.json();
77
+
78
+ const hasDomain = siteConfig.domainLocaleConfig?.some((c: any) =>
79
+ c.locale === language &&
80
+ c.domain &&
81
+ c.domain !== 'undefined' &&
82
+ c.domain.trim() !== ''
83
+ );
84
+
85
+ if (!hasDomain) {
86
+ setShowDomainPrompt(true);
87
+ setIsCheckingDomain(false);
88
+ return; // Stop here, wait for domain prompt
89
+ }
90
+ } catch (error) {
91
+ console.warn('Failed to check domain configuration:', error);
92
+ } finally {
93
+ setIsCheckingDomain(false);
94
+ }
95
+ }
96
+
64
97
  try {
65
98
  setIsSending(true);
66
99
  setSendError(null);
@@ -71,13 +104,19 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
71
104
  credentials: 'include',
72
105
  body: JSON.stringify({
73
106
  language,
74
- testEmail: sendMode === 'test' ? testEmail : undefined
107
+ testEmail: sendMode === 'test' ? testEmail : undefined
75
108
  }),
76
109
  });
77
110
 
78
111
  const data = await response.json();
79
112
 
80
113
  if (!response.ok) {
114
+ // If server detects missing domain, trigger prompt
115
+ if (data.code === 'DOMAIN_MISSING') {
116
+ setShowDomainPrompt(true);
117
+ setIsSending(false);
118
+ return;
119
+ }
81
120
  throw new Error(data.error || 'Failed to send newsletter');
82
121
  }
83
122
 
@@ -103,6 +142,43 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
103
142
  }
104
143
  };
105
144
 
145
+ const handleSaveDomain = async (domain: string) => {
146
+ try {
147
+ // Get current site config
148
+ const response = await fetch('/api/plugin-website/settings');
149
+ const siteConfig = await response.json();
150
+
151
+ const currentConfigs = siteConfig.domainLocaleConfig || [];
152
+ const newConfigs = [...currentConfigs];
153
+
154
+ const existingIndex = newConfigs.findIndex((c: any) => c.locale === language);
155
+ if (existingIndex >= 0) {
156
+ newConfigs[existingIndex] = { ...newConfigs[existingIndex], domain };
157
+ } else {
158
+ newConfigs.push({ locale: language, domain });
159
+ }
160
+
161
+ // Save updated site config
162
+ const saveResponse = await fetch('/api/plugin-website/settings', {
163
+ method: 'POST',
164
+ headers: { 'Content-Type': 'application/json' },
165
+ body: JSON.stringify({
166
+ ...siteConfig,
167
+ domainLocaleConfig: newConfigs
168
+ }),
169
+ });
170
+
171
+ if (!saveResponse.ok) throw new Error('Failed to save website settings');
172
+
173
+ // Re-trigger send with domain check skipped
174
+ setShowDomainPrompt(false);
175
+ handleSend(true);
176
+ } catch (error) {
177
+ console.error('Failed to save domain:', error);
178
+ throw error;
179
+ }
180
+ };
181
+
106
182
  if (!isOpen) return null;
107
183
 
108
184
  const canSend = newsletter.hasContent && (sendMode === 'test' || subscriberCount > 0);
@@ -110,213 +186,223 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
110
186
  const noSubscribersError = sendMode === 'subscribers' && subscriberCount === 0;
111
187
 
112
188
  return (
113
- <div className="fixed inset-0 z-50 flex items-center justify-center">
114
- {/* Backdrop */}
115
- <div
116
- className="absolute inset-0 bg-black/50 backdrop-blur-sm"
117
- onClick={() => !isSending && onClose()}
118
- />
119
-
120
- {/* Modal */}
121
- <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">
122
- {/* Header */}
123
- <div className="flex items-center justify-between px-8 py-6 border-b border-dashboard-border">
124
- <div className="flex items-center gap-3">
125
- <div className="w-10 h-10 rounded-2xl bg-primary/10 flex items-center justify-center">
126
- <Send className="w-5 h-5 text-primary" />
127
- </div>
128
- <div>
129
- <h2 className="text-xl font-black text-dashboard-text uppercase tracking-tight">
130
- {isAlreadySent ? 'Resend' : 'Send'} Newsletter
131
- </h2>
132
- <p className="text-xs text-dashboard-text-secondary truncate max-w-[200px]">
133
- {newsletter.title}
134
- </p>
135
- </div>
136
- </div>
137
- <button
138
- onClick={() => !isSending && onClose()}
139
- disabled={isSending}
140
- className="p-2 rounded-full hover:bg-dashboard-border transition-colors"
141
- >
142
- <X size={20} className="text-dashboard-text-secondary" />
143
- </button>
144
- </div>
145
-
146
- {/* Content */}
147
- <div className="p-8">
148
- {isAlreadySent && (
149
- <div className="mb-4 p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
150
- <div className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
151
- <RefreshCw size={16} />
152
- <span className="text-xs font-bold uppercase">This newsletter was already sent - sending again</span>
189
+ <>
190
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
191
+ {/* Backdrop */}
192
+ <div
193
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
194
+ onClick={() => !isSending && onClose()}
195
+ />
196
+
197
+ {/* Modal */}
198
+ <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">
199
+ {/* Header */}
200
+ <div className="flex items-center justify-between px-8 py-6 border-b border-dashboard-border">
201
+ <div className="flex items-center gap-3">
202
+ <div className="w-10 h-10 rounded-2xl bg-primary/10 flex items-center justify-center">
203
+ <Send className="w-5 h-5 text-primary" />
204
+ </div>
205
+ <div>
206
+ <h2 className="text-xl font-black text-dashboard-text uppercase tracking-tight">
207
+ {isAlreadySent ? 'Resend' : 'Send'} Newsletter
208
+ </h2>
209
+ <p className="text-xs text-dashboard-text-secondary truncate max-w-[200px]">
210
+ {newsletter.title}
211
+ </p>
153
212
  </div>
154
213
  </div>
155
- )}
214
+ <button
215
+ onClick={() => !isSending && onClose()}
216
+ disabled={isSending}
217
+ className="p-2 rounded-full hover:bg-dashboard-border transition-colors"
218
+ >
219
+ <X size={20} className="text-dashboard-text-secondary" />
220
+ </button>
221
+ </div>
156
222
 
157
- {!newsletter.hasContent && (
158
- <div className="mb-4 p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
159
- <div className="flex items-center gap-2 text-red-700 dark:text-red-400">
160
- <AlertCircle size={16} />
161
- <span className="text-xs font-bold uppercase">This newsletter has no content</span>
223
+ {/* Content */}
224
+ <div className="p-8">
225
+ {isAlreadySent && (
226
+ <div className="mb-4 p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
227
+ <div className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
228
+ <RefreshCw size={16} />
229
+ <span className="text-xs font-bold uppercase">This newsletter was already sent - sending again</span>
230
+ </div>
162
231
  </div>
163
- </div>
164
- )}
232
+ )}
165
233
 
166
- <div className="space-y-4">
167
- {/* Send Mode Toggle */}
168
- <div>
169
- <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
170
- Send To
171
- </label>
172
- <div className="flex gap-2">
173
- <button
174
- onClick={() => setSendMode('subscribers')}
175
- 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 ${
176
- sendMode === 'subscribers'
177
- ? 'bg-primary text-white'
178
- : subscriberCount > 0
179
- ? 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
180
- : 'bg-dashboard-bg border border-dashboard-border text-dashboard-text-secondary opacity-50'
181
- }`}
182
- >
183
- <Users size={14} />
184
- Subscribers
185
- {subscriberCount > 0 && (
186
- <span className="text-[10px] opacity-70">({subscriberCount})</span>
187
- )}
188
- </button>
189
- <button
190
- onClick={() => setSendMode('test')}
191
- 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 ${
192
- sendMode === 'test'
193
- ? 'bg-primary text-white'
194
- : 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
195
- }`}
196
- >
197
- <Mail size={14} />
198
- Test
199
- </button>
234
+ {!newsletter.hasContent && (
235
+ <div className="mb-4 p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
236
+ <div className="flex items-center gap-2 text-red-700 dark:text-red-400">
237
+ <AlertCircle size={16} />
238
+ <span className="text-xs font-bold uppercase">This newsletter has no content</span>
239
+ </div>
200
240
  </div>
201
- </div>
241
+ )}
202
242
 
203
- {sendMode === 'test' && (
243
+ <div className="space-y-4">
244
+ {/* Send Mode Toggle */}
204
245
  <div>
205
246
  <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
206
- Test Email Address
247
+ Send To
207
248
  </label>
208
- <input
209
- type="email"
210
- value={testEmail}
211
- onChange={(e) => {
212
- setTestEmail(e.target.value);
213
- setSendError(null);
214
- }}
215
- onKeyDown={(e) => e.key === 'Enter' && handleSend()}
216
- placeholder="your@email.com"
217
- 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"
218
- />
219
- </div>
220
- )}
221
-
222
- <div>
223
- <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
224
- <Globe size={12} className="inline mr-1" />
225
- Language
226
- </label>
227
- <div className="flex gap-2">
228
- {languages.map((lang) => (
249
+ <div className="flex gap-2">
229
250
  <button
230
- key={lang.code}
231
- onClick={() => setLanguage(lang.code)}
232
- className={`px-4 py-2 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${
233
- language === lang.code
251
+ onClick={() => setSendMode('subscribers')}
252
+ 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 ${
253
+ sendMode === 'subscribers'
254
+ ? 'bg-primary text-white'
255
+ : subscriberCount > 0
256
+ ? 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
257
+ : 'bg-dashboard-bg border border-dashboard-border text-dashboard-text-secondary opacity-50'
258
+ }`}
259
+ >
260
+ <Users size={14} />
261
+ Subscribers
262
+ {subscriberCount > 0 && (
263
+ <span className="text-[10px] opacity-70">({subscriberCount})</span>
264
+ )}
265
+ </button>
266
+ <button
267
+ onClick={() => setSendMode('test')}
268
+ 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 ${
269
+ sendMode === 'test'
234
270
  ? 'bg-primary text-white'
235
271
  : 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
236
272
  }`}
237
273
  >
238
- {lang.label}
274
+ <Mail size={14} />
275
+ Test
239
276
  </button>
240
- ))}
277
+ </div>
241
278
  </div>
242
- </div>
243
279
 
244
- {sendError && (
245
- <div className="flex items-center gap-2 text-red-500 text-sm">
246
- <AlertCircle size={16} />
247
- {sendError}
248
- </div>
249
- )}
280
+ {sendMode === 'test' && (
281
+ <div>
282
+ <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
283
+ Test Email Address
284
+ </label>
285
+ <input
286
+ type="email"
287
+ value={testEmail}
288
+ onChange={(e) => {
289
+ setTestEmail(e.target.value);
290
+ setSendError(null);
291
+ }}
292
+ onKeyDown={(e) => e.key === 'Enter' && handleSend()}
293
+ placeholder="your@email.com"
294
+ 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"
295
+ />
296
+ </div>
297
+ )}
250
298
 
251
- {noSubscribersError && (
252
- <div className="flex items-center gap-2 text-amber-600 dark:text-amber-400 text-sm">
253
- <AlertCircle size={16} />
254
- No subscribers found. Add subscribers or use Test mode.
299
+ <div>
300
+ <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
301
+ <Globe size={12} className="inline mr-1" />
302
+ Language
303
+ </label>
304
+ <div className="flex gap-2">
305
+ {languages.map((lang) => (
306
+ <button
307
+ key={lang.code}
308
+ onClick={() => setLanguage(lang.code)}
309
+ className={`px-4 py-2 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${
310
+ language === lang.code
311
+ ? 'bg-primary text-white'
312
+ : 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
313
+ }`}
314
+ >
315
+ {lang.label}
316
+ </button>
317
+ ))}
318
+ </div>
255
319
  </div>
256
- )}
257
320
 
258
- {sendSuccess && resultDetails && (
259
- <div className="flex items-center gap-2 text-green-600 dark:text-green-400 text-sm">
260
- <CheckCircle2 size={16} />
261
- {sendMode === 'test'
262
- ? `Test newsletter sent successfully!`
263
- : `Sent to ${resultDetails.successCount} subscriber${resultDetails.successCount !== 1 ? 's' : ''}${resultDetails.failedCount > 0 ? ` (${resultDetails.failedCount} failed)` : ''}`
264
- }
265
- </div>
266
- )}
321
+ {sendError && (
322
+ <div className="flex items-center gap-2 text-red-500 text-sm">
323
+ <AlertCircle size={16} />
324
+ {sendError}
325
+ </div>
326
+ )}
267
327
 
268
- {!sendSuccess && (
269
- <p className="text-xs text-dashboard-text-secondary">
270
- {sendMode === 'subscribers'
271
- ? `This will send the newsletter to all ${subscriberCount} active subscriber${subscriberCount !== 1 ? 's' : ''}.`
272
- : 'Send a test newsletter to verify the email looks correct.'
273
- }
274
- </p>
275
- )}
328
+ {noSubscribersError && (
329
+ <div className="flex items-center gap-2 text-amber-600 dark:text-amber-400 text-sm">
330
+ <AlertCircle size={16} />
331
+ No subscribers found. Add subscribers or use Test mode.
332
+ </div>
333
+ )}
334
+
335
+ {sendSuccess && resultDetails && (
336
+ <div className="flex items-center gap-2 text-green-600 dark:text-green-400 text-sm">
337
+ <CheckCircle2 size={16} />
338
+ {sendMode === 'test'
339
+ ? `Test newsletter sent successfully!`
340
+ : `Sent to ${resultDetails.successCount} subscriber${resultDetails.successCount !== 1 ? 's' : ''}${resultDetails.failedCount > 0 ? ` (${resultDetails.failedCount} failed)` : ''}`
341
+ }
342
+ </div>
343
+ )}
344
+
345
+ {!sendSuccess && (
346
+ <p className="text-xs text-dashboard-text-secondary">
347
+ {sendMode === 'subscribers'
348
+ ? `This will send the newsletter to all ${subscriberCount} active subscriber${subscriberCount !== 1 ? 's' : ''}.`
349
+ : 'Send a test newsletter to verify the email looks correct.'
350
+ }
351
+ </p>
352
+ )}
353
+ </div>
276
354
  </div>
277
- </div>
278
355
 
279
- {/* Footer */}
280
- <div className="flex items-center justify-end gap-3 px-8 py-6 border-t border-dashboard-border bg-dashboard-bg/50">
281
- <button
282
- onClick={onClose}
283
- disabled={isSending}
284
- 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"
285
- >
286
- {sendSuccess ? 'Close' : 'Cancel'}
287
- </button>
288
- <button
289
- onClick={handleSend}
290
- disabled={isSending || sendSuccess || !canSend}
291
- className={`inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg ${
292
- isSending
293
- ? 'bg-neutral-400 text-white cursor-not-allowed'
294
- : sendSuccess
295
- ? 'bg-green-600 text-white'
296
- : !canSend
297
- ? 'bg-neutral-400 text-white cursor-not-allowed'
298
- : 'bg-primary text-white hover:bg-primary/90'
299
- }`}
300
- >
301
- {isSending ? (
302
- <>
303
- <RefreshCw className="w-4 h-4 animate-spin" />
304
- Sending...
305
- </>
306
- ) : sendSuccess ? (
307
- <>
308
- <CheckCircle2 className="w-4 h-4" />
309
- Sent!
310
- </>
311
- ) : (
312
- <>
313
- <Send className="w-4 h-4" />
314
- {sendMode === 'subscribers' ? 'Send to Subscribers' : 'Send Test'}
315
- </>
316
- )}
317
- </button>
356
+ {/* Footer */}
357
+ <div className="flex items-center justify-end gap-3 px-8 py-6 border-t border-dashboard-border bg-dashboard-bg/50">
358
+ <button
359
+ onClick={onClose}
360
+ disabled={isSending}
361
+ 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"
362
+ >
363
+ {sendSuccess ? 'Close' : 'Cancel'}
364
+ </button>
365
+ <button
366
+ onClick={() => handleSend()}
367
+ disabled={isSending || sendSuccess || !canSend || isCheckingDomain}
368
+ className={`inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg ${
369
+ isSending || isCheckingDomain
370
+ ? 'bg-neutral-400 text-white cursor-not-allowed'
371
+ : sendSuccess
372
+ ? 'bg-green-600 text-white'
373
+ : !canSend
374
+ ? 'bg-neutral-400 text-white cursor-not-allowed'
375
+ : 'bg-primary text-white hover:bg-primary/90'
376
+ }`}
377
+ >
378
+ {isSending || isCheckingDomain ? (
379
+ <>
380
+ <RefreshCw className="w-4 h-4 animate-spin" />
381
+ {isCheckingDomain ? 'Checking...' : 'Sending...'}
382
+ </>
383
+ ) : sendSuccess ? (
384
+ <>
385
+ <CheckCircle2 className="w-4 h-4" />
386
+ Sent!
387
+ </>
388
+ ) : (
389
+ <>
390
+ <Send className="w-4 h-4" />
391
+ {sendMode === 'subscribers' ? 'Send to Subscribers' : 'Send Test'}
392
+ </>
393
+ )}
394
+ </button>
395
+ </div>
318
396
  </div>
319
397
  </div>
320
- </div>
398
+
399
+ {/* Domain Prompt Modal */}
400
+ <DomainPromptModal
401
+ isOpen={showDomainPrompt}
402
+ onClose={() => setShowDomainPrompt(false)}
403
+ onSave={handleSaveDomain}
404
+ language={language}
405
+ />
406
+ </>
321
407
  );
322
408
  }
@@ -16,6 +16,7 @@ interface SmtpSettings {
16
16
  fromName: string;
17
17
  primaryLanguage?: string;
18
18
  logoUrl?: string;
19
+ unsubscribeTranslations?: Record<string, string>;
19
20
  }
20
21
 
21
22
  interface SmtpSettingsModalProps {
@@ -33,6 +34,11 @@ export function SmtpSettingsModal({ isOpen, onClose }: SmtpSettingsModalProps) {
33
34
  fromName: '',
34
35
  primaryLanguage: 'en',
35
36
  logoUrl: '',
37
+ unsubscribeTranslations: {
38
+ en: 'Unsubscribe',
39
+ nl: 'Afmelden',
40
+ sv: 'Avanmälan',
41
+ },
36
42
  });
37
43
  const [smtpLoading, setSmtpLoading] = useState(true);
38
44
  const [smtpSaving, setSmtpSaving] = useState(false);
@@ -66,6 +72,11 @@ export function SmtpSettingsModal({ isOpen, onClose }: SmtpSettingsModalProps) {
66
72
  fromName: data.fromName || '',
67
73
  primaryLanguage: data.primaryLanguage || 'en',
68
74
  logoUrl: data.logoUrl || '',
75
+ unsubscribeTranslations: data.unsubscribeTranslations || {
76
+ en: 'Unsubscribe',
77
+ nl: 'Afmelden',
78
+ sv: 'Avanmälan',
79
+ },
69
80
  });
70
81
  setFromEmailEditable(!!data.from && data.from !== data.user);
71
82
  }