@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.
- package/dist/api/handlers/newsletters.d.ts.map +1 -1
- package/dist/api/handlers/newsletters.js +49 -9
- package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
- package/dist/api/handlers/send-newsletter.js +31 -4
- package/dist/api/handlers/welcome-email.d.ts.map +1 -1
- package/dist/api/handlers/welcome-email.js +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/state/EditorContext.d.ts +3 -1
- package/dist/state/EditorContext.d.ts.map +1 -1
- package/dist/state/EditorContext.js +2 -2
- package/dist/state/types.d.ts +3 -1
- package/dist/state/types.d.ts.map +1 -1
- package/dist/types/newsletter.d.ts +33 -0
- package/dist/types/newsletter.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.js +28 -7
- package/dist/views/CanvasEditor/hooks/useNewsletterLoader.js +4 -4
- package/dist/views/NewsletterManager.d.ts.map +1 -1
- package/dist/views/NewsletterManager.js +10 -5
- package/dist/views/components/SendNewsletterModal.js +1 -1
- package/package.json +1 -1
- package/src/api/handlers/newsletters.ts +55 -9
- package/src/api/handlers/send-newsletter.ts +33 -4
- package/src/api/handlers/welcome-email.ts +5 -2
- package/src/index.tsx +4 -1
- package/src/state/types.ts +1 -1
- package/src/types/newsletter.ts +42 -0
- package/src/views/CanvasEditor/CanvasEditorView.tsx +28 -8
- package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +4 -4
- package/src/views/NewsletterManager.tsx +39 -14
- 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,
|
|
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
|
|
91
|
-
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:
|
|
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,
|
|
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
|
-
|
|
51
|
-
const
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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:
|
|
77
|
+
metadata: {
|
|
78
|
+
...(languages[language]?.metadata || primaryContent.metadata),
|
|
79
|
+
lang: language
|
|
80
|
+
},
|
|
78
81
|
languages,
|
|
79
82
|
isCopy: false,
|
|
80
83
|
});
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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;
|
|
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]);
|
package/dist/state/types.d.ts
CHANGED
|
@@ -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: (
|
|
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":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,MAAM,WAAW,UAAU;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,IAAI,GAAG,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,QAAQ,GAAG,cAAc,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE;QACP,CAAC,GAAG,EAAE,MAAM,GAAG;YACX,KAAK,EAAE,MAAM,CAAC;YACd,OAAO,EAAE,MAAM,CAAC;SACnB,CAAC;KACL,CAAC;IACF,SAAS,CAAC,EAAE,IAAI,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,WAAW,GAAG,MAAM,GAAG,UAAU,CAAC;AAE3E;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACtC,yBAAyB;IACzB,MAAM,EAAE,gBAAgB,CAAC;IAEzB,uCAAuC;IACvC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,gBAAgB;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;
|
|
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,
|
|
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 -
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
79
|
-
if (
|
|
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
|
-
|
|
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
|
-
//
|
|
11
|
-
if (
|
|
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
|
|
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,
|
|
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: [
|
|
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: [
|
|
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
|
@@ -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
|
|
123
|
-
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:
|
|
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
|
-
|
|
83
|
-
const
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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',
|
package/src/state/types.ts
CHANGED
|
@@ -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;
|
package/src/types/newsletter.ts
CHANGED
|
@@ -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 -
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
112
|
-
if (
|
|
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
|
-
|
|
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
|
-
//
|
|
21
|
-
if (
|
|
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
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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-
|
|
150
|
-
<div className="flex items-center gap-2 text-
|
|
151
|
-
<
|
|
152
|
-
<span className="text-xs font-bold uppercase">This newsletter
|
|
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
|
)}
|