@jhits/plugin-newsletter 0.0.13 → 0.0.15

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 (32) hide show
  1. package/dist/api/handlers/newsletters.d.ts.map +1 -1
  2. package/dist/api/handlers/newsletters.js +49 -9
  3. package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
  4. package/dist/api/handlers/send-newsletter.js +31 -4
  5. package/dist/api/handlers/welcome-email.d.ts.map +1 -1
  6. package/dist/api/handlers/welcome-email.js +5 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -1
  9. package/dist/state/EditorContext.d.ts +3 -1
  10. package/dist/state/EditorContext.d.ts.map +1 -1
  11. package/dist/state/EditorContext.js +2 -2
  12. package/dist/state/types.d.ts +3 -1
  13. package/dist/state/types.d.ts.map +1 -1
  14. package/dist/types/newsletter.d.ts +33 -0
  15. package/dist/types/newsletter.d.ts.map +1 -1
  16. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  17. package/dist/views/CanvasEditor/CanvasEditorView.js +28 -7
  18. package/dist/views/CanvasEditor/hooks/useNewsletterLoader.js +4 -4
  19. package/dist/views/NewsletterManager.d.ts.map +1 -1
  20. package/dist/views/NewsletterManager.js +10 -5
  21. package/dist/views/components/SendNewsletterModal.js +1 -1
  22. package/package.json +1 -1
  23. package/src/api/handlers/newsletters.ts +55 -9
  24. package/src/api/handlers/send-newsletter.ts +33 -4
  25. package/src/api/handlers/welcome-email.ts +5 -2
  26. package/src/index.tsx +4 -1
  27. package/src/state/types.ts +1 -1
  28. package/src/types/newsletter.ts +42 -0
  29. package/src/views/CanvasEditor/CanvasEditorView.tsx +28 -8
  30. package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +4 -4
  31. package/src/views/NewsletterManager.tsx +39 -14
  32. package/src/views/components/SendNewsletterModal.tsx +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"newsletters.d.ts","sourceRoot":"","sources":["../../../src/api/handlers/newsletters.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAkC,MAAM,wBAAwB,CAAC;AAc7F,wBAAsB,eAAe,CACjC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAgEvB;AAED,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAkDvB;AAED,wBAAsB,eAAe,CACjC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAqEvB;AAED,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAuEvB;AAED,wBAAsB,iBAAiB,CACnC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA8BvB"}
1
+ {"version":3,"file":"newsletters.d.ts","sourceRoot":"","sources":["../../../src/api/handlers/newsletters.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAkC,MAAM,wBAAwB,CAAC;AAc7F,wBAAsB,eAAe,CACjC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAkEvB;AAED,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAuEvB;AAED,wBAAsB,eAAe,CACjC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAqEvB;AAED,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA+FvB;AAED,wBAAsB,iBAAiB,CACnC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA8BvB"}
@@ -35,6 +35,7 @@ export async function GET_NEWSLETTERS(req, config) {
35
35
  }
36
36
  const filter = {
37
37
  ...query,
38
+ type: { $ne: 'welcome_email' },
38
39
  $or: [
39
40
  { hidden: { $exists: false } },
40
41
  { hidden: { $eq: false } }
@@ -60,6 +61,7 @@ export async function GET_NEWSLETTERS(req, config) {
60
61
  updatedAt: newsletter.updatedAt || newsletter.createdAt,
61
62
  recipientCount: newsletter.recipientCount,
62
63
  hidden: newsletter.hidden,
64
+ sendHistory: newsletter.sendHistory || [],
63
65
  }));
64
66
  return NextResponse.json(listItems);
65
67
  }
