@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,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Manager View
|
|
3
|
+
* List and manage newsletters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useState, useEffect } from 'react';
|
|
9
|
+
import { Plus, Mail, Calendar, Trash2, Edit2, Settings2, Sparkles, CheckCircle2, Clock, Send, Users } from 'lucide-react';
|
|
10
|
+
import { NewsletterListItem, NewsletterStatus } from '../types/newsletter';
|
|
11
|
+
import { SmtpSettingsModal } from './components/SmtpSettingsModal';
|
|
12
|
+
import { TestEmailModal } from './components/TestEmailModal';
|
|
13
|
+
import { SendNewsletterModal } from './components/SendNewsletterModal';
|
|
14
|
+
|
|
15
|
+
export interface NewsletterManagerViewProps {
|
|
16
|
+
siteId: string;
|
|
17
|
+
locale: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getStatusBadgeColor(status: NewsletterStatus) {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case 'sent':
|
|
23
|
+
return 'bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20';
|
|
24
|
+
case 'scheduled':
|
|
25
|
+
return 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20';
|
|
26
|
+
case 'draft':
|
|
27
|
+
return 'bg-neutral-500/10 text-dashboard-text-secondary border-neutral-500/20';
|
|
28
|
+
case 'archived':
|
|
29
|
+
return 'bg-gray-500/10 text-gray-600 dark:text-gray-400 border-gray-500/20';
|
|
30
|
+
default:
|
|
31
|
+
return 'bg-neutral-500/10 text-dashboard-text-secondary border-neutral-500/20';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewProps) {
|
|
36
|
+
const [newsletters, setNewsletters] = useState<NewsletterListItem[]>([]);
|
|
37
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
38
|
+
const [statusFilter, setStatusFilter] = useState<NewsletterStatus | 'all'>('all');
|
|
39
|
+
const [showSmtpModal, setShowSmtpModal] = useState(false);
|
|
40
|
+
const [showTestEmailModal, setShowTestEmailModal] = useState(false);
|
|
41
|
+
const [showSendModal, setShowSendModal] = useState(false);
|
|
42
|
+
const [selectedNewsletter, setSelectedNewsletter] = useState<(NewsletterListItem & { hasContent: boolean }) | null>(null);
|
|
43
|
+
const [smtpStatus, setSmtpStatus] = useState<'configured' | 'not_configured' | 'loading'>('loading');
|
|
44
|
+
|
|
45
|
+
// Welcome Email Status
|
|
46
|
+
const [welcomeEmailStatus, setWelcomeEmailStatus] = useState<'configured' | 'not_configured'>('not_configured');
|
|
47
|
+
const [welcomeEmailLastUpdated, setWelcomeEmailLastUpdated] = useState<string | null>(null);
|
|
48
|
+
const [subscriberCount, setSubscriberCount] = useState(0);
|
|
49
|
+
|
|
50
|
+
// Fetch newsletters
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const fetchNewsletters = async () => {
|
|
53
|
+
try {
|
|
54
|
+
setIsLoading(true);
|
|
55
|
+
const response = await fetch('/api/plugin-newsletter/newsletters', {
|
|
56
|
+
credentials: 'include',
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error('Failed to fetch newsletters');
|
|
60
|
+
}
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
setNewsletters(Array.isArray(data) ? data : []);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Failed to load newsletters:', error);
|
|
65
|
+
} finally {
|
|
66
|
+
setIsLoading(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
fetchNewsletters();
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
// Fetch welcome email status
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const fetchWelcomeEmailStatus = async () => {
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch('/api/plugin-newsletter/welcome-email/status', {
|
|
77
|
+
credentials: 'include',
|
|
78
|
+
});
|
|
79
|
+
if (response.ok) {
|
|
80
|
+
const data = await response.json();
|
|
81
|
+
setWelcomeEmailStatus(data.configured ? 'configured' : 'not_configured');
|
|
82
|
+
setWelcomeEmailLastUpdated(data.lastUpdated || null);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Failed to load welcome email status:', error);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
fetchWelcomeEmailStatus();
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
// Fetch SMTP status
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const fetchSmtpStatus = async () => {
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch('/api/plugin-newsletter/smtp', {
|
|
96
|
+
credentials: 'include',
|
|
97
|
+
});
|
|
98
|
+
if (response.ok) {
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
setSmtpStatus(data.host ? 'configured' : 'not_configured');
|
|
101
|
+
} else {
|
|
102
|
+
setSmtpStatus('not_configured');
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Failed to load SMTP status:', error);
|
|
106
|
+
setSmtpStatus('not_configured');
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
fetchSmtpStatus();
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
// Fetch subscriber count
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const fetchSubscriberCount = async () => {
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch('/api/plugin-newsletter/subscribers', {
|
|
117
|
+
credentials: 'include',
|
|
118
|
+
});
|
|
119
|
+
if (response.ok) {
|
|
120
|
+
const data = await response.json();
|
|
121
|
+
setSubscriberCount(Array.isArray(data) ? data.length : 0);
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Failed to load subscriber count:', error);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
fetchSubscriberCount();
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
// Filter newsletters
|
|
131
|
+
const filteredNewsletters = statusFilter === 'all'
|
|
132
|
+
? newsletters
|
|
133
|
+
: newsletters.filter(n => n.status === statusFilter);
|
|
134
|
+
|
|
135
|
+
// Handle create new newsletter
|
|
136
|
+
const handleCreate = () => {
|
|
137
|
+
window.location.href = '/dashboard/newsletter/new';
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Handle edit newsletter
|
|
141
|
+
const handleEdit = (id: string | undefined) => {
|
|
142
|
+
if (!id) {
|
|
143
|
+
alert('Cannot edit: newsletter id is missing');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
window.location.href = `/dashboard/newsletter/editor/${id}`;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Handle send newsletter
|
|
150
|
+
const handleSend = async (newsletter: NewsletterListItem) => {
|
|
151
|
+
if (smtpStatus === 'not_configured') {
|
|
152
|
+
alert('Please configure SMTP settings first');
|
|
153
|
+
setShowSmtpModal(true);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (newsletter.status === 'sent') {
|
|
157
|
+
alert('This newsletter has already been sent');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletter.id}/send`, {
|
|
163
|
+
credentials: 'include',
|
|
164
|
+
});
|
|
165
|
+
const data = await response.json();
|
|
166
|
+
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
alert(data.error || 'Failed to load newsletter details');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
setSelectedNewsletter({
|
|
173
|
+
...newsletter,
|
|
174
|
+
hasContent: data.hasContent,
|
|
175
|
+
} as NewsletterListItem & { hasContent: boolean });
|
|
176
|
+
setShowSendModal(true);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error('Failed to load newsletter:', error);
|
|
179
|
+
alert('Failed to load newsletter details');
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Handle delete newsletter
|
|
184
|
+
const handleDelete = async (id: string | undefined, title: string) => {
|
|
185
|
+
if (!id) {
|
|
186
|
+
alert('Cannot delete: newsletter id is missing');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!confirm(`Are you sure you want to delete "${title}"?`)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch(`/api/plugin-newsletter/newsletters/${id}`, {
|
|
196
|
+
method: 'DELETE',
|
|
197
|
+
credentials: 'include',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
throw new Error('Failed to delete newsletter');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Remove from local state
|
|
205
|
+
setNewsletters(prev => prev.filter(n => n.id !== id));
|
|
206
|
+
} catch (error: any) {
|
|
207
|
+
console.error('Failed to delete newsletter:', error);
|
|
208
|
+
alert(error.message || 'Failed to delete newsletter');
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Format date
|
|
213
|
+
const formatDate = (dateString: string | undefined) => {
|
|
214
|
+
if (!dateString) return 'N/A';
|
|
215
|
+
const date = new Date(dateString);
|
|
216
|
+
return date.toLocaleDateString(locale, {
|
|
217
|
+
day: 'numeric',
|
|
218
|
+
month: 'short',
|
|
219
|
+
year: 'numeric',
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<>
|
|
225
|
+
<div className="h-full w-full rounded-[2.5rem] bg-white dark:bg-neutral-900 p-8 overflow-y-auto">
|
|
226
|
+
<div className="max-w-7xl mx-auto">
|
|
227
|
+
{/* Header */}
|
|
228
|
+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
|
229
|
+
<div>
|
|
230
|
+
<h1 className="text-3xl font-black text-dashboard-text uppercase tracking-tighter mb-2">
|
|
231
|
+
Newsletters
|
|
232
|
+
</h1>
|
|
233
|
+
<p className="text-sm text-dashboard-text-secondary">
|
|
234
|
+
Create and manage your email newsletters
|
|
235
|
+
</p>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div className="flex items-center gap-3">
|
|
239
|
+
{/* Subscribers Button */}
|
|
240
|
+
<button
|
|
241
|
+
onClick={() => window.location.href = '/dashboard/newsletter/subscribers'}
|
|
242
|
+
className="inline-flex items-center gap-2 px-4 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"
|
|
243
|
+
>
|
|
244
|
+
<Users size={14} />
|
|
245
|
+
Subscribers
|
|
246
|
+
</button>
|
|
247
|
+
|
|
248
|
+
{/* Test Email Button */}
|
|
249
|
+
<button
|
|
250
|
+
onClick={() => {
|
|
251
|
+
if (smtpStatus === 'not_configured') {
|
|
252
|
+
alert('Please configure SMTP settings first');
|
|
253
|
+
setShowSmtpModal(true);
|
|
254
|
+
} else {
|
|
255
|
+
setShowTestEmailModal(true);
|
|
256
|
+
}
|
|
257
|
+
}}
|
|
258
|
+
className="inline-flex items-center gap-2 px-4 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"
|
|
259
|
+
>
|
|
260
|
+
<Send size={14} />
|
|
261
|
+
Test Email
|
|
262
|
+
</button>
|
|
263
|
+
|
|
264
|
+
{/* SMTP Settings Button */}
|
|
265
|
+
<button
|
|
266
|
+
onClick={() => setShowSmtpModal(true)}
|
|
267
|
+
className={`inline-flex items-center gap-2 px-4 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors ${
|
|
268
|
+
smtpStatus === 'not_configured'
|
|
269
|
+
? 'bg-amber-50 border-amber-300 text-amber-700 dark:bg-amber-900/20 dark:border-amber-700 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/40'
|
|
270
|
+
: 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
|
|
271
|
+
}`}
|
|
272
|
+
>
|
|
273
|
+
<Settings2 size={14} />
|
|
274
|
+
SMTP
|
|
275
|
+
{smtpStatus === 'not_configured' && (
|
|
276
|
+
<span className="w-2 h-2 rounded-full bg-amber-500" />
|
|
277
|
+
)}
|
|
278
|
+
</button>
|
|
279
|
+
|
|
280
|
+
{/* Status Filter */}
|
|
281
|
+
<select
|
|
282
|
+
value={statusFilter}
|
|
283
|
+
onChange={(e) => setStatusFilter(e.target.value as NewsletterStatus | 'all')}
|
|
284
|
+
className="bg-dashboard-bg border border-dashboard-border rounded-xl px-4 py-2.5 text-xs font-bold text-dashboard-text outline-none cursor-pointer uppercase tracking-widest"
|
|
285
|
+
>
|
|
286
|
+
<option value="all">All Status</option>
|
|
287
|
+
<option value="draft">Draft</option>
|
|
288
|
+
<option value="scheduled">Scheduled</option>
|
|
289
|
+
<option value="sent">Sent</option>
|
|
290
|
+
<option value="archived">Archived</option>
|
|
291
|
+
</select>
|
|
292
|
+
|
|
293
|
+
{/* Create Button */}
|
|
294
|
+
<button
|
|
295
|
+
onClick={handleCreate}
|
|
296
|
+
className="inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90"
|
|
297
|
+
>
|
|
298
|
+
<Plus size={14} />
|
|
299
|
+
New Newsletter
|
|
300
|
+
</button>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{/* Welcome Email Card */}
|
|
305
|
+
<div className="mb-6 p-6 rounded-2xl bg-gradient-to-br from-primary/5 via-primary/10 to-transparent border border-dashboard-border">
|
|
306
|
+
<div className="flex items-center justify-between">
|
|
307
|
+
<div className="flex items-center gap-5">
|
|
308
|
+
<div className="w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center">
|
|
309
|
+
<Sparkles className="w-6 h-6 text-primary" />
|
|
310
|
+
</div>
|
|
311
|
+
<div>
|
|
312
|
+
<h3 className="text-sm font-black text-dashboard-text uppercase tracking-tight mb-1">
|
|
313
|
+
Welcome Email
|
|
314
|
+
</h3>
|
|
315
|
+
<p className="text-xs text-dashboard-text-secondary mb-2">
|
|
316
|
+
The email sent automatically when someone subscribes
|
|
317
|
+
</p>
|
|
318
|
+
<div className="flex items-center gap-3 flex-wrap">
|
|
319
|
+
{welcomeEmailStatus === 'configured' ? (
|
|
320
|
+
<span className="inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-green-600 dark:text-green-400">
|
|
321
|
+
<CheckCircle2 size={12} />
|
|
322
|
+
Configured
|
|
323
|
+
</span>
|
|
324
|
+
) : (
|
|
325
|
+
<span className="inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-amber-600 dark:text-amber-400">
|
|
326
|
+
<Clock size={12} />
|
|
327
|
+
Not configured
|
|
328
|
+
</span>
|
|
329
|
+
)}
|
|
330
|
+
<button
|
|
331
|
+
onClick={() => window.location.href = '/dashboard/newsletter/subscribers'}
|
|
332
|
+
className="inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-primary hover:underline"
|
|
333
|
+
>
|
|
334
|
+
<Users size={12} />
|
|
335
|
+
{subscriberCount} subscriber{subscriberCount !== 1 ? 's' : ''}
|
|
336
|
+
</button>
|
|
337
|
+
{welcomeEmailLastUpdated && (
|
|
338
|
+
<span className="text-[10px] text-dashboard-text-secondary">
|
|
339
|
+
Last updated: {new Date(welcomeEmailLastUpdated).toLocaleDateString()}
|
|
340
|
+
</span>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
<button
|
|
346
|
+
onClick={() => window.location.href = '/dashboard/newsletter/welcome'}
|
|
347
|
+
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors bg-primary text-white hover:bg-primary/90"
|
|
348
|
+
>
|
|
349
|
+
<Edit2 size={14} />
|
|
350
|
+
{welcomeEmailStatus === 'configured' ? 'Edit' : 'Configure'}
|
|
351
|
+
</button>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
{/* Newsletters Table */}
|
|
356
|
+
<div className="bg-dashboard-bg rounded-3xl border border-dashboard-border overflow-hidden">
|
|
357
|
+
{isLoading ? (
|
|
358
|
+
<div className="flex items-center justify-center py-20">
|
|
359
|
+
<div className="w-8 h-8 border-4 border-primary/20 border-t-primary rounded-full animate-spin" />
|
|
360
|
+
</div>
|
|
361
|
+
) : filteredNewsletters.length === 0 ? (
|
|
362
|
+
<div className="py-24 text-center">
|
|
363
|
+
<Mail size={64} className="mx-auto text-dashboard-text-secondary mb-4" />
|
|
364
|
+
<p className="text-dashboard-text-secondary font-serif italic text-lg mb-6">
|
|
365
|
+
{statusFilter === 'all' ? 'No newsletters yet.' : `No newsletters found with status "${statusFilter}".`}
|
|
366
|
+
</p>
|
|
367
|
+
<button
|
|
368
|
+
onClick={handleCreate}
|
|
369
|
+
className="inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90"
|
|
370
|
+
>
|
|
371
|
+
<Plus size={14} />
|
|
372
|
+
Create Your First Newsletter
|
|
373
|
+
</button>
|
|
374
|
+
</div>
|
|
375
|
+
) : (
|
|
376
|
+
<div className="overflow-x-auto">
|
|
377
|
+
<table className="w-full text-left border-collapse">
|
|
378
|
+
<thead>
|
|
379
|
+
<tr className="bg-dashboard-bg text-dashboard-text text-[10px] uppercase tracking-[0.2em] font-black border-b border-dashboard-border">
|
|
380
|
+
<th className="px-8 py-5">Title</th>
|
|
381
|
+
<th className="px-8 py-5">Subject</th>
|
|
382
|
+
<th className="px-8 py-5">Status</th>
|
|
383
|
+
<th className="px-8 py-5 text-right">Updated</th>
|
|
384
|
+
<th className="px-8 py-5 text-right">Actions</th>
|
|
385
|
+
</tr>
|
|
386
|
+
</thead>
|
|
387
|
+
<tbody className="divide-y divide-dashboard-border">
|
|
388
|
+
{filteredNewsletters.map((newsletter) => (
|
|
389
|
+
<tr
|
|
390
|
+
key={newsletter.id}
|
|
391
|
+
className="hover:bg-dashboard-bg transition-colors group"
|
|
392
|
+
>
|
|
393
|
+
<td className="px-8 py-5">
|
|
394
|
+
<div className="flex items-center gap-4">
|
|
395
|
+
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover:bg-primary group-hover:text-white transition-colors">
|
|
396
|
+
<Mail size={18} />
|
|
397
|
+
</div>
|
|
398
|
+
<span className="text-sm font-medium text-dashboard-text tracking-tight">
|
|
399
|
+
{newsletter.title}
|
|
400
|
+
</span>
|
|
401
|
+
</div>
|
|
402
|
+
</td>
|
|
403
|
+
<td className="px-8 py-5">
|
|
404
|
+
<span className="text-sm text-dashboard-text-secondary">
|
|
405
|
+
{newsletter.subject || 'No subject'}
|
|
406
|
+
</span>
|
|
407
|
+
</td>
|
|
408
|
+
<td className="px-8 py-5">
|
|
409
|
+
<span className={`text-[10px] font-black px-3 py-1 rounded-full uppercase border ${getStatusBadgeColor(newsletter.status)}`}>
|
|
410
|
+
{newsletter.status}
|
|
411
|
+
</span>
|
|
412
|
+
</td>
|
|
413
|
+
<td className="px-8 py-5 text-right text-xs text-dashboard-text-secondary font-medium">
|
|
414
|
+
<div className="flex items-center justify-end gap-2">
|
|
415
|
+
<Calendar size={14} />
|
|
416
|
+
{formatDate(newsletter.updatedAt)}
|
|
417
|
+
</div>
|
|
418
|
+
</td>
|
|
419
|
+
<td className="px-8 py-5 text-right">
|
|
420
|
+
<div className="flex items-center justify-end gap-2">
|
|
421
|
+
{newsletter.status !== 'sent' && (
|
|
422
|
+
<button
|
|
423
|
+
onClick={() => handleSend(newsletter)}
|
|
424
|
+
className="p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors"
|
|
425
|
+
title="Send newsletter"
|
|
426
|
+
>
|
|
427
|
+
<Send size={18} />
|
|
428
|
+
</button>
|
|
429
|
+
)}
|
|
430
|
+
<button
|
|
431
|
+
onClick={() => handleEdit(newsletter.id)}
|
|
432
|
+
className="p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors"
|
|
433
|
+
title="Edit newsletter"
|
|
434
|
+
>
|
|
435
|
+
<Edit2 size={18} />
|
|
436
|
+
</button>
|
|
437
|
+
<button
|
|
438
|
+
onClick={() => handleDelete(newsletter.id, newsletter.title)}
|
|
439
|
+
className="p-2.5 rounded-full text-dashboard-text-secondary hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
440
|
+
title="Delete newsletter"
|
|
441
|
+
>
|
|
442
|
+
<Trash2 size={18} />
|
|
443
|
+
</button>
|
|
444
|
+
</div>
|
|
445
|
+
</td>
|
|
446
|
+
</tr>
|
|
447
|
+
))}
|
|
448
|
+
</tbody>
|
|
449
|
+
</table>
|
|
450
|
+
</div>
|
|
451
|
+
)}
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<SmtpSettingsModal
|
|
457
|
+
isOpen={showSmtpModal}
|
|
458
|
+
onClose={() => setShowSmtpModal(false)}
|
|
459
|
+
/>
|
|
460
|
+
<TestEmailModal
|
|
461
|
+
isOpen={showTestEmailModal}
|
|
462
|
+
onClose={() => setShowTestEmailModal(false)}
|
|
463
|
+
/>
|
|
464
|
+
{selectedNewsletter && (
|
|
465
|
+
<SendNewsletterModal
|
|
466
|
+
isOpen={showSendModal}
|
|
467
|
+
onClose={() => {
|
|
468
|
+
setShowSendModal(false);
|
|
469
|
+
setSelectedNewsletter(null);
|
|
470
|
+
}}
|
|
471
|
+
newsletter={{
|
|
472
|
+
id: selectedNewsletter.id,
|
|
473
|
+
title: selectedNewsletter.title,
|
|
474
|
+
subject: selectedNewsletter.subject,
|
|
475
|
+
status: selectedNewsletter.status,
|
|
476
|
+
hasContent: selectedNewsletter.hasContent,
|
|
477
|
+
}}
|
|
478
|
+
subscriberCount={subscriberCount}
|
|
479
|
+
/>
|
|
480
|
+
)}
|
|
481
|
+
</>
|
|
482
|
+
);
|
|
483
|
+
}
|