@jhits/plugin-newsletter 0.0.14 → 0.0.16
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 +80 -24
- 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/types/newsletter.d.ts +35 -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 +96 -9
- package/dist/views/components/NewsletterCard.d.ts +16 -0
- package/dist/views/components/NewsletterCard.d.ts.map +1 -0
- package/dist/views/components/NewsletterCard.js +94 -0
- package/dist/views/components/NewsletterGrid.d.ts +16 -0
- package/dist/views/components/NewsletterGrid.d.ts.map +1 -0
- package/dist/views/components/NewsletterGrid.js +13 -0
- package/dist/views/components/SendNewsletterModal.js +1 -1
- package/package.json +1 -1
- package/src/api/handlers/newsletters.ts +94 -28
- 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/types/newsletter.ts +44 -0
- package/src/views/CanvasEditor/CanvasEditorView.tsx +28 -8
- package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +4 -4
- package/src/views/NewsletterManager.tsx +203 -20
- package/src/views/components/NewsletterCard.tsx +212 -0
- package/src/views/components/NewsletterGrid.tsx +48 -0
- package/src/views/components/SendNewsletterModal.tsx +5 -5
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
'use client';
|
|
6
6
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
7
7
|
import { useState, useEffect } from 'react';
|
|
8
|
-
import { Plus, Mail, Calendar, Trash2, Edit2, Settings2, Sparkles, CheckCircle2, Clock, Send, Users } from 'lucide-react';
|
|
8
|
+
import { Plus, Mail, Calendar, Trash2, Edit2, Settings2, Sparkles, CheckCircle2, Clock, Send, Users, Grid, List } from 'lucide-react';
|
|
9
9
|
import { SmtpSettingsModal } from './components/SmtpSettingsModal';
|
|
10
10
|
import { TestEmailModal } from './components/TestEmailModal';
|
|
11
11
|
import { SendNewsletterModal } from './components/SendNewsletterModal';
|
|
12
|
+
import { NewsletterGrid } from './components/NewsletterGrid';
|
|
12
13
|
function getStatusBadgeColor(status) {
|
|
13
14
|
switch (status) {
|
|
14
15
|
case 'sent':
|
|
@@ -27,6 +28,8 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
27
28
|
const [newsletters, setNewsletters] = useState([]);
|
|
28
29
|
const [isLoading, setIsLoading] = useState(true);
|
|
29
30
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
31
|
+
const [primaryLanguage, setPrimaryLanguage] = useState('en');
|
|
32
|
+
const [viewMode, setViewMode] = useState('grid');
|
|
30
33
|
const [showSmtpModal, setShowSmtpModal] = useState(false);
|
|
31
34
|
const [showTestEmailModal, setShowTestEmailModal] = useState(false);
|
|
32
35
|
const [showSendModal, setShowSendModal] = useState(false);
|
|
@@ -36,12 +39,33 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
36
39
|
const [welcomeEmailStatus, setWelcomeEmailStatus] = useState('not_configured');
|
|
37
40
|
const [welcomeEmailLastUpdated, setWelcomeEmailLastUpdated] = useState(null);
|
|
38
41
|
const [subscriberCount, setSubscriberCount] = useState(0);
|
|
42
|
+
// Hover state for language tooltip
|
|
43
|
+
const [hoveredNewsletter, setHoveredNewsletter] = useState(null);
|
|
44
|
+
// Fetch primary language from SMTP settings
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const fetchPrimaryLanguage = async () => {
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch('/api/plugin-newsletter/smtp', {
|
|
49
|
+
credentials: 'include',
|
|
50
|
+
});
|
|
51
|
+
if (response.ok) {
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
setPrimaryLanguage(data.primaryLanguage || 'en');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('Failed to fetch SMTP settings:', error);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
fetchPrimaryLanguage();
|
|
61
|
+
}, []);
|
|
39
62
|
// Fetch newsletters
|
|
40
63
|
useEffect(() => {
|
|
41
64
|
const fetchNewsletters = async () => {
|
|
42
65
|
try {
|
|
43
66
|
setIsLoading(true);
|
|
44
|
-
const
|
|
67
|
+
const langParam = primaryLanguage ? `?language=${primaryLanguage}` : '';
|
|
68
|
+
const response = await fetch(`/api/plugin-newsletter/newsletters${langParam}`, {
|
|
45
69
|
credentials: 'include',
|
|
46
70
|
});
|
|
47
71
|
if (!response.ok) {
|
|
@@ -58,7 +82,7 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
58
82
|
}
|
|
59
83
|
};
|
|
60
84
|
fetchNewsletters();
|
|
61
|
-
}, []);
|
|
85
|
+
}, [primaryLanguage]);
|
|
62
86
|
// Fetch welcome email status
|
|
63
87
|
useEffect(() => {
|
|
64
88
|
const fetchWelcomeEmailStatus = async () => {
|
|
@@ -141,10 +165,6 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
141
165
|
setShowSmtpModal(true);
|
|
142
166
|
return;
|
|
143
167
|
}
|
|
144
|
-
if (newsletter.status === 'sent') {
|
|
145
|
-
alert('This newsletter has already been sent');
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
168
|
try {
|
|
149
169
|
const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletter.id}/send`, {
|
|
150
170
|
credentials: 'include',
|
|
@@ -201,6 +221,46 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
201
221
|
year: 'numeric',
|
|
202
222
|
});
|
|
203
223
|
};
|
|
224
|
+
// Format date with time
|
|
225
|
+
const formatDateTime = (dateString) => {
|
|
226
|
+
if (!dateString)
|
|
227
|
+
return 'Never';
|
|
228
|
+
const date = new Date(dateString);
|
|
229
|
+
return date.toLocaleString(locale, {
|
|
230
|
+
day: 'numeric',
|
|
231
|
+
month: 'short',
|
|
232
|
+
year: 'numeric',
|
|
233
|
+
hour: '2-digit',
|
|
234
|
+
minute: '2-digit',
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
// Get send history for a specific language
|
|
238
|
+
const getSendHistoryForLanguage = (sendHistory, language) => {
|
|
239
|
+
return sendHistory
|
|
240
|
+
.filter(entry => entry.language === language)
|
|
241
|
+
.sort((a, b) => new Date(b.sentAt).getTime() - new Date(a.sentAt).getTime());
|
|
242
|
+
};
|
|
243
|
+
// Sort languages: those with send history first (most recent first), then by language code
|
|
244
|
+
const sortLanguages = (langs, sendHistory) => {
|
|
245
|
+
return [...langs].sort((a, b) => {
|
|
246
|
+
const aHasHistory = sendHistory.some(h => h.language === a);
|
|
247
|
+
const bHasHistory = sendHistory.some(h => h.language === b);
|
|
248
|
+
if (aHasHistory && !bHasHistory)
|
|
249
|
+
return -1;
|
|
250
|
+
if (!aHasHistory && bHasHistory)
|
|
251
|
+
return 1;
|
|
252
|
+
return a.localeCompare(b);
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
// Format last sent info
|
|
256
|
+
const formatLastSent = (sendHistory) => {
|
|
257
|
+
if (!sendHistory || sendHistory.length === 0)
|
|
258
|
+
return null;
|
|
259
|
+
const lastEntry = sendHistory[sendHistory.length - 1];
|
|
260
|
+
const langLabel = lastEntry.language.toUpperCase();
|
|
261
|
+
const dateStr = formatDate(lastEntry.sentAt);
|
|
262
|
+
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, ")"] }))] }));
|
|
263
|
+
};
|
|
204
264
|
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
265
|
if (smtpStatus === 'not_configured') {
|
|
206
266
|
alert('Please configure SMTP settings first');
|
|
@@ -211,7 +271,18 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
211
271
|
}
|
|
212
272
|
}, 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
273
|
? '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("
|
|
274
|
+
: '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("div", { className: "flex items-center bg-dashboard-bg border border-dashboard-border rounded-xl p-1 gap-1", children: [_jsx("button", { onClick: () => setViewMode('grid'), className: `p-2 rounded-lg transition-colors ${viewMode === 'grid'
|
|
275
|
+
? 'bg-primary text-white'
|
|
276
|
+
: 'text-dashboard-text-secondary hover:text-dashboard-text'}`, title: "Grid view", children: _jsx(Grid, { size: 16 }) }), _jsx("button", { onClick: () => setViewMode('list'), className: `p-2 rounded-lg transition-colors ${viewMode === 'list'
|
|
277
|
+
? 'bg-primary text-white'
|
|
278
|
+
: 'text-dashboard-text-secondary hover:text-dashboard-text'}`, title: "List view", children: _jsx(List, { size: 16 }) })] }), _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"] })] })) : viewMode === 'grid' ? (_jsx(NewsletterGrid, { newsletters: filteredNewsletters, onEdit: handleEdit, onSend: handleSend, onDelete: handleDelete, formatDateTime: formatDateTime, locale: locale })) : (_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("div", { className: "relative", children: _jsx("span", { className: "text-sm font-medium text-dashboard-text tracking-tight cursor-pointer hover:text-primary transition-colors", onMouseEnter: (e) => {
|
|
279
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
280
|
+
setHoveredNewsletter({
|
|
281
|
+
id: newsletter.id,
|
|
282
|
+
x: rect.left + rect.width / 2,
|
|
283
|
+
y: rect.bottom + 8
|
|
284
|
+
});
|
|
285
|
+
}, onMouseLeave: () => setHoveredNewsletter(null), 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
286
|
setShowSendModal(false);
|
|
216
287
|
setSelectedNewsletter(null);
|
|
217
288
|
}, newsletter: {
|
|
@@ -220,5 +291,21 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
220
291
|
subject: selectedNewsletter.subject,
|
|
221
292
|
status: selectedNewsletter.status,
|
|
222
293
|
hasContent: selectedNewsletter.hasContent,
|
|
223
|
-
}, subscriberCount: subscriberCount }))
|
|
294
|
+
}, subscriberCount: subscriberCount })), hoveredNewsletter && (() => {
|
|
295
|
+
const tooltipNewsletter = newsletters.find(n => n.id === hoveredNewsletter.id);
|
|
296
|
+
if (!tooltipNewsletter?.languages)
|
|
297
|
+
return null;
|
|
298
|
+
const tooltipLanguages = tooltipNewsletter.languages;
|
|
299
|
+
const tooltipSendHistory = tooltipNewsletter.sendHistory || [];
|
|
300
|
+
return (_jsxs("div", { className: "fixed z-50 w-72 bg-dashboard-card border border-dashboard-border rounded-xl shadow-2xl p-3 pointer-events-none", style: {
|
|
301
|
+
left: hoveredNewsletter.x,
|
|
302
|
+
top: hoveredNewsletter.y,
|
|
303
|
+
transform: 'translateX(-50%)'
|
|
304
|
+
}, children: [_jsx("div", { className: "text-[10px] uppercase tracking-wider text-dashboard-text-secondary font-bold mb-2", children: "Translations & Send History" }), sortLanguages(Object.keys(tooltipLanguages), tooltipSendHistory).map((lang) => {
|
|
305
|
+
const data = tooltipLanguages[lang];
|
|
306
|
+
const langSendHistory = getSendHistoryForLanguage(tooltipSendHistory, lang);
|
|
307
|
+
const lastSend = langSendHistory[0];
|
|
308
|
+
return (_jsxs("div", { className: "py-2 border-b border-dashboard-border last:border-0", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-[10px] font-black uppercase text-primary", children: lang }), lastSend && (_jsxs("div", { className: "text-[9px] text-green-600 dark:text-green-400 flex items-center gap-1", children: [_jsx(Send, { size: 10 }), "Sent"] }))] }), _jsx("div", { className: "text-xs text-dashboard-text truncate", children: data?.metadata?.subject || 'No subject' }), lastSend && (_jsxs("div", { className: "text-[9px] text-dashboard-text-secondary mt-1", children: [formatDateTime(lastSend.sentAt), " \u2022 ", lastSend.recipientCount, " recipients"] })), langSendHistory.length > 1 && (_jsxs("div", { className: "text-[9px] text-dashboard-text-secondary mt-0.5", children: ["+ ", langSendHistory.length - 1, " more send(s)"] }))] }, lang));
|
|
309
|
+
})] }));
|
|
310
|
+
})()] }));
|
|
224
311
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Card Component
|
|
3
|
+
* Modern, distinctive card design for newsletters
|
|
4
|
+
*/
|
|
5
|
+
import { NewsletterListItem } from '../../types/newsletter';
|
|
6
|
+
interface NewsletterCardProps {
|
|
7
|
+
newsletter: NewsletterListItem;
|
|
8
|
+
onEdit: (id: string) => void;
|
|
9
|
+
onSend: (newsletter: NewsletterListItem) => void;
|
|
10
|
+
onDelete: (id: string, title: string) => void;
|
|
11
|
+
formatDateTime: (dateString: string | undefined) => string;
|
|
12
|
+
locale: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function NewsletterCard({ newsletter, onEdit, onSend, onDelete, formatDateTime, locale }: NewsletterCardProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=NewsletterCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NewsletterCard.d.ts","sourceRoot":"","sources":["../../../src/views/components/NewsletterCard.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,kBAAkB,EAAoB,MAAM,wBAAwB,CAAC;AAE9E,UAAU,mBAAmB;IACzB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,MAAM,EAAE,CAAC,UAAU,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACjD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC;IAC3D,MAAM,EAAE,MAAM,CAAC;CAClB;AA4ED,wBAAgB,cAAc,CAAC,EAC3B,UAAU,EACV,MAAM,EACN,MAAM,EACN,QAAQ,EACR,cAAc,EACd,MAAM,EACT,EAAE,mBAAmB,2CA8GrB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Card Component
|
|
3
|
+
* Modern, distinctive card design for newsletters
|
|
4
|
+
*/
|
|
5
|
+
'use client';
|
|
6
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
7
|
+
import { Trash2, Edit2, Send } from 'lucide-react';
|
|
8
|
+
function getStatusConfig(status) {
|
|
9
|
+
switch (status) {
|
|
10
|
+
case 'sent':
|
|
11
|
+
return {
|
|
12
|
+
label: 'Sent',
|
|
13
|
+
bg: 'bg-green-500',
|
|
14
|
+
text: 'text-green-600 dark:text-green-400',
|
|
15
|
+
};
|
|
16
|
+
case 'scheduled':
|
|
17
|
+
return {
|
|
18
|
+
label: 'Scheduled',
|
|
19
|
+
bg: 'bg-blue-500',
|
|
20
|
+
text: 'text-blue-600 dark:text-blue-400',
|
|
21
|
+
};
|
|
22
|
+
case 'draft':
|
|
23
|
+
return {
|
|
24
|
+
label: 'Draft',
|
|
25
|
+
bg: 'bg-gray-400',
|
|
26
|
+
text: 'text-gray-500 dark:text-gray-400',
|
|
27
|
+
};
|
|
28
|
+
case 'archived':
|
|
29
|
+
return {
|
|
30
|
+
label: 'Archived',
|
|
31
|
+
bg: 'bg-neutral-400',
|
|
32
|
+
text: 'text-neutral-500 dark:text-neutral-400',
|
|
33
|
+
};
|
|
34
|
+
default:
|
|
35
|
+
return {
|
|
36
|
+
label: status,
|
|
37
|
+
bg: 'bg-gray-400',
|
|
38
|
+
text: 'text-gray-500',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const LANGUAGE_NAMES = {
|
|
43
|
+
en: 'English',
|
|
44
|
+
nl: 'Dutch',
|
|
45
|
+
sv: 'Swedish',
|
|
46
|
+
de: 'German',
|
|
47
|
+
fr: 'French',
|
|
48
|
+
es: 'Spanish',
|
|
49
|
+
it: 'Italian',
|
|
50
|
+
pt: 'Portuguese',
|
|
51
|
+
pl: 'Polish',
|
|
52
|
+
ru: 'Russian',
|
|
53
|
+
ja: 'Japanese',
|
|
54
|
+
zh: 'Chinese',
|
|
55
|
+
ar: 'Arabic',
|
|
56
|
+
tr: 'Turkish',
|
|
57
|
+
};
|
|
58
|
+
const LANGUAGE_COUNTRY_CODES = {
|
|
59
|
+
en: 'gb',
|
|
60
|
+
nl: 'nl',
|
|
61
|
+
sv: 'se',
|
|
62
|
+
de: 'de',
|
|
63
|
+
fr: 'fr',
|
|
64
|
+
es: 'es',
|
|
65
|
+
it: 'it',
|
|
66
|
+
pt: 'pt',
|
|
67
|
+
pl: 'pl',
|
|
68
|
+
ru: 'ru',
|
|
69
|
+
ja: 'jp',
|
|
70
|
+
zh: 'cn',
|
|
71
|
+
ar: 'sa',
|
|
72
|
+
tr: 'tr',
|
|
73
|
+
};
|
|
74
|
+
const getFlagUrl = (lang, size = 32) => {
|
|
75
|
+
const countryCode = LANGUAGE_COUNTRY_CODES[lang] || lang;
|
|
76
|
+
return `https://flagcdn.com/${size}x${Math.round(size * 0.75)}/${countryCode}.png`;
|
|
77
|
+
};
|
|
78
|
+
export function NewsletterCard({ newsletter, onEdit, onSend, onDelete, formatDateTime, locale }) {
|
|
79
|
+
const statusConfig = getStatusConfig(newsletter.status);
|
|
80
|
+
const languages = newsletter.availableLanguages || [];
|
|
81
|
+
const sendHistory = newsletter.sendHistory || [];
|
|
82
|
+
const sentLanguages = [...new Set(sendHistory.map(h => h.language))];
|
|
83
|
+
return (_jsxs("div", { className: "group relative bg-white dark:bg-neutral-900 rounded-2xl border border-dashboard-border overflow-hidden hover:border-primary/40 hover:shadow-xl hover:shadow-primary/5 transition-all duration-300", children: [_jsx("div", { className: `h-1.5 ${statusConfig.bg}` }), _jsxs("div", { className: "p-5", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsx("div", { className: "flex items-center gap-2", children: _jsx("span", { className: `text-[10px] font-black uppercase tracking-widest ${statusConfig.text}`, children: statusConfig.label }) }), _jsxs("div", { className: "flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity", children: [_jsx("button", { onClick: () => onSend(newsletter), className: "p-2 rounded-lg text-neutral-400 hover:text-primary hover:bg-primary/10 transition-all", title: newsletter.status === 'sent' ? 'Resend' : 'Send', children: _jsx(Send, { size: 16 }) }), _jsx("button", { onClick: () => onEdit(newsletter.id), className: "p-2 rounded-lg text-neutral-400 hover:text-primary hover:bg-primary/10 transition-all", title: "Edit", children: _jsx(Edit2, { size: 16 }) }), _jsx("button", { onClick: () => onDelete(newsletter.id, newsletter.title), className: "p-2 rounded-lg text-neutral-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all", title: "Delete", children: _jsx(Trash2, { size: 16 }) })] })] }), _jsx("h3", { className: "text-lg font-bold text-dashboard-text mb-4 line-clamp-2 group-hover:text-primary transition-colors", children: newsletter.title || 'Untitled Newsletter' }), _jsx("div", { className: "space-y-2", children: languages.map((lang) => {
|
|
84
|
+
const hasBeenSent = sentLanguages.includes(lang);
|
|
85
|
+
const langHistory = sendHistory.filter(h => h.language === lang);
|
|
86
|
+
const lastLangSend = langHistory[0];
|
|
87
|
+
const langData = newsletter.languages?.[lang];
|
|
88
|
+
const langSubject = langData?.metadata?.subject || newsletter.title;
|
|
89
|
+
return (_jsxs("div", { className: `relative group/lang flex items-center justify-between px-3 py-2 rounded-lg border transition-all hover:border-primary/50 ${hasBeenSent ? '' : 'bg-dashboard-bg border-dashboard-border'}`, style: {
|
|
90
|
+
backgroundColor: hasBeenSent ? 'rgba(34, 197, 94, 0.08)' : undefined,
|
|
91
|
+
borderColor: hasBeenSent ? 'rgba(34, 197, 94, 0.3)' : undefined,
|
|
92
|
+
}, children: [hasBeenSent && (_jsx("div", { className: "absolute -top-2 -right-2 min-w-[20px] h-5 px-1.5 bg-green-500 rounded-full flex items-center justify-center shadow-sm", children: _jsx("span", { className: "text-[10px] font-bold text-white", children: langHistory.length }) })), _jsx("div", { className: "flex items-center gap-3 flex-1 min-w-0", children: _jsx("img", { src: getFlagUrl(lang, 28), alt: LANGUAGE_NAMES[lang] || lang, className: "w-7 h-5 rounded object-cover shrink-0", loading: "lazy" }) }), _jsx("div", { className: "flex items-center gap-4 text-xs shrink-0", children: hasBeenSent && lastLangSend ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-dashboard-text-secondary", children: formatDateTime(lastLangSend.sentAt) }), _jsxs("div", { className: "text-dashboard-text-secondary", children: [lastLangSend.recipientCount, " recipients"] })] })) : (_jsx("span", { className: "text-dashboard-text-secondary", children: "Not sent" })) }), _jsxs("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-neutral-900 dark:bg-neutral-800 text-white text-xs rounded-lg opacity-0 group-hover/lang:opacity-100 transition-opacity pointer-events-none z-10 w-64 shadow-lg", children: [_jsx("div", { className: "font-bold mb-1", children: LANGUAGE_NAMES[lang] || lang.toUpperCase() }), _jsx("div", { className: "text-neutral-300 break-words", children: langSubject || 'Untitled' }), _jsx("div", { className: "absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-0 h-0 border-4 border-transparent border-t-neutral-900 dark:border-t-neutral-800" })] })] }, lang));
|
|
93
|
+
}) })] })] }));
|
|
94
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Grid View Component
|
|
3
|
+
* Card-based grid layout for newsletters
|
|
4
|
+
*/
|
|
5
|
+
import { NewsletterListItem } from '../../types/newsletter';
|
|
6
|
+
interface NewsletterGridProps {
|
|
7
|
+
newsletters: NewsletterListItem[];
|
|
8
|
+
onEdit: (id: string) => void;
|
|
9
|
+
onSend: (newsletter: NewsletterListItem) => void;
|
|
10
|
+
onDelete: (id: string, title: string) => void;
|
|
11
|
+
formatDateTime: (dateString: string | undefined) => string;
|
|
12
|
+
locale: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function NewsletterGrid({ newsletters, onEdit, onSend, onDelete, formatDateTime, locale }: NewsletterGridProps): import("react/jsx-runtime").JSX.Element | null;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=NewsletterGrid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NewsletterGrid.d.ts","sourceRoot":"","sources":["../../../src/views/components/NewsletterGrid.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAG5D,UAAU,mBAAmB;IACzB,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,MAAM,EAAE,CAAC,UAAU,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACjD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC;IAC3D,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,cAAc,CAAC,EAC3B,WAAW,EACX,MAAM,EACN,MAAM,EACN,QAAQ,EACR,cAAc,EACd,MAAM,EACT,EAAE,mBAAmB,kDAoBrB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Grid View Component
|
|
3
|
+
* Card-based grid layout for newsletters
|
|
4
|
+
*/
|
|
5
|
+
'use client';
|
|
6
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
7
|
+
import { NewsletterCard } from './NewsletterCard';
|
|
8
|
+
export function NewsletterGrid({ newsletters, onEdit, onSend, onDelete, formatDateTime, locale }) {
|
|
9
|
+
if (newsletters.length === 0) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return (_jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4", children: newsletters.map((newsletter) => (_jsx(NewsletterCard, { newsletter: newsletter, onEdit: onEdit, onSend: onSend, onDelete: onDelete, formatDateTime: formatDateTime, locale: locale }, newsletter.id))) }));
|
|
13
|
+
}
|
|
@@ -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
|
@@ -40,8 +40,9 @@ export async function GET_NEWSLETTERS(
|
|
|
40
40
|
const skip = parseInt(searchParams.get('skip') || '0', 10);
|
|
41
41
|
const sortBy = searchParams.get('sortBy') || 'updatedAt';
|
|
42
42
|
const sortOrder = searchParams.get('sortOrder') || 'desc';
|
|
43
|
+
const language = searchParams.get('language') || 'en';
|
|
43
44
|
|
|
44
|
-
const query:
|
|
45
|
+
const query: Record<string, unknown> = {};
|
|
45
46
|
if (status) {
|
|
46
47
|
query['publication.status'] = status;
|
|
47
48
|
}
|
|
@@ -55,7 +56,7 @@ export async function GET_NEWSLETTERS(
|
|
|
55
56
|
]
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
const sort:
|
|
59
|
+
const sort: Record<string, 1 | -1> = {};
|
|
59
60
|
sort[sortBy] = sortOrder === 'asc' ? 1 : -1;
|
|
60
61
|
|
|
61
62
|
const newsletterList = await newsletters
|
|
@@ -65,19 +66,39 @@ export async function GET_NEWSLETTERS(
|
|
|
65
66
|
.skip(skip)
|
|
66
67
|
.toArray();
|
|
67
68
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
69
|
+
const primaryLanguage = 'en'; // Default primary
|
|
70
|
+
|
|
71
|
+
const listItems: NewsletterListItem[] = newsletterList.map((newsletter: Record<string, any>) => {
|
|
72
|
+
const languages = newsletter.languages || {};
|
|
73
|
+
const newsletterPrimaryLang = newsletter.metadata?.lang || primaryLanguage;
|
|
74
|
+
|
|
75
|
+
// Get title (from subject) and subject from language-specific content
|
|
76
|
+
let title = newsletter.metadata?.subject || '';
|
|
77
|
+
|
|
78
|
+
if (languages[language]) {
|
|
79
|
+
title = languages[language].metadata?.subject || title;
|
|
80
|
+
} else if (language !== newsletterPrimaryLang && languages[newsletterPrimaryLang]) {
|
|
81
|
+
// Fall back to primary language
|
|
82
|
+
title = languages[newsletterPrimaryLang].metadata?.subject || title;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
id: newsletter._id?.toString() || newsletter.id,
|
|
87
|
+
title: title || newsletter.title || 'Untitled',
|
|
88
|
+
slug: newsletter.slug,
|
|
89
|
+
status: newsletter.publication?.status || 'draft',
|
|
90
|
+
subject: title,
|
|
91
|
+
scheduledDate: newsletter.publication?.scheduledDate,
|
|
92
|
+
sentDate: newsletter.publication?.sentDate,
|
|
93
|
+
authorId: newsletter.publication?.authorId,
|
|
94
|
+
updatedAt: newsletter.updatedAt || newsletter.createdAt,
|
|
95
|
+
recipientCount: newsletter.recipientCount,
|
|
96
|
+
hidden: newsletter.hidden,
|
|
97
|
+
sendHistory: newsletter.sendHistory || [],
|
|
98
|
+
availableLanguages: Object.keys(languages),
|
|
99
|
+
languages,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
81
102
|
|
|
82
103
|
return NextResponse.json(listItems);
|
|
83
104
|
} catch (error: any) {
|
|
@@ -100,6 +121,9 @@ export async function GET_NEWSLETTER(
|
|
|
100
121
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
101
122
|
}
|
|
102
123
|
|
|
124
|
+
const { searchParams } = new URL(req.url);
|
|
125
|
+
const language = searchParams.get('language') || 'en';
|
|
126
|
+
|
|
103
127
|
const dbConnection = await config.getDb();
|
|
104
128
|
const db = dbConnection.db();
|
|
105
129
|
const collectionName = config.collectionName || 'newsletters';
|
|
@@ -115,21 +139,39 @@ export async function GET_NEWSLETTER(
|
|
|
115
139
|
);
|
|
116
140
|
}
|
|
117
141
|
|
|
142
|
+
const languages = newsletter.languages || {};
|
|
143
|
+
const primaryLanguage = newsletter.metadata?.lang || 'en';
|
|
144
|
+
|
|
145
|
+
let blocks = newsletter.blocks || [];
|
|
146
|
+
let metadata = newsletter.metadata || {
|
|
147
|
+
subject: '',
|
|
148
|
+
previewText: '',
|
|
149
|
+
lang: 'en',
|
|
150
|
+
recipientFilter: { type: 'all' },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// If requested language has content, use it
|
|
154
|
+
if (languages[language]) {
|
|
155
|
+
blocks = languages[language].blocks || [];
|
|
156
|
+
metadata = { ...metadata, ...languages[language].metadata, lang: language };
|
|
157
|
+
} else if (language !== primaryLanguage && languages[primaryLanguage]) {
|
|
158
|
+
// Copy from primary language if exists
|
|
159
|
+
blocks = languages[primaryLanguage].blocks || [];
|
|
160
|
+
metadata = { ...metadata, ...languages[primaryLanguage].metadata, lang: language };
|
|
161
|
+
}
|
|
162
|
+
|
|
118
163
|
const result: Newsletter = {
|
|
119
164
|
id: newsletter._id?.toString() || newsletter.id,
|
|
120
|
-
title:
|
|
165
|
+
title: metadata.subject || 'Untitled',
|
|
121
166
|
slug: newsletter.slug,
|
|
122
|
-
blocks
|
|
123
|
-
metadata
|
|
124
|
-
subject: '',
|
|
125
|
-
previewText: '',
|
|
126
|
-
lang: 'en',
|
|
127
|
-
recipientFilter: { type: 'all' },
|
|
128
|
-
},
|
|
167
|
+
blocks,
|
|
168
|
+
metadata,
|
|
129
169
|
publication: newsletter.publication || {
|
|
130
170
|
status: 'draft',
|
|
131
171
|
updatedAt: new Date().toISOString(),
|
|
132
172
|
},
|
|
173
|
+
languages,
|
|
174
|
+
sendHistory: newsletter.sendHistory,
|
|
133
175
|
createdAt: newsletter.createdAt || new Date().toISOString(),
|
|
134
176
|
updatedAt: newsletter.updatedAt || new Date().toISOString(),
|
|
135
177
|
version: newsletter.version,
|
|
@@ -179,7 +221,7 @@ export async function POST_NEWSLETTER(
|
|
|
179
221
|
|
|
180
222
|
// Generate slug for backwards compatibility, but id is primary
|
|
181
223
|
const existingNewsletters = await newsletters.find({}, { projection: { slug: 1 } }).toArray();
|
|
182
|
-
const existingSlugs = existingNewsletters.map((n: any) => n.slug).filter(Boolean);
|
|
224
|
+
const existingSlugs = existingNewsletters.map((n: Record<string, any>) => n.slug).filter(Boolean);
|
|
183
225
|
const slug = generateSlugFromTitle(finalTitle, existingSlugs);
|
|
184
226
|
|
|
185
227
|
const newsletterDocument = {
|
|
@@ -230,6 +272,9 @@ export async function PUT_NEWSLETTER(
|
|
|
230
272
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
231
273
|
}
|
|
232
274
|
|
|
275
|
+
const { searchParams } = new URL(req.url);
|
|
276
|
+
const language = searchParams.get('language') || 'en';
|
|
277
|
+
|
|
233
278
|
const body = await req.json();
|
|
234
279
|
const { title, blocks, metadata, publication } = body;
|
|
235
280
|
|
|
@@ -261,15 +306,36 @@ export async function PUT_NEWSLETTER(
|
|
|
261
306
|
);
|
|
262
307
|
}
|
|
263
308
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
309
|
+
// Preserve existing languages or initialize
|
|
310
|
+
const existingLanguages = existing.languages || {};
|
|
311
|
+
|
|
312
|
+
// Update the specific language content
|
|
313
|
+
const updatedLanguages = {
|
|
314
|
+
...existingLanguages,
|
|
315
|
+
[language]: {
|
|
316
|
+
blocks: blocks || [],
|
|
317
|
+
metadata: {
|
|
318
|
+
subject: metadata.subject.trim(),
|
|
319
|
+
previewText: metadata.previewText?.trim() || '',
|
|
320
|
+
lang: language,
|
|
321
|
+
recipientFilter: metadata.recipientFilter || { type: 'all' },
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// Set primary language if not set
|
|
327
|
+
const primaryLanguage = existing.metadata?.lang || language;
|
|
328
|
+
|
|
329
|
+
const updateData: Record<string, unknown> = {
|
|
330
|
+
title: finalTitle, // Keep root title for backwards compatibility
|
|
331
|
+
blocks: blocks || [], // Keep blocks at root for backwards compatibility
|
|
267
332
|
metadata: {
|
|
268
333
|
subject: metadata.subject.trim(),
|
|
269
334
|
previewText: metadata.previewText?.trim() || '',
|
|
270
|
-
lang:
|
|
335
|
+
lang: primaryLanguage,
|
|
271
336
|
recipientFilter: metadata.recipientFilter || { type: 'all' },
|
|
272
337
|
},
|
|
338
|
+
languages: updatedLanguages,
|
|
273
339
|
publication: {
|
|
274
340
|
...existing.publication,
|
|
275
341
|
status: publication?.status || existing.publication?.status || 'draft',
|