@@ -74,6 +76,8 @@ export async function GET_NEWSLETTER(req, idOrSlug, config) {
74
76
  if (!userId) {
75
77
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
76
78
  }
79
+ const { searchParams } = new URL(req.url);
80
+ const language = searchParams.get('language') || 'en';
77
81
  const dbConnection = await config.getDb();
78
82
  const db = dbConnection.db();
79
83
  const collectionName = config.collectionName || 'newsletters';
@@ -83,21 +87,37 @@ export async function GET_NEWSLETTER(req, idOrSlug, config) {
83
87
  if (!newsletter) {
84
88
  return NextResponse.json({ error: 'Newsletter not found' }, { status: 404 });
85
89
  }
90
+ const languages = newsletter.languages || {};
91
+ const primaryLanguage = newsletter.metadata?.lang || 'en';
92
+ let blocks = newsletter.blocks || [];
93
+ let metadata = newsletter.metadata || {
94
+ subject: '',
95
+ previewText: '',
96
+ lang: 'en',
97
+ recipientFilter: { type: 'all' },
98
+ };
99
+ // If requested language has content, use it
100
+ if (languages[language]) {
101
+ blocks = languages[language].blocks || [];
102
+ metadata = { ...metadata, ...languages[language].metadata, lang: language };
103
+ }
104
+ else if (language !== primaryLanguage && languages[primaryLanguage]) {
105
+ // Copy from primary language if exists
106
+ blocks = languages[primaryLanguage].blocks || [];
107
+ metadata = { ...metadata, ...languages[primaryLanguage].metadata, lang: language };
108
+ }
86
109
  const result = {
87
110
  id: newsletter._id?.toString() || newsletter.id,
88
111
  title: newsletter.title,
89
112
  slug: newsletter.slug,
90
- blocks: newsletter.blocks || [],
91
- metadata: newsletter.metadata || {
92
- subject: '',
93
- previewText: '',
94
- lang: 'en',
95
- recipientFilter: { type: 'all' },
96
- },
113
+ blocks,
114
+ metadata,
97
115
  publication: newsletter.publication || {
98
116
  status: 'draft',
99
117
  updatedAt: new Date().toISOString(),
100
118
  },
119
+ languages,
120
+ sendHistory: newsletter.sendHistory,
101
121
  createdAt: newsletter.createdAt || new Date().toISOString(),
102
122
  updatedAt: newsletter.updatedAt || new Date().toISOString(),
103
123
  version: newsletter.version,
@@ -174,6 +194,8 @@ export async function PUT_NEWSLETTER(req, idOrSlug, config) {
174
194
  if (!userId) {
175
195
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
176
196
  }
197
+ const { searchParams } = new URL(req.url);
198
+ const language = searchParams.get('language') || 'en';
177
199
  const body = await req.json();
178
200
  const { title, blocks, metadata, publication } = body;
179
201
  const errors = [];
@@ -196,15 +218,33 @@ export async function PUT_NEWSLETTER(req, idOrSlug, config) {
196
218
  if (!existing) {
197
219
  return NextResponse.json({ error: 'Newsletter not found' }, { status: 404 });
198
220
  }
221
+ // Preserve existing languages or initialize
222
+ const existingLanguages = existing.languages || {};
223
+ // Update the specific language content
224
+ const updatedLanguages = {
225
+ ...existingLanguages,
226
+ [language]: {
227
+ blocks: blocks || [],
228
+ metadata: {
229
+ subject: metadata.subject.trim(),
230
+ previewText: metadata.previewText?.trim() || '',
231
+ lang: language,
232
+ recipientFilter: metadata.recipientFilter || { type: 'all' },
233
+ },
234
+ },
235
+ };
236
+ // Set primary language if not set
237
+ const primaryLanguage = existing.metadata?.lang || language;
199
238
  const updateData = {
200
239
  title: finalTitle,
201
- blocks: blocks || [],
240
+ blocks: blocks || [], // Keep blocks at root for backwards compatibility
202
241
  metadata: {
203
242
  subject: metadata.subject.trim(),
204
243
  previewText: metadata.previewText?.trim() || '',
205
- lang: metadata.lang || 'en',
244
+ lang: primaryLanguage,
206
245
  recipientFilter: metadata.recipientFilter || { type: 'all' },
207
246
  },
247
+ languages: updatedLanguages,
208
248
  publication: {
209
249
  ...existing.publication,
210
250
  status: publication?.status || existing.publication?.status || 'draft',
@@ -1 +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"}
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,CAwNvB;AAED,wBAAsB,uBAAuB,CACzC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAuCvB"}
@@ -47,8 +47,21 @@ export async function POST_SEND_NEWSLETTER(req, idOrSlug, config) {
47
47
  if (!newsletter) {
48
48
  return NextResponse.json({ error: 'Newsletter not found' }, { status: 404 });
49
49
  }
50
- const blocks = newsletter.blocks || [];
51
- const metadata = newsletter.metadata || {};
50
+ // Get language-specific content if available
51
+ const languages = newsletter.languages || {};
52
+ const primaryLanguage = newsletter.metadata?.lang || 'en';
53
+ let blocks = newsletter.blocks || [];
54
+ let metadata = newsletter.metadata || {};
55
+ // If the requested language has content, use it
56
+ if (language && languages[language]) {
57
+ blocks = languages[language].blocks || [];
58
+ metadata = { ...metadata, ...languages[language].metadata };
59
+ }
60
+ else if (language !== primaryLanguage && languages[primaryLanguage]) {
61
+ // Fall back to primary language content
62
+ blocks = languages[primaryLanguage].blocks || [];
63
+ metadata = { ...metadata, ...languages[primaryLanguage].metadata };
64
+ }
52
65
  if (!blocks || blocks.length === 0) {
53
66
  return NextResponse.json({ error: 'Newsletter has no content' }, { status: 400 });
54
67
  }
@@ -102,11 +115,16 @@ export async function POST_SEND_NEWSLETTER(req, idOrSlug, config) {
102
115
  recipientCount = 1;
103
116
  }
104
117
  else {
105
- const subscriberList = await subscribers.find({ status: 'active' }).toArray();
118
+ // Filter subscribers by the selected language
119
+ const subscriberFilter = { status: 'active' };
120
+ if (language) {
121
+ subscriberFilter.language = language;
122
+ }
123
+ const subscriberList = await subscribers.find(subscriberFilter).toArray();
106
124
  recipientEmails = subscriberList.map((s) => s.email);
107
125
  recipientCount = recipientEmails.length;
108
126
  if (recipientCount === 0) {
109
- return NextResponse.json({ error: 'No active subscribers found' }, { status: 400 });
127
+ return NextResponse.json({ error: `No active subscribers found for language: ${language?.toUpperCase() || 'en'}` }, { status: 400 });
110
128
  }
111
129
  }
112
130
  const subject = metadata.subject || 'Newsletter';
@@ -147,12 +165,21 @@ export async function POST_SEND_NEWSLETTER(req, idOrSlug, config) {
147
165
  }
148
166
  }
149
167
  if (!isTest && successCount > 0) {
168
+ const sendHistoryEntry = {
169
+ language,
170
+ sentAt: new Date().toISOString(),
171
+ recipientCount: successCount,
172
+ authorId: userId,
173
+ };
150
174
  await newsletters.updateOne(filter, {
151
175
  $set: {
152
176
  'publication.status': 'sent',
153
177
  'publication.sentDate': new Date().toISOString(),
154
178
  'publication.recipientCount': successCount,
155
179
  updatedAt: new Date().toISOString(),
180
+ },
181
+ $push: {
182
+ sendHistory: sendHistoryEntry,
156
183
  }
157
184
  });
158
185
  }
@@ -1 +1 @@
1
- {"version":3,"file":"welcome-email.d.ts","sourceRoot":"","sources":["../../../src/api/handlers/welcome-email.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAajF,wBAAsB,wBAAwB,CAC1C,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAsCvB;AAED,wBAAsB,iBAAiB,CACnC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA6DvB;AAED,wBAAsB,kBAAkB,CACpC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAyDvB;AAED,wBAAsB,yBAAyB,CAC3C,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,EAC3B,QAAQ,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IAAC,QAAQ,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAc1D"}
1
+ {"version":3,"file":"welcome-email.d.ts","sourceRoot":"","sources":["../../../src/api/handlers/welcome-email.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAajF,wBAAsB,wBAAwB,CAC1C,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAsCvB;AAED,wBAAsB,iBAAiB,CACnC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAgEvB;AAED,wBAAsB,kBAAkB,CACpC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAyDvB;AAED,wBAAsB,yBAAyB,CAC3C,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,EAC3B,QAAQ,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IAAC,QAAQ,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAc1D"}
@@ -62,7 +62,7 @@ export async function GET_WELCOME_EMAIL(req, config) {
62
62
  title: 'Welcome Email',
63
63
  language,
64
64
  blocks: primaryContent.blocks,
65
- metadata: primaryContent.metadata,
65
+ metadata: { ...primaryContent.metadata, lang: language },
66
66
  languages,
67
67
  isCopy: true,
68
68
  copyFrom: primaryLanguage,
@@ -74,7 +74,10 @@ export async function GET_WELCOME_EMAIL(req, config) {
74
74
  title: 'Welcome Email',
75
75
  language,
76
76
  blocks: languages[language]?.blocks || primaryContent.blocks,
77
- metadata: languages[language]?.metadata || primaryContent.metadata,
77
+ metadata: {
78
+ ...(languages[language]?.metadata || primaryContent.metadata),
79
+ lang: language
80
+ },
78
81
  languages,
79
82
  isCopy: false,
80
83
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAOtD;;;GAGG;AACH,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,gBAAgB,CAAC,EAAE;QACf,iDAAiD;QACjD,KAAK,EAAE,MAAM,CAAC;QACd,gDAAgD;QAChD,IAAI,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACL;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,KAAK,EAAE,WAAW,2CA8Q1D;AAGD,OAAO,EAAE,gBAAgB,IAAI,KAAK,EAAE,CAAC;AAGrC,YAAY,EACR,KAAK,EACL,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,eAAe,GAClB,MAAM,eAAe,CAAC;AAGvB,YAAY,EACR,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,GAC1B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AAC9C,YAAY,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAC;AAGrD,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAGpF,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAOtD;;;GAGG;AACH,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,gBAAgB,CAAC,EAAE;QACf,iDAAiD;QACjD,KAAK,EAAE,MAAM,CAAC;QACd,gDAAgD;QAChD,IAAI,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACL;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,KAAK,EAAE,WAAW,2CAiR1D;AAGD,OAAO,EAAE,gBAAgB,IAAI,KAAK,EAAE,CAAC;AAGrC,YAAY,EACR,KAAK,EACL,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,eAAe,GAClB,MAAM,eAAe,CAAC;AAGvB,YAAY,EACR,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,GAC1B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AAC9C,YAAY,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAC;AAGrD,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAGpF,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -130,10 +130,12 @@ export default function NewsletterPlugin(props) {
130
130
  apiData.metadata = apiData.metadata || {};
131
131
  apiData.metadata.lang = extraData.language;
132
132
  }
133
+ // Build URL with language param if provided
134
+ const langParam = extraData?.language ? `?language=${extraData.language}` : '';
133
135
  // If we have an id, try to update first
134
136
  if (originalId) {
135
137
  console.log('[NewsletterPlugin] Attempting to update newsletter with id:', originalId);
136
- const updateResponse = await fetch(`/api/plugin-newsletter/newsletters/${originalId}`, {
138
+ const updateResponse = await fetch(`/api/plugin-newsletter/newsletters/${originalId}${langParam}`, {
137
139
  method: 'PUT',
138
140
  headers: { 'Content-Type': 'application/json' },
139
141
  credentials: 'include',
@@ -14,7 +14,9 @@ export interface EditorProviderProps {
14
14
  /** Initial state (optional) */
15
15
  initialState?: Partial<EditorState>;
16
16
  /** Callback when save is triggered */
17
- onSave?: (state: EditorState) => Promise<void>;
17
+ onSave?: (state: EditorState, extraData?: {
18
+ language?: string;
19
+ }) => Promise<void>;
18
20
  /**
19
21
  * Custom blocks from client application
20
22
  * These blocks will be registered in the BlockRegistry on mount
@@ -1 +1 @@
1
- {"version":3,"file":"EditorContext.d.ts","sourceRoot":"","sources":["../../src/state/EditorContext.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAmG,MAAM,OAAO,CAAC;AAExH,OAAO,EAAE,WAAW,EAAoC,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAG5F,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAMvD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,+BAA+B;IAC/B,YAAY,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IACpC,sCAAsC;IACtC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;;;OAGG;IACH,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,gBAAgB,CAAC,EAAE;QACf,iDAAiD;QACjD,KAAK,EAAE,MAAM,CAAC;QACd,gDAAgD;QAChD,IAAI,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,oDAAoD;IACpD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC3B,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,YAAiB,EACjB,QAAe,EACf,gBAAgB,EAChB,cAAc,EACjB,EAAE,mBAAmB,2CA8MrB;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,kBAAkB,CAM9C"}
1
+ {"version":3,"file":"EditorContext.d.ts","sourceRoot":"","sources":["../../src/state/EditorContext.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAmG,MAAM,OAAO,CAAC;AAExH,OAAO,EAAE,WAAW,EAAoC,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAG5F,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAMvD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,+BAA+B;IAC/B,YAAY,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IACpC,sCAAsC;IACtC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF;;;OAGG;IACH,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,gBAAgB,CAAC,EAAE;QACf,iDAAiD;QACjD,KAAK,EAAE,MAAM,CAAC;QACd,gDAAgD;QAChD,IAAI,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,oDAAoD;IACpD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC3B,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,YAAiB,EACjB,QAAe,EACf,gBAAgB,EAChB,cAAc,EACjB,EAAE,mBAAmB,2CA8MrB;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,kBAAkB,CAM9C"}
@@ -116,9 +116,9 @@ export function EditorProvider({ children, initialState, onSave, customBlocks =
116
116
  const resetEditor = useCallback(() => {
117
117
  dispatch({ type: 'RESET_EDITOR' });
118
118
  }, []);
119
- const save = useCallback(async () => {
119
+ const save = useCallback(async (extraData) => {
120
120
  if (onSave) {
121
- await onSave(stateRef.current);
121
+ await onSave(stateRef.current, extraData);
122
122
  dispatch({ type: 'MARK_CLEAN' });
123
123
  }
124
124
  }, [onSave]);
@@ -139,7 +139,9 @@ export interface EditorContextValue {
139
139
  /** Reset editor to initial state */
140
140
  resetEditor: () => void;
141
141
  /** Save current state (triggers save callback) */
142
- save: () => Promise<void>;
142
+ save: (extraData?: {
143
+ language?: string;
144
+ }) => Promise<void>;
143
145
  /** Undo last action */
144
146
  undo: () => void;
145
147
  /** Redo last undone action */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/state/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/state/types.ts"],"names":[],"mappings":"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"}
@@ -38,6 +38,21 @@ export interface NewsletterPublicationData {
38
38
  authorId?: string;
39
39
  /** Last modified date */
40
40
  updatedAt?: string;
41
+ /** Recipient count for last send */
42
+ recipientCount?: number;
43
+ }
44
+ /**
45
+ * Send history entry for tracking sends per language
46
+ */
47
+ export interface SendHistoryEntry {
48
+ /** Language code sent to */
49
+ language: string;
50
+ /** When this was sent */
51
+ sentAt: string;
52
+ /** Number of recipients */
53
+ recipientCount: number;
54
+ /** Who sent it (author ID) */
55
+ authorId?: string;
41
56
  }
42
57
  /**
43
58
  * Newsletter Metadata
@@ -55,6 +70,19 @@ export interface NewsletterMetadata {
55
70
  value?: string;
56
71
  };
57
72
  }
73
+ /**
74
+ * Newsletter language content (blocks + metadata per language)
75
+ */
76
+ export interface NewsletterLanguageContent {
77
+ blocks: Block[];
78
+ metadata: NewsletterMetadata;
79
+ }
80
+ /**
81
+ * Languages object storing content per language
82
+ */
83
+ export interface NewsletterLanguages {
84
+ [key: string]: NewsletterLanguageContent;
85
+ }
58
86
  /**
59
87
  * Complete Newsletter Structure
60
88
  * This is the headless JSON structure stored in the database
@@ -70,6 +98,10 @@ export interface Newsletter {
70
98
  blocks: Block[];
71
99
  /** Publication data */
72
100
  publication: NewsletterPublicationData;
101
+ /** Send history for tracking multiple sends per language */
102
+ sendHistory?: SendHistoryEntry[];
103
+ /** Content per language (for multi-language newsletters) */
104
+ languages?: NewsletterLanguages;
73
105
  /** Additional metadata */
74
106
  metadata: NewsletterMetadata;
75
107
  /** Creation timestamp */
@@ -95,6 +127,7 @@ export interface NewsletterListItem {
95
127
  updatedAt: string;
96
128
  recipientCount?: number;
97
129
  hidden?: boolean;
130
+ sendHistory?: SendHistoryEntry[];
98
131
  }
99
132
  /**
100
133
  * Newsletter Filter Options
@@ -1 +1 @@
1
- {"version":3,"file":"newsletter.d.ts","sourceRoot":"","sources":["../../src/types/newsletter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"newsletter.d.ts","sourceRoot":"","sources":["../../src/types/newsletter.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CanvasEditorView.d.ts","sourceRoot":"","sources":["../../../src/views/CanvasEditor/CanvasEditorView.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,qBAAqB;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,gBAAgB,CAAC,EAAE;QACf,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,oDAAoD;IACpD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,gBAAgB,CAAC,EAAE,YAAY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,qBAAqB,2CAwT1J"}
1
+ {"version":3,"file":"CanvasEditorView.d.ts","sourceRoot":"","sources":["../../../src/views/CanvasEditor/CanvasEditorView.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,qBAAqB;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,gBAAgB,CAAC,EAAE;QACf,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,oDAAoD;IACpD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,gBAAgB,CAAC,EAAE,YAAY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,qBAAqB,2CA4U1J"}
@@ -25,12 +25,23 @@ export function CanvasEditorView({ newsletterId, darkMode, backgroundColors: pro
25
25
  const [currentLanguage, setCurrentLanguage] = useState(locale || 'en');
26
26
  // Get registered blocks
27
27
  const registeredBlocks = useRegisteredBlocks();
28
- // Newsletter loading - use current language for welcome email (wait until language settings are loaded)
28
+ // Newsletter loading - wait for language settings to be loaded first
29
29
  const { isLoadingNewsletter } = useNewsletterLoader(newsletterId, state.newsletterId, (newsletter) => {
30
30
  helpers.loadNewsletter(newsletter);
31
- // Set current language from loaded newsletter metadata
32
- if (newsletter.metadata?.lang && !isWelcomeEmail) {
33
- setCurrentLanguage(newsletter.metadata.lang);
31
+ // Set current language from loaded newsletter (check both metadata.lang and top-level language)
32
+ const loadedLanguage = newsletter.metadata?.lang || newsletter.language;
33
+ if (loadedLanguage) {
34
+ setCurrentLanguage(loadedLanguage);
35
+ }
36
+ // Update available languages from newsletter's languages object (for regular newsletters)
37
+ if (!isWelcomeEmail && newsletter.languages && Object.keys(newsletter.languages).length > 0) {
38
+ const langs = Object.keys(newsletter.languages);
39
+ setAvailableLanguages(langs);
40
+ }
41
+ // For welcome emails, also update available languages
42
+ if (isWelcomeEmail && newsletter.languages && Object.keys(newsletter.languages).length > 0) {
43
+ const langs = Object.keys(newsletter.languages);
44
+ setAvailableLanguages(langs);
34
45
  }
35
46
  setTimeout(() => {
36
47
  dispatch({ type: 'MARK_CLEAN' });
@@ -75,8 +86,17 @@ export function CanvasEditorView({ newsletterId, darkMode, backgroundColors: pro
75
86
  }
76
87
  setCurrentLanguage(newLanguage);
77
88
  // Reload with new language
78
- const response = await fetch(`/api/plugin-newsletter/welcome-email?language=${newLanguage}`);
79
- if (response.ok) {
89
+ let response;
90
+ if (isWelcomeEmail) {
91
+ response = await fetch(`/api/plugin-newsletter/welcome-email?language=${newLanguage}`);
92
+ }
93
+ else if (newsletterId) {
94
+ response = await fetch(`/api/plugin-newsletter/newsletters/${newsletterId}?language=${newLanguage}`);
95
+ }
96
+ else {
97
+ return;
98
+ }
99
+ if (response && response.ok) {
80
100
  const newsletter = await response.json();
81
101
  helpers.loadNewsletter(newsletter);
82
102
  dispatch({ type: 'MARK_CLEAN' });
@@ -170,7 +190,8 @@ export function CanvasEditorView({ newsletterId, darkMode, backgroundColors: pro
170
190
  setIsSaving(true);
171
191
  setSaveError(null);
172
192
  try {
173
- await helpers.save();
193
+ // Always pass language for saving (both welcome email and regular newsletters)
194
+ await helpers.save({ language: currentLanguage });
174
195
  setIsSaving(false);
175
196
  }
176
197
  catch (error) {
@@ -7,13 +7,12 @@ export function useNewsletterLoader(newsletterId, currentNewsletterId, loadNewsl
7
7
  if (hasLoadedRef.current) {
8
8
  return;
9
9
  }
10
- // For welcome emails, wait until language is provided
11
- if (isWelcomeEmail && !language) {
10
+ // Wait until language is provided (for both welcome emails and regular newsletters)
11
+ if (!language) {
12
12
  return;
13
13
  }
14
14
  // Skip if we have a regular newsletter id but no id yet, or if this is welcome email mode
15
15
  if (isWelcomeEmail) {
16
- // Load welcome email with language
17
16
  // Load welcome email with language
18
17
  const loadWelcomeEmail = async () => {
19
18
  try {
@@ -41,7 +40,8 @@ export function useNewsletterLoader(newsletterId, currentNewsletterId, loadNewsl
41
40
  const loadNewsletterData = async () => {
42
41
  try {
43
42
  setIsLoadingNewsletter(true);
44
- const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletterId}`);
43
+ const langParam = language ? `?language=${language}` : '';
44
+ const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletterId}${langParam}`);
45
45
  if (!response.ok) {
46
46
  throw new Error('Failed to load newsletter');
47
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NewsletterManager.d.ts","sourceRoot":"","sources":["../../src/views/NewsletterManager.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,WAAW,0BAA0B;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAClB;AAiBD,wBAAgB,qBAAqB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,0BAA0B,2CAgcnF"}
1
+ {"version":3,"file":"NewsletterManager.d.ts","sourceRoot":"","sources":["../../src/views/NewsletterManager.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,WAAW,0BAA0B;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAClB;AAiBD,wBAAgB,qBAAqB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,0BAA0B,2CAydnF"}
@@ -141,10 +141,6 @@ export function NewsletterManagerView({ siteId, locale }) {
141
141
  setShowSmtpModal(true);
142
142
  return;
143
143
  }
144
- if (newsletter.status === 'sent') {
145
- alert('This newsletter has already been sent');
146
- return;
147
- }
148
144
  try {
149
145
  const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletter.id}/send`, {
150
146
  credentials: 'include',
@@ -201,6 +197,15 @@ export function NewsletterManagerView({ siteId, locale }) {
201
197
  year: 'numeric',
202
198
  });
203
199
  };
200
+ // Format last sent info
201
+ const formatLastSent = (sendHistory) => {
202
+ if (!sendHistory || sendHistory.length === 0)
203
+ return null;
204
+ const lastEntry = sendHistory[sendHistory.length - 1];
205
+ const langLabel = lastEntry.language.toUpperCase();
206
+ const dateStr = formatDate(lastEntry.sentAt);
207
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-[10px] font-black px-2 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/20", children: langLabel }), _jsx("span", { className: "text-xs text-dashboard-text-secondary", children: dateStr }), sendHistory.length > 1 && (_jsxs("span", { className: "text-[10px] text-dashboard-text-secondary", title: "Total sends", children: ["(", sendHistory.length, ")"] }))] }));
208
+ };
204
209
  return (_jsxs(_Fragment, { children: [_jsx("div", { className: "h-full w-full rounded-[2.5rem] bg-white dark:bg-neutral-900 p-8 overflow-y-auto", children: _jsxs("div", { className: "max-w-7xl mx-auto", children: [_jsxs("div", { className: "flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-black text-dashboard-text uppercase tracking-tighter mb-2", children: "Newsletters" }), _jsx("p", { className: "text-sm text-dashboard-text-secondary", children: "Create and manage your email newsletters" })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("button", { onClick: () => window.location.href = '/dashboard/newsletter/subscribers', className: "inline-flex items-center gap-2 px-4 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border", children: [_jsx(Users, { size: 14 }), "Subscribers"] }), _jsxs("button", { onClick: () => {
205
210
  if (smtpStatus === 'not_configured') {
206
211
  alert('Please configure SMTP settings first');
@@ -211,7 +216,7 @@ export function NewsletterManagerView({ siteId, locale }) {
211
216
  }
212
217
  }, className: "inline-flex items-center gap-2 px-4 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border", children: [_jsx(Send, { size: 14 }), "Test Email"] }), _jsxs("button", { onClick: () => setShowSmtpModal(true), className: `inline-flex items-center gap-2 px-4 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors ${smtpStatus === 'not_configured'
213
218
  ? 'bg-amber-50 border-amber-300 text-amber-700 dark:bg-amber-900/20 dark:border-amber-700 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/40'
214
- : 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'}`, children: [_jsx(Settings2, { size: 14 }), "SMTP", smtpStatus === 'not_configured' && (_jsx("span", { className: "w-2 h-2 rounded-full bg-amber-500" }))] }), _jsxs("select", { value: statusFilter, onChange: (e) => setStatusFilter(e.target.value), className: "bg-dashboard-bg border border-dashboard-border rounded-xl px-4 py-2.5 text-xs font-bold text-dashboard-text outline-none cursor-pointer uppercase tracking-widest", children: [_jsx("option", { value: "all", children: "All Status" }), _jsx("option", { value: "draft", children: "Draft" }), _jsx("option", { value: "scheduled", children: "Scheduled" }), _jsx("option", { value: "sent", children: "Sent" }), _jsx("option", { value: "archived", children: "Archived" })] }), _jsxs("button", { onClick: handleCreate, className: "inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90", children: [_jsx(Plus, { size: 14 }), "New Newsletter"] })] })] }), _jsx("div", { className: "mb-6 p-6 rounded-2xl bg-gradient-to-br from-primary/5 via-primary/10 to-transparent border border-dashboard-border", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-5", children: [_jsx("div", { className: "w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Sparkles, { className: "w-6 h-6 text-primary" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-black text-dashboard-text uppercase tracking-tight mb-1", children: "Welcome Email" }), _jsx("p", { className: "text-xs text-dashboard-text-secondary mb-2", children: "The email sent automatically when someone subscribes" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [welcomeEmailStatus === 'configured' ? (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-green-600 dark:text-green-400", children: [_jsx(CheckCircle2, { size: 12 }), "Configured"] })) : (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-amber-600 dark:text-amber-400", children: [_jsx(Clock, { size: 12 }), "Not configured"] })), _jsxs("button", { onClick: () => window.location.href = '/dashboard/newsletter/subscribers', className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-primary hover:underline", children: [_jsx(Users, { size: 12 }), subscriberCount, " subscriber", subscriberCount !== 1 ? 's' : ''] }), welcomeEmailLastUpdated && (_jsxs("span", { className: "text-[10px] text-dashboard-text-secondary", children: ["Last updated: ", new Date(welcomeEmailLastUpdated).toLocaleDateString()] }))] })] })] }), _jsxs("button", { onClick: () => window.location.href = '/dashboard/newsletter/welcome', className: "inline-flex items-center gap-2 px-4 py-2.5 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors bg-primary text-white hover:bg-primary/90", children: [_jsx(Edit2, { size: 14 }), welcomeEmailStatus === 'configured' ? 'Edit' : 'Configure'] })] }) }), _jsx("div", { className: "bg-dashboard-bg rounded-3xl border border-dashboard-border overflow-hidden", children: isLoading ? (_jsx("div", { className: "flex items-center justify-center py-20", children: _jsx("div", { className: "w-8 h-8 border-4 border-primary/20 border-t-primary rounded-full animate-spin" }) })) : filteredNewsletters.length === 0 ? (_jsxs("div", { className: "py-24 text-center", children: [_jsx(Mail, { size: 64, className: "mx-auto text-dashboard-text-secondary mb-4" }), _jsx("p", { className: "text-dashboard-text-secondary font-serif italic text-lg mb-6", children: statusFilter === 'all' ? 'No newsletters yet.' : `No newsletters found with status "${statusFilter}".` }), _jsxs("button", { onClick: handleCreate, className: "inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90", children: [_jsx(Plus, { size: 14 }), "Create Your First Newsletter"] })] })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-left border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "bg-dashboard-bg text-dashboard-text text-[10px] uppercase tracking-[0.2em] font-black border-b border-dashboard-border", children: [_jsx("th", { className: "px-8 py-5", children: "Title" }), _jsx("th", { className: "px-8 py-5", children: "Subject" }), _jsx("th", { className: "px-8 py-5", children: "Status" }), _jsx("th", { className: "px-8 py-5 text-right", children: "Updated" }), _jsx("th", { className: "px-8 py-5 text-right", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-dashboard-border", children: filteredNewsletters.map((newsletter) => (_jsxs("tr", { className: "hover:bg-dashboard-bg transition-colors group", children: [_jsx("td", { className: "px-8 py-5", children: _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover:bg-primary group-hover:text-white transition-colors", children: _jsx(Mail, { size: 18 }) }), _jsx("span", { className: "text-sm font-medium text-dashboard-text tracking-tight", children: newsletter.title })] }) }), _jsx("td", { className: "px-8 py-5", children: _jsx("span", { className: "text-sm text-dashboard-text-secondary", children: newsletter.subject || 'No subject' }) }), _jsx("td", { className: "px-8 py-5", children: _jsx("span", { className: `text-[10px] font-black px-3 py-1 rounded-full uppercase border ${getStatusBadgeColor(newsletter.status)}`, children: newsletter.status }) }), _jsx("td", { className: "px-8 py-5 text-right text-xs text-dashboard-text-secondary font-medium", children: _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(Calendar, { size: 14 }), formatDate(newsletter.updatedAt)] }) }), _jsx("td", { className: "px-8 py-5 text-right", children: _jsxs("div", { className: "flex items-center justify-end gap-2", children: [newsletter.status !== 'sent' && (_jsx("button", { onClick: () => handleSend(newsletter), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors", title: "Send newsletter", children: _jsx(Send, { size: 18 }) })), _jsx("button", { onClick: () => handleEdit(newsletter.id), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors", title: "Edit newsletter", children: _jsx(Edit2, { size: 18 }) }), _jsx("button", { onClick: () => handleDelete(newsletter.id, newsletter.title), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors", title: "Delete newsletter", children: _jsx(Trash2, { size: 18 }) })] }) })] }, newsletter.id))) })] }) })) })] }) }), _jsx(SmtpSettingsModal, { isOpen: showSmtpModal, onClose: () => setShowSmtpModal(false) }), _jsx(TestEmailModal, { isOpen: showTestEmailModal, onClose: () => setShowTestEmailModal(false) }), selectedNewsletter && (_jsx(SendNewsletterModal, { isOpen: showSendModal, onClose: () => {
219
+ : 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'}`, children: [_jsx(Settings2, { size: 14 }), "SMTP", smtpStatus === 'not_configured' && (_jsx("span", { className: "w-2 h-2 rounded-full bg-amber-500" }))] }), _jsxs("select", { value: statusFilter, onChange: (e) => setStatusFilter(e.target.value), className: "bg-dashboard-bg border border-dashboard-border rounded-xl px-4 py-2.5 text-xs font-bold text-dashboard-text outline-none cursor-pointer uppercase tracking-widest", children: [_jsx("option", { value: "all", children: "All Status" }), _jsx("option", { value: "draft", children: "Draft" }), _jsx("option", { value: "scheduled", children: "Scheduled" }), _jsx("option", { value: "sent", children: "Sent" }), _jsx("option", { value: "archived", children: "Archived" })] }), _jsxs("button", { onClick: handleCreate, className: "inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90", children: [_jsx(Plus, { size: 14 }), "New Newsletter"] })] })] }), _jsx("div", { className: "mb-6 p-6 rounded-2xl bg-gradient-to-br from-primary/5 via-primary/10 to-transparent border border-dashboard-border", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-5", children: [_jsx("div", { className: "w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Sparkles, { className: "w-6 h-6 text-primary" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-black text-dashboard-text uppercase tracking-tight mb-1", children: "Welcome Email" }), _jsx("p", { className: "text-xs text-dashboard-text-secondary mb-2", children: "The email sent automatically when someone subscribes" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [welcomeEmailStatus === 'configured' ? (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-green-600 dark:text-green-400", children: [_jsx(CheckCircle2, { size: 12 }), "Configured"] })) : (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-amber-600 dark:text-amber-400", children: [_jsx(Clock, { size: 12 }), "Not configured"] })), _jsxs("button", { onClick: () => window.location.href = '/dashboard/newsletter/subscribers', className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-primary hover:underline", children: [_jsx(Users, { size: 12 }), subscriberCount, " subscriber", subscriberCount !== 1 ? 's' : ''] }), welcomeEmailLastUpdated && (_jsxs("span", { className: "text-[10px] text-dashboard-text-secondary", children: ["Last updated: ", new Date(welcomeEmailLastUpdated).toLocaleDateString()] }))] })] })] }), _jsxs("button", { onClick: () => window.location.href = '/dashboard/newsletter/welcome', className: "inline-flex items-center gap-2 px-4 py-2.5 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors bg-primary text-white hover:bg-primary/90", children: [_jsx(Edit2, { size: 14 }), welcomeEmailStatus === 'configured' ? 'Edit' : 'Configure'] })] }) }), _jsx("div", { className: "bg-dashboard-bg rounded-3xl border border-dashboard-border overflow-hidden", children: isLoading ? (_jsx("div", { className: "flex items-center justify-center py-20", children: _jsx("div", { className: "w-8 h-8 border-4 border-primary/20 border-t-primary rounded-full animate-spin" }) })) : filteredNewsletters.length === 0 ? (_jsxs("div", { className: "py-24 text-center", children: [_jsx(Mail, { size: 64, className: "mx-auto text-dashboard-text-secondary mb-4" }), _jsx("p", { className: "text-dashboard-text-secondary font-serif italic text-lg mb-6", children: statusFilter === 'all' ? 'No newsletters yet.' : `No newsletters found with status "${statusFilter}".` }), _jsxs("button", { onClick: handleCreate, className: "inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90", children: [_jsx(Plus, { size: 14 }), "Create Your First Newsletter"] })] })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-left border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "bg-dashboard-bg text-dashboard-text text-[10px] uppercase tracking-[0.2em] font-black border-b border-dashboard-border", children: [_jsx("th", { className: "px-8 py-5", children: "Title" }), _jsx("th", { className: "px-8 py-5", children: "Subject" }), _jsx("th", { className: "px-8 py-5", children: "Status" }), _jsx("th", { className: "px-8 py-5", children: "Last Sent" }), _jsx("th", { className: "px-8 py-5 text-right", children: "Updated" }), _jsx("th", { className: "px-8 py-5 text-right", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-dashboard-border", children: filteredNewsletters.map((newsletter) => (_jsxs("tr", { className: "hover:bg-dashboard-bg transition-colors group", children: [_jsx("td", { className: "px-8 py-5", children: _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover:bg-primary group-hover:text-white transition-colors", children: _jsx(Mail, { size: 18 }) }), _jsx("span", { className: "text-sm font-medium text-dashboard-text tracking-tight", children: newsletter.title })] }) }), _jsx("td", { className: "px-8 py-5", children: _jsx("span", { className: "text-sm text-dashboard-text-secondary", children: newsletter.subject || 'No subject' }) }), _jsx("td", { className: "px-8 py-5", children: _jsx("span", { className: `text-[10px] font-black px-3 py-1 rounded-full uppercase border ${getStatusBadgeColor(newsletter.status)}`, children: newsletter.status }) }), _jsx("td", { className: "px-8 py-5", children: formatLastSent(newsletter.sendHistory) || (_jsx("span", { className: "text-xs text-dashboard-text-secondary", children: "Never" })) }), _jsx("td", { className: "px-8 py-5 text-right text-xs text-dashboard-text-secondary font-medium", children: _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(Calendar, { size: 14 }), formatDate(newsletter.updatedAt)] }) }), _jsx("td", { className: "px-8 py-5 text-right", children: _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx("button", { onClick: () => handleSend(newsletter), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors", title: newsletter.status === 'sent' ? 'Resend newsletter' : 'Send newsletter', children: _jsx(Send, { size: 18 }) }), _jsx("button", { onClick: () => handleEdit(newsletter.id), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors", title: "Edit newsletter", children: _jsx(Edit2, { size: 18 }) }), _jsx("button", { onClick: () => handleDelete(newsletter.id, newsletter.title), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors", title: "Delete newsletter", children: _jsx(Trash2, { size: 18 }) })] }) })] }, newsletter.id))) })] }) })) })] }) }), _jsx(SmtpSettingsModal, { isOpen: showSmtpModal, onClose: () => setShowSmtpModal(false) }), _jsx(TestEmailModal, { isOpen: showTestEmailModal, onClose: () => setShowTestEmailModal(false) }), selectedNewsletter && (_jsx(SendNewsletterModal, { isOpen: showSendModal, onClose: () => {
215
220
  setShowSendModal(false);
216
221
  setSelectedNewsletter(null);
217
222
  }, newsletter: {
@@ -82,7 +82,7 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
82
82
  const canSend = newsletter.hasContent && (sendMode === 'test' || subscriberCount > 0);
83
83
  const isAlreadySent = newsletter.status === 'sent';
84
84
  const noSubscribersError = sendMode === 'subscribers' && subscriberCount === 0;
85
- return (_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [_jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-sm", onClick: () => !isSending && onClose() }), _jsxs("div", { className: "relative w-full max-w-md mx-4 bg-white dark:bg-neutral-900 rounded-3xl border border-dashboard-border shadow-2xl overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-8 py-6 border-b border-dashboard-border", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-10 h-10 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Send, { className: "w-5 h-5 text-primary" }) }), _jsxs("div", { children: [_jsx("h2", { className: "text-xl font-black text-dashboard-text uppercase tracking-tight", children: "Send Newsletter" }), _jsx("p", { className: "text-xs text-dashboard-text-secondary truncate max-w-[200px]", children: newsletter.title })] })] }), _jsx("button", { onClick: () => !isSending && onClose(), disabled: isSending, className: "p-2 rounded-full hover:bg-dashboard-border transition-colors", children: _jsx(X, { size: 20, className: "text-dashboard-text-secondary" }) })] }), _jsxs("div", { className: "p-8", children: [isAlreadySent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800", children: _jsxs("div", { className: "flex items-center gap-2 text-amber-700 dark:text-amber-400", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter has already been sent" })] }) })), !newsletter.hasContent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800", children: _jsxs("div", { className: "flex items-center gap-2 text-red-700 dark:text-red-400", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter has no content" })] }) })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Send To" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: () => setSendMode('subscribers'), className: `flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${sendMode === 'subscribers'
85
+ return (_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [_jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-sm", onClick: () => !isSending && onClose() }), _jsxs("div", { className: "relative w-full max-w-md mx-4 bg-white dark:bg-neutral-900 rounded-3xl border border-dashboard-border shadow-2xl overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-8 py-6 border-b border-dashboard-border", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-10 h-10 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Send, { className: "w-5 h-5 text-primary" }) }), _jsxs("div", { children: [_jsxs("h2", { className: "text-xl font-black text-dashboard-text uppercase tracking-tight", children: [isAlreadySent ? 'Resend' : 'Send', " Newsletter"] }), _jsx("p", { className: "text-xs text-dashboard-text-secondary truncate max-w-[200px]", children: newsletter.title })] })] }), _jsx("button", { onClick: () => !isSending && onClose(), disabled: isSending, className: "p-2 rounded-full hover:bg-dashboard-border transition-colors", children: _jsx(X, { size: 20, className: "text-dashboard-text-secondary" }) })] }), _jsxs("div", { className: "p-8", children: [isAlreadySent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800", children: _jsxs("div", { className: "flex items-center gap-2 text-blue-700 dark:text-blue-400", children: [_jsx(RefreshCw, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter was already sent - sending again" })] }) })), !newsletter.hasContent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800", children: _jsxs("div", { className: "flex items-center gap-2 text-red-700 dark:text-red-400", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter has no content" })] }) })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Send To" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: () => setSendMode('subscribers'), className: `flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${sendMode === 'subscribers'
86
86
  ? 'bg-primary text-white'
87
87
  : subscriberCount > 0
88
88
  ? 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhits/plugin-newsletter",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Newsletter management and email delivery plugin for the JHITS ecosystem",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -77,6 +77,7 @@ export async function GET_NEWSLETTERS(
77
77
  updatedAt: newsletter.updatedAt || newsletter.createdAt,
78
78
  recipientCount: newsletter.recipientCount,
79
79
  hidden: newsletter.hidden,
80
+ sendHistory: newsletter.sendHistory || [],
80
81
  }));
81
82
 
82
83
  return NextResponse.json(listItems);
@@ -100,6 +101,9 @@ export async function GET_NEWSLETTER(
100
101
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
101
102
  }
102
103
 
104
+ const { searchParams } = new URL(req.url);
105
+ const language = searchParams.get('language') || 'en';
106
+
103
107
  const dbConnection = await config.getDb();
104
108
  const db = dbConnection.db();
105
109
  const collectionName = config.collectionName || 'newsletters';
@@ -115,21 +119,39 @@ export async function GET_NEWSLETTER(
115
119
  );
116
120
  }
117
121
 
122
+ const languages = newsletter.languages || {};
123
+ const primaryLanguage = newsletter.metadata?.lang || 'en';
124
+
125
+ let blocks = newsletter.blocks || [];
126
+ let metadata = newsletter.metadata || {
127
+ subject: '',
128
+ previewText: '',
129
+ lang: 'en',
130
+ recipientFilter: { type: 'all' },
131
+ };
132
+
133
+ // If requested language has content, use it
134
+ if (languages[language]) {
135
+ blocks = languages[language].blocks || [];
136
+ metadata = { ...metadata, ...languages[language].metadata, lang: language };
137
+ } else if (language !== primaryLanguage && languages[primaryLanguage]) {
138
+ // Copy from primary language if exists
139
+ blocks = languages[primaryLanguage].blocks || [];
140
+ metadata = { ...metadata, ...languages[primaryLanguage].metadata, lang: language };
141
+ }
142
+
118
143
  const result: Newsletter = {
119
144
  id: newsletter._id?.toString() || newsletter.id,
120
145
  title: newsletter.title,
121
146
  slug: newsletter.slug,
122
- blocks: newsletter.blocks || [],
123
- metadata: newsletter.metadata || {
124
- subject: '',
125
- previewText: '',
126
- lang: 'en',
127
- recipientFilter: { type: 'all' },
128
- },
147
+ blocks,
148
+ metadata,
129
149
  publication: newsletter.publication || {
130
150
  status: 'draft',
131
151
  updatedAt: new Date().toISOString(),
132
152
  },
153
+ languages,
154
+ sendHistory: newsletter.sendHistory,
133
155
  createdAt: newsletter.createdAt || new Date().toISOString(),
134
156
  updatedAt: newsletter.updatedAt || new Date().toISOString(),
135
157
  version: newsletter.version,
@@ -230,6 +252,9 @@ export async function PUT_NEWSLETTER(
230
252
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
231
253
  }
232
254
 
255
+ const { searchParams } = new URL(req.url);
256
+ const language = searchParams.get('language') || 'en';
257
+
233
258
  const body = await req.json();
234
259
  const { title, blocks, metadata, publication } = body;
235
260
 
@@ -261,15 +286,36 @@ export async function PUT_NEWSLETTER(
261
286
  );
262
287
  }
263
288
 
289
+ // Preserve existing languages or initialize
290
+ const existingLanguages = existing.languages || {};
291
+
292
+ // Update the specific language content
293
+ const updatedLanguages = {
294
+ ...existingLanguages,
295
+ [language]: {
296
+ blocks: blocks || [],
297
+ metadata: {
298
+ subject: metadata.subject.trim(),
299
+ previewText: metadata.previewText?.trim() || '',
300
+ lang: language,
301
+ recipientFilter: metadata.recipientFilter || { type: 'all' },
302
+ },
303
+ },
304
+ };
305
+
306
+ // Set primary language if not set
307
+ const primaryLanguage = existing.metadata?.lang || language;
308
+
264
309
  const updateData: any = {
265
310
  title: finalTitle,
266
- blocks: blocks || [],
311
+ blocks: blocks || [], // Keep blocks at root for backwards compatibility
267
312
  metadata: {
268
313
  subject: metadata.subject.trim(),
269
314
  previewText: metadata.previewText?.trim() || '',
270
- lang: metadata.lang || 'en',
315
+ lang: primaryLanguage,
271
316
  recipientFilter: metadata.recipientFilter || { type: 'all' },
272
317
  },
318
+ languages: updatedLanguages,
273
319
  publication: {
274
320
  ...existing.publication,
275
321
  status: publication?.status || existing.publication?.status || 'draft',
@@ -79,8 +79,22 @@ export async function POST_SEND_NEWSLETTER(
79
79
  );
80
80
  }
81
81
 
82
- const blocks = newsletter.blocks || [];
83
- const metadata = newsletter.metadata || {};
82
+ // Get language-specific content if available
83
+ const languages = newsletter.languages || {};
84
+ const primaryLanguage = newsletter.metadata?.lang || 'en';
85
+
86
+ let blocks = newsletter.blocks || [];
87
+ let metadata = newsletter.metadata || {};
88
+
89
+ // If the requested language has content, use it
90
+ if (language && languages[language]) {
91
+ blocks = languages[language].blocks || [];
92
+ metadata = { ...metadata, ...languages[language].metadata };
93
+ } else if (language !== primaryLanguage && languages[primaryLanguage]) {
94
+ // Fall back to primary language content
95
+ blocks = languages[primaryLanguage].blocks || [];
96
+ metadata = { ...metadata, ...languages[primaryLanguage].metadata };
97
+ }
84
98
 
85
99
  if (!blocks || blocks.length === 0) {
86
100
  return NextResponse.json(
@@ -150,13 +164,18 @@ export async function POST_SEND_NEWSLETTER(
150
164
  recipientEmails = [testEmail];
151
165
  recipientCount = 1;
152
166
  } else {
153
- const subscriberList = await subscribers.find({ status: 'active' }).toArray();
167
+ // Filter subscribers by the selected language
168
+ const subscriberFilter: any = { status: 'active' };
169
+ if (language) {
170
+ subscriberFilter.language = language;
171
+ }
172
+ const subscriberList = await subscribers.find(subscriberFilter).toArray();
154
173
  recipientEmails = subscriberList.map((s: any) => s.email);
155
174
  recipientCount = recipientEmails.length;
156
175
 
157
176
  if (recipientCount === 0) {
158
177
  return NextResponse.json(
159
- { error: 'No active subscribers found' },
178
+ { error: `No active subscribers found for language: ${language?.toUpperCase() || 'en'}` },
160
179
  { status: 400 }
161
180
  );
162
181
  }
@@ -211,12 +230,22 @@ export async function POST_SEND_NEWSLETTER(
211
230
  }
212
231
 
213
232
  if (!isTest && successCount > 0) {
233
+ const sendHistoryEntry = {
234
+ language,
235
+ sentAt: new Date().toISOString(),
236
+ recipientCount: successCount,
237
+ authorId: userId,
238
+ };
239
+
214
240
  await newsletters.updateOne(filter, {
215
241
  $set: {
216
242
  'publication.status': 'sent',
217
243
  'publication.sentDate': new Date().toISOString(),
218
244
  'publication.recipientCount': successCount,
219
245
  updatedAt: new Date().toISOString(),
246
+ },
247
+ $push: {
248
+ sendHistory: sendHistoryEntry,
220
249
  }
221
250
  });
222
251
  }
@@ -101,7 +101,7 @@ export async function GET_WELCOME_EMAIL(
101
101
  title: 'Welcome Email',
102
102
  language,
103
103
  blocks: primaryContent.blocks,
104
- metadata: primaryContent.metadata,
104
+ metadata: { ...primaryContent.metadata, lang: language },
105
105
  languages,
106
106
  isCopy: true,
107
107
  copyFrom: primaryLanguage,
@@ -114,7 +114,10 @@ export async function GET_WELCOME_EMAIL(
114
114
  title: 'Welcome Email',
115
115
  language,
116
116
  blocks: languages[language as keyof WelcomeEmailLanguages]?.blocks || primaryContent.blocks,
117
- metadata: languages[language as keyof WelcomeEmailLanguages]?.metadata || primaryContent.metadata,
117
+ metadata: {
118
+ ...(languages[language as keyof WelcomeEmailLanguages]?.metadata || primaryContent.metadata),
119
+ lang: language
120
+ },
118
121
  languages,
119
122
  isCopy: false,
120
123
  });
package/src/index.tsx CHANGED
@@ -175,10 +175,13 @@ export default function NewsletterPlugin(props: PluginProps) {
175
175
  apiData.metadata.lang = extraData.language;
176
176
  }
177
177
 
178
+ // Build URL with language param if provided
179
+ const langParam = extraData?.language ? `?language=${extraData.language}` : '';
180
+
178
181
  // If we have an id, try to update first
179
182
  if (originalId) {
180
183
  console.log('[NewsletterPlugin] Attempting to update newsletter with id:', originalId);
181
- const updateResponse = await fetch(`/api/plugin-newsletter/newsletters/${originalId}`, {
184
+ const updateResponse = await fetch(`/api/plugin-newsletter/newsletters/${originalId}${langParam}`, {
182
185
  method: 'PUT',
183
186
  headers: { 'Content-Type': 'application/json' },
184
187
  credentials: 'include',
@@ -114,7 +114,7 @@ export interface EditorContextValue {
114
114
  resetEditor: () => void;
115
115
 
116
116
  /** Save current state (triggers save callback) */
117
- save: () => Promise<void>;
117
+ save: (extraData?: { language?: string }) => Promise<void>;
118
118
 
119
119
  /** Undo last action */
120
120
  undo: () => void;
@@ -47,6 +47,26 @@ export interface NewsletterPublicationData {
47
47
 
48
48
  /** Last modified date */
49
49
  updatedAt?: string;
50
+
51
+ /** Recipient count for last send */
52
+ recipientCount?: number;
53
+ }
54
+
55
+ /**
56
+ * Send history entry for tracking sends per language
57
+ */
58
+ export interface SendHistoryEntry {
59
+ /** Language code sent to */
60
+ language: string;
61
+
62
+ /** When this was sent */
63
+ sentAt: string;
64
+
65
+ /** Number of recipients */
66
+ recipientCount: number;
67
+
68
+ /** Who sent it (author ID) */
69
+ authorId?: string;
50
70
  }
51
71
 
52
72
  /**
@@ -69,6 +89,21 @@ export interface NewsletterMetadata {
69
89
  };
70
90
  }
71
91
 
92
+ /**
93
+ * Newsletter language content (blocks + metadata per language)
94
+ */
95
+ export interface NewsletterLanguageContent {
96
+ blocks: Block[];
97
+ metadata: NewsletterMetadata;
98
+ }
99
+
100
+ /**
101
+ * Languages object storing content per language
102
+ */
103
+ export interface NewsletterLanguages {
104
+ [key: string]: NewsletterLanguageContent;
105
+ }
106
+
72
107
  /**
73
108
  * Complete Newsletter Structure
74
109
  * This is the headless JSON structure stored in the database
@@ -89,6 +124,12 @@ export interface Newsletter {
89
124
  /** Publication data */
90
125
  publication: NewsletterPublicationData;
91
126
 
127
+ /** Send history for tracking multiple sends per language */
128
+ sendHistory?: SendHistoryEntry[];
129
+
130
+ /** Content per language (for multi-language newsletters) */
131
+ languages?: NewsletterLanguages;
132
+
92
133
  /** Additional metadata */
93
134
  metadata: NewsletterMetadata;
94
135
 
@@ -118,6 +159,7 @@ export interface NewsletterListItem {
118
159
  updatedAt: string;
119
160
  recipientCount?: number;
120
161
  hidden?: boolean;
162
+ sendHistory?: SendHistoryEntry[];
121
163
  }
122
164
 
123
165
  /**
@@ -46,15 +46,26 @@ export function CanvasEditorView({ newsletterId, darkMode, backgroundColors: pro
46
46
  // Get registered blocks
47
47
  const registeredBlocks = useRegisteredBlocks();
48
48
 
49
- // Newsletter loading - use current language for welcome email (wait until language settings are loaded)
49
+ // Newsletter loading - wait for language settings to be loaded first
50
50
  const { isLoadingNewsletter } = useNewsletterLoader(
51
51
  newsletterId,
52
52
  state.newsletterId,
53
- (newsletter) => {
53
+ (newsletter: any) => {
54
54
  helpers.loadNewsletter(newsletter);
55
- // Set current language from loaded newsletter metadata
56
- if (newsletter.metadata?.lang && !isWelcomeEmail) {
57
- setCurrentLanguage(newsletter.metadata.lang);
55
+ // Set current language from loaded newsletter (check both metadata.lang and top-level language)
56
+ const loadedLanguage = newsletter.metadata?.lang || newsletter.language;
57
+ if (loadedLanguage) {
58
+ setCurrentLanguage(loadedLanguage);
59
+ }
60
+ // Update available languages from newsletter's languages object (for regular newsletters)
61
+ if (!isWelcomeEmail && newsletter.languages && Object.keys(newsletter.languages).length > 0) {
62
+ const langs = Object.keys(newsletter.languages);
63
+ setAvailableLanguages(langs);
64
+ }
65
+ // For welcome emails, also update available languages
66
+ if (isWelcomeEmail && newsletter.languages && Object.keys(newsletter.languages).length > 0) {
67
+ const langs = Object.keys(newsletter.languages);
68
+ setAvailableLanguages(langs);
58
69
  }
59
70
  setTimeout(() => {
60
71
  dispatch({ type: 'MARK_CLEAN' });
@@ -108,8 +119,16 @@ export function CanvasEditorView({ newsletterId, darkMode, backgroundColors: pro
108
119
  setCurrentLanguage(newLanguage);
109
120
 
110
121
  // Reload with new language
111
- const response = await fetch(`/api/plugin-newsletter/welcome-email?language=${newLanguage}`);
112
- if (response.ok) {
122
+ let response;
123
+ if (isWelcomeEmail) {
124
+ response = await fetch(`/api/plugin-newsletter/welcome-email?language=${newLanguage}`);
125
+ } else if (newsletterId) {
126
+ response = await fetch(`/api/plugin-newsletter/newsletters/${newsletterId}?language=${newLanguage}`);
127
+ } else {
128
+ return;
129
+ }
130
+
131
+ if (response && response.ok) {
113
132
  const newsletter = await response.json();
114
133
  helpers.loadNewsletter(newsletter);
115
134
  dispatch({ type: 'MARK_CLEAN' });
@@ -218,7 +237,8 @@ export function CanvasEditorView({ newsletterId, darkMode, backgroundColors: pro
218
237
  setIsSaving(true);
219
238
  setSaveError(null);
220
239
  try {
221
- await helpers.save(isWelcomeEmail ? { language: currentLanguage } : undefined);
240
+ // Always pass language for saving (both welcome email and regular newsletters)
241
+ await helpers.save({ language: currentLanguage });
222
242
  setIsSaving(false);
223
243
  } catch (error: any) {
224
244
  console.error('[CanvasEditorView] Save error:', error);
@@ -17,14 +17,13 @@ export function useNewsletterLoader(
17
17
  return;
18
18
  }
19
19
 
20
- // For welcome emails, wait until language is provided
21
- if (isWelcomeEmail && !language) {
20
+ // Wait until language is provided (for both welcome emails and regular newsletters)
21
+ if (!language) {
22
22
  return;
23
23
  }
24
24
 
25
25
  // Skip if we have a regular newsletter id but no id yet, or if this is welcome email mode
26
26
  if (isWelcomeEmail) {
27
- // Load welcome email with language
28
27
  // Load welcome email with language
29
28
  const loadWelcomeEmail = async () => {
30
29
  try {
@@ -51,7 +50,8 @@ export function useNewsletterLoader(
51
50
  const loadNewsletterData = async () => {
52
51
  try {
53
52
  setIsLoadingNewsletter(true);
54
- const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletterId}`);
53
+ const langParam = language ? `?language=${language}` : '';
54
+ const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletterId}${langParam}`);
55
55
  if (!response.ok) {
56
56
  throw new Error('Failed to load newsletter');
57
57
  }
@@ -7,7 +7,7 @@
7
7
 
8
8
  import React, { useState, useEffect } from 'react';
9
9
  import { Plus, Mail, Calendar, Trash2, Edit2, Settings2, Sparkles, CheckCircle2, Clock, Send, Users } from 'lucide-react';
10
- import { NewsletterListItem, NewsletterStatus } from '../types/newsletter';
10
+ import { NewsletterListItem, NewsletterStatus, SendHistoryEntry } from '../types/newsletter';
11
11
  import { SmtpSettingsModal } from './components/SmtpSettingsModal';
12
12
  import { TestEmailModal } from './components/TestEmailModal';
13
13
  import { SendNewsletterModal } from './components/SendNewsletterModal';
@@ -153,10 +153,6 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
153
153
  setShowSmtpModal(true);
154
154
  return;
155
155
  }
156
- if (newsletter.status === 'sent') {
157
- alert('This newsletter has already been sent');
158
- return;
159
- }
160
156
 
161
157
  try {
162
158
  const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletter.id}/send`, {
@@ -220,6 +216,31 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
220
216
  });
221
217
  };
222
218
 
219
+ // Format last sent info
220
+ const formatLastSent = (sendHistory: SendHistoryEntry[] | undefined) => {
221
+ if (!sendHistory || sendHistory.length === 0) return null;
222
+
223
+ const lastEntry = sendHistory[sendHistory.length - 1];
224
+ const langLabel = lastEntry.language.toUpperCase();
225
+ const dateStr = formatDate(lastEntry.sentAt);
226
+
227
+ return (
228
+ <div className="flex items-center gap-2">
229
+ <span className="text-[10px] font-black px-2 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/20">
230
+ {langLabel}
231
+ </span>
232
+ <span className="text-xs text-dashboard-text-secondary">
233
+ {dateStr}
234
+ </span>
235
+ {sendHistory.length > 1 && (
236
+ <span className="text-[10px] text-dashboard-text-secondary" title="Total sends">
237
+ ({sendHistory.length})
238
+ </span>
239
+ )}
240
+ </div>
241
+ );
242
+ };
243
+
223
244
  return (
224
245
  <>
225
246
  <div className="h-full w-full rounded-[2.5rem] bg-white dark:bg-neutral-900 p-8 overflow-y-auto">
@@ -380,6 +401,7 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
380
401
  <th className="px-8 py-5">Title</th>
381
402
  <th className="px-8 py-5">Subject</th>
382
403
  <th className="px-8 py-5">Status</th>
404
+ <th className="px-8 py-5">Last Sent</th>
383
405
  <th className="px-8 py-5 text-right">Updated</th>
384
406
  <th className="px-8 py-5 text-right">Actions</th>
385
407
  </tr>
@@ -410,6 +432,11 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
410
432
  {newsletter.status}
411
433
  </span>
412
434
  </td>
435
+ <td className="px-8 py-5">
436
+ {formatLastSent(newsletter.sendHistory) || (
437
+ <span className="text-xs text-dashboard-text-secondary">Never</span>
438
+ )}
439
+ </td>
413
440
  <td className="px-8 py-5 text-right text-xs text-dashboard-text-secondary font-medium">
414
441
  <div className="flex items-center justify-end gap-2">
415
442
  <Calendar size={14} />
@@ -418,15 +445,13 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
418
445
  </td>
419
446
  <td className="px-8 py-5 text-right">
420
447
  <div className="flex items-center justify-end gap-2">
421
- {newsletter.status !== 'sent' && (
422
- <button
423
- onClick={() => handleSend(newsletter)}
424
- className="p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors"
425
- title="Send newsletter"
426
- >
427
- <Send size={18} />
428
- </button>
429
- )}
448
+ <button
449
+ onClick={() => handleSend(newsletter)}
450
+ className="p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors"
451
+ title={newsletter.status === 'sent' ? 'Resend newsletter' : 'Send newsletter'}
452
+ >
453
+ <Send size={18} />
454
+ </button>
430
455
  <button
431
456
  onClick={() => handleEdit(newsletter.id)}
432
457
  className="p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors"
@@ -127,7 +127,7 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
127
127
  </div>
128
128
  <div>
129
129
  <h2 className="text-xl font-black text-dashboard-text uppercase tracking-tight">
130
- Send Newsletter
130
+ {isAlreadySent ? 'Resend' : 'Send'} Newsletter
131
131
  </h2>
132
132
  <p className="text-xs text-dashboard-text-secondary truncate max-w-[200px]">
133
133
  {newsletter.title}
@@ -146,10 +146,10 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
146
146
  {/* Content */}
147
147
  <div className="p-8">
148
148
  {isAlreadySent && (
149
- <div className="mb-4 p-4 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800">
150
- <div className="flex items-center gap-2 text-amber-700 dark:text-amber-400">
151
- <AlertCircle size={16} />
152
- <span className="text-xs font-bold uppercase">This newsletter has already been sent</span>
149
+ <div className="mb-4 p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
150
+ <div className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
151
+ <RefreshCw size={16} />
152
+ <span className="text-xs font-bold uppercase">This newsletter was already sent - sending again</span>
153
153
  </div>
154
154
  </div>
155
155
  )}