@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.
Files changed (45) hide show
  1. package/package.json +2 -3
  2. package/src/api/handler.ts +0 -693
  3. package/src/api/router.ts +0 -111
  4. package/src/index.server.ts +0 -12
  5. package/src/index.tsx +0 -313
  6. package/src/index.tsx.patch +0 -98
  7. package/src/init.tsx +0 -72
  8. package/src/lib/blocks/BlockRenderer.tsx +0 -125
  9. package/src/lib/email/EmailRenderer.tsx +0 -425
  10. package/src/lib/email/index.ts +0 -6
  11. package/src/lib/mappers/apiMapper.ts +0 -57
  12. package/src/lib/utils/blockHelpers.ts +0 -71
  13. package/src/lib/utils/slugify.ts +0 -43
  14. package/src/registry/BlockRegistry.ts +0 -53
  15. package/src/registry/index.ts +0 -5
  16. package/src/state/EditorContext.tsx +0 -279
  17. package/src/state/index.ts +0 -10
  18. package/src/state/reducer.ts +0 -561
  19. package/src/state/types.ts +0 -154
  20. package/src/types/block.ts +0 -275
  21. package/src/types/newsletter.ts +0 -151
  22. package/src/types/registry.ts +0 -14
  23. package/src/views/CanvasEditor/BlockWrapper.tsx +0 -143
  24. package/src/views/CanvasEditor/CanvasEditorView.tsx +0 -249
  25. package/src/views/CanvasEditor/EditorBody.tsx +0 -95
  26. package/src/views/CanvasEditor/EditorHeader.tsx +0 -139
  27. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +0 -83
  28. package/src/views/CanvasEditor/components/EditorCanvas.tsx +0 -674
  29. package/src/views/CanvasEditor/components/EditorLibrary.tsx +0 -120
  30. package/src/views/CanvasEditor/components/EditorSidebar.tsx +0 -156
  31. package/src/views/CanvasEditor/components/ErrorBanner.tsx +0 -31
  32. package/src/views/CanvasEditor/components/LibraryItem.tsx +0 -71
  33. package/src/views/CanvasEditor/components/SlashCommandDetector.tsx +0 -196
  34. package/src/views/CanvasEditor/components/SlashCommandMenu.tsx +0 -131
  35. package/src/views/CanvasEditor/components/index.ts +0 -16
  36. package/src/views/CanvasEditor/hooks/index.ts +0 -7
  37. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +0 -136
  38. package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +0 -34
  39. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +0 -54
  40. package/src/views/CanvasEditor/hooks/useSlashCommand.ts +0 -106
  41. package/src/views/CanvasEditor/index.ts +0 -12
  42. package/src/views/NewsletterEditor.tsx +0 -38
  43. package/src/views/NewsletterManager.tsx +0 -240
  44. package/src/views/SettingsView.tsx +0 -216
  45. package/src/views/SubscribersView.tsx +0 -269
@@ -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
- }