@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.
Files changed (57) hide show
  1. package/package.json +3 -2
  2. package/src/api/email-utils.ts +165 -0
  3. package/src/api/handler.ts +28 -0
  4. package/src/api/handlers/index.ts +44 -0
  5. package/src/api/handlers/newsletters.ts +332 -0
  6. package/src/api/handlers/send-newsletter.ts +288 -0
  7. package/src/api/handlers/settings.ts +403 -0
  8. package/src/api/handlers/subscribers.ts +152 -0
  9. package/src/api/handlers/upload.ts +47 -0
  10. package/src/api/handlers/welcome-email.ts +210 -0
  11. package/src/api/router.ts +166 -0
  12. package/src/index.server.ts +12 -0
  13. package/src/index.tsx +353 -0
  14. package/src/index.tsx.patch +98 -0
  15. package/src/init.tsx +72 -0
  16. package/src/lib/blocks/BlockRenderer.tsx +125 -0
  17. package/src/lib/email/EmailRenderer.tsx +420 -0
  18. package/src/lib/email/index.ts +6 -0
  19. package/src/lib/i18n.ts +82 -0
  20. package/src/lib/mappers/apiMapper.ts +57 -0
  21. package/src/lib/utils/blockHelpers.ts +71 -0
  22. package/src/lib/utils/slugify.ts +43 -0
  23. package/src/registry/BlockRegistry.ts +53 -0
  24. package/src/registry/index.ts +5 -0
  25. package/src/state/EditorContext.tsx +278 -0
  26. package/src/state/index.ts +10 -0
  27. package/src/state/reducer.ts +561 -0
  28. package/src/state/types.ts +154 -0
  29. package/src/types/block.ts +275 -0
  30. package/src/types/newsletter.ts +152 -0
  31. package/src/types/registry.ts +14 -0
  32. package/src/views/CanvasEditor/BlockWrapper.tsx +143 -0
  33. package/src/views/CanvasEditor/CanvasEditorView.tsx +343 -0
  34. package/src/views/CanvasEditor/EditorBody.tsx +95 -0
  35. package/src/views/CanvasEditor/EditorHeader.tsx +255 -0
  36. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +83 -0
  37. package/src/views/CanvasEditor/components/EditorCanvas.tsx +674 -0
  38. package/src/views/CanvasEditor/components/EditorLibrary.tsx +120 -0
  39. package/src/views/CanvasEditor/components/EditorSidebar.tsx +139 -0
  40. package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
  41. package/src/views/CanvasEditor/components/LibraryItem.tsx +71 -0
  42. package/src/views/CanvasEditor/components/SlashCommandDetector.tsx +196 -0
  43. package/src/views/CanvasEditor/components/SlashCommandMenu.tsx +131 -0
  44. package/src/views/CanvasEditor/components/index.ts +16 -0
  45. package/src/views/CanvasEditor/hooks/index.ts +7 -0
  46. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +136 -0
  47. package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +73 -0
  48. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +54 -0
  49. package/src/views/CanvasEditor/hooks/useSlashCommand.ts +106 -0
  50. package/src/views/CanvasEditor/index.ts +12 -0
  51. package/src/views/NewsletterEditor.tsx +42 -0
  52. package/src/views/NewsletterManager.tsx +483 -0
  53. package/src/views/SettingsView.tsx +216 -0
  54. package/src/views/SubscribersView.tsx +269 -0
  55. package/src/views/components/SendNewsletterModal.tsx +322 -0
  56. package/src/views/components/SmtpSettingsModal.tsx +433 -0
  57. 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
+ }