@jhits/plugin-newsletter 0.0.8 → 0.0.10

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 (75) hide show
  1. package/data/locales/en/common.json +12 -0
  2. package/data/locales/nl/common.json +12 -0
  3. package/data/locales/sv/common.json +12 -0
  4. package/dist/api/email-utils.d.ts +6 -0
  5. package/dist/api/email-utils.d.ts.map +1 -0
  6. package/dist/api/email-utils.js +134 -0
  7. package/dist/api/handler.d.ts +2 -47
  8. package/dist/api/handler.d.ts.map +1 -1
  9. package/dist/api/handler.js +2 -523
  10. package/dist/api/handlers/index.d.ts +12 -0
  11. package/dist/api/handlers/index.d.ts.map +1 -0
  12. package/dist/api/handlers/index.js +11 -0
  13. package/dist/api/handlers/newsletters.d.ts +11 -0
  14. package/dist/api/handlers/newsletters.d.ts.map +1 -0
  15. package/dist/api/handlers/newsletters.js +250 -0
  16. package/dist/api/handlers/send-newsletter.d.ts +8 -0
  17. package/dist/api/handlers/send-newsletter.d.ts.map +1 -0
  18. package/dist/api/handlers/send-newsletter.js +206 -0
  19. package/dist/api/handlers/settings.d.ts +11 -0
  20. package/dist/api/handlers/settings.d.ts.map +1 -0
  21. package/dist/api/handlers/settings.js +304 -0
  22. package/dist/api/handlers/subscribers.d.ts +10 -0
  23. package/dist/api/handlers/subscribers.d.ts.map +1 -0
  24. package/dist/api/handlers/subscribers.js +96 -0
  25. package/dist/api/handlers/upload.d.ts +7 -0
  26. package/dist/api/handlers/upload.d.ts.map +1 -0
  27. package/dist/api/handlers/upload.js +32 -0
  28. package/dist/api/handlers/welcome-email.d.ts +13 -0
  29. package/dist/api/handlers/welcome-email.d.ts.map +1 -0
  30. package/dist/api/handlers/welcome-email.js +142 -0
  31. package/dist/api/router.d.ts.map +1 -1
  32. package/dist/api/router.js +45 -6
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +42 -16
  35. package/dist/lib/email/EmailRenderer.d.ts.map +1 -1
  36. package/dist/lib/email/EmailRenderer.js +3 -7
  37. package/dist/lib/i18n.d.ts +16 -0
  38. package/dist/lib/i18n.d.ts.map +1 -0
  39. package/dist/lib/i18n.js +60 -0
  40. package/dist/state/EditorContext.d.ts +3 -1
  41. package/dist/state/EditorContext.d.ts.map +1 -1
  42. package/dist/state/EditorContext.js +1 -5
  43. package/dist/state/reducer.js +5 -5
  44. package/dist/types/newsletter.d.ts +1 -0
  45. package/dist/types/newsletter.d.ts.map +1 -1
  46. package/dist/views/CanvasEditor/CanvasEditorView.d.ts +4 -2
  47. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  48. package/dist/views/CanvasEditor/CanvasEditorView.js +73 -10
  49. package/dist/views/CanvasEditor/EditorHeader.d.ts +6 -1
  50. package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
  51. package/dist/views/CanvasEditor/EditorHeader.js +66 -11
  52. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +6 -2
  53. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
  54. package/dist/views/CanvasEditor/components/EditorSidebar.js +3 -3
  55. package/dist/views/CanvasEditor/hooks/useNewsletterLoader.d.ts +1 -1
  56. package/dist/views/CanvasEditor/hooks/useNewsletterLoader.d.ts.map +1 -1
  57. package/dist/views/CanvasEditor/hooks/useNewsletterLoader.js +41 -5
  58. package/dist/views/NewsletterEditor.d.ts +4 -2
  59. package/dist/views/NewsletterEditor.d.ts.map +1 -1
  60. package/dist/views/NewsletterEditor.js +2 -2
  61. package/dist/views/NewsletterManager.d.ts.map +1 -1
  62. package/dist/views/NewsletterManager.js +137 -8
  63. package/dist/views/components/SendNewsletterModal.d.ts +18 -0
  64. package/dist/views/components/SendNewsletterModal.d.ts.map +1 -0
  65. package/dist/views/components/SendNewsletterModal.js +107 -0
  66. package/dist/views/components/SmtpSettingsModal.d.ts +10 -0
  67. package/dist/views/components/SmtpSettingsModal.d.ts.map +1 -0
  68. package/dist/views/components/SmtpSettingsModal.js +145 -0
  69. package/dist/views/components/TestEmailModal.d.ts +10 -0
  70. package/dist/views/components/TestEmailModal.d.ts.map +1 -0
  71. package/dist/views/components/TestEmailModal.js +99 -0
  72. package/package.json +20 -9
  73. package/templates/logo.png +0 -0
  74. package/templates/test-email.hbs +221 -0
  75. package/templates/welcome-email-legacy.hbs +35 -0
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Newsletter API Handler
3
+ */
4
+ 'use server';
5
+ import { NextResponse } from 'next/server';
6
+ import { generateSlugFromTitle } from '../../lib/utils/slugify';
7
+ const ObjectId = require('mongodb').ObjectId;
8
+ function getNewsletterFilter(idOrSlug) {
9
+ // Check if it's a valid MongoDB ObjectId
10
+ if (ObjectId.isValid(idOrSlug) && new ObjectId(idOrSlug).toString() === idOrSlug) {
11
+ return { _id: new ObjectId(idOrSlug) };
12
+ }
13
+ // Otherwise try by slug
14
+ return { slug: idOrSlug };
15
+ }
16
+ export async function GET_NEWSLETTERS(req, config) {
17
+ try {
18
+ const userId = await config.getUserId?.(req);
19
+ if (!userId) {
20
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
21
+ }
22
+ const dbConnection = await config.getDb();
23
+ const db = dbConnection.db();
24
+ const collectionName = config.collectionName || 'newsletters';
25
+ const newsletters = db.collection(collectionName);
26
+ const { searchParams } = new URL(req.url);
27
+ const status = searchParams.get('status');
28
+ const limit = parseInt(searchParams.get('limit') || '50', 10);
29
+ const skip = parseInt(searchParams.get('skip') || '0', 10);
30
+ const sortBy = searchParams.get('sortBy') || 'updatedAt';
31
+ const sortOrder = searchParams.get('sortOrder') || 'desc';
32
+ const query = {};
33
+ if (status) {
34
+ query['publication.status'] = status;
35
+ }
36
+ const filter = {
37
+ ...query,
38
+ $or: [
39
+ { hidden: { $exists: false } },
40
+ { hidden: { $eq: false } }
41
+ ]
42
+ };
43
+ const sort = {};
44
+ sort[sortBy] = sortOrder === 'asc' ? 1 : -1;
45
+ const newsletterList = await newsletters
46
+ .find(filter)
47
+ .sort(sort)
48
+ .limit(limit)
49
+ .skip(skip)
50
+ .toArray();
51
+ const listItems = newsletterList.map((newsletter) => ({
52
+ id: newsletter._id?.toString() || newsletter.id,
53
+ title: newsletter.title,
54
+ slug: newsletter.slug,
55
+ status: newsletter.publication?.status || 'draft',
56
+ subject: newsletter.metadata?.subject || '',
57
+ scheduledDate: newsletter.publication?.scheduledDate,
58
+ sentDate: newsletter.publication?.sentDate,
59
+ authorId: newsletter.publication?.authorId,
60
+ updatedAt: newsletter.updatedAt || newsletter.createdAt,
61
+ recipientCount: newsletter.recipientCount,
62
+ hidden: newsletter.hidden,
63
+ }));
64
+ return NextResponse.json(listItems);
65
+ }
66
+ catch (error) {
67
+ console.error('[NewsletterAPI] GET_NEWSLETTERS error:', error);
68
+ return NextResponse.json({ error: 'Failed to fetch newsletters', detail: error.message }, { status: 500 });
69
+ }
70
+ }
71
+ export async function GET_NEWSLETTER(req, idOrSlug, config) {
72
+ try {
73
+ const userId = await config.getUserId?.(req);
74
+ if (!userId) {
75
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
76
+ }
77
+ const dbConnection = await config.getDb();
78
+ const db = dbConnection.db();
79
+ const collectionName = config.collectionName || 'newsletters';
80
+ const newsletters = db.collection(collectionName);
81
+ const filter = getNewsletterFilter(idOrSlug);
82
+ const newsletter = await newsletters.findOne(filter);
83
+ if (!newsletter) {
84
+ return NextResponse.json({ error: 'Newsletter not found' }, { status: 404 });
85
+ }
86
+ const result = {
87
+ id: newsletter._id?.toString() || newsletter.id,
88
+ title: newsletter.title,
89
+ slug: newsletter.slug,
90
+ blocks: newsletter.blocks || [],
91
+ metadata: newsletter.metadata || {
92
+ subject: '',
93
+ previewText: '',
94
+ lang: 'en',
95
+ recipientFilter: { type: 'all' },
96
+ },
97
+ publication: newsletter.publication || {
98
+ status: 'draft',
99
+ updatedAt: new Date().toISOString(),
100
+ },
101
+ createdAt: newsletter.createdAt || new Date().toISOString(),
102
+ updatedAt: newsletter.updatedAt || new Date().toISOString(),
103
+ version: newsletter.version,
104
+ };
105
+ return NextResponse.json(result);
106
+ }
107
+ catch (error) {
108
+ console.error('[NewsletterAPI] GET_NEWSLETTER error:', error);
109
+ return NextResponse.json({ error: 'Failed to fetch newsletter', detail: error.message }, { status: 500 });
110
+ }
111
+ }
112
+ export async function POST_NEWSLETTER(req, config) {
113
+ try {
114
+ const userId = await config.getUserId?.(req);
115
+ if (!userId) {
116
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
117
+ }
118
+ const body = await req.json();
119
+ const { title, blocks, metadata, publication } = body;
120
+ const errors = [];
121
+ if (!title || typeof title !== 'string' || title.trim().length === 0) {
122
+ errors.push('Title is required');
123
+ }
124
+ if (!metadata?.subject || typeof metadata.subject !== 'string' || metadata.subject.trim().length === 0) {
125
+ errors.push('Subject is required');
126
+ }
127
+ if (errors.length > 0) {
128
+ return NextResponse.json({ message: errors[0], allErrors: errors }, { status: 400 });
129
+ }
130
+ const dbConnection = await config.getDb();
131
+ const db = dbConnection.db();
132
+ const collectionName = config.collectionName || 'newsletters';
133
+ const newsletters = db.collection(collectionName);
134
+ const finalTitle = (title?.trim() || metadata?.subject?.trim() || '').trim();
135
+ // Generate slug for backwards compatibility, but id is primary
136
+ const existingNewsletters = await newsletters.find({}, { projection: { slug: 1 } }).toArray();
137
+ const existingSlugs = existingNewsletters.map((n) => n.slug).filter(Boolean);
138
+ const slug = generateSlugFromTitle(finalTitle, existingSlugs);
139
+ const newsletterDocument = {
140
+ title: finalTitle,
141
+ slug,
142
+ blocks: blocks || [],
143
+ metadata: {
144
+ subject: metadata.subject.trim(),
145
+ previewText: metadata.previewText?.trim() || '',
146
+ lang: metadata.lang || 'en',
147
+ recipientFilter: metadata.recipientFilter || { type: 'all' },
148
+ },
149
+ publication: {
150
+ status: publication?.status || 'draft',
151
+ scheduledDate: publication?.scheduledDate,
152
+ authorId: userId,
153
+ updatedAt: new Date().toISOString(),
154
+ },
155
+ createdAt: new Date(),
156
+ updatedAt: new Date(),
157
+ version: 1,
158
+ };
159
+ const result = await newsletters.insertOne(newsletterDocument);
160
+ return NextResponse.json({
161
+ message: 'Newsletter created successfully',
162
+ id: result.insertedId.toString(),
163
+ slug,
164
+ }, { status: 201 });
165
+ }
166
+ catch (error) {
167
+ console.error('[NewsletterAPI] POST_NEWSLETTER error:', error);
168
+ return NextResponse.json({ error: 'Failed to create newsletter', detail: error.message }, { status: 500 });
169
+ }
170
+ }
171
+ export async function PUT_NEWSLETTER(req, idOrSlug, config) {
172
+ try {
173
+ const userId = await config.getUserId?.(req);
174
+ if (!userId) {
175
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
176
+ }
177
+ const body = await req.json();
178
+ const { title, blocks, metadata, publication } = body;
179
+ const errors = [];
180
+ if (!metadata?.subject || typeof metadata.subject !== 'string' || metadata.subject.trim().length === 0) {
181
+ errors.push('Subject is required');
182
+ }
183
+ const finalTitle = (title?.trim() || metadata?.subject?.trim() || '').trim();
184
+ if (!finalTitle) {
185
+ errors.push('Title is required (use subject if no title is provided)');
186
+ }
187
+ if (errors.length > 0) {
188
+ return NextResponse.json({ message: errors[0], allErrors: errors }, { status: 400 });
189
+ }
190
+ const dbConnection = await config.getDb();
191
+ const db = dbConnection.db();
192
+ const collectionName = config.collectionName || 'newsletters';
193
+ const newsletters = db.collection(collectionName);
194
+ const filter = getNewsletterFilter(idOrSlug);
195
+ const existing = await newsletters.findOne(filter);
196
+ if (!existing) {
197
+ return NextResponse.json({ error: 'Newsletter not found' }, { status: 404 });
198
+ }
199
+ const updateData = {
200
+ title: finalTitle,
201
+ blocks: blocks || [],
202
+ metadata: {
203
+ subject: metadata.subject.trim(),
204
+ previewText: metadata.previewText?.trim() || '',
205
+ lang: metadata.lang || 'en',
206
+ recipientFilter: metadata.recipientFilter || { type: 'all' },
207
+ },
208
+ publication: {
209
+ ...existing.publication,
210
+ status: publication?.status || existing.publication?.status || 'draft',
211
+ scheduledDate: publication?.scheduledDate,
212
+ authorId: userId,
213
+ updatedAt: new Date().toISOString(),
214
+ },
215
+ updatedAt: new Date(),
216
+ version: (existing.version || 1) + 1,
217
+ };
218
+ await newsletters.updateOne(filter, { $set: updateData });
219
+ return NextResponse.json({
220
+ message: 'Newsletter updated successfully',
221
+ id: existing._id?.toString(),
222
+ });
223
+ }
224
+ catch (error) {
225
+ console.error('[NewsletterAPI] PUT_NEWSLETTER error:', error);
226
+ return NextResponse.json({ error: 'Failed to update newsletter', detail: error.message }, { status: 500 });
227
+ }
228
+ }
229
+ export async function DELETE_NEWSLETTER(req, idOrSlug, config) {
230
+ try {
231
+ const userId = await config.getUserId?.(req);
232
+ if (!userId) {
233
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
234
+ }
235
+ const dbConnection = await config.getDb();
236
+ const db = dbConnection.db();
237
+ const collectionName = config.collectionName || 'newsletters';
238
+ const newsletters = db.collection(collectionName);
239
+ const filter = getNewsletterFilter(idOrSlug);
240
+ const result = await newsletters.deleteOne(filter);
241
+ if (result.deletedCount === 0) {
242
+ return NextResponse.json({ error: 'Newsletter not found' }, { status: 404 });
243
+ }
244
+ return NextResponse.json({ message: 'Newsletter deleted successfully' });
245
+ }
246
+ catch (error) {
247
+ console.error('[NewsletterAPI] DELETE_NEWSLETTER error:', error);
248
+ return NextResponse.json({ error: 'Failed to delete newsletter', detail: error.message }, { status: 500 });
249
+ }
250
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Send Newsletter API Handler
3
+ */
4
+ import { NextRequest, NextResponse } from 'next/server';
5
+ import { NewsletterApiConfig } from '../../types/newsletter';
6
+ export declare function POST_SEND_NEWSLETTER(req: NextRequest, idOrSlug: string, config: NewsletterApiConfig): Promise<NextResponse>;
7
+ export declare function GET_NEWSLETTER_FOR_SEND(req: NextRequest, idOrSlug: string, config: NewsletterApiConfig): Promise<NextResponse>;
8
+ //# sourceMappingURL=send-newsletter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send-newsletter.d.ts","sourceRoot":"","sources":["../../../src/api/handlers/send-newsletter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AA4C7D,wBAAsB,oBAAoB,CACtC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA2LvB;AAED,wBAAsB,uBAAuB,CACzC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAuCvB"}
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Send Newsletter API Handler
3
+ */
4
+ 'use server';
5
+ import { NextResponse } from 'next/server';
6
+ import nodemailer from 'nodemailer';
7
+ const ObjectId = require('mongodb').ObjectId;
8
+ async function getSmtpConfig(config) {
9
+ const dbConnection = await config.getDb();
10
+ const db = dbConnection.db();
11
+ const settings = db.collection('settings');
12
+ const smtpConfig = await settings.findOne({ key: 'smtp' });
13
+ if (smtpConfig && smtpConfig.host) {
14
+ return {
15
+ host: smtpConfig.host,
16
+ port: smtpConfig.port || 465,
17
+ user: smtpConfig.user,
18
+ password: smtpConfig.password,
19
+ from: smtpConfig.from,
20
+ fromName: smtpConfig.fromName || '',
21
+ logoUrl: smtpConfig.logoUrl || '',
22
+ };
23
+ }
24
+ return null;
25
+ }
26
+ function getNewsletterFilter(idOrSlug) {
27
+ if (ObjectId.isValid(idOrSlug) && new ObjectId(idOrSlug).toString() === idOrSlug) {
28
+ return { _id: new ObjectId(idOrSlug) };
29
+ }
30
+ return { slug: idOrSlug };
31
+ }
32
+ export async function POST_SEND_NEWSLETTER(req, idOrSlug, config) {
33
+ try {
34
+ const userId = await config.getUserId?.(req);
35
+ if (!userId) {
36
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
37
+ }
38
+ const body = await req.json();
39
+ const { testEmail, language = 'en' } = body;
40
+ const isTest = !!testEmail;
41
+ const dbConnection = await config.getDb();
42
+ const db = dbConnection.db();
43
+ const newsletters = db.collection('newsletters');
44
+ const subscribers = db.collection('subscribers');
45
+ const filter = getNewsletterFilter(idOrSlug);
46
+ const newsletter = await newsletters.findOne(filter);
47
+ if (!newsletter) {
48
+ return NextResponse.json({ error: 'Newsletter not found' }, { status: 404 });
49
+ }
50
+ const blocks = newsletter.blocks || [];
51
+ const metadata = newsletter.metadata || {};
52
+ if (!blocks || blocks.length === 0) {
53
+ return NextResponse.json({ error: 'Newsletter has no content' }, { status: 400 });
54
+ }
55
+ let smtpConfig = await getSmtpConfig(config);
56
+ if (!smtpConfig || !smtpConfig.host || !smtpConfig.user || !smtpConfig.from) {
57
+ return NextResponse.json({ error: 'SMTP not configured. Please configure SMTP settings first.' }, { status: 400 });
58
+ }
59
+ const transporter = nodemailer.createTransport({
60
+ host: smtpConfig.host,
61
+ port: smtpConfig.port,
62
+ secure: smtpConfig.port === 465,
63
+ auth: {
64
+ user: smtpConfig.user,
65
+ pass: smtpConfig.password,
66
+ },
67
+ connectionTimeout: 30000,
68
+ });
69
+ const baseUrl = config.baseUrl || 'http://localhost:3001';
70
+ let logoAttachment = undefined;
71
+ let logoSrc = smtpConfig.logoUrl || `${baseUrl}/logo_black.svg`;
72
+ if (smtpConfig.logoUrl && smtpConfig.logoUrl.startsWith('data:')) {
73
+ const matches = smtpConfig.logoUrl.match(/^data:([^;]+);base64,(.+)$/);
74
+ if (matches) {
75
+ const mimeType = matches[1];
76
+ const base64Data = matches[2];
77
+ const ext = mimeType === 'image/png' ? 'png' : 'jpg';
78
+ logoAttachment = {
79
+ filename: `logo.${ext}`,
80
+ content: Buffer.from(base64Data, 'base64'),
81
+ cid: 'logo',
82
+ contentType: mimeType,
83
+ };
84
+ logoSrc = 'cid:logo';
85
+ }
86
+ }
87
+ const { generateNewsletterEmailHtml } = await import('../../lib/email/EmailRenderer');
88
+ const slugs = {
89
+ sv: '/avmälla',
90
+ nl: '/afmelden',
91
+ en: '/unsubscribe',
92
+ };
93
+ const slug = slugs[language] || slugs.en;
94
+ let recipientEmails = [];
95
+ let recipientCount = 0;
96
+ if (isTest) {
97
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
98
+ if (!emailRegex.test(testEmail)) {
99
+ return NextResponse.json({ error: 'Invalid test email address' }, { status: 400 });
100
+ }
101
+ recipientEmails = [testEmail];
102
+ recipientCount = 1;
103
+ }
104
+ else {
105
+ const subscriberList = await subscribers.find({ status: 'active' }).toArray();
106
+ recipientEmails = subscriberList.map((s) => s.email);
107
+ recipientCount = recipientEmails.length;
108
+ if (recipientCount === 0) {
109
+ return NextResponse.json({ error: 'No active subscribers found' }, { status: 400 });
110
+ }
111
+ }
112
+ const subject = metadata.subject || 'Newsletter';
113
+ const recipientsWithUrls = await Promise.all(recipientEmails.map(async (email) => {
114
+ const subscriber = isTest ? null : await subscribers.findOne({ email });
115
+ const subscriberLang = subscriber?.language || language;
116
+ const subscriberSlug = slugs[subscriberLang] || slugs.en;
117
+ const unsubscribeUrl = `${baseUrl}${subscriberSlug}?email=${encodeURIComponent(email)}`;
118
+ return { email, unsubscribeUrl };
119
+ }));
120
+ let successCount = 0;
121
+ let failedCount = 0;
122
+ for (const { email, unsubscribeUrl } of recipientsWithUrls) {
123
+ try {
124
+ const html = generateNewsletterEmailHtml(blocks, { subject: metadata.subject || '', previewText: metadata.previewText || '' }, {
125
+ baseUrl,
126
+ locale: language,
127
+ logoUrl: logoSrc,
128
+ unsubscribeUrl,
129
+ footerText: `© ${new Date().getFullYear()} ${smtpConfig.fromName || 'Botanics & You'}`,
130
+ });
131
+ await transporter.sendMail({
132
+ from: smtpConfig.fromName
133
+ ? `"${smtpConfig.fromName}" <${smtpConfig.from}>`
134
+ : smtpConfig.from,
135
+ to: email,
136
+ subject,
137
+ html,
138
+ ...(logoAttachment && {
139
+ attachments: [logoAttachment],
140
+ }),
141
+ });
142
+ successCount++;
143
+ }
144
+ catch (err) {
145
+ console.error(`Failed to send email to ${email}:`, err);
146
+ failedCount++;
147
+ }
148
+ }
149
+ if (!isTest && successCount > 0) {
150
+ await newsletters.updateOne(filter, {
151
+ $set: {
152
+ 'publication.status': 'sent',
153
+ 'publication.sentDate': new Date().toISOString(),
154
+ 'publication.recipientCount': successCount,
155
+ updatedAt: new Date().toISOString(),
156
+ }
157
+ });
158
+ }
159
+ return NextResponse.json({
160
+ success: true,
161
+ message: isTest
162
+ ? `Test newsletter sent to ${testEmail}`
163
+ : `Newsletter sent to ${successCount} subscriber${successCount !== 1 ? 's' : ''}${failedCount > 0 ? ` (${failedCount} failed)` : ''}`,
164
+ details: {
165
+ successCount,
166
+ failedCount,
167
+ totalRecipients: recipientCount,
168
+ isTest,
169
+ }
170
+ });
171
+ }
172
+ catch (error) {
173
+ console.error('[NewsletterAPI] POST_SEND_NEWSLETTER error:', error);
174
+ return NextResponse.json({ error: 'Failed to send newsletter', detail: error.message }, { status: 500 });
175
+ }
176
+ }
177
+ export async function GET_NEWSLETTER_FOR_SEND(req, idOrSlug, config) {
178
+ try {
179
+ const userId = await config.getUserId?.(req);
180
+ if (!userId) {
181
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
182
+ }
183
+ const dbConnection = await config.getDb();
184
+ const db = dbConnection.db();
185
+ const newsletters = db.collection('newsletters');
186
+ const subscribers = db.collection('subscribers');
187
+ const filter = getNewsletterFilter(idOrSlug);
188
+ const newsletter = await newsletters.findOne(filter);
189
+ if (!newsletter) {
190
+ return NextResponse.json({ error: 'Newsletter not found' }, { status: 404 });
191
+ }
192
+ const subscriberCount = await subscribers.countDocuments({ status: 'active' });
193
+ return NextResponse.json({
194
+ id: newsletter._id?.toString() || newsletter.id,
195
+ title: newsletter.title,
196
+ subject: newsletter.metadata?.subject || '',
197
+ status: newsletter.publication?.status || 'draft',
198
+ hasContent: (newsletter.blocks?.length || 0) > 0,
199
+ subscriberCount,
200
+ });
201
+ }
202
+ catch (error) {
203
+ console.error('[NewsletterAPI] GET_NEWSLETTER_FOR_SEND error:', error);
204
+ return NextResponse.json({ error: 'Failed to fetch newsletter', detail: error.message }, { status: 500 });
205
+ }
206
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Settings API Handler
3
+ */
4
+ import { NextRequest, NextResponse } from 'next/server';
5
+ import { NewsletterApiConfig } from '../../types/newsletter';
6
+ export declare function GET_SETTINGS(req: NextRequest, config: NewsletterApiConfig): Promise<NextResponse>;
7
+ export declare function POST_SETTINGS(req: NextRequest, config: NewsletterApiConfig): Promise<NextResponse>;
8
+ export declare function GET_SMTP_SETTINGS(req: NextRequest, config: NewsletterApiConfig): Promise<NextResponse>;
9
+ export declare function POST_SMTP_SETTINGS(req: NextRequest, config: NewsletterApiConfig): Promise<NextResponse>;
10
+ export declare function POST_TEST_EMAIL(req: NextRequest, config: NewsletterApiConfig): Promise<NextResponse>;
11
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/api/handlers/settings.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAsB,MAAM,wBAAwB,CAAC;AAoBjF,wBAAsB,YAAY,CAC9B,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA2BvB;AAED,wBAAsB,aAAa,CAC/B,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAgCvB;AAED,wBAAsB,iBAAiB,CACnC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA2CvB;AAED,wBAAsB,kBAAkB,CACpC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAgDvB;AAwBD,wBAAsB,eAAe,CACjC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAoLvB"}