@jhits/plugin-newsletter 0.0.7 → 0.0.8
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 +2 -3
- package/src/api/handler.ts +0 -693
- package/src/api/router.ts +0 -111
- package/src/index.server.ts +0 -12
- package/src/index.tsx +0 -313
- package/src/index.tsx.patch +0 -98
- package/src/init.tsx +0 -72
- package/src/lib/blocks/BlockRenderer.tsx +0 -125
- package/src/lib/email/EmailRenderer.tsx +0 -425
- package/src/lib/email/index.ts +0 -6
- package/src/lib/mappers/apiMapper.ts +0 -57
- package/src/lib/utils/blockHelpers.ts +0 -71
- package/src/lib/utils/slugify.ts +0 -43
- package/src/registry/BlockRegistry.ts +0 -53
- package/src/registry/index.ts +0 -5
- package/src/state/EditorContext.tsx +0 -279
- package/src/state/index.ts +0 -10
- package/src/state/reducer.ts +0 -561
- package/src/state/types.ts +0 -154
- package/src/types/block.ts +0 -275
- package/src/types/newsletter.ts +0 -151
- package/src/types/registry.ts +0 -14
- package/src/views/CanvasEditor/BlockWrapper.tsx +0 -143
- package/src/views/CanvasEditor/CanvasEditorView.tsx +0 -249
- package/src/views/CanvasEditor/EditorBody.tsx +0 -95
- package/src/views/CanvasEditor/EditorHeader.tsx +0 -139
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +0 -83
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +0 -674
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +0 -120
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +0 -156
- package/src/views/CanvasEditor/components/ErrorBanner.tsx +0 -31
- package/src/views/CanvasEditor/components/LibraryItem.tsx +0 -71
- package/src/views/CanvasEditor/components/SlashCommandDetector.tsx +0 -196
- package/src/views/CanvasEditor/components/SlashCommandMenu.tsx +0 -131
- package/src/views/CanvasEditor/components/index.ts +0 -16
- package/src/views/CanvasEditor/hooks/index.ts +0 -7
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +0 -136
- package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +0 -34
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +0 -54
- package/src/views/CanvasEditor/hooks/useSlashCommand.ts +0 -106
- package/src/views/CanvasEditor/index.ts +0 -12
- package/src/views/NewsletterEditor.tsx +0 -38
- package/src/views/NewsletterManager.tsx +0 -240
- package/src/views/SettingsView.tsx +0 -216
- package/src/views/SubscribersView.tsx +0 -269
package/src/api/handler.ts
DELETED
|
@@ -1,693 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Newsletter API Handler
|
|
3
|
-
* Handles all newsletter-related API requests
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use server';
|
|
7
|
-
|
|
8
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
9
|
-
import { NewsletterApiConfig, Newsletter, NewsletterListItem } from '../types/newsletter';
|
|
10
|
-
import { generateSlugFromTitle } from '../lib/utils/slugify';
|
|
11
|
-
import nodemailer from 'nodemailer';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* GET /api/plugin-newsletter/subscribers - List all subscribers
|
|
15
|
-
*/
|
|
16
|
-
export async function GET_SUBSCRIBERS(
|
|
17
|
-
req: NextRequest,
|
|
18
|
-
config: NewsletterApiConfig
|
|
19
|
-
): Promise<NextResponse> {
|
|
20
|
-
try {
|
|
21
|
-
const userId = await config.getUserId?.(req);
|
|
22
|
-
if (!userId) {
|
|
23
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const dbConnection = await config.getDb();
|
|
27
|
-
const db = dbConnection.db();
|
|
28
|
-
const subscribers = db.collection('subscribers');
|
|
29
|
-
|
|
30
|
-
const subscriberList = await subscribers
|
|
31
|
-
.find({})
|
|
32
|
-
.sort({ subscribedAt: -1 })
|
|
33
|
-
.toArray();
|
|
34
|
-
|
|
35
|
-
return NextResponse.json(subscriberList);
|
|
36
|
-
} catch (error: any) {
|
|
37
|
-
console.error('[NewsletterAPI] GET_SUBSCRIBERS error:', error);
|
|
38
|
-
return NextResponse.json(
|
|
39
|
-
{ error: 'Failed to fetch subscribers', detail: error.message },
|
|
40
|
-
{ status: 500 }
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* POST /api/plugin-newsletter/subscribers - Subscribe new email
|
|
47
|
-
*/
|
|
48
|
-
export async function POST_SUBSCRIBE(
|
|
49
|
-
req: NextRequest,
|
|
50
|
-
config: NewsletterApiConfig
|
|
51
|
-
): Promise<NextResponse> {
|
|
52
|
-
try {
|
|
53
|
-
const body = await req.json();
|
|
54
|
-
const { email, language } = body;
|
|
55
|
-
|
|
56
|
-
if (!email || !email.includes('@')) {
|
|
57
|
-
return NextResponse.json(
|
|
58
|
-
{ error: 'Invalid email address' },
|
|
59
|
-
{ status: 400 }
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const dbConnection = await config.getDb();
|
|
64
|
-
const db = dbConnection.db();
|
|
65
|
-
const subscribers = db.collection('subscribers');
|
|
66
|
-
|
|
67
|
-
// Check if already subscribed
|
|
68
|
-
const existing = await subscribers.findOne({ email: email.toLowerCase() });
|
|
69
|
-
if (existing) {
|
|
70
|
-
return NextResponse.json(
|
|
71
|
-
{ error: 'You are already subscribed!' },
|
|
72
|
-
{ status: 409 }
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Add subscriber
|
|
77
|
-
await subscribers.insertOne({
|
|
78
|
-
email: email.toLowerCase(),
|
|
79
|
-
language: language || 'en',
|
|
80
|
-
subscribedAt: new Date(),
|
|
81
|
-
status: 'active',
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Send welcome email if configured
|
|
85
|
-
if (config.emailConfig) {
|
|
86
|
-
const headers = await req.headers;
|
|
87
|
-
const host = headers.get('host') || undefined;
|
|
88
|
-
await sendWelcomeEmail(config, email, language || 'en', host);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return NextResponse.json({ message: 'Successfully subscribed' }, { status: 201 });
|
|
92
|
-
} catch (error: any) {
|
|
93
|
-
console.error('[NewsletterAPI] POST_SUBSCRIBE error:', error);
|
|
94
|
-
return NextResponse.json(
|
|
95
|
-
{ error: 'Failed to subscribe', detail: error.message },
|
|
96
|
-
{ status: 500 }
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* GET /api/plugin-newsletter/subscribers/[email] - Get specific subscriber
|
|
103
|
-
*/
|
|
104
|
-
export async function GET_SUBSCRIBER(
|
|
105
|
-
req: NextRequest,
|
|
106
|
-
email: string,
|
|
107
|
-
config: NewsletterApiConfig
|
|
108
|
-
): Promise<NextResponse> {
|
|
109
|
-
try {
|
|
110
|
-
const userId = await config.getUserId?.(req);
|
|
111
|
-
if (!userId) {
|
|
112
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const dbConnection = await config.getDb();
|
|
116
|
-
const db = dbConnection.db();
|
|
117
|
-
const subscribers = db.collection('subscribers');
|
|
118
|
-
|
|
119
|
-
const subscriber = await subscribers.findOne({ email: email.toLowerCase() });
|
|
120
|
-
if (!subscriber) {
|
|
121
|
-
return NextResponse.json(
|
|
122
|
-
{ error: 'Subscriber not found' },
|
|
123
|
-
{ status: 404 }
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return NextResponse.json(subscriber);
|
|
128
|
-
} catch (error: any) {
|
|
129
|
-
console.error('[NewsletterAPI] GET_SUBSCRIBER error:', error);
|
|
130
|
-
return NextResponse.json(
|
|
131
|
-
{ error: 'Failed to fetch subscriber', detail: error.message },
|
|
132
|
-
{ status: 500 }
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* DELETE /api/plugin-newsletter/subscribers/[email] - Unsubscribe/delete subscriber
|
|
139
|
-
*/
|
|
140
|
-
export async function DELETE_SUBSCRIBER(
|
|
141
|
-
req: NextRequest,
|
|
142
|
-
email: string,
|
|
143
|
-
config: NewsletterApiConfig
|
|
144
|
-
): Promise<NextResponse> {
|
|
145
|
-
try {
|
|
146
|
-
const userId = await config.getUserId?.(req);
|
|
147
|
-
if (!userId) {
|
|
148
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const dbConnection = await config.getDb();
|
|
152
|
-
const db = dbConnection.db();
|
|
153
|
-
const subscribers = db.collection('subscribers');
|
|
154
|
-
|
|
155
|
-
const result = await subscribers.deleteOne({ email: email.toLowerCase() });
|
|
156
|
-
if (result.deletedCount === 0) {
|
|
157
|
-
return NextResponse.json(
|
|
158
|
-
{ error: 'Subscriber not found' },
|
|
159
|
-
{ status: 404 }
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return NextResponse.json({ message: 'Subscriber successfully removed' });
|
|
164
|
-
} catch (error: any) {
|
|
165
|
-
console.error('[NewsletterAPI] DELETE_SUBSCRIBER error:', error);
|
|
166
|
-
return NextResponse.json(
|
|
167
|
-
{ error: 'Failed to delete subscriber', detail: error.message },
|
|
168
|
-
{ status: 500 }
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* GET /api/plugin-newsletter/settings - Get newsletter settings
|
|
175
|
-
*/
|
|
176
|
-
export async function GET_SETTINGS(
|
|
177
|
-
req: NextRequest,
|
|
178
|
-
config: NewsletterApiConfig
|
|
179
|
-
): Promise<NextResponse> {
|
|
180
|
-
try {
|
|
181
|
-
const userId = await config.getUserId?.(req);
|
|
182
|
-
if (!userId) {
|
|
183
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const dbConnection = await config.getDb();
|
|
187
|
-
const db = dbConnection.db();
|
|
188
|
-
const newsletters = db.collection('newsletters');
|
|
189
|
-
|
|
190
|
-
const settings = await newsletters.findOne({ id: 'welcome_automation' });
|
|
191
|
-
return NextResponse.json(settings || {
|
|
192
|
-
id: 'welcome_automation',
|
|
193
|
-
languages: {
|
|
194
|
-
nl: { title: '', message: '' },
|
|
195
|
-
en: { title: '', message: '' },
|
|
196
|
-
sv: { title: '', message: '' },
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
} catch (error: any) {
|
|
200
|
-
console.error('[NewsletterAPI] GET_SETTINGS error:', error);
|
|
201
|
-
return NextResponse.json(
|
|
202
|
-
{ error: 'Failed to fetch settings', detail: error.message },
|
|
203
|
-
{ status: 500 }
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* POST /api/plugin-newsletter/settings - Update newsletter settings
|
|
210
|
-
*/
|
|
211
|
-
export async function POST_SETTINGS(
|
|
212
|
-
req: NextRequest,
|
|
213
|
-
config: NewsletterApiConfig
|
|
214
|
-
): Promise<NextResponse> {
|
|
215
|
-
try {
|
|
216
|
-
const userId = await config.getUserId?.(req);
|
|
217
|
-
if (!userId) {
|
|
218
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const body = await req.json();
|
|
222
|
-
const dbConnection = await config.getDb();
|
|
223
|
-
const db = dbConnection.db();
|
|
224
|
-
const newsletters = db.collection('newsletters');
|
|
225
|
-
|
|
226
|
-
await newsletters.updateOne(
|
|
227
|
-
{ id: 'welcome_automation' },
|
|
228
|
-
{
|
|
229
|
-
$set: {
|
|
230
|
-
id: 'welcome_automation',
|
|
231
|
-
languages: body.languages || {},
|
|
232
|
-
updatedAt: new Date(),
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
{ upsert: true }
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
return NextResponse.json({ success: true, message: 'Settings updated successfully' });
|
|
239
|
-
} catch (error: any) {
|
|
240
|
-
console.error('[NewsletterAPI] POST_SETTINGS error:', error);
|
|
241
|
-
return NextResponse.json(
|
|
242
|
-
{ error: 'Failed to update settings', detail: error.message },
|
|
243
|
-
{ status: 500 }
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Send welcome email to new subscriber
|
|
250
|
-
*/
|
|
251
|
-
async function sendWelcomeEmail(
|
|
252
|
-
config: NewsletterApiConfig,
|
|
253
|
-
email: string,
|
|
254
|
-
language: string,
|
|
255
|
-
host?: string
|
|
256
|
-
): Promise<void> {
|
|
257
|
-
if (!config.emailConfig) return;
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
const transporter = nodemailer.createTransport({
|
|
261
|
-
host: config.emailConfig.host,
|
|
262
|
-
port: config.emailConfig.port,
|
|
263
|
-
secure: true,
|
|
264
|
-
auth: {
|
|
265
|
-
user: config.emailConfig.user,
|
|
266
|
-
pass: config.emailConfig.password,
|
|
267
|
-
},
|
|
268
|
-
connectionTimeout: 10000,
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const baseUrl = host
|
|
272
|
-
? (host.includes('localhost') ? 'http' : 'https') + '://' + host
|
|
273
|
-
: config.baseUrl || 'https://bya.jorishummel.com';
|
|
274
|
-
|
|
275
|
-
const slugs: Record<string, string> = {
|
|
276
|
-
sv: '/avmälla',
|
|
277
|
-
nl: '/afmelden',
|
|
278
|
-
en: '/unsubscribe',
|
|
279
|
-
};
|
|
280
|
-
const slug = slugs[language] || slugs.en;
|
|
281
|
-
const unsubscribeUrl = `${baseUrl}${slug}?email=${encodeURIComponent(email)}`;
|
|
282
|
-
|
|
283
|
-
const isDutch = language === 'nl';
|
|
284
|
-
const message = isDutch
|
|
285
|
-
? `Bedankt dat je deel uitmaakt van de **Botanics & You** community.\n\n` +
|
|
286
|
-
`Wij geloven dat de natuur alles biedt wat we werkelijk nodig hebben. Terwijl we achter de schermen hard werken aan de lancering, nemen we je graag mee op reis door de wereld van kruiden en natuurlijke vitaliteit.\n\n` +
|
|
287
|
-
`**Wat kun je van ons verwachten:**\n` +
|
|
288
|
-
`• Exclusieve updates over onze lancering\n` +
|
|
289
|
-
`• Inzichten in de kracht van lokale kruiden\n` +
|
|
290
|
-
`• Tips voor een diepere verbinding met de natuur\n\n` +
|
|
291
|
-
`Bedankt voor je geduld en interesse in een natuurlijke manier van leven. We spreken je snel!`
|
|
292
|
-
: `Thank you for joining the **Botanics & You** community.\n\n` +
|
|
293
|
-
`We believe that nature provides everything we truly need. While we work hard behind the scenes for our launch, we look forward to taking you on a journey through the world of herbs and natural vitality.\n\n` +
|
|
294
|
-
`**What to expect from us:**\n` +
|
|
295
|
-
`• Exclusive updates on our upcoming launch\n` +
|
|
296
|
-
`• Insights into the healing properties of local herbs\n` +
|
|
297
|
-
`• Tips for restoring your connection with nature\n\n` +
|
|
298
|
-
`Thank you for your patience and your interest in a more natural way of living. We'll be in touch soon!`;
|
|
299
|
-
|
|
300
|
-
const formattedMessage = message
|
|
301
|
-
.replace(/\n/g, '<br/>')
|
|
302
|
-
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
303
|
-
.replace(/• (.*?)(<br\/>|$)/g, '<div style="margin-left: 10px; margin-bottom: 5px;">• $1</div>');
|
|
304
|
-
|
|
305
|
-
const html = `
|
|
306
|
-
<!DOCTYPE html>
|
|
307
|
-
<html>
|
|
308
|
-
<head>
|
|
309
|
-
<meta charset="utf-8">
|
|
310
|
-
<style>
|
|
311
|
-
body { background-color: #faf9f6; margin: 0; padding: 0; font-family: 'Georgia', serif; }
|
|
312
|
-
.container { max-width: 600px; margin: 20px auto; background-color: #ffffff; border-radius: 40px; border: 1px solid #1a2e260d; overflow: hidden; }
|
|
313
|
-
.header { padding: 40px 0 20px 0; text-align: center; }
|
|
314
|
-
.logo { width: 180px; height: auto; }
|
|
315
|
-
.content { padding: 0 50px 40px 50px; color: #1a2e26; line-height: 1.8; font-size: 15px; }
|
|
316
|
-
.footer { padding: 40px 50px; text-align: center; font-family: sans-serif; font-size: 10px; color: #a1a1aa; letter-spacing: 1px; border-top: 1px solid #faf9f6; }
|
|
317
|
-
h1 { font-weight: normal; font-style: italic; font-size: 30px; margin-bottom: 30px; color: #1a2e26; text-align: center; }
|
|
318
|
-
.divider { height: 1px; width: 40px; background-color: #1a2e2620; margin: 30px auto; }
|
|
319
|
-
</style>
|
|
320
|
-
</head>
|
|
321
|
-
<body>
|
|
322
|
-
<div class="container">
|
|
323
|
-
<div class="header">
|
|
324
|
-
<img src="cid:botanics-logo" alt="Botanics & You" class="logo">
|
|
325
|
-
</div>
|
|
326
|
-
<div class="content">
|
|
327
|
-
<h1>${isDutch ? 'Welkom!' : 'Welcome!'}</h1>
|
|
328
|
-
${formattedMessage}
|
|
329
|
-
<div class="divider"></div>
|
|
330
|
-
<p style="text-align: center; font-size: 12px; color: #a1a1aa;">
|
|
331
|
-
<a href="${unsubscribeUrl}" style="color: #a1a1aa; text-decoration: none;">
|
|
332
|
-
${isDutch ? 'Afmelden' : 'Unsubscribe'}
|
|
333
|
-
</a>
|
|
334
|
-
</p>
|
|
335
|
-
</div>
|
|
336
|
-
<div class="footer">
|
|
337
|
-
© ${new Date().getFullYear()} Botanics & You. All rights reserved.
|
|
338
|
-
</div>
|
|
339
|
-
</div>
|
|
340
|
-
</body>
|
|
341
|
-
</html>
|
|
342
|
-
`;
|
|
343
|
-
|
|
344
|
-
await transporter.sendMail({
|
|
345
|
-
from: config.emailConfig.from,
|
|
346
|
-
to: email,
|
|
347
|
-
subject: isDutch ? 'Welkom bij Botanics & You!' : 'Welcome to Botanics & You!',
|
|
348
|
-
html,
|
|
349
|
-
});
|
|
350
|
-
} catch (error) {
|
|
351
|
-
console.error('[NewsletterAPI] Failed to send welcome email:', error);
|
|
352
|
-
// Don't throw - subscription should still succeed even if email fails
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* GET /api/plugin-newsletter/newsletters - List all newsletters
|
|
358
|
-
*/
|
|
359
|
-
export async function GET_NEWSLETTERS(
|
|
360
|
-
req: NextRequest,
|
|
361
|
-
config: NewsletterApiConfig
|
|
362
|
-
): Promise<NextResponse> {
|
|
363
|
-
try {
|
|
364
|
-
const userId = await config.getUserId?.(req);
|
|
365
|
-
if (!userId) {
|
|
366
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const dbConnection = await config.getDb();
|
|
370
|
-
const db = dbConnection.db();
|
|
371
|
-
const collectionName = config.collectionName || 'newsletters';
|
|
372
|
-
const newsletters = db.collection(collectionName);
|
|
373
|
-
|
|
374
|
-
// Get query parameters
|
|
375
|
-
const { searchParams } = new URL(req.url);
|
|
376
|
-
const status = searchParams.get('status');
|
|
377
|
-
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
|
378
|
-
const skip = parseInt(searchParams.get('skip') || '0', 10);
|
|
379
|
-
const sortBy = searchParams.get('sortBy') || 'updatedAt';
|
|
380
|
-
const sortOrder = searchParams.get('sortOrder') || 'desc';
|
|
381
|
-
|
|
382
|
-
// Build query
|
|
383
|
-
const query: any = {};
|
|
384
|
-
if (status) {
|
|
385
|
-
query['publication.status'] = status;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Build sort
|
|
389
|
-
const sort: any = {};
|
|
390
|
-
sort[sortBy] = sortOrder === 'asc' ? 1 : -1;
|
|
391
|
-
|
|
392
|
-
const newsletterList = await newsletters
|
|
393
|
-
.find(query)
|
|
394
|
-
.sort(sort)
|
|
395
|
-
.limit(limit)
|
|
396
|
-
.skip(skip)
|
|
397
|
-
.toArray();
|
|
398
|
-
|
|
399
|
-
// Convert to list items
|
|
400
|
-
const listItems: NewsletterListItem[] = newsletterList.map((newsletter: any) => ({
|
|
401
|
-
id: newsletter._id?.toString() || newsletter.id,
|
|
402
|
-
title: newsletter.title,
|
|
403
|
-
slug: newsletter.slug,
|
|
404
|
-
status: newsletter.publication?.status || 'draft',
|
|
405
|
-
subject: newsletter.metadata?.subject || '',
|
|
406
|
-
scheduledDate: newsletter.publication?.scheduledDate,
|
|
407
|
-
sentDate: newsletter.publication?.sentDate,
|
|
408
|
-
authorId: newsletter.publication?.authorId,
|
|
409
|
-
updatedAt: newsletter.updatedAt || newsletter.createdAt,
|
|
410
|
-
recipientCount: newsletter.recipientCount,
|
|
411
|
-
}));
|
|
412
|
-
|
|
413
|
-
return NextResponse.json(listItems);
|
|
414
|
-
} catch (error: any) {
|
|
415
|
-
console.error('[NewsletterAPI] GET_NEWSLETTERS error:', error);
|
|
416
|
-
return NextResponse.json(
|
|
417
|
-
{ error: 'Failed to fetch newsletters', detail: error.message },
|
|
418
|
-
{ status: 500 }
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* GET /api/plugin-newsletter/newsletters/[slug] - Get specific newsletter
|
|
425
|
-
*/
|
|
426
|
-
export async function GET_NEWSLETTER(
|
|
427
|
-
req: NextRequest,
|
|
428
|
-
slug: string,
|
|
429
|
-
config: NewsletterApiConfig
|
|
430
|
-
): Promise<NextResponse> {
|
|
431
|
-
try {
|
|
432
|
-
const userId = await config.getUserId?.(req);
|
|
433
|
-
if (!userId) {
|
|
434
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const dbConnection = await config.getDb();
|
|
438
|
-
const db = dbConnection.db();
|
|
439
|
-
const collectionName = config.collectionName || 'newsletters';
|
|
440
|
-
const newsletters = db.collection(collectionName);
|
|
441
|
-
|
|
442
|
-
const newsletter = await newsletters.findOne({ slug });
|
|
443
|
-
if (!newsletter) {
|
|
444
|
-
return NextResponse.json(
|
|
445
|
-
{ error: 'Newsletter not found' },
|
|
446
|
-
{ status: 404 }
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Convert MongoDB document to Newsletter format
|
|
451
|
-
const result: Newsletter = {
|
|
452
|
-
id: newsletter._id?.toString() || newsletter.id,
|
|
453
|
-
title: newsletter.title,
|
|
454
|
-
slug: newsletter.slug,
|
|
455
|
-
blocks: newsletter.blocks || [],
|
|
456
|
-
metadata: newsletter.metadata || {
|
|
457
|
-
subject: '',
|
|
458
|
-
previewText: '',
|
|
459
|
-
lang: 'en',
|
|
460
|
-
recipientFilter: { type: 'all' },
|
|
461
|
-
},
|
|
462
|
-
publication: newsletter.publication || {
|
|
463
|
-
status: 'draft',
|
|
464
|
-
updatedAt: new Date().toISOString(),
|
|
465
|
-
},
|
|
466
|
-
createdAt: newsletter.createdAt || new Date().toISOString(),
|
|
467
|
-
updatedAt: newsletter.updatedAt || new Date().toISOString(),
|
|
468
|
-
version: newsletter.version,
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
return NextResponse.json(result);
|
|
472
|
-
} catch (error: any) {
|
|
473
|
-
console.error('[NewsletterAPI] GET_NEWSLETTER error:', error);
|
|
474
|
-
return NextResponse.json(
|
|
475
|
-
{ error: 'Failed to fetch newsletter', detail: error.message },
|
|
476
|
-
{ status: 500 }
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* POST /api/plugin-newsletter/newsletters/new - Create new newsletter
|
|
483
|
-
*/
|
|
484
|
-
export async function POST_NEWSLETTER(
|
|
485
|
-
req: NextRequest,
|
|
486
|
-
config: NewsletterApiConfig
|
|
487
|
-
): Promise<NextResponse> {
|
|
488
|
-
try {
|
|
489
|
-
const userId = await config.getUserId?.(req);
|
|
490
|
-
if (!userId) {
|
|
491
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const body = await req.json();
|
|
495
|
-
const { title, blocks, metadata, publication } = body;
|
|
496
|
-
|
|
497
|
-
// Validation
|
|
498
|
-
const errors: string[] = [];
|
|
499
|
-
if (!title || typeof title !== 'string' || title.trim().length === 0) {
|
|
500
|
-
errors.push('Title is required');
|
|
501
|
-
}
|
|
502
|
-
if (!metadata?.subject || typeof metadata.subject !== 'string' || metadata.subject.trim().length === 0) {
|
|
503
|
-
errors.push('Subject is required');
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (errors.length > 0) {
|
|
507
|
-
return NextResponse.json({ message: errors[0], allErrors: errors }, { status: 400 });
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const dbConnection = await config.getDb();
|
|
511
|
-
const db = dbConnection.db();
|
|
512
|
-
const collectionName = config.collectionName || 'newsletters';
|
|
513
|
-
const newsletters = db.collection(collectionName);
|
|
514
|
-
|
|
515
|
-
// Get existing slugs for collision check
|
|
516
|
-
const existingNewsletters = await newsletters.find({}, { projection: { slug: 1 } }).toArray();
|
|
517
|
-
const existingSlugs = existingNewsletters.map((n: any) => n.slug).filter(Boolean);
|
|
518
|
-
|
|
519
|
-
// Use subject as title if title is empty
|
|
520
|
-
const finalTitle = (title?.trim() || metadata?.subject?.trim() || '').trim();
|
|
521
|
-
|
|
522
|
-
// Generate slug
|
|
523
|
-
const slug = generateSlugFromTitle(finalTitle, existingSlugs);
|
|
524
|
-
|
|
525
|
-
const newsletterDocument = {
|
|
526
|
-
title: finalTitle,
|
|
527
|
-
slug,
|
|
528
|
-
blocks: blocks || [],
|
|
529
|
-
metadata: {
|
|
530
|
-
subject: metadata.subject.trim(),
|
|
531
|
-
previewText: metadata.previewText?.trim() || '',
|
|
532
|
-
lang: metadata.lang || 'en',
|
|
533
|
-
recipientFilter: metadata.recipientFilter || { type: 'all' },
|
|
534
|
-
},
|
|
535
|
-
publication: {
|
|
536
|
-
status: publication?.status || 'draft',
|
|
537
|
-
scheduledDate: publication?.scheduledDate,
|
|
538
|
-
authorId: userId,
|
|
539
|
-
updatedAt: new Date().toISOString(),
|
|
540
|
-
},
|
|
541
|
-
createdAt: new Date(),
|
|
542
|
-
updatedAt: new Date(),
|
|
543
|
-
version: 1,
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
const result = await newsletters.insertOne(newsletterDocument);
|
|
547
|
-
|
|
548
|
-
return NextResponse.json({
|
|
549
|
-
message: 'Newsletter created successfully',
|
|
550
|
-
id: result.insertedId.toString(),
|
|
551
|
-
slug,
|
|
552
|
-
}, { status: 201 });
|
|
553
|
-
} catch (error: any) {
|
|
554
|
-
console.error('[NewsletterAPI] POST_NEWSLETTER error:', error);
|
|
555
|
-
return NextResponse.json(
|
|
556
|
-
{ error: 'Failed to create newsletter', detail: error.message },
|
|
557
|
-
{ status: 500 }
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* PUT /api/plugin-newsletter/newsletters/[slug] - Update existing newsletter
|
|
564
|
-
*/
|
|
565
|
-
export async function PUT_NEWSLETTER(
|
|
566
|
-
req: NextRequest,
|
|
567
|
-
slug: string,
|
|
568
|
-
config: NewsletterApiConfig
|
|
569
|
-
): Promise<NextResponse> {
|
|
570
|
-
try {
|
|
571
|
-
const userId = await config.getUserId?.(req);
|
|
572
|
-
if (!userId) {
|
|
573
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const body = await req.json();
|
|
577
|
-
const { title, blocks, metadata, publication } = body;
|
|
578
|
-
|
|
579
|
-
// Validation
|
|
580
|
-
// For newsletters, subject is required and can serve as title
|
|
581
|
-
const errors: string[] = [];
|
|
582
|
-
if (!metadata?.subject || typeof metadata.subject !== 'string' || metadata.subject.trim().length === 0) {
|
|
583
|
-
errors.push('Subject is required');
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Use subject as title if title is empty (for newsletters, subject is the primary identifier)
|
|
587
|
-
const finalTitle = (title?.trim() || metadata?.subject?.trim() || '').trim();
|
|
588
|
-
if (!finalTitle) {
|
|
589
|
-
errors.push('Title is required (use subject if no title is provided)');
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
if (errors.length > 0) {
|
|
593
|
-
return NextResponse.json({ message: errors[0], allErrors: errors }, { status: 400 });
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const dbConnection = await config.getDb();
|
|
597
|
-
const db = dbConnection.db();
|
|
598
|
-
const collectionName = config.collectionName || 'newsletters';
|
|
599
|
-
const newsletters = db.collection(collectionName);
|
|
600
|
-
|
|
601
|
-
// Check if newsletter exists
|
|
602
|
-
const existing = await newsletters.findOne({ slug });
|
|
603
|
-
if (!existing) {
|
|
604
|
-
return NextResponse.json(
|
|
605
|
-
{ error: 'Newsletter not found' },
|
|
606
|
-
{ status: 404 }
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Generate new slug if title changed
|
|
611
|
-
let newSlug = slug;
|
|
612
|
-
if (finalTitle !== existing.title) {
|
|
613
|
-
const existingNewsletters = await newsletters.find({ _id: { $ne: existing._id } }, { projection: { slug: 1 } }).toArray();
|
|
614
|
-
const existingSlugs = existingNewsletters.map((n: any) => n.slug).filter(Boolean);
|
|
615
|
-
newSlug = generateSlugFromTitle(finalTitle, existingSlugs);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// Update newsletter
|
|
619
|
-
const updateData: any = {
|
|
620
|
-
title: finalTitle,
|
|
621
|
-
slug: newSlug,
|
|
622
|
-
blocks: blocks || [],
|
|
623
|
-
metadata: {
|
|
624
|
-
subject: metadata.subject.trim(),
|
|
625
|
-
previewText: metadata.previewText?.trim() || '',
|
|
626
|
-
lang: metadata.lang || 'en',
|
|
627
|
-
recipientFilter: metadata.recipientFilter || { type: 'all' },
|
|
628
|
-
},
|
|
629
|
-
publication: {
|
|
630
|
-
...existing.publication,
|
|
631
|
-
status: publication?.status || existing.publication?.status || 'draft',
|
|
632
|
-
scheduledDate: publication?.scheduledDate,
|
|
633
|
-
authorId: userId,
|
|
634
|
-
updatedAt: new Date().toISOString(),
|
|
635
|
-
},
|
|
636
|
-
updatedAt: new Date(),
|
|
637
|
-
version: (existing.version || 1) + 1,
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
await newsletters.updateOne(
|
|
641
|
-
{ slug },
|
|
642
|
-
{ $set: updateData }
|
|
643
|
-
);
|
|
644
|
-
|
|
645
|
-
return NextResponse.json({
|
|
646
|
-
message: 'Newsletter updated successfully',
|
|
647
|
-
slug: newSlug,
|
|
648
|
-
});
|
|
649
|
-
} catch (error: any) {
|
|
650
|
-
console.error('[NewsletterAPI] PUT_NEWSLETTER error:', error);
|
|
651
|
-
return NextResponse.json(
|
|
652
|
-
{ error: 'Failed to update newsletter', detail: error.message },
|
|
653
|
-
{ status: 500 }
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* DELETE /api/plugin-newsletter/newsletters/[slug] - Delete newsletter
|
|
660
|
-
*/
|
|
661
|
-
export async function DELETE_NEWSLETTER(
|
|
662
|
-
req: NextRequest,
|
|
663
|
-
slug: string,
|
|
664
|
-
config: NewsletterApiConfig
|
|
665
|
-
): Promise<NextResponse> {
|
|
666
|
-
try {
|
|
667
|
-
const userId = await config.getUserId?.(req);
|
|
668
|
-
if (!userId) {
|
|
669
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const dbConnection = await config.getDb();
|
|
673
|
-
const db = dbConnection.db();
|
|
674
|
-
const collectionName = config.collectionName || 'newsletters';
|
|
675
|
-
const newsletters = db.collection(collectionName);
|
|
676
|
-
|
|
677
|
-
const result = await newsletters.deleteOne({ slug });
|
|
678
|
-
if (result.deletedCount === 0) {
|
|
679
|
-
return NextResponse.json(
|
|
680
|
-
{ error: 'Newsletter not found' },
|
|
681
|
-
{ status: 404 }
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
return NextResponse.json({ message: 'Newsletter deleted successfully' });
|
|
686
|
-
} catch (error: any) {
|
|
687
|
-
console.error('[NewsletterAPI] DELETE_NEWSLETTER error:', error);
|
|
688
|
-
return NextResponse.json(
|
|
689
|
-
{ error: 'Failed to delete newsletter', detail: error.message },
|
|
690
|
-
{ status: 500 }
|
|
691
|
-
);
|
|
692
|
-
}
|
|
693
|
-
}
|