@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,322 @@
1
+ /**
2
+ * Send Newsletter Modal Component
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import React, { useState, useEffect } from 'react';
8
+ import { X, Send, RefreshCw, CheckCircle2, AlertCircle, Mail, Globe, Users, Radio } from 'lucide-react';
9
+
10
+ interface SendNewsletterModalProps {
11
+ isOpen: boolean;
12
+ onClose: () => void;
13
+ newsletter: {
14
+ id: string;
15
+ title: string;
16
+ subject: string;
17
+ status: string;
18
+ hasContent: boolean;
19
+ };
20
+ subscriberCount: number;
21
+ }
22
+
23
+ type Language = 'en' | 'nl' | 'sv';
24
+ type SendMode = 'subscribers' | 'test';
25
+
26
+ const languages: { code: Language; label: string }[] = [
27
+ { code: 'en', label: 'EN' },
28
+ { code: 'nl', label: 'NL' },
29
+ { code: 'sv', label: 'SV' },
30
+ ];
31
+
32
+ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCount }: SendNewsletterModalProps) {
33
+ const [language, setLanguage] = useState<Language>('en');
34
+ const [sendMode, setSendMode] = useState<SendMode>('subscribers');
35
+ const [testEmail, setTestEmail] = useState('');
36
+ const [isSending, setIsSending] = useState(false);
37
+ const [sendSuccess, setSendSuccess] = useState(false);
38
+ const [sendError, setSendError] = useState<string | null>(null);
39
+ const [resultDetails, setResultDetails] = useState<{ successCount: number; failedCount: number } | null>(null);
40
+
41
+ useEffect(() => {
42
+ if (isOpen) {
43
+ setSendMode('subscribers');
44
+ setSendError(null);
45
+ setResultDetails(null);
46
+ setSendSuccess(false);
47
+ }
48
+ }, [isOpen]);
49
+
50
+ const handleSend = async () => {
51
+ if (sendMode === 'test' && !testEmail.trim()) {
52
+ setSendError('Please enter an email address');
53
+ return;
54
+ }
55
+
56
+ if (sendMode === 'test') {
57
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
58
+ if (!emailRegex.test(testEmail)) {
59
+ setSendError('Please enter a valid email address');
60
+ return;
61
+ }
62
+ }
63
+
64
+ try {
65
+ setIsSending(true);
66
+ setSendError(null);
67
+
68
+ const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletter.id}/send`, {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ credentials: 'include',
72
+ body: JSON.stringify({
73
+ language,
74
+ testEmail: sendMode === 'test' ? testEmail : undefined
75
+ }),
76
+ });
77
+
78
+ const data = await response.json();
79
+
80
+ if (!response.ok) {
81
+ throw new Error(data.error || 'Failed to send newsletter');
82
+ }
83
+
84
+ setResultDetails(data.details);
85
+ setSendSuccess(true);
86
+
87
+ if (sendMode === 'subscribers') {
88
+ setTimeout(() => {
89
+ onClose();
90
+ window.location.reload();
91
+ }, 2000);
92
+ } else {
93
+ setTimeout(() => {
94
+ setSendSuccess(false);
95
+ setTestEmail('');
96
+ }, 3000);
97
+ }
98
+ } catch (error: any) {
99
+ console.error('Failed to send newsletter:', error);
100
+ setSendError(error.message || 'Failed to send newsletter');
101
+ } finally {
102
+ setIsSending(false);
103
+ }
104
+ };
105
+
106
+ if (!isOpen) return null;
107
+
108
+ const canSend = newsletter.hasContent && (sendMode === 'test' || subscriberCount > 0);
109
+ const isAlreadySent = newsletter.status === 'sent';
110
+ const noSubscribersError = sendMode === 'subscribers' && subscriberCount === 0;
111
+
112
+ 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
+ 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-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800">
150
+ <div className="flex items-center gap-2 text-amber-700 dark:text-amber-400">
151
+ <AlertCircle size={16} />
152
+ <span className="text-xs font-bold uppercase">This newsletter has already been sent</span>
153
+ </div>
154
+ </div>
155
+ )}
156
+
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>
162
+ </div>
163
+ </div>
164
+ )}
165
+
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>
200
+ </div>
201
+ </div>
202
+
203
+ {sendMode === 'test' && (
204
+ <div>
205
+ <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2">
206
+ Test Email Address
207
+ </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) => (
229
+ <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
234
+ ? 'bg-primary text-white'
235
+ : 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
236
+ }`}
237
+ >
238
+ {lang.label}
239
+ </button>
240
+ ))}
241
+ </div>
242
+ </div>
243
+
244
+ {sendError && (
245
+ <div className="flex items-center gap-2 text-red-500 text-sm">
246
+ <AlertCircle size={16} />
247
+ {sendError}
248
+ </div>
249
+ )}
250
+
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.
255
+ </div>
256
+ )}
257
+
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
+ )}
267
+
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
+ )}
276
+ </div>
277
+ </div>
278
+
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>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ );
322
+ }