@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhits/plugin-newsletter",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "Newsletter management and email delivery plugin for the JHITS ecosystem",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -30,7 +30,7 @@
30
30
  "mongodb": "^7.1.0",
31
31
  "next-auth": "^4.24.13",
32
32
  "nodemailer": "^8.0.1",
33
- "@jhits/plugin-core": "0.0.8"
33
+ "@jhits/plugin-core": "0.0.9"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "next": ">=15.0.0",
@@ -49,6 +49,7 @@
49
49
  "typescript": "^5.9.3"
50
50
  },
51
51
  "files": [
52
+ "src",
52
53
  "dist",
53
54
  "package.json",
54
55
  "templates",
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Email Utilities
3
+ */
4
+
5
+ 'use server';
6
+
7
+ import { NewsletterApiConfig, NewsletterMetadata } from '../types/newsletter';
8
+ import nodemailer from 'nodemailer';
9
+ import path from 'path';
10
+ import fs from 'fs';
11
+ import handlebars from 'handlebars';
12
+ import { GET_WELCOME_EMAIL_CONTENT } from './handlers/welcome-email';
13
+
14
+ interface SmtpConfig {
15
+ host: string;
16
+ port: number;
17
+ user: string;
18
+ password: string;
19
+ from: string;
20
+ fromName: string;
21
+ logoUrl?: string;
22
+ }
23
+
24
+ async function getSmtpConfig(config: NewsletterApiConfig): Promise<SmtpConfig | null> {
25
+ const dbConnection = await config.getDb();
26
+ const db = dbConnection.db();
27
+ const settings = db.collection('settings');
28
+ const smtpConfig = await settings.findOne({ key: 'smtp' });
29
+
30
+ if (smtpConfig && smtpConfig.host) {
31
+ return {
32
+ host: smtpConfig.host,
33
+ port: smtpConfig.port || 465,
34
+ user: smtpConfig.user,
35
+ password: smtpConfig.password,
36
+ from: smtpConfig.from,
37
+ fromName: smtpConfig.fromName || '',
38
+ logoUrl: smtpConfig.logoUrl || '',
39
+ };
40
+ }
41
+ return null;
42
+ }
43
+
44
+ export async function sendWelcomeEmail(
45
+ config: NewsletterApiConfig,
46
+ email: string,
47
+ language: string,
48
+ host?: string
49
+ ): Promise<void> {
50
+ const smtpConfig = await getSmtpConfig(config);
51
+ if (!smtpConfig) return;
52
+
53
+ const { blocks, metadata } = await GET_WELCOME_EMAIL_CONTENT(undefined as any, config, language);
54
+
55
+ const isDutch = language === 'nl';
56
+ const isSwedish = language === 'sv';
57
+ const baseUrl = host
58
+ ? (host.includes('localhost') ? 'http' : 'https') + '://' + host
59
+ : config.baseUrl || 'https://bya.jorishummel.com';
60
+
61
+ const slugs: Record<string, string> = {
62
+ sv: '/avmälla',
63
+ nl: '/afmelden',
64
+ en: '/unsubscribe',
65
+ };
66
+ const slug = slugs[language] || slugs.en;
67
+ const unsubscribeUrl = `${baseUrl}${slug}?email=${encodeURIComponent(email)}`;
68
+
69
+ try {
70
+ const transporter = nodemailer.createTransport({
71
+ host: smtpConfig.host,
72
+ port: smtpConfig.port,
73
+ secure: smtpConfig.port === 465,
74
+ auth: {
75
+ user: smtpConfig.user,
76
+ pass: smtpConfig.password,
77
+ },
78
+ connectionTimeout: 10000,
79
+ });
80
+
81
+ let html: string;
82
+ let subject: string;
83
+ let logoAttachment = undefined;
84
+ let logoSrc = smtpConfig.logoUrl || `${baseUrl}/logo_black.svg`;
85
+
86
+ // If logoUrl is a data URL (base64), embed it as CID attachment
87
+ if (smtpConfig.logoUrl && smtpConfig.logoUrl.startsWith('data:')) {
88
+ const matches = smtpConfig.logoUrl.match(/^data:([^;]+);base64,(.+)$/);
89
+ if (matches) {
90
+ const mimeType = matches[1];
91
+ const base64Data = matches[2];
92
+ const ext = mimeType === 'image/png' ? 'png' : 'jpg';
93
+ logoAttachment = {
94
+ filename: `logo.${ext}`,
95
+ content: Buffer.from(base64Data, 'base64'),
96
+ cid: 'logo',
97
+ contentType: mimeType,
98
+ };
99
+ logoSrc = 'cid:logo';
100
+ }
101
+ }
102
+
103
+ if (blocks && blocks.length > 0) {
104
+ const { generateNewsletterEmailHtml } = await import('../lib/email/EmailRenderer');
105
+
106
+ html = generateNewsletterEmailHtml(
107
+ blocks,
108
+ { subject: metadata?.subject || '', previewText: metadata?.previewText || '' },
109
+ {
110
+ baseUrl,
111
+ locale: language,
112
+ logoUrl: logoSrc,
113
+ unsubscribeUrl,
114
+ footerText: `© ${new Date().getFullYear()} ${smtpConfig.fromName || 'Botanics & You'}`,
115
+ }
116
+ );
117
+ subject = metadata?.subject || (isDutch ? 'Welkom!' : isSwedish ? 'Välkommen!' : 'Welcome!');
118
+ } else {
119
+ const legacyTemplatePath = path.join(process.cwd(), 'node_modules', '@jhits', 'plugin-newsletter', 'templates', 'welcome-email-legacy.hbs');
120
+ let templateContent: string;
121
+
122
+ if (fs.existsSync(legacyTemplatePath)) {
123
+ templateContent = fs.readFileSync(legacyTemplatePath, 'utf-8');
124
+ } else {
125
+ const altPath = path.join(__dirname, '..', '..', '..', 'templates', 'welcome-email-legacy.hbs');
126
+ if (fs.existsSync(altPath)) {
127
+ templateContent = fs.readFileSync(altPath, 'utf-8');
128
+ } else {
129
+ console.error('[NewsletterAPI] Legacy email template not found');
130
+ return;
131
+ }
132
+ }
133
+
134
+ const template = handlebars.compile(templateContent);
135
+ html = template({
136
+ fromName: smtpConfig.fromName || 'Botanics & You',
137
+ title: isDutch ? 'Fijn dat je er bent' : isSwedish ? 'Tack för att du är här' : "We're glad you're here",
138
+ message: isDutch
139
+ ? 'Bedankt dat je deel uitmaakt van de community. Wij geloven dat de natuur alles biedt wat we werkelijk nodig hebben.'
140
+ : isSwedish
141
+ ? 'Tack för att du är en del av vår community. Vi tror att naturen ger oss allt vi verkligen behöver.'
142
+ : 'Thank you for joining our community. We believe that nature provides everything we truly need.',
143
+ unsubscribeUrl,
144
+ unsubscribeText: isDutch ? 'Afmelden' : isSwedish ? 'Avanmälan' : 'Unsubscribe',
145
+ tagline: isDutch ? 'Natuurlijk verbonden' : isSwedish ? 'Naturligt ansluten' : 'Naturally connected',
146
+ currentYear: new Date().getFullYear(),
147
+ });
148
+ subject = isDutch ? 'Welkom bij Botanics & You' : isSwedish ? 'Välkommen till Botanics & You' : 'Welcome to Botanics & You';
149
+ }
150
+
151
+ await transporter.sendMail({
152
+ from: smtpConfig.fromName
153
+ ? `"${smtpConfig.fromName}" <${smtpConfig.from}>`
154
+ : smtpConfig.from,
155
+ to: email,
156
+ subject,
157
+ html,
158
+ ...(logoAttachment && {
159
+ attachments: [logoAttachment],
160
+ }),
161
+ });
162
+ } catch (error) {
163
+ console.error('[NewsletterAPI] Failed to send welcome email:', error);
164
+ }
165
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Newsletter API Handler
3
+ * Re-exports all handler functions from handlers folder
4
+ */
5
+
6
+ export {
7
+ GET_SUBSCRIBERS,
8
+ POST_SUBSCRIBE,
9
+ GET_SUBSCRIBER,
10
+ DELETE_SUBSCRIBER,
11
+ GET_SETTINGS,
12
+ POST_SETTINGS,
13
+ GET_NEWSLETTERS,
14
+ GET_NEWSLETTER,
15
+ POST_NEWSLETTER,
16
+ PUT_NEWSLETTER,
17
+ DELETE_NEWSLETTER,
18
+ GET_WELCOME_EMAIL_STATUS,
19
+ GET_WELCOME_EMAIL,
20
+ POST_WELCOME_EMAIL,
21
+ GET_SMTP_SETTINGS,
22
+ POST_SMTP_SETTINGS,
23
+ POST_TEST_EMAIL,
24
+ POST_LOGO_UPLOAD,
25
+ POST_SEND_NEWSLETTER,
26
+ GET_NEWSLETTER_FOR_SEND,
27
+ sendWelcomeEmail,
28
+ } from './handlers';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * API Handlers Index
3
+ * Re-exports all handler functions
4
+ */
5
+
6
+ export {
7
+ GET_SUBSCRIBERS,
8
+ POST_SUBSCRIBE,
9
+ GET_SUBSCRIBER,
10
+ DELETE_SUBSCRIBER,
11
+ } from './subscribers';
12
+
13
+ export {
14
+ GET_SETTINGS,
15
+ POST_SETTINGS,
16
+ GET_SMTP_SETTINGS,
17
+ POST_SMTP_SETTINGS,
18
+ POST_TEST_EMAIL,
19
+ } from './settings';
20
+
21
+ export {
22
+ POST_LOGO_UPLOAD,
23
+ } from './upload';
24
+
25
+ export {
26
+ GET_NEWSLETTERS,
27
+ GET_NEWSLETTER,
28
+ POST_NEWSLETTER,
29
+ PUT_NEWSLETTER,
30
+ DELETE_NEWSLETTER,
31
+ } from './newsletters';
32
+
33
+ export {
34
+ POST_SEND_NEWSLETTER,
35
+ GET_NEWSLETTER_FOR_SEND,
36
+ } from './send-newsletter';
37
+
38
+ export {
39
+ GET_WELCOME_EMAIL_STATUS,
40
+ GET_WELCOME_EMAIL,
41
+ POST_WELCOME_EMAIL,
42
+ } from './welcome-email';
43
+
44
+ export { sendWelcomeEmail } from '../email-utils';
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Newsletter API Handler
3
+ */
4
+
5
+ 'use server';
6
+
7
+ import { NextRequest, NextResponse } from 'next/server';
8
+ import { NewsletterApiConfig, Newsletter, NewsletterListItem } from '../../types/newsletter';
9
+ import { generateSlugFromTitle } from '../../lib/utils/slugify';
10
+
11
+ const ObjectId = require('mongodb').ObjectId;
12
+
13
+ function getNewsletterFilter(idOrSlug: string) {
14
+ // Check if it's a valid MongoDB ObjectId
15
+ if (ObjectId.isValid(idOrSlug) && new ObjectId(idOrSlug).toString() === idOrSlug) {
16
+ return { _id: new ObjectId(idOrSlug) };
17
+ }
18
+ // Otherwise try by slug
19
+ return { slug: idOrSlug };
20
+ }
21
+
22
+ export async function GET_NEWSLETTERS(
23
+ req: NextRequest,
24
+ config: NewsletterApiConfig
25
+ ): Promise<NextResponse> {
26
+ try {
27
+ const userId = await config.getUserId?.(req);
28
+ if (!userId) {
29
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
30
+ }
31
+
32
+ const dbConnection = await config.getDb();
33
+ const db = dbConnection.db();
34
+ const collectionName = config.collectionName || 'newsletters';
35
+ const newsletters = db.collection(collectionName);
36
+
37
+ const { searchParams } = new URL(req.url);
38
+ const status = searchParams.get('status');
39
+ const limit = parseInt(searchParams.get('limit') || '50', 10);
40
+ const skip = parseInt(searchParams.get('skip') || '0', 10);
41
+ const sortBy = searchParams.get('sortBy') || 'updatedAt';
42
+ const sortOrder = searchParams.get('sortOrder') || 'desc';
43
+
44
+ const query: any = {};
45
+ if (status) {
46
+ query['publication.status'] = status;
47
+ }
48
+
49
+ const filter = {
50
+ ...query,
51
+ $or: [
52
+ { hidden: { $exists: false } },
53
+ { hidden: { $eq: false } }
54
+ ]
55
+ };
56
+
57
+ const sort: any = {};
58
+ sort[sortBy] = sortOrder === 'asc' ? 1 : -1;
59
+
60
+ const newsletterList = await newsletters
61
+ .find(filter)
62
+ .sort(sort)
63
+ .limit(limit)
64
+ .skip(skip)
65
+ .toArray();
66
+
67
+ const listItems: NewsletterListItem[] = newsletterList.map((newsletter: any) => ({
68
+ id: newsletter._id?.toString() || newsletter.id,
69
+ title: newsletter.title,
70
+ slug: newsletter.slug,
71
+ status: newsletter.publication?.status || 'draft',
72
+ subject: newsletter.metadata?.subject || '',
73
+ scheduledDate: newsletter.publication?.scheduledDate,
74
+ sentDate: newsletter.publication?.sentDate,
75
+ authorId: newsletter.publication?.authorId,
76
+ updatedAt: newsletter.updatedAt || newsletter.createdAt,
77
+ recipientCount: newsletter.recipientCount,
78
+ hidden: newsletter.hidden,
79
+ }));
80
+
81
+ return NextResponse.json(listItems);
82
+ } catch (error: any) {
83
+ console.error('[NewsletterAPI] GET_NEWSLETTERS error:', error);
84
+ return NextResponse.json(
85
+ { error: 'Failed to fetch newsletters', detail: error.message },
86
+ { status: 500 }
87
+ );
88
+ }
89
+ }
90
+
91
+ export async function GET_NEWSLETTER(
92
+ req: NextRequest,
93
+ idOrSlug: string,
94
+ config: NewsletterApiConfig
95
+ ): Promise<NextResponse> {
96
+ try {
97
+ const userId = await config.getUserId?.(req);
98
+ if (!userId) {
99
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
100
+ }
101
+
102
+ const dbConnection = await config.getDb();
103
+ const db = dbConnection.db();
104
+ const collectionName = config.collectionName || 'newsletters';
105
+ const newsletters = db.collection(collectionName);
106
+
107
+ const filter = getNewsletterFilter(idOrSlug);
108
+ const newsletter = await newsletters.findOne(filter);
109
+
110
+ if (!newsletter) {
111
+ return NextResponse.json(
112
+ { error: 'Newsletter not found' },
113
+ { status: 404 }
114
+ );
115
+ }
116
+
117
+ const result: Newsletter = {
118
+ id: newsletter._id?.toString() || newsletter.id,
119
+ title: newsletter.title,
120
+ slug: newsletter.slug,
121
+ blocks: newsletter.blocks || [],
122
+ metadata: newsletter.metadata || {
123
+ subject: '',
124
+ previewText: '',
125
+ lang: 'en',
126
+ recipientFilter: { type: 'all' },
127
+ },
128
+ publication: newsletter.publication || {
129
+ status: 'draft',
130
+ updatedAt: new Date().toISOString(),
131
+ },
132
+ createdAt: newsletter.createdAt || new Date().toISOString(),
133
+ updatedAt: newsletter.updatedAt || new Date().toISOString(),
134
+ version: newsletter.version,
135
+ };
136
+
137
+ return NextResponse.json(result);
138
+ } catch (error: any) {
139
+ console.error('[NewsletterAPI] GET_NEWSLETTER error:', error);
140
+ return NextResponse.json(
141
+ { error: 'Failed to fetch newsletter', detail: error.message },
142
+ { status: 500 }
143
+ );
144
+ }
145
+ }
146
+
147
+ export async function POST_NEWSLETTER(
148
+ req: NextRequest,
149
+ config: NewsletterApiConfig
150
+ ): Promise<NextResponse> {
151
+ try {
152
+ const userId = await config.getUserId?.(req);
153
+ if (!userId) {
154
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
155
+ }
156
+
157
+ const body = await req.json();
158
+ const { title, blocks, metadata, publication } = body;
159
+
160
+ const errors: string[] = [];
161
+ if (!title || typeof title !== 'string' || title.trim().length === 0) {
162
+ errors.push('Title is required');
163
+ }
164
+ if (!metadata?.subject || typeof metadata.subject !== 'string' || metadata.subject.trim().length === 0) {
165
+ errors.push('Subject is required');
166
+ }
167
+
168
+ if (errors.length > 0) {
169
+ return NextResponse.json({ message: errors[0], allErrors: errors }, { status: 400 });
170
+ }
171
+
172
+ const dbConnection = await config.getDb();
173
+ const db = dbConnection.db();
174
+ const collectionName = config.collectionName || 'newsletters';
175
+ const newsletters = db.collection(collectionName);
176
+
177
+ const finalTitle = (title?.trim() || metadata?.subject?.trim() || '').trim();
178
+
179
+ // Generate slug for backwards compatibility, but id is primary
180
+ const existingNewsletters = await newsletters.find({}, { projection: { slug: 1 } }).toArray();
181
+ const existingSlugs = existingNewsletters.map((n: any) => n.slug).filter(Boolean);
182
+ const slug = generateSlugFromTitle(finalTitle, existingSlugs);
183
+
184
+ const newsletterDocument = {
185
+ title: finalTitle,
186
+ slug,
187
+ blocks: blocks || [],
188
+ metadata: {
189
+ subject: metadata.subject.trim(),
190
+ previewText: metadata.previewText?.trim() || '',
191
+ lang: metadata.lang || 'en',
192
+ recipientFilter: metadata.recipientFilter || { type: 'all' },
193
+ },
194
+ publication: {
195
+ status: publication?.status || 'draft',
196
+ scheduledDate: publication?.scheduledDate,
197
+ authorId: userId,
198
+ updatedAt: new Date().toISOString(),
199
+ },
200
+ createdAt: new Date(),
201
+ updatedAt: new Date(),
202
+ version: 1,
203
+ };
204
+
205
+ const result = await newsletters.insertOne(newsletterDocument);
206
+
207
+ return NextResponse.json({
208
+ message: 'Newsletter created successfully',
209
+ id: result.insertedId.toString(),
210
+ slug,
211
+ }, { status: 201 });
212
+ } catch (error: any) {
213
+ console.error('[NewsletterAPI] POST_NEWSLETTER error:', error);
214
+ return NextResponse.json(
215
+ { error: 'Failed to create newsletter', detail: error.message },
216
+ { status: 500 }
217
+ );
218
+ }
219
+ }
220
+
221
+ export async function PUT_NEWSLETTER(
222
+ req: NextRequest,
223
+ idOrSlug: string,
224
+ config: NewsletterApiConfig
225
+ ): Promise<NextResponse> {
226
+ try {
227
+ const userId = await config.getUserId?.(req);
228
+ if (!userId) {
229
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
230
+ }
231
+
232
+ const body = await req.json();
233
+ const { title, blocks, metadata, publication } = body;
234
+
235
+ const errors: string[] = [];
236
+ if (!metadata?.subject || typeof metadata.subject !== 'string' || metadata.subject.trim().length === 0) {
237
+ errors.push('Subject is required');
238
+ }
239
+
240
+ const finalTitle = (title?.trim() || metadata?.subject?.trim() || '').trim();
241
+ if (!finalTitle) {
242
+ errors.push('Title is required (use subject if no title is provided)');
243
+ }
244
+
245
+ if (errors.length > 0) {
246
+ return NextResponse.json({ message: errors[0], allErrors: errors }, { status: 400 });
247
+ }
248
+
249
+ const dbConnection = await config.getDb();
250
+ const db = dbConnection.db();
251
+ const collectionName = config.collectionName || 'newsletters';
252
+ const newsletters = db.collection(collectionName);
253
+
254
+ const filter = getNewsletterFilter(idOrSlug);
255
+ const existing = await newsletters.findOne(filter);
256
+ if (!existing) {
257
+ return NextResponse.json(
258
+ { error: 'Newsletter not found' },
259
+ { status: 404 }
260
+ );
261
+ }
262
+
263
+ const updateData: any = {
264
+ title: finalTitle,
265
+ blocks: blocks || [],
266
+ metadata: {
267
+ subject: metadata.subject.trim(),
268
+ previewText: metadata.previewText?.trim() || '',
269
+ lang: metadata.lang || 'en',
270
+ recipientFilter: metadata.recipientFilter || { type: 'all' },
271
+ },
272
+ publication: {
273
+ ...existing.publication,
274
+ status: publication?.status || existing.publication?.status || 'draft',
275
+ scheduledDate: publication?.scheduledDate,
276
+ authorId: userId,
277
+ updatedAt: new Date().toISOString(),
278
+ },
279
+ updatedAt: new Date(),
280
+ version: (existing.version || 1) + 1,
281
+ };
282
+
283
+ await newsletters.updateOne(filter, { $set: updateData });
284
+
285
+ return NextResponse.json({
286
+ message: 'Newsletter updated successfully',
287
+ id: existing._id?.toString(),
288
+ });
289
+ } catch (error: any) {
290
+ console.error('[NewsletterAPI] PUT_NEWSLETTER error:', error);
291
+ return NextResponse.json(
292
+ { error: 'Failed to update newsletter', detail: error.message },
293
+ { status: 500 }
294
+ );
295
+ }
296
+ }
297
+
298
+ export async function DELETE_NEWSLETTER(
299
+ req: NextRequest,
300
+ idOrSlug: string,
301
+ config: NewsletterApiConfig
302
+ ): Promise<NextResponse> {
303
+ try {
304
+ const userId = await config.getUserId?.(req);
305
+ if (!userId) {
306
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
307
+ }
308
+
309
+ const dbConnection = await config.getDb();
310
+ const db = dbConnection.db();
311
+ const collectionName = config.collectionName || 'newsletters';
312
+ const newsletters = db.collection(collectionName);
313
+
314
+ const filter = getNewsletterFilter(idOrSlug);
315
+ const result = await newsletters.deleteOne(filter);
316
+
317
+ if (result.deletedCount === 0) {
318
+ return NextResponse.json(
319
+ { error: 'Newsletter not found' },
320
+ { status: 404 }
321
+ );
322
+ }
323
+
324
+ return NextResponse.json({ message: 'Newsletter deleted successfully' });
325
+ } catch (error: any) {
326
+ console.error('[NewsletterAPI] DELETE_NEWSLETTER error:', error);
327
+ return NextResponse.json(
328
+ { error: 'Failed to delete newsletter', detail: error.message },
329
+ { status: 500 }
330
+ );
331
+ }
332
+ }