@jhits/plugin-newsletter 0.0.10 → 0.0.11
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/package.json +3 -2
- package/src/api/email-utils.ts +165 -0
- package/src/api/handler.ts +28 -0
- package/src/api/handlers/index.ts +44 -0
- package/src/api/handlers/newsletters.ts +332 -0
- package/src/api/handlers/send-newsletter.ts +288 -0
- package/src/api/handlers/settings.ts +403 -0
- package/src/api/handlers/subscribers.ts +152 -0
- package/src/api/handlers/upload.ts +47 -0
- package/src/api/handlers/welcome-email.ts +210 -0
- package/src/api/router.ts +166 -0
- package/src/index.server.ts +12 -0
- package/src/index.tsx +353 -0
- package/src/index.tsx.patch +98 -0
- package/src/init.tsx +72 -0
- package/src/lib/blocks/BlockRenderer.tsx +125 -0
- package/src/lib/email/EmailRenderer.tsx +420 -0
- package/src/lib/email/index.ts +6 -0
- package/src/lib/i18n.ts +82 -0
- package/src/lib/mappers/apiMapper.ts +57 -0
- package/src/lib/utils/blockHelpers.ts +71 -0
- package/src/lib/utils/slugify.ts +43 -0
- package/src/registry/BlockRegistry.ts +53 -0
- package/src/registry/index.ts +5 -0
- package/src/state/EditorContext.tsx +278 -0
- package/src/state/index.ts +10 -0
- package/src/state/reducer.ts +561 -0
- package/src/state/types.ts +154 -0
- package/src/types/block.ts +275 -0
- package/src/types/newsletter.ts +152 -0
- package/src/types/registry.ts +14 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +143 -0
- package/src/views/CanvasEditor/CanvasEditorView.tsx +343 -0
- package/src/views/CanvasEditor/EditorBody.tsx +95 -0
- package/src/views/CanvasEditor/EditorHeader.tsx +255 -0
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +83 -0
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +674 -0
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +120 -0
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +139 -0
- package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
- package/src/views/CanvasEditor/components/LibraryItem.tsx +71 -0
- package/src/views/CanvasEditor/components/SlashCommandDetector.tsx +196 -0
- package/src/views/CanvasEditor/components/SlashCommandMenu.tsx +131 -0
- package/src/views/CanvasEditor/components/index.ts +16 -0
- package/src/views/CanvasEditor/hooks/index.ts +7 -0
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +136 -0
- package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +73 -0
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +54 -0
- package/src/views/CanvasEditor/hooks/useSlashCommand.ts +106 -0
- package/src/views/CanvasEditor/index.ts +12 -0
- package/src/views/NewsletterEditor.tsx +42 -0
- package/src/views/NewsletterManager.tsx +483 -0
- package/src/views/SettingsView.tsx +216 -0
- package/src/views/SubscribersView.tsx +269 -0
- package/src/views/components/SendNewsletterModal.tsx +322 -0
- package/src/views/components/SmtpSettingsModal.tsx +433 -0
- package/src/views/components/TestEmailModal.tsx +268 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMTP Settings Modal Component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use client';
|
|
6
|
+
|
|
7
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
8
|
+
import { Settings2, X, Save, Eye, EyeOff, RefreshCw, CheckCircle2, Pencil, Unlink, Image as ImageIcon, Upload, XCircle } from 'lucide-react';
|
|
9
|
+
|
|
10
|
+
interface SmtpSettings {
|
|
11
|
+
host: string;
|
|
12
|
+
port: number;
|
|
13
|
+
user: string;
|
|
14
|
+
password: string;
|
|
15
|
+
from: string;
|
|
16
|
+
fromName: string;
|
|
17
|
+
primaryLanguage?: string;
|
|
18
|
+
logoUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SmtpSettingsModalProps {
|
|
22
|
+
isOpen: boolean;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SmtpSettingsModal({ isOpen, onClose }: SmtpSettingsModalProps) {
|
|
27
|
+
const [smtpSettings, setSmtpSettings] = useState<SmtpSettings>({
|
|
28
|
+
host: '',
|
|
29
|
+
port: 465,
|
|
30
|
+
user: '',
|
|
31
|
+
password: '',
|
|
32
|
+
from: '',
|
|
33
|
+
fromName: '',
|
|
34
|
+
primaryLanguage: 'en',
|
|
35
|
+
logoUrl: '',
|
|
36
|
+
});
|
|
37
|
+
const [smtpLoading, setSmtpLoading] = useState(true);
|
|
38
|
+
const [smtpSaving, setSmtpSaving] = useState(false);
|
|
39
|
+
const [smtpShowPassword, setSmtpShowPassword] = useState(false);
|
|
40
|
+
const [smtpSuccess, setSmtpSuccess] = useState(false);
|
|
41
|
+
const [fromEmailEditable, setFromEmailEditable] = useState(false);
|
|
42
|
+
const [isLogoUploading, setIsLogoUploading] = useState(false);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (isOpen) {
|
|
46
|
+
fetchSmtpSettings();
|
|
47
|
+
} else {
|
|
48
|
+
setFromEmailEditable(false);
|
|
49
|
+
}
|
|
50
|
+
}, [isOpen]);
|
|
51
|
+
|
|
52
|
+
const fetchSmtpSettings = async () => {
|
|
53
|
+
try {
|
|
54
|
+
setSmtpLoading(true);
|
|
55
|
+
const response = await fetch('/api/plugin-newsletter/smtp', {
|
|
56
|
+
credentials: 'include',
|
|
57
|
+
});
|
|
58
|
+
if (response.ok) {
|
|
59
|
+
const data = await response.json();
|
|
60
|
+
setSmtpSettings({
|
|
61
|
+
host: data.host || '',
|
|
62
|
+
port: data.port || 465,
|
|
63
|
+
user: data.user || '',
|
|
64
|
+
password: data.password || '',
|
|
65
|
+
from: data.from || '',
|
|
66
|
+
fromName: data.fromName || '',
|
|
67
|
+
primaryLanguage: data.primaryLanguage || 'en',
|
|
68
|
+
logoUrl: data.logoUrl || '',
|
|
69
|
+
});
|
|
70
|
+
setFromEmailEditable(!!data.from && data.from !== data.user);
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Failed to load SMTP settings:', error);
|
|
74
|
+
} finally {
|
|
75
|
+
setSmtpLoading(false);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleSave = async () => {
|
|
80
|
+
try {
|
|
81
|
+
setSmtpSaving(true);
|
|
82
|
+
|
|
83
|
+
const settingsToSave = {
|
|
84
|
+
...smtpSettings,
|
|
85
|
+
from: smtpSettings.from || smtpSettings.user,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const response = await fetch('/api/plugin-newsletter/smtp', {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
credentials: 'include',
|
|
92
|
+
body: JSON.stringify(settingsToSave),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const error = await response.json();
|
|
97
|
+
throw new Error(error.error || 'Failed to save SMTP settings');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setSmtpSuccess(true);
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
setSmtpSuccess(false);
|
|
103
|
+
onClose();
|
|
104
|
+
}, 1500);
|
|
105
|
+
} catch (error: any) {
|
|
106
|
+
console.error('Failed to save SMTP settings:', error);
|
|
107
|
+
alert(error.message || 'Failed to save SMTP settings');
|
|
108
|
+
} finally {
|
|
109
|
+
setSmtpSaving(false);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
114
|
+
const file = e.target.files?.[0];
|
|
115
|
+
if (!file) return;
|
|
116
|
+
|
|
117
|
+
const allowedTypes = ['image/png', 'image/jpeg'];
|
|
118
|
+
if (!allowedTypes.includes(file.type)) {
|
|
119
|
+
alert('Please upload a PNG or JPEG image');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (file.size > 2 * 1024 * 1024) {
|
|
124
|
+
alert('Image must be smaller than 2MB');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
setIsLogoUploading(true);
|
|
130
|
+
const formData = new FormData();
|
|
131
|
+
formData.append('file', file);
|
|
132
|
+
formData.append('type', 'logo');
|
|
133
|
+
|
|
134
|
+
const response = await fetch('/api/plugin-newsletter/upload', {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
credentials: 'include',
|
|
137
|
+
body: formData,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const errorData = await response.json();
|
|
142
|
+
throw new Error(errorData.error || 'Failed to upload logo');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const data = await response.json();
|
|
146
|
+
setSmtpSettings(prev => ({ ...prev, logoUrl: data.url }));
|
|
147
|
+
} catch (error: any) {
|
|
148
|
+
console.error('Failed to upload logo:', error);
|
|
149
|
+
alert(error.message || 'Failed to upload logo. Please try again.');
|
|
150
|
+
} finally {
|
|
151
|
+
setIsLogoUploading(false);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (!isOpen) return null;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
159
|
+
{/* Backdrop */}
|
|
160
|
+
<div
|
|
161
|
+
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
162
|
+
onClick={() => !smtpSaving && onClose()}
|
|
163
|
+
/>
|
|
164
|
+
|
|
165
|
+
{/* Modal */}
|
|
166
|
+
<div className="relative w-full max-w-2xl mx-4 bg-white dark:bg-neutral-900 rounded-3xl border border-dashboard-border shadow-2xl overflow-hidden">
|
|
167
|
+
{/* Header */}
|
|
168
|
+
<div className="flex items-center justify-between px-8 py-6 border-b border-dashboard-border">
|
|
169
|
+
<div className="flex items-center gap-3">
|
|
170
|
+
<div className="w-10 h-10 rounded-2xl bg-primary/10 flex items-center justify-center">
|
|
171
|
+
<Settings2 className="w-5 h-5 text-primary" />
|
|
172
|
+
</div>
|
|
173
|
+
<div>
|
|
174
|
+
<h2 className="text-xl font-black text-dashboard-text uppercase tracking-tight">
|
|
175
|
+
SMTP Settings
|
|
176
|
+
</h2>
|
|
177
|
+
<p className="text-xs text-dashboard-text-secondary">
|
|
178
|
+
Configure email server for sending newsletters
|
|
179
|
+
</p>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<button
|
|
183
|
+
onClick={() => !smtpSaving && onClose()}
|
|
184
|
+
disabled={smtpSaving}
|
|
185
|
+
className="p-2 rounded-full hover:bg-dashboard-border transition-colors"
|
|
186
|
+
>
|
|
187
|
+
<X size={20} className="text-dashboard-text-secondary" />
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* Content */}
|
|
192
|
+
<div className="p-8 space-y-6 max-h-[60vh] overflow-y-auto">
|
|
193
|
+
{smtpLoading ? (
|
|
194
|
+
<div className="flex items-center justify-center py-12">
|
|
195
|
+
<RefreshCw className="w-6 h-6 animate-spin text-primary" />
|
|
196
|
+
</div>
|
|
197
|
+
) : (
|
|
198
|
+
<>
|
|
199
|
+
<div className="grid grid-cols-2 gap-6">
|
|
200
|
+
<div className="col-span-2">
|
|
201
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
|
|
202
|
+
SMTP Host
|
|
203
|
+
</label>
|
|
204
|
+
<input
|
|
205
|
+
type="text"
|
|
206
|
+
value={smtpSettings.host}
|
|
207
|
+
onChange={(e) => setSmtpSettings(prev => ({ ...prev, host: e.target.value }))}
|
|
208
|
+
placeholder="smtp.example.com"
|
|
209
|
+
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"
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div>
|
|
214
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
|
|
215
|
+
Port
|
|
216
|
+
</label>
|
|
217
|
+
<input
|
|
218
|
+
type="number"
|
|
219
|
+
value={smtpSettings.port}
|
|
220
|
+
onChange={(e) => setSmtpSettings(prev => ({ ...prev, port: parseInt(e.target.value) || 465 }))}
|
|
221
|
+
placeholder="465"
|
|
222
|
+
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"
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div>
|
|
227
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
|
|
228
|
+
From Name
|
|
229
|
+
</label>
|
|
230
|
+
<input
|
|
231
|
+
type="text"
|
|
232
|
+
value={smtpSettings.fromName}
|
|
233
|
+
onChange={(e) => setSmtpSettings(prev => ({ ...prev, fromName: e.target.value }))}
|
|
234
|
+
placeholder="My Newsletter"
|
|
235
|
+
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"
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div className="col-span-2">
|
|
240
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
|
|
241
|
+
Username / Email
|
|
242
|
+
</label>
|
|
243
|
+
<input
|
|
244
|
+
type="text"
|
|
245
|
+
value={smtpSettings.user}
|
|
246
|
+
onChange={(e) => setSmtpSettings(prev => ({ ...prev, user: e.target.value }))}
|
|
247
|
+
placeholder="smtp@example.com"
|
|
248
|
+
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"
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div className="col-span-2">
|
|
253
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
|
|
254
|
+
Password / App Password
|
|
255
|
+
</label>
|
|
256
|
+
<div className="relative">
|
|
257
|
+
<input
|
|
258
|
+
type={smtpShowPassword ? 'text' : 'password'}
|
|
259
|
+
value={smtpSettings.password}
|
|
260
|
+
onChange={(e) => setSmtpSettings(prev => ({ ...prev, password: e.target.value }))}
|
|
261
|
+
placeholder="Enter password or app password"
|
|
262
|
+
className="w-full px-4 py-3 pr-12 bg-dashboard-bg border border-dashboard-border rounded-xl outline-none focus:ring-2 focus:ring-primary transition-colors text-dashboard-text"
|
|
263
|
+
/>
|
|
264
|
+
<button
|
|
265
|
+
type="button"
|
|
266
|
+
onClick={() => setSmtpShowPassword(!smtpShowPassword)}
|
|
267
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-dashboard-text-secondary hover:text-dashboard-text"
|
|
268
|
+
>
|
|
269
|
+
{smtpShowPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div className="col-span-2">
|
|
275
|
+
<div className="flex items-center justify-between mb-2">
|
|
276
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest">
|
|
277
|
+
From Email
|
|
278
|
+
</label>
|
|
279
|
+
<button
|
|
280
|
+
type="button"
|
|
281
|
+
onClick={() => {
|
|
282
|
+
if (fromEmailEditable) {
|
|
283
|
+
setFromEmailEditable(false);
|
|
284
|
+
setSmtpSettings(prev => ({ ...prev, from: prev.user }));
|
|
285
|
+
} else {
|
|
286
|
+
setFromEmailEditable(true);
|
|
287
|
+
}
|
|
288
|
+
}}
|
|
289
|
+
className="inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-primary hover:underline"
|
|
290
|
+
>
|
|
291
|
+
{fromEmailEditable ? (
|
|
292
|
+
<>
|
|
293
|
+
<Unlink size={12} />
|
|
294
|
+
Use same as username
|
|
295
|
+
</>
|
|
296
|
+
) : (
|
|
297
|
+
<>
|
|
298
|
+
<Pencil size={12} />
|
|
299
|
+
Edit
|
|
300
|
+
</>
|
|
301
|
+
)}
|
|
302
|
+
</button>
|
|
303
|
+
</div>
|
|
304
|
+
<input
|
|
305
|
+
type="email"
|
|
306
|
+
value={smtpSettings.from || smtpSettings.user}
|
|
307
|
+
onChange={(e) => setSmtpSettings(prev => ({ ...prev, from: e.target.value }))}
|
|
308
|
+
disabled={!fromEmailEditable}
|
|
309
|
+
placeholder="newsletter@example.com"
|
|
310
|
+
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 ${
|
|
311
|
+
!fromEmailEditable ? 'opacity-60 cursor-not-allowed' : ''
|
|
312
|
+
}`}
|
|
313
|
+
/>
|
|
314
|
+
{!fromEmailEditable && smtpSettings.user && (
|
|
315
|
+
<p className="text-[10px] text-dashboard-text-secondary mt-1">
|
|
316
|
+
Using: {smtpSettings.user}
|
|
317
|
+
</p>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{/* Primary Language */}
|
|
322
|
+
<div className="col-span-2">
|
|
323
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
|
|
324
|
+
Primary Language
|
|
325
|
+
</label>
|
|
326
|
+
<p className="text-[10px] text-dashboard-text-secondary mb-2">
|
|
327
|
+
Default language for welcome emails and newsletters
|
|
328
|
+
</p>
|
|
329
|
+
<select
|
|
330
|
+
value={smtpSettings.primaryLanguage || 'en'}
|
|
331
|
+
onChange={(e) => setSmtpSettings(prev => ({ ...prev, primaryLanguage: e.target.value }))}
|
|
332
|
+
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"
|
|
333
|
+
>
|
|
334
|
+
<option value="en">English</option>
|
|
335
|
+
<option value="nl">Dutch</option>
|
|
336
|
+
<option value="sv">Swedish</option>
|
|
337
|
+
</select>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
{/* Logo Upload */}
|
|
341
|
+
<div className="col-span-2">
|
|
342
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
|
|
343
|
+
<span className="flex items-center gap-2">
|
|
344
|
+
<ImageIcon size={14} />
|
|
345
|
+
Email Logo
|
|
346
|
+
</span>
|
|
347
|
+
</label>
|
|
348
|
+
<p className="text-[10px] text-dashboard-text-secondary mb-2">
|
|
349
|
+
Logo to use in email headers (recommended: 200x50px PNG/JPEG)
|
|
350
|
+
</p>
|
|
351
|
+
<div className="border-2 border-dashboard-border border-dashed rounded-xl p-6 bg-dashboard-bg text-center">
|
|
352
|
+
{isLogoUploading ? (
|
|
353
|
+
<div className="flex flex-col items-center gap-2 text-dashboard-text-secondary">
|
|
354
|
+
<RefreshCw size={24} className="animate-spin" />
|
|
355
|
+
<span className="text-xs">Uploading...</span>
|
|
356
|
+
</div>
|
|
357
|
+
) : smtpSettings.logoUrl ? (
|
|
358
|
+
<div className="relative inline-block">
|
|
359
|
+
<img
|
|
360
|
+
src={smtpSettings.logoUrl}
|
|
361
|
+
alt="Logo preview"
|
|
362
|
+
className="max-h-16 mx-auto"
|
|
363
|
+
/>
|
|
364
|
+
<button
|
|
365
|
+
onClick={() => setSmtpSettings(prev => ({ ...prev, logoUrl: '' }))}
|
|
366
|
+
className="absolute -top-2 -right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600"
|
|
367
|
+
>
|
|
368
|
+
<XCircle size={16} />
|
|
369
|
+
</button>
|
|
370
|
+
</div>
|
|
371
|
+
) : (
|
|
372
|
+
<label className="cursor-pointer block">
|
|
373
|
+
<input
|
|
374
|
+
type="file"
|
|
375
|
+
accept="image/png,image/jpeg"
|
|
376
|
+
onChange={handleLogoUpload}
|
|
377
|
+
className="hidden"
|
|
378
|
+
/>
|
|
379
|
+
<div className="flex flex-col items-center gap-2 text-dashboard-text-secondary hover:text-dashboard-text">
|
|
380
|
+
<Upload size={24} />
|
|
381
|
+
<span className="text-xs">Click to upload logo</span>
|
|
382
|
+
</div>
|
|
383
|
+
</label>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
</>
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
{/* Footer */}
|
|
393
|
+
<div className="flex items-center justify-end gap-3 px-8 py-6 border-t border-dashboard-border bg-dashboard-bg/50">
|
|
394
|
+
<button
|
|
395
|
+
onClick={onClose}
|
|
396
|
+
disabled={smtpSaving}
|
|
397
|
+
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"
|
|
398
|
+
>
|
|
399
|
+
Cancel
|
|
400
|
+
</button>
|
|
401
|
+
<button
|
|
402
|
+
onClick={handleSave}
|
|
403
|
+
disabled={smtpSaving || smtpLoading}
|
|
404
|
+
className={`inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg ${
|
|
405
|
+
smtpSaving
|
|
406
|
+
? 'bg-neutral-400 text-white cursor-not-allowed'
|
|
407
|
+
: smtpSuccess
|
|
408
|
+
? 'bg-green-600 text-white'
|
|
409
|
+
: 'bg-primary text-white hover:bg-primary/90'
|
|
410
|
+
}`}
|
|
411
|
+
>
|
|
412
|
+
{smtpSaving ? (
|
|
413
|
+
<>
|
|
414
|
+
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
415
|
+
Saving...
|
|
416
|
+
</>
|
|
417
|
+
) : smtpSuccess ? (
|
|
418
|
+
<>
|
|
419
|
+
<CheckCircle2 className="w-4 h-4" />
|
|
420
|
+
Saved!
|
|
421
|
+
</>
|
|
422
|
+
) : (
|
|
423
|
+
<>
|
|
424
|
+
<Save className="w-4 h-4" />
|
|
425
|
+
Save Settings
|
|
426
|
+
</>
|
|
427
|
+
)}
|
|
428
|
+
</button>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}
|