@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,674 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useRef, useEffect, useState } from 'react';
|
|
4
|
+
import { BlockWrapper } from '../BlockWrapper';
|
|
5
|
+
import { EditorBody } from '../EditorBody';
|
|
6
|
+
import { BlockRenderer } from '../../../lib/blocks/BlockRenderer';
|
|
7
|
+
import { renderBlocksToEmail, generateNewsletterEmailHtml } from '../../../lib/email/EmailRenderer';
|
|
8
|
+
import type { Block } from '../../../types/block';
|
|
9
|
+
import type { NewsletterMetadata } from '../../../types/newsletter';
|
|
10
|
+
import type { useSlashCommand } from '../hooks/useSlashCommand';
|
|
11
|
+
|
|
12
|
+
// Add email-like styling for editor blocks
|
|
13
|
+
const emailEditorStyles = `
|
|
14
|
+
.email-editor-content p,
|
|
15
|
+
.email-editor-content .text-md {
|
|
16
|
+
font-size: 15px !important;
|
|
17
|
+
line-height: 1.8 !important;
|
|
18
|
+
color: #1a2e26 !important;
|
|
19
|
+
margin: 0 0 15px 0 !important;
|
|
20
|
+
font-family: 'Georgia', serif !important;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.email-editor-content h1,
|
|
24
|
+
.email-editor-content h2,
|
|
25
|
+
.email-editor-content h3,
|
|
26
|
+
.email-editor-content h4,
|
|
27
|
+
.email-editor-content h5,
|
|
28
|
+
.email-editor-content h6 {
|
|
29
|
+
font-family: 'Georgia', serif !important;
|
|
30
|
+
font-weight: bold !important;
|
|
31
|
+
color: #1a2e26 !important;
|
|
32
|
+
margin: 20px 0 10px 0 !important;
|
|
33
|
+
line-height: 1.3 !important;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.email-editor-content h1 {
|
|
37
|
+
font-size: 32px !important;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.email-editor-content h2 {
|
|
41
|
+
font-size: 24px !important;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.email-editor-content h3 {
|
|
45
|
+
font-size: 20px !important;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.email-editor-content h4,
|
|
49
|
+
.email-editor-content h5,
|
|
50
|
+
.email-editor-content h6 {
|
|
51
|
+
font-size: 18px !important;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.email-editor-content ul,
|
|
55
|
+
.email-editor-content ol {
|
|
56
|
+
margin: 15px 0 !important;
|
|
57
|
+
padding-left: 25px !important;
|
|
58
|
+
color: #1a2e26 !important;
|
|
59
|
+
font-size: 15px !important;
|
|
60
|
+
line-height: 1.8 !important;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.email-editor-content li {
|
|
64
|
+
margin: 8px 0 !important;
|
|
65
|
+
padding-left: 5px !important;
|
|
66
|
+
line-height: 1.8 !important;
|
|
67
|
+
color: #1a2e26 !important;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.email-editor-content img {
|
|
71
|
+
max-width: 100% !important;
|
|
72
|
+
height: auto !important;
|
|
73
|
+
display: block !important;
|
|
74
|
+
margin: 20px 0 !important;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.email-block-wrapper {
|
|
78
|
+
font-family: 'Georgia', serif;
|
|
79
|
+
color: #1a2e26;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Ensure all blocks are visible */
|
|
83
|
+
.email-block-content > * {
|
|
84
|
+
display: block !important;
|
|
85
|
+
visibility: visible !important;
|
|
86
|
+
opacity: 1 !important;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Heading input styling to match preview */
|
|
90
|
+
.email-block-content input[type="text"] {
|
|
91
|
+
font-family: 'Georgia', serif !important;
|
|
92
|
+
color: #1a2e26 !important;
|
|
93
|
+
background: transparent !important;
|
|
94
|
+
border: none !important;
|
|
95
|
+
outline: none !important;
|
|
96
|
+
width: 100% !important;
|
|
97
|
+
padding: 0 !important;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.email-block-content input[type="text"]::placeholder {
|
|
101
|
+
color: #a1a1aa !important;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Table block styling */
|
|
105
|
+
.email-block-content table {
|
|
106
|
+
width: 100% !important;
|
|
107
|
+
border-collapse: collapse !important;
|
|
108
|
+
margin: 20px 0 !important;
|
|
109
|
+
display: table !important;
|
|
110
|
+
visibility: visible !important;
|
|
111
|
+
opacity: 1 !important;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.email-block-content table th,
|
|
115
|
+
.email-block-content table td {
|
|
116
|
+
padding: 12px !important;
|
|
117
|
+
border: 1px solid #e5e7eb !important;
|
|
118
|
+
text-align: left !important;
|
|
119
|
+
color: #1a2e26 !important;
|
|
120
|
+
font-size: 15px !important;
|
|
121
|
+
line-height: 1.8 !important;
|
|
122
|
+
background-color: transparent !important;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.email-block-content table th {
|
|
126
|
+
background-color: #f9fafb !important;
|
|
127
|
+
font-weight: bold !important;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Table container styling */
|
|
131
|
+
.email-block-content div[class*="overflow-hidden"],
|
|
132
|
+
.email-block-content div[class*="rounded"] {
|
|
133
|
+
display: block !important;
|
|
134
|
+
visibility: visible !important;
|
|
135
|
+
opacity: 1 !important;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* List block styling */
|
|
139
|
+
.email-block-content ul,
|
|
140
|
+
.email-block-content ol {
|
|
141
|
+
display: block !important;
|
|
142
|
+
margin: 15px 0 !important;
|
|
143
|
+
padding-left: 25px !important;
|
|
144
|
+
visibility: visible !important;
|
|
145
|
+
opacity: 1 !important;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.email-block-content li {
|
|
149
|
+
display: list-item !important;
|
|
150
|
+
margin: 8px 0 !important;
|
|
151
|
+
visibility: visible !important;
|
|
152
|
+
opacity: 1 !important;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* List container styling */
|
|
156
|
+
.email-block-content div[class*="bg-white"][class*="rounded"] {
|
|
157
|
+
display: block !important;
|
|
158
|
+
visibility: visible !important;
|
|
159
|
+
opacity: 1 !important;
|
|
160
|
+
background-color: #ffffff !important;
|
|
161
|
+
border: 1px solid #e5e7eb !important;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Recipe block styling */
|
|
165
|
+
.email-block-content div[class*="recipe"],
|
|
166
|
+
.email-block-content div[class*="Recipe"] {
|
|
167
|
+
display: block !important;
|
|
168
|
+
visibility: visible !important;
|
|
169
|
+
opacity: 1 !important;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Ensure all container divs are visible */
|
|
173
|
+
.email-block-content > div {
|
|
174
|
+
display: block !important;
|
|
175
|
+
visibility: visible !important;
|
|
176
|
+
opacity: 1 !important;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.email-block-content div[class*="bg-white"],
|
|
180
|
+
.email-block-content div[class*="border"] {
|
|
181
|
+
display: block !important;
|
|
182
|
+
visibility: visible !important;
|
|
183
|
+
opacity: 1 !important;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Ensure buttons and inputs are visible */
|
|
187
|
+
.email-block-content button,
|
|
188
|
+
.email-block-content input {
|
|
189
|
+
visibility: visible !important;
|
|
190
|
+
opacity: 1 !important;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Override any hidden or invisible states */
|
|
194
|
+
.email-block-content [style*="display: none"],
|
|
195
|
+
.email-block-content [style*="visibility: hidden"],
|
|
196
|
+
.email-block-content [style*="opacity: 0"] {
|
|
197
|
+
display: block !important;
|
|
198
|
+
visibility: visible !important;
|
|
199
|
+
opacity: 1 !important;
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
export interface EditorCanvasProps {
|
|
204
|
+
isPreviewMode: boolean;
|
|
205
|
+
contentBlocks: Block[];
|
|
206
|
+
title: string;
|
|
207
|
+
siteId: string;
|
|
208
|
+
locale: string;
|
|
209
|
+
darkMode: boolean;
|
|
210
|
+
backgroundColors?: {
|
|
211
|
+
light: string;
|
|
212
|
+
dark?: string;
|
|
213
|
+
};
|
|
214
|
+
metadata?: NewsletterMetadata;
|
|
215
|
+
onTitleChange: (title: string) => void;
|
|
216
|
+
onMetadataChange?: (metadata: Partial<NewsletterMetadata>) => void;
|
|
217
|
+
onBlockAdd: (type: string, index: number, containerId?: string) => void;
|
|
218
|
+
onBlockUpdate: (id: string, data: Partial<Block['data']>) => void;
|
|
219
|
+
onBlockDelete: (id: string) => void;
|
|
220
|
+
onBlockMove: (id: string, newIndex: number, containerId?: string) => void;
|
|
221
|
+
slashCommand?: ReturnType<typeof useSlashCommand>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function EditorCanvas({
|
|
225
|
+
isPreviewMode,
|
|
226
|
+
contentBlocks,
|
|
227
|
+
title,
|
|
228
|
+
siteId,
|
|
229
|
+
locale,
|
|
230
|
+
darkMode,
|
|
231
|
+
backgroundColors,
|
|
232
|
+
metadata,
|
|
233
|
+
onTitleChange,
|
|
234
|
+
onMetadataChange,
|
|
235
|
+
onBlockAdd,
|
|
236
|
+
onBlockUpdate,
|
|
237
|
+
onBlockDelete,
|
|
238
|
+
onBlockMove,
|
|
239
|
+
slashCommand,
|
|
240
|
+
}: EditorCanvasProps) {
|
|
241
|
+
const titleRef = useRef<HTMLTextAreaElement>(null);
|
|
242
|
+
|
|
243
|
+
// Inject email-like styles for editor
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
const styleId = 'email-editor-styles';
|
|
246
|
+
if (!document.getElementById(styleId)) {
|
|
247
|
+
const style = document.createElement('style');
|
|
248
|
+
style.id = styleId;
|
|
249
|
+
style.textContent = emailEditorStyles;
|
|
250
|
+
document.head.appendChild(style);
|
|
251
|
+
}
|
|
252
|
+
}, []);
|
|
253
|
+
|
|
254
|
+
// Handle Title Auto-resize
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (titleRef.current) {
|
|
257
|
+
titleRef.current.style.height = 'auto';
|
|
258
|
+
titleRef.current.style.height = `${titleRef.current.scrollHeight}px`;
|
|
259
|
+
}
|
|
260
|
+
}, [title]);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div
|
|
264
|
+
className="flex-1 overflow-y-auto overflow-x-hidden pb-40 custom-scrollbar selection:bg-primary/20 dark:selection:bg-primary/30 min-h-0"
|
|
265
|
+
style={{
|
|
266
|
+
backgroundColor: backgroundColors
|
|
267
|
+
? (darkMode && backgroundColors.dark
|
|
268
|
+
? backgroundColors.dark
|
|
269
|
+
: backgroundColors.light)
|
|
270
|
+
: '#faf9f6',
|
|
271
|
+
}}
|
|
272
|
+
>
|
|
273
|
+
{isPreviewMode ? (
|
|
274
|
+
<div className="mx-auto transition-all duration-500 max-w-[800px] px-6 py-6 w-full">
|
|
275
|
+
<EmailPreview
|
|
276
|
+
title={title}
|
|
277
|
+
blocks={contentBlocks}
|
|
278
|
+
siteId={siteId}
|
|
279
|
+
locale={locale}
|
|
280
|
+
metadata={metadata}
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
) : (
|
|
284
|
+
<div className="mx-auto transition-all duration-500 max-w-[800px] px-6 py-6 w-full">
|
|
285
|
+
{/* Email Client Window - Gmail/Outlook Style */}
|
|
286
|
+
<div className="bg-gradient-to-b from-neutral-50 to-neutral-100 dark:from-neutral-900 dark:to-neutral-800 rounded-2xl p-6 border border-neutral-200 dark:border-neutral-700 shadow-xl">
|
|
287
|
+
{/* Email Client Window */}
|
|
288
|
+
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-2xl overflow-hidden">
|
|
289
|
+
{/* Email Client Header - Gmail Style */}
|
|
290
|
+
<div className="bg-white dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700">
|
|
291
|
+
{/* Toolbar */}
|
|
292
|
+
<div className="px-4 py-2 flex items-center justify-between border-b border-neutral-100 dark:border-neutral-700">
|
|
293
|
+
<div className="flex items-center gap-2">
|
|
294
|
+
<button className="p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded" title="Close">
|
|
295
|
+
<svg className="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
296
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
297
|
+
</svg>
|
|
298
|
+
</button>
|
|
299
|
+
<button className="p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded" title="Minimize">
|
|
300
|
+
<svg className="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
301
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
|
302
|
+
</svg>
|
|
303
|
+
</button>
|
|
304
|
+
<button className="p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded" title="Fullscreen">
|
|
305
|
+
<svg className="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
306
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
|
|
307
|
+
</svg>
|
|
308
|
+
</button>
|
|
309
|
+
</div>
|
|
310
|
+
<div className="flex items-center gap-2">
|
|
311
|
+
<span className="text-xs text-neutral-500 dark:text-neutral-400 font-medium">Compose Email</span>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{/* Email Headers */}
|
|
316
|
+
<div className="px-4 py-3 border-b border-neutral-100 dark:border-neutral-700">
|
|
317
|
+
<div className="space-y-3">
|
|
318
|
+
<div className="flex items-center gap-2">
|
|
319
|
+
<label className="text-xs text-neutral-500 dark:text-neutral-400 font-medium w-16 flex-shrink-0">To:</label>
|
|
320
|
+
<input
|
|
321
|
+
type="text"
|
|
322
|
+
value="All Subscribers"
|
|
323
|
+
readOnly
|
|
324
|
+
className="flex-1 text-sm bg-transparent border-none outline-none text-neutral-700 dark:text-neutral-300 placeholder:text-neutral-400"
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
327
|
+
<div className="flex items-center gap-2">
|
|
328
|
+
<label className="text-xs text-neutral-500 dark:text-neutral-400 font-medium w-16 flex-shrink-0">Subject:</label>
|
|
329
|
+
<input
|
|
330
|
+
type="text"
|
|
331
|
+
value={metadata?.subject || ''}
|
|
332
|
+
onChange={(e) => onMetadataChange?.({
|
|
333
|
+
subject: e.target.value,
|
|
334
|
+
previewText: metadata?.previewText,
|
|
335
|
+
lang: metadata?.lang,
|
|
336
|
+
recipientFilter: metadata?.recipientFilter,
|
|
337
|
+
})}
|
|
338
|
+
placeholder="Enter email subject..."
|
|
339
|
+
className="flex-1 text-sm bg-transparent border-none outline-none text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 font-medium"
|
|
340
|
+
/>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{/* Email Content Container - Matches email HTML structure */}
|
|
347
|
+
<div className="bg-white dark:bg-neutral-800" style={{
|
|
348
|
+
backgroundColor: '#ffffff',
|
|
349
|
+
}}>
|
|
350
|
+
{/* Email Header with Logo - Centered like in email */}
|
|
351
|
+
{(() => {
|
|
352
|
+
// Get email config from window global (set by initNewsletterPlugin)
|
|
353
|
+
let emailConfig: { logoUrl?: string; logoAlt?: string } = {};
|
|
354
|
+
if (typeof window !== 'undefined' && (window as any).__JHITS_PLUGIN_PROPS__) {
|
|
355
|
+
const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__['plugin-newsletter'];
|
|
356
|
+
if (pluginProps?.emailConfig) {
|
|
357
|
+
emailConfig = pluginProps.emailConfig;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return emailConfig.logoUrl ? (
|
|
362
|
+
<div style={{
|
|
363
|
+
padding: '40px 0 20px 0',
|
|
364
|
+
textAlign: 'center',
|
|
365
|
+
backgroundColor: '#ffffff',
|
|
366
|
+
display: 'flex',
|
|
367
|
+
justifyContent: 'center',
|
|
368
|
+
alignItems: 'center',
|
|
369
|
+
}}>
|
|
370
|
+
<img
|
|
371
|
+
src={emailConfig.logoUrl}
|
|
372
|
+
alt={emailConfig.logoAlt || 'Logo'}
|
|
373
|
+
style={{
|
|
374
|
+
width: '180px',
|
|
375
|
+
height: 'auto',
|
|
376
|
+
display: 'block',
|
|
377
|
+
margin: '0 auto',
|
|
378
|
+
}}
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
) : null;
|
|
382
|
+
})()}
|
|
383
|
+
|
|
384
|
+
{/* Email Content Area */}
|
|
385
|
+
<div className="email-editor-content" style={{
|
|
386
|
+
fontFamily: "'Georgia', serif",
|
|
387
|
+
color: '#1a2e26',
|
|
388
|
+
lineHeight: '1.8',
|
|
389
|
+
fontSize: '15px',
|
|
390
|
+
padding: '0 50px 40px 50px',
|
|
391
|
+
minHeight: '400px',
|
|
392
|
+
backgroundColor: '#ffffff',
|
|
393
|
+
}}>
|
|
394
|
+
<EditorBody
|
|
395
|
+
blocks={contentBlocks}
|
|
396
|
+
darkMode={darkMode}
|
|
397
|
+
backgroundColors={backgroundColors}
|
|
398
|
+
onBlockAdd={onBlockAdd}
|
|
399
|
+
onBlockUpdate={onBlockUpdate}
|
|
400
|
+
onBlockDelete={onBlockDelete}
|
|
401
|
+
onBlockMove={onBlockMove}
|
|
402
|
+
slashCommand={slashCommand}
|
|
403
|
+
/>
|
|
404
|
+
|
|
405
|
+
{/* Unsubscribe Link - Matching email preview */}
|
|
406
|
+
{(() => {
|
|
407
|
+
// Generate unsubscribe URL (similar to welcome email)
|
|
408
|
+
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
|
|
409
|
+
const slugs: Record<string, string> = {
|
|
410
|
+
sv: '/avmälla',
|
|
411
|
+
nl: '/afmelden',
|
|
412
|
+
en: '/unsubscribe',
|
|
413
|
+
};
|
|
414
|
+
const slug = slugs[locale] || slugs.en;
|
|
415
|
+
const unsubscribeUrl = `${baseUrl}${slug}?email=subscriber@example.com`;
|
|
416
|
+
|
|
417
|
+
// Get unsubscribe text based on locale
|
|
418
|
+
const isDutch = locale === 'nl';
|
|
419
|
+
const unsubscribeText = isDutch ? 'Afmelden' : 'Unsubscribe';
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<>
|
|
423
|
+
{/* Divider */}
|
|
424
|
+
<div style={{
|
|
425
|
+
height: '1px',
|
|
426
|
+
width: '40px',
|
|
427
|
+
backgroundColor: '#1a2e2620',
|
|
428
|
+
margin: '30px auto',
|
|
429
|
+
}}></div>
|
|
430
|
+
|
|
431
|
+
{/* Unsubscribe Link */}
|
|
432
|
+
<p style={{
|
|
433
|
+
textAlign: 'center',
|
|
434
|
+
fontSize: '12px',
|
|
435
|
+
color: '#a1a1aa',
|
|
436
|
+
margin: 0,
|
|
437
|
+
}}>
|
|
438
|
+
<a
|
|
439
|
+
href={unsubscribeUrl}
|
|
440
|
+
style={{
|
|
441
|
+
color: '#a1a1aa',
|
|
442
|
+
textDecoration: 'none',
|
|
443
|
+
}}
|
|
444
|
+
onClick={(e) => e.preventDefault()}
|
|
445
|
+
>
|
|
446
|
+
{unsubscribeText}
|
|
447
|
+
</a>
|
|
448
|
+
</p>
|
|
449
|
+
</>
|
|
450
|
+
);
|
|
451
|
+
})()}
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
{/* Email Footer - Centered like in email */}
|
|
455
|
+
{(() => {
|
|
456
|
+
// Get email config from window global
|
|
457
|
+
let emailConfig: { footerText?: string } = {};
|
|
458
|
+
if (typeof window !== 'undefined' && (window as any).__JHITS_PLUGIN_PROPS__) {
|
|
459
|
+
const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__['plugin-newsletter'];
|
|
460
|
+
if (pluginProps?.emailConfig) {
|
|
461
|
+
emailConfig = pluginProps.emailConfig;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return emailConfig.footerText ? (
|
|
466
|
+
<div style={{
|
|
467
|
+
padding: '40px 50px',
|
|
468
|
+
textAlign: 'center',
|
|
469
|
+
fontFamily: 'sans-serif',
|
|
470
|
+
fontSize: '10px',
|
|
471
|
+
color: '#a1a1aa',
|
|
472
|
+
letterSpacing: '1px',
|
|
473
|
+
borderTop: '1px solid #faf9f6',
|
|
474
|
+
backgroundColor: '#ffffff',
|
|
475
|
+
}}>
|
|
476
|
+
{emailConfig.footerText}
|
|
477
|
+
</div>
|
|
478
|
+
) : null;
|
|
479
|
+
})()}
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
)}
|
|
485
|
+
</div>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Email Preview Component
|
|
491
|
+
* Shows how the newsletter will look in email clients
|
|
492
|
+
* Matches the editor view structure for consistency
|
|
493
|
+
*/
|
|
494
|
+
function EmailPreview({ title, blocks, siteId, locale, metadata }: { title: string; blocks: Block[]; siteId: string; locale: string; metadata?: NewsletterMetadata }) {
|
|
495
|
+
const [emailHtml, setEmailHtml] = useState<string>('');
|
|
496
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
497
|
+
|
|
498
|
+
useEffect(() => {
|
|
499
|
+
// Get email config from window global (set by initNewsletterPlugin)
|
|
500
|
+
let emailConfig: { logoUrl?: string; logoAlt?: string; footerText?: string } = {};
|
|
501
|
+
if (typeof window !== 'undefined' && (window as any).__JHITS_PLUGIN_PROPS__) {
|
|
502
|
+
const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__['plugin-newsletter'];
|
|
503
|
+
if (pluginProps?.emailConfig) {
|
|
504
|
+
emailConfig = pluginProps.emailConfig;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Resolve image IDs to filenames before generating HTML
|
|
509
|
+
const resolveImageBlocks = async () => {
|
|
510
|
+
const resolvedBlocks = await Promise.all(blocks.map(async (block) => {
|
|
511
|
+
if (block.type === 'image' && block.data.imageId) {
|
|
512
|
+
const imageId = block.data.imageId as string;
|
|
513
|
+
// Check if already a filename
|
|
514
|
+
const hasFileExtension = /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(imageId);
|
|
515
|
+
const looksLikeTimestamp = /^\d+-/.test(imageId);
|
|
516
|
+
|
|
517
|
+
if (hasFileExtension || looksLikeTimestamp) {
|
|
518
|
+
// Already a filename, use it directly
|
|
519
|
+
return block;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Need to resolve semantic ID to filename
|
|
523
|
+
try {
|
|
524
|
+
const response = await fetch(`/api/plugin-images/resolve?id=${encodeURIComponent(imageId)}`);
|
|
525
|
+
if (response.ok) {
|
|
526
|
+
const data = await response.json();
|
|
527
|
+
return {
|
|
528
|
+
...block,
|
|
529
|
+
data: {
|
|
530
|
+
...block.data,
|
|
531
|
+
imageId: data.filename || imageId, // Use resolved filename
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
} catch (error) {
|
|
536
|
+
console.warn(`Failed to resolve image ID ${imageId}:`, error);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return block;
|
|
540
|
+
}));
|
|
541
|
+
|
|
542
|
+
// Generate unsubscribe URL (similar to welcome email)
|
|
543
|
+
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
|
|
544
|
+
const slugs: Record<string, string> = {
|
|
545
|
+
sv: '/avmälla',
|
|
546
|
+
nl: '/afmelden',
|
|
547
|
+
en: '/unsubscribe',
|
|
548
|
+
};
|
|
549
|
+
const slug = slugs[locale] || slugs.en;
|
|
550
|
+
// Use placeholder email for preview
|
|
551
|
+
const unsubscribeUrl = `${baseUrl}${slug}?email=subscriber@example.com`;
|
|
552
|
+
|
|
553
|
+
// Generate email HTML with resolved image blocks
|
|
554
|
+
const html = generateNewsletterEmailHtml(
|
|
555
|
+
resolvedBlocks,
|
|
556
|
+
{ subject: metadata?.subject || '', previewText: metadata?.previewText || '' },
|
|
557
|
+
{
|
|
558
|
+
siteId,
|
|
559
|
+
locale,
|
|
560
|
+
baseUrl,
|
|
561
|
+
logoUrl: emailConfig.logoUrl,
|
|
562
|
+
logoAlt: emailConfig.logoAlt,
|
|
563
|
+
footerText: emailConfig.footerText,
|
|
564
|
+
unsubscribeUrl,
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
setEmailHtml(html);
|
|
568
|
+
|
|
569
|
+
// Update iframe content
|
|
570
|
+
if (iframeRef.current) {
|
|
571
|
+
const iframe = iframeRef.current;
|
|
572
|
+
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
573
|
+
if (iframeDoc) {
|
|
574
|
+
iframeDoc.open();
|
|
575
|
+
iframeDoc.write(html);
|
|
576
|
+
iframeDoc.close();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
resolveImageBlocks();
|
|
582
|
+
}, [title, blocks, siteId, locale, metadata]);
|
|
583
|
+
|
|
584
|
+
if (blocks.length === 0) {
|
|
585
|
+
return (
|
|
586
|
+
<div className="bg-gradient-to-b from-neutral-50 to-neutral-100 dark:from-neutral-900 dark:to-neutral-800 rounded-2xl p-6 border border-neutral-200 dark:border-neutral-700 shadow-xl">
|
|
587
|
+
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-2xl overflow-hidden">
|
|
588
|
+
<div className="text-center py-20 text-neutral-400 dark:text-neutral-500">
|
|
589
|
+
<p className="text-sm">No content blocks yet. Switch to Edit mode to add blocks.</p>
|
|
590
|
+
<p className="text-xs mt-2 text-neutral-500 dark:text-neutral-600">
|
|
591
|
+
Email preview will appear here once you add blocks.
|
|
592
|
+
</p>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return (
|
|
600
|
+
<div>
|
|
601
|
+
{/* Email Client Window - Gmail/Outlook Style - Same as Editor */}
|
|
602
|
+
<div className="bg-gradient-to-b from-neutral-50 to-neutral-100 dark:from-neutral-900 dark:to-neutral-800 rounded-2xl p-6 border border-neutral-200 dark:border-neutral-700 shadow-xl">
|
|
603
|
+
{/* Email Client Window */}
|
|
604
|
+
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-2xl overflow-hidden">
|
|
605
|
+
{/* Email Client Header - Gmail Style */}
|
|
606
|
+
<div className="bg-white dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700">
|
|
607
|
+
{/* Toolbar */}
|
|
608
|
+
<div className="px-4 py-2 flex items-center justify-between border-b border-neutral-100 dark:border-neutral-700">
|
|
609
|
+
<div className="flex items-center gap-2">
|
|
610
|
+
<button className="p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded" title="Close">
|
|
611
|
+
<svg className="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
612
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
613
|
+
</svg>
|
|
614
|
+
</button>
|
|
615
|
+
<button className="p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded" title="Minimize">
|
|
616
|
+
<svg className="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
617
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
|
618
|
+
</svg>
|
|
619
|
+
</button>
|
|
620
|
+
<button className="p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded" title="Fullscreen">
|
|
621
|
+
<svg className="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
622
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
|
|
623
|
+
</svg>
|
|
624
|
+
</button>
|
|
625
|
+
</div>
|
|
626
|
+
<div className="flex items-center gap-2">
|
|
627
|
+
<span className="text-xs text-neutral-500 dark:text-neutral-400 font-medium">Email Preview</span>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
{/* Email Headers */}
|
|
632
|
+
<div className="px-4 py-3 border-b border-neutral-100 dark:border-neutral-700">
|
|
633
|
+
<div className="space-y-3">
|
|
634
|
+
<div className="flex items-center gap-2">
|
|
635
|
+
<label className="text-xs text-neutral-500 dark:text-neutral-400 font-medium w-16 flex-shrink-0">To:</label>
|
|
636
|
+
<input
|
|
637
|
+
type="text"
|
|
638
|
+
value="All Subscribers"
|
|
639
|
+
readOnly
|
|
640
|
+
className="flex-1 text-sm bg-transparent border-none outline-none text-neutral-700 dark:text-neutral-300 placeholder:text-neutral-400"
|
|
641
|
+
/>
|
|
642
|
+
</div>
|
|
643
|
+
<div className="flex items-center gap-2">
|
|
644
|
+
<label className="text-xs text-neutral-500 dark:text-neutral-400 font-medium w-16 flex-shrink-0">Subject:</label>
|
|
645
|
+
<input
|
|
646
|
+
type="text"
|
|
647
|
+
value={metadata?.subject || 'Newsletter Subject'}
|
|
648
|
+
readOnly
|
|
649
|
+
className="flex-1 text-sm bg-transparent border-none outline-none text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 font-medium"
|
|
650
|
+
/>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
</div>
|
|
654
|
+
</div>
|
|
655
|
+
|
|
656
|
+
{/* Email Content - iframe showing actual email */}
|
|
657
|
+
<div className="bg-white dark:bg-neutral-800">
|
|
658
|
+
<iframe
|
|
659
|
+
ref={iframeRef}
|
|
660
|
+
title="Email Preview"
|
|
661
|
+
className="w-full border-0 bg-white"
|
|
662
|
+
style={{
|
|
663
|
+
minHeight: '500px',
|
|
664
|
+
maxHeight: '900px',
|
|
665
|
+
display: 'block',
|
|
666
|
+
}}
|
|
667
|
+
sandbox="allow-same-origin"
|
|
668
|
+
/>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
);
|
|
674
|
+
}
|