@jhits/plugin-newsletter 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/email-utils.d.ts.map +1 -1
- package/dist/api/email-utils.js +45 -4
- package/dist/api/handlers/newsletters.d.ts.map +1 -1
- package/dist/api/handlers/newsletters.js +33 -16
- package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
- package/dist/api/handlers/send-newsletter.js +54 -6
- package/dist/api/handlers/settings.d.ts.map +1 -1
- package/dist/api/handlers/settings.js +51 -1
- package/dist/index.d.ts +27 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -122
- package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
- package/dist/lib/blocks/BlockRenderer.js +14 -2
- package/dist/lib/email/EmailRenderer.d.ts +1 -0
- package/dist/lib/email/EmailRenderer.d.ts.map +1 -1
- package/dist/lib/email/EmailRenderer.js +31 -19
- package/dist/lib/utils/config-resolver.d.ts +33 -0
- package/dist/lib/utils/config-resolver.d.ts.map +1 -0
- package/dist/lib/utils/config-resolver.js +47 -0
- package/dist/registry/BlockRegistry.d.ts +9 -1
- package/dist/registry/BlockRegistry.d.ts.map +1 -1
- package/dist/registry/BlockRegistry.js +126 -8
- package/dist/state/EditorContext.d.ts +11 -1
- package/dist/state/EditorContext.d.ts.map +1 -1
- package/dist/state/EditorContext.js +23 -5
- package/dist/state/types.d.ts +12 -0
- package/dist/state/types.d.ts.map +1 -1
- package/dist/types/block.d.ts +9 -0
- package/dist/types/block.d.ts.map +1 -1
- package/dist/types/newsletter.d.ts +4 -0
- package/dist/types/newsletter.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.js +24 -3
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.js +77 -17
- package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
- package/dist/views/CanvasEditor/EditorBody.js +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.js +158 -100
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +3 -1
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorSidebar.js +3 -3
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +1 -1
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -1
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.js +6 -40
- package/dist/views/NewsletterManager.d.ts.map +1 -1
- package/dist/views/NewsletterManager.js +87 -5
- package/dist/views/components/DomainPromptModal.d.ts +13 -0
- package/dist/views/components/DomainPromptModal.d.ts.map +1 -0
- package/dist/views/components/DomainPromptModal.js +58 -0
- package/dist/views/components/NewsletterCard.d.ts +16 -0
- package/dist/views/components/NewsletterCard.d.ts.map +1 -0
- package/dist/views/components/NewsletterCard.js +94 -0
- package/dist/views/components/NewsletterGrid.d.ts +16 -0
- package/dist/views/components/NewsletterGrid.d.ts.map +1 -0
- package/dist/views/components/NewsletterGrid.js +13 -0
- package/dist/views/components/SendNewsletterModal.d.ts.map +1 -1
- package/dist/views/components/SendNewsletterModal.js +91 -22
- package/dist/views/components/SmtpSettingsModal.d.ts.map +1 -1
- package/dist/views/components/SmtpSettingsModal.js +10 -0
- package/dist/views/components/TestEmailModal.d.ts.map +1 -1
- package/dist/views/components/TestEmailModal.js +86 -17
- package/package.json +53 -9
- package/src/api/email-utils.ts +53 -4
- package/src/api/handlers/newsletters.ts +40 -20
- package/src/api/handlers/send-newsletter.ts +65 -6
- package/src/api/handlers/settings.ts +60 -2
- package/src/index.tsx +49 -155
- package/src/lib/blocks/BlockRenderer.tsx +16 -2
- package/src/lib/email/EmailRenderer.tsx +31 -20
- package/src/lib/utils/config-resolver.ts +71 -0
- package/src/registry/BlockRegistry.tsx +255 -0
- package/src/state/EditorContext.tsx +43 -8
- package/src/state/types.ts +16 -0
- package/src/types/block.ts +10 -0
- package/src/types/newsletter.ts +5 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +27 -2
- package/src/views/CanvasEditor/CanvasEditorView.tsx +142 -61
- package/src/views/CanvasEditor/EditorBody.tsx +17 -13
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +178 -115
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +57 -2
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +6 -45
- package/src/views/NewsletterManager.tsx +164 -6
- package/src/views/components/DomainPromptModal.tsx +160 -0
- package/src/views/components/NewsletterCard.tsx +212 -0
- package/src/views/components/NewsletterGrid.tsx +48 -0
- package/src/views/components/SendNewsletterModal.tsx +270 -184
- package/src/views/components/SmtpSettingsModal.tsx +11 -0
- package/src/views/components/TestEmailModal.tsx +235 -149
- package/src/registry/BlockRegistry.ts +0 -53
|
@@ -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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<div className="flex items-center
|
|
125
|
-
<div className="
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
{
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<
|
|
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
|
-
|
|
164
|
-
)}
|
|
232
|
+
)}
|
|
165
233
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
241
|
+
)}
|
|
202
242
|
|
|
203
|
-
|
|
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
|
-
|
|
247
|
+
Send To
|
|
207
248
|
</label>
|
|
208
|
-
<
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
{
|
|
274
|
+
<Mail size={14} />
|
|
275
|
+
Test
|
|
239
276
|
</button>
|
|
240
|
-
|
|
277
|
+
</div>
|
|
241
278
|
</div>
|
|
242
|
-
</div>
|
|
243
279
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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
|
}
|