@jhits/plugin-newsletter 0.0.15 → 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 +33 -16
- package/dist/types/newsletter.d.ts +2 -0
- package/dist/types/newsletter.d.ts.map +1 -1
- package/dist/views/NewsletterManager.d.ts.map +1 -1
- package/dist/views/NewsletterManager.js +87 -5
- 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/package.json +1 -1
- package/src/api/handlers/newsletters.ts +40 -20
- package/src/types/newsletter.ts +2 -0
- package/src/views/NewsletterManager.tsx +164 -6
- package/src/views/components/NewsletterCard.tsx +212 -0
- package/src/views/components/NewsletterGrid.tsx +48 -0
|
@@ -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,CAsFvB;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"}
|
|
@@ -29,6 +29,7 @@ export async function GET_NEWSLETTERS(req, config) {
|
|
|
29
29
|
const skip = parseInt(searchParams.get('skip') || '0', 10);
|
|
30
30
|
const sortBy = searchParams.get('sortBy') || 'updatedAt';
|
|
31
31
|
const sortOrder = searchParams.get('sortOrder') || 'desc';
|
|
32
|
+
const language = searchParams.get('language') || 'en';
|
|
32
33
|
const query = {};
|
|
33
34
|
if (status) {
|
|
34
35
|
query['publication.status'] = status;
|
|
@@ -49,20 +50,36 @@ export async function GET_NEWSLETTERS(req, config) {
|
|
|
49
50
|
.limit(limit)
|
|
50
51
|
.skip(skip)
|
|
51
52
|
.toArray();
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
const primaryLanguage = 'en'; // Default primary
|
|
54
|
+
const listItems = newsletterList.map((newsletter) => {
|
|
55
|
+
const languages = newsletter.languages || {};
|
|
56
|
+
const newsletterPrimaryLang = newsletter.metadata?.lang || primaryLanguage;
|
|
57
|
+
// Get title (from subject) and subject from language-specific content
|
|
58
|
+
let title = newsletter.metadata?.subject || '';
|
|
59
|
+
if (languages[language]) {
|
|
60
|
+
title = languages[language].metadata?.subject || title;
|
|
61
|
+
}
|
|
62
|
+
else if (language !== newsletterPrimaryLang && languages[newsletterPrimaryLang]) {
|
|
63
|
+
// Fall back to primary language
|
|
64
|
+
title = languages[newsletterPrimaryLang].metadata?.subject || title;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
id: newsletter._id?.toString() || newsletter.id,
|
|
68
|
+
title: title || newsletter.title || 'Untitled',
|
|
69
|
+
slug: newsletter.slug,
|
|
70
|
+
status: newsletter.publication?.status || 'draft',
|
|
71
|
+
subject: title,
|
|
72
|
+
scheduledDate: newsletter.publication?.scheduledDate,
|
|
73
|
+
sentDate: newsletter.publication?.sentDate,
|
|
74
|
+
authorId: newsletter.publication?.authorId,
|
|
75
|
+
updatedAt: newsletter.updatedAt || newsletter.createdAt,
|
|
76
|
+
recipientCount: newsletter.recipientCount,
|
|
77
|
+
hidden: newsletter.hidden,
|
|
78
|
+
sendHistory: newsletter.sendHistory || [],
|
|
79
|
+
availableLanguages: Object.keys(languages),
|
|
80
|
+
languages,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
66
83
|
return NextResponse.json(listItems);
|
|
67
84
|
}
|
|
68
85
|
catch (error) {
|
|
@@ -108,7 +125,7 @@ export async function GET_NEWSLETTER(req, idOrSlug, config) {
|
|
|
108
125
|
}
|
|
109
126
|
const result = {
|
|
110
127
|
id: newsletter._id?.toString() || newsletter.id,
|
|
111
|
-
title:
|
|
128
|
+
title: metadata.subject || 'Untitled',
|
|
112
129
|
slug: newsletter.slug,
|
|
113
130
|
blocks,
|
|
114
131
|
metadata,
|
|
@@ -236,7 +253,7 @@ export async function PUT_NEWSLETTER(req, idOrSlug, config) {
|
|
|
236
253
|
// Set primary language if not set
|
|
237
254
|
const primaryLanguage = existing.metadata?.lang || language;
|
|
238
255
|
const updateData = {
|
|
239
|
-
title: finalTitle,
|
|
256
|
+
title: finalTitle, // Keep root title for backwards compatibility
|
|
240
257
|
blocks: blocks || [], // Keep blocks at root for backwards compatibility
|
|
241
258
|
metadata: {
|
|
242
259
|
subject: metadata.subject.trim(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"newsletter.d.ts","sourceRoot":"","sources":["../../src/types/newsletter.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"newsletter.d.ts","sourceRoot":"","sources":["../../src/types/newsletter.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NewsletterManager.d.ts","sourceRoot":"","sources":["../../src/views/NewsletterManager.tsx"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"NewsletterManager.d.ts","sourceRoot":"","sources":["../../src/views/NewsletterManager.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAaH,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,2CAqnBnF"}
|
|
@@ -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 () => {
|
|
@@ -197,6 +221,37 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
197
221
|
year: 'numeric',
|
|
198
222
|
});
|
|
199
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
|
+
};
|
|
200
255
|
// Format last sent info
|
|
201
256
|
const formatLastSent = (sendHistory) => {
|
|
202
257
|
if (!sendHistory || sendHistory.length === 0)
|
|
@@ -216,7 +271,18 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
216
271
|
}
|
|
217
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'
|
|
218
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'
|
|
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("
|
|
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: () => {
|
|
220
286
|
setShowSendModal(false);
|
|
221
287
|
setSelectedNewsletter(null);
|
|
222
288
|
}, newsletter: {
|
|
@@ -225,5 +291,21 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
225
291
|
subject: selectedNewsletter.subject,
|
|
226
292
|
status: selectedNewsletter.status,
|
|
227
293
|
hasContent: selectedNewsletter.hasContent,
|
|
228
|
-
}, 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
|
+
})()] }));
|
|
229
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
|
+
}
|
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,20 +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
|
-
|
|
81
|
-
|
|
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
|
+
});
|
|
82
102
|
|
|
83
103
|
return NextResponse.json(listItems);
|
|
84
104
|
} catch (error: any) {
|
|
@@ -142,7 +162,7 @@ export async function GET_NEWSLETTER(
|
|
|
142
162
|
|
|
143
163
|
const result: Newsletter = {
|
|
144
164
|
id: newsletter._id?.toString() || newsletter.id,
|
|
145
|
-
title:
|
|
165
|
+
title: metadata.subject || 'Untitled',
|
|
146
166
|
slug: newsletter.slug,
|
|
147
167
|
blocks,
|
|
148
168
|
metadata,
|
|
@@ -201,7 +221,7 @@ export async function POST_NEWSLETTER(
|
|
|
201
221
|
|
|
202
222
|
// Generate slug for backwards compatibility, but id is primary
|
|
203
223
|
const existingNewsletters = await newsletters.find({}, { projection: { slug: 1 } }).toArray();
|
|
204
|
-
const existingSlugs = existingNewsletters.map((n: any) => n.slug).filter(Boolean);
|
|
224
|
+
const existingSlugs = existingNewsletters.map((n: Record<string, any>) => n.slug).filter(Boolean);
|
|
205
225
|
const slug = generateSlugFromTitle(finalTitle, existingSlugs);
|
|
206
226
|
|
|
207
227
|
const newsletterDocument = {
|
|
@@ -306,8 +326,8 @@ export async function PUT_NEWSLETTER(
|
|
|
306
326
|
// Set primary language if not set
|
|
307
327
|
const primaryLanguage = existing.metadata?.lang || language;
|
|
308
328
|
|
|
309
|
-
const updateData:
|
|
310
|
-
title: finalTitle,
|
|
329
|
+
const updateData: Record<string, unknown> = {
|
|
330
|
+
title: finalTitle, // Keep root title for backwards compatibility
|
|
311
331
|
blocks: blocks || [], // Keep blocks at root for backwards compatibility
|
|
312
332
|
metadata: {
|
|
313
333
|
subject: metadata.subject.trim(),
|
package/src/types/newsletter.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
8
|
import React, { useState, useEffect } from 'react';
|
|
9
|
-
import { Plus, Mail, Calendar, Trash2, Edit2, Settings2, Sparkles, CheckCircle2, Clock, Send, Users } from 'lucide-react';
|
|
9
|
+
import { Plus, Mail, Calendar, Trash2, Edit2, Settings2, Sparkles, CheckCircle2, Clock, Send, Users, Grid, List } from 'lucide-react';
|
|
10
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';
|
|
14
|
+
import { NewsletterGrid } from './components/NewsletterGrid';
|
|
15
|
+
import { NewsletterCard } from './components/NewsletterCard';
|
|
14
16
|
|
|
15
17
|
export interface NewsletterManagerViewProps {
|
|
16
18
|
siteId: string;
|
|
@@ -36,6 +38,8 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
|
|
|
36
38
|
const [newsletters, setNewsletters] = useState<NewsletterListItem[]>([]);
|
|
37
39
|
const [isLoading, setIsLoading] = useState(true);
|
|
38
40
|
const [statusFilter, setStatusFilter] = useState<NewsletterStatus | 'all'>('all');
|
|
41
|
+
const [primaryLanguage, setPrimaryLanguage] = useState<string>('en');
|
|
42
|
+
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
|
39
43
|
const [showSmtpModal, setShowSmtpModal] = useState(false);
|
|
40
44
|
const [showTestEmailModal, setShowTestEmailModal] = useState(false);
|
|
41
45
|
const [showSendModal, setShowSendModal] = useState(false);
|
|
@@ -47,12 +51,34 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
|
|
|
47
51
|
const [welcomeEmailLastUpdated, setWelcomeEmailLastUpdated] = useState<string | null>(null);
|
|
48
52
|
const [subscriberCount, setSubscriberCount] = useState(0);
|
|
49
53
|
|
|
54
|
+
// Hover state for language tooltip
|
|
55
|
+
const [hoveredNewsletter, setHoveredNewsletter] = useState<{ id: string; x: number; y: number } | null>(null);
|
|
56
|
+
|
|
57
|
+
// Fetch primary language from SMTP settings
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const fetchPrimaryLanguage = async () => {
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch('/api/plugin-newsletter/smtp', {
|
|
62
|
+
credentials: 'include',
|
|
63
|
+
});
|
|
64
|
+
if (response.ok) {
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
setPrimaryLanguage(data.primaryLanguage || 'en');
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Failed to fetch SMTP settings:', error);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
fetchPrimaryLanguage();
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
50
75
|
// Fetch newsletters
|
|
51
76
|
useEffect(() => {
|
|
52
77
|
const fetchNewsletters = async () => {
|
|
53
78
|
try {
|
|
54
79
|
setIsLoading(true);
|
|
55
|
-
const
|
|
80
|
+
const langParam = primaryLanguage ? `?language=${primaryLanguage}` : '';
|
|
81
|
+
const response = await fetch(`/api/plugin-newsletter/newsletters${langParam}`, {
|
|
56
82
|
credentials: 'include',
|
|
57
83
|
});
|
|
58
84
|
if (!response.ok) {
|
|
@@ -67,7 +93,7 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
|
|
|
67
93
|
}
|
|
68
94
|
};
|
|
69
95
|
fetchNewsletters();
|
|
70
|
-
}, []);
|
|
96
|
+
}, [primaryLanguage]);
|
|
71
97
|
|
|
72
98
|
// Fetch welcome email status
|
|
73
99
|
useEffect(() => {
|
|
@@ -216,6 +242,37 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
|
|
|
216
242
|
});
|
|
217
243
|
};
|
|
218
244
|
|
|
245
|
+
// Format date with time
|
|
246
|
+
const formatDateTime = (dateString: string | undefined) => {
|
|
247
|
+
if (!dateString) return 'Never';
|
|
248
|
+
const date = new Date(dateString);
|
|
249
|
+
return date.toLocaleString(locale, {
|
|
250
|
+
day: 'numeric',
|
|
251
|
+
month: 'short',
|
|
252
|
+
year: 'numeric',
|
|
253
|
+
hour: '2-digit',
|
|
254
|
+
minute: '2-digit',
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Get send history for a specific language
|
|
259
|
+
const getSendHistoryForLanguage = (sendHistory: SendHistoryEntry[], language: string) => {
|
|
260
|
+
return sendHistory
|
|
261
|
+
.filter(entry => entry.language === language)
|
|
262
|
+
.sort((a, b) => new Date(b.sentAt).getTime() - new Date(a.sentAt).getTime());
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Sort languages: those with send history first (most recent first), then by language code
|
|
266
|
+
const sortLanguages = (langs: string[], sendHistory: SendHistoryEntry[]) => {
|
|
267
|
+
return [...langs].sort((a, b) => {
|
|
268
|
+
const aHasHistory = sendHistory.some(h => h.language === a);
|
|
269
|
+
const bHasHistory = sendHistory.some(h => h.language === b);
|
|
270
|
+
if (aHasHistory && !bHasHistory) return -1;
|
|
271
|
+
if (!aHasHistory && bHasHistory) return 1;
|
|
272
|
+
return a.localeCompare(b);
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
|
|
219
276
|
// Format last sent info
|
|
220
277
|
const formatLastSent = (sendHistory: SendHistoryEntry[] | undefined) => {
|
|
221
278
|
if (!sendHistory || sendHistory.length === 0) return null;
|
|
@@ -311,6 +368,32 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
|
|
|
311
368
|
<option value="archived">Archived</option>
|
|
312
369
|
</select>
|
|
313
370
|
|
|
371
|
+
{/* View Toggle */}
|
|
372
|
+
<div className="flex items-center bg-dashboard-bg border border-dashboard-border rounded-xl p-1 gap-1">
|
|
373
|
+
<button
|
|
374
|
+
onClick={() => setViewMode('grid')}
|
|
375
|
+
className={`p-2 rounded-lg transition-colors ${
|
|
376
|
+
viewMode === 'grid'
|
|
377
|
+
? 'bg-primary text-white'
|
|
378
|
+
: 'text-dashboard-text-secondary hover:text-dashboard-text'
|
|
379
|
+
}`}
|
|
380
|
+
title="Grid view"
|
|
381
|
+
>
|
|
382
|
+
<Grid size={16} />
|
|
383
|
+
</button>
|
|
384
|
+
<button
|
|
385
|
+
onClick={() => setViewMode('list')}
|
|
386
|
+
className={`p-2 rounded-lg transition-colors ${
|
|
387
|
+
viewMode === 'list'
|
|
388
|
+
? 'bg-primary text-white'
|
|
389
|
+
: 'text-dashboard-text-secondary hover:text-dashboard-text'
|
|
390
|
+
}`}
|
|
391
|
+
title="List view"
|
|
392
|
+
>
|
|
393
|
+
<List size={16} />
|
|
394
|
+
</button>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
314
397
|
{/* Create Button */}
|
|
315
398
|
<button
|
|
316
399
|
onClick={handleCreate}
|
|
@@ -393,6 +476,15 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
|
|
|
393
476
|
Create Your First Newsletter
|
|
394
477
|
</button>
|
|
395
478
|
</div>
|
|
479
|
+
) : viewMode === 'grid' ? (
|
|
480
|
+
<NewsletterGrid
|
|
481
|
+
newsletters={filteredNewsletters}
|
|
482
|
+
onEdit={handleEdit}
|
|
483
|
+
onSend={handleSend}
|
|
484
|
+
onDelete={handleDelete}
|
|
485
|
+
formatDateTime={formatDateTime}
|
|
486
|
+
locale={locale}
|
|
487
|
+
/>
|
|
396
488
|
) : (
|
|
397
489
|
<div className="overflow-x-auto">
|
|
398
490
|
<table className="w-full text-left border-collapse">
|
|
@@ -417,9 +509,22 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
|
|
|
417
509
|
<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">
|
|
418
510
|
<Mail size={18} />
|
|
419
511
|
</div>
|
|
420
|
-
<
|
|
421
|
-
|
|
422
|
-
|
|
512
|
+
<div className="relative">
|
|
513
|
+
<span
|
|
514
|
+
className="text-sm font-medium text-dashboard-text tracking-tight cursor-pointer hover:text-primary transition-colors"
|
|
515
|
+
onMouseEnter={(e) => {
|
|
516
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
517
|
+
setHoveredNewsletter({
|
|
518
|
+
id: newsletter.id,
|
|
519
|
+
x: rect.left + rect.width / 2,
|
|
520
|
+
y: rect.bottom + 8
|
|
521
|
+
});
|
|
522
|
+
}}
|
|
523
|
+
onMouseLeave={() => setHoveredNewsletter(null)}
|
|
524
|
+
>
|
|
525
|
+
{newsletter.title}
|
|
526
|
+
</span>
|
|
527
|
+
</div>
|
|
423
528
|
</div>
|
|
424
529
|
</td>
|
|
425
530
|
<td className="px-8 py-5">
|
|
@@ -503,6 +608,59 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
|
|
|
503
608
|
subscriberCount={subscriberCount}
|
|
504
609
|
/>
|
|
505
610
|
)}
|
|
611
|
+
|
|
612
|
+
{/* Fixed Language Tooltip */}
|
|
613
|
+
{hoveredNewsletter && (() => {
|
|
614
|
+
const tooltipNewsletter = newsletters.find(n => n.id === hoveredNewsletter.id);
|
|
615
|
+
if (!tooltipNewsletter?.languages) return null;
|
|
616
|
+
const tooltipLanguages = tooltipNewsletter.languages;
|
|
617
|
+
const tooltipSendHistory = tooltipNewsletter.sendHistory || [];
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
<div
|
|
621
|
+
className="fixed z-50 w-72 bg-dashboard-card border border-dashboard-border rounded-xl shadow-2xl p-3 pointer-events-none"
|
|
622
|
+
style={{
|
|
623
|
+
left: hoveredNewsletter.x,
|
|
624
|
+
top: hoveredNewsletter.y,
|
|
625
|
+
transform: 'translateX(-50%)'
|
|
626
|
+
}}
|
|
627
|
+
>
|
|
628
|
+
<div className="text-[10px] uppercase tracking-wider text-dashboard-text-secondary font-bold mb-2">
|
|
629
|
+
Translations & Send History
|
|
630
|
+
</div>
|
|
631
|
+
{sortLanguages(Object.keys(tooltipLanguages), tooltipSendHistory).map((lang: string) => {
|
|
632
|
+
const data = tooltipLanguages[lang];
|
|
633
|
+
const langSendHistory = getSendHistoryForLanguage(tooltipSendHistory, lang);
|
|
634
|
+
const lastSend = langSendHistory[0];
|
|
635
|
+
|
|
636
|
+
return (
|
|
637
|
+
<div key={lang} className="py-2 border-b border-dashboard-border last:border-0">
|
|
638
|
+
<div className="flex items-center justify-between">
|
|
639
|
+
<div className="text-[10px] font-black uppercase text-primary">{lang}</div>
|
|
640
|
+
{lastSend && (
|
|
641
|
+
<div className="text-[9px] text-green-600 dark:text-green-400 flex items-center gap-1">
|
|
642
|
+
<Send size={10} />
|
|
643
|
+
Sent
|
|
644
|
+
</div>
|
|
645
|
+
)}
|
|
646
|
+
</div>
|
|
647
|
+
<div className="text-xs text-dashboard-text truncate">{data?.metadata?.subject || 'No subject'}</div>
|
|
648
|
+
{lastSend && (
|
|
649
|
+
<div className="text-[9px] text-dashboard-text-secondary mt-1">
|
|
650
|
+
{formatDateTime(lastSend.sentAt)} • {lastSend.recipientCount} recipients
|
|
651
|
+
</div>
|
|
652
|
+
)}
|
|
653
|
+
{langSendHistory.length > 1 && (
|
|
654
|
+
<div className="text-[9px] text-dashboard-text-secondary mt-0.5">
|
|
655
|
+
+ {langSendHistory.length - 1} more send(s)
|
|
656
|
+
</div>
|
|
657
|
+
)}
|
|
658
|
+
</div>
|
|
659
|
+
);
|
|
660
|
+
})}
|
|
661
|
+
</div>
|
|
662
|
+
);
|
|
663
|
+
})()}
|
|
506
664
|
</>
|
|
507
665
|
);
|
|
508
666
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Card Component
|
|
3
|
+
* Modern, distinctive card design for newsletters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { Trash2, Edit2, Send, Check } from 'lucide-react';
|
|
10
|
+
import { NewsletterListItem, NewsletterStatus } from '../../types/newsletter';
|
|
11
|
+
|
|
12
|
+
interface NewsletterCardProps {
|
|
13
|
+
newsletter: NewsletterListItem;
|
|
14
|
+
onEdit: (id: string) => void;
|
|
15
|
+
onSend: (newsletter: NewsletterListItem) => void;
|
|
16
|
+
onDelete: (id: string, title: string) => void;
|
|
17
|
+
formatDateTime: (dateString: string | undefined) => string;
|
|
18
|
+
locale: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getStatusConfig(status: NewsletterStatus) {
|
|
22
|
+
switch (status) {
|
|
23
|
+
case 'sent':
|
|
24
|
+
return {
|
|
25
|
+
label: 'Sent',
|
|
26
|
+
bg: 'bg-green-500',
|
|
27
|
+
text: 'text-green-600 dark:text-green-400',
|
|
28
|
+
};
|
|
29
|
+
case 'scheduled':
|
|
30
|
+
return {
|
|
31
|
+
label: 'Scheduled',
|
|
32
|
+
bg: 'bg-blue-500',
|
|
33
|
+
text: 'text-blue-600 dark:text-blue-400',
|
|
34
|
+
};
|
|
35
|
+
case 'draft':
|
|
36
|
+
return {
|
|
37
|
+
label: 'Draft',
|
|
38
|
+
bg: 'bg-gray-400',
|
|
39
|
+
text: 'text-gray-500 dark:text-gray-400',
|
|
40
|
+
};
|
|
41
|
+
case 'archived':
|
|
42
|
+
return {
|
|
43
|
+
label: 'Archived',
|
|
44
|
+
bg: 'bg-neutral-400',
|
|
45
|
+
text: 'text-neutral-500 dark:text-neutral-400',
|
|
46
|
+
};
|
|
47
|
+
default:
|
|
48
|
+
return {
|
|
49
|
+
label: status,
|
|
50
|
+
bg: 'bg-gray-400',
|
|
51
|
+
text: 'text-gray-500',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const LANGUAGE_NAMES: Record<string, string> = {
|
|
57
|
+
en: 'English',
|
|
58
|
+
nl: 'Dutch',
|
|
59
|
+
sv: 'Swedish',
|
|
60
|
+
de: 'German',
|
|
61
|
+
fr: 'French',
|
|
62
|
+
es: 'Spanish',
|
|
63
|
+
it: 'Italian',
|
|
64
|
+
pt: 'Portuguese',
|
|
65
|
+
pl: 'Polish',
|
|
66
|
+
ru: 'Russian',
|
|
67
|
+
ja: 'Japanese',
|
|
68
|
+
zh: 'Chinese',
|
|
69
|
+
ar: 'Arabic',
|
|
70
|
+
tr: 'Turkish',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const LANGUAGE_COUNTRY_CODES: Record<string, string> = {
|
|
74
|
+
en: 'gb',
|
|
75
|
+
nl: 'nl',
|
|
76
|
+
sv: 'se',
|
|
77
|
+
de: 'de',
|
|
78
|
+
fr: 'fr',
|
|
79
|
+
es: 'es',
|
|
80
|
+
it: 'it',
|
|
81
|
+
pt: 'pt',
|
|
82
|
+
pl: 'pl',
|
|
83
|
+
ru: 'ru',
|
|
84
|
+
ja: 'jp',
|
|
85
|
+
zh: 'cn',
|
|
86
|
+
ar: 'sa',
|
|
87
|
+
tr: 'tr',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const getFlagUrl = (lang: string, size: number = 32) => {
|
|
91
|
+
const countryCode = LANGUAGE_COUNTRY_CODES[lang] || lang;
|
|
92
|
+
return `https://flagcdn.com/${size}x${Math.round(size * 0.75)}/${countryCode}.png`;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export function NewsletterCard({
|
|
96
|
+
newsletter,
|
|
97
|
+
onEdit,
|
|
98
|
+
onSend,
|
|
99
|
+
onDelete,
|
|
100
|
+
formatDateTime,
|
|
101
|
+
locale
|
|
102
|
+
}: NewsletterCardProps) {
|
|
103
|
+
const statusConfig = getStatusConfig(newsletter.status);
|
|
104
|
+
const languages = newsletter.availableLanguages || [];
|
|
105
|
+
const sendHistory = newsletter.sendHistory || [];
|
|
106
|
+
|
|
107
|
+
const sentLanguages = [...new Set(sendHistory.map(h => h.language))];
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<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">
|
|
111
|
+
<div className={`h-1.5 ${statusConfig.bg}`} />
|
|
112
|
+
|
|
113
|
+
<div className="p-5">
|
|
114
|
+
<div className="flex items-center justify-between mb-4">
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
<span className={`text-[10px] font-black uppercase tracking-widest ${statusConfig.text}`}>
|
|
117
|
+
{statusConfig.label}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
122
|
+
<button
|
|
123
|
+
onClick={() => onSend(newsletter)}
|
|
124
|
+
className="p-2 rounded-lg text-neutral-400 hover:text-primary hover:bg-primary/10 transition-all"
|
|
125
|
+
title={newsletter.status === 'sent' ? 'Resend' : 'Send'}
|
|
126
|
+
>
|
|
127
|
+
<Send size={16} />
|
|
128
|
+
</button>
|
|
129
|
+
<button
|
|
130
|
+
onClick={() => onEdit(newsletter.id)}
|
|
131
|
+
className="p-2 rounded-lg text-neutral-400 hover:text-primary hover:bg-primary/10 transition-all"
|
|
132
|
+
title="Edit"
|
|
133
|
+
>
|
|
134
|
+
<Edit2 size={16} />
|
|
135
|
+
</button>
|
|
136
|
+
<button
|
|
137
|
+
onClick={() => onDelete(newsletter.id, newsletter.title)}
|
|
138
|
+
className="p-2 rounded-lg text-neutral-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all"
|
|
139
|
+
title="Delete"
|
|
140
|
+
>
|
|
141
|
+
<Trash2 size={16} />
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<h3 className="text-lg font-bold text-dashboard-text mb-4 line-clamp-2 group-hover:text-primary transition-colors">
|
|
147
|
+
{newsletter.title || 'Untitled Newsletter'}
|
|
148
|
+
</h3>
|
|
149
|
+
|
|
150
|
+
<div className="space-y-2">
|
|
151
|
+
{languages.map((lang) => {
|
|
152
|
+
const hasBeenSent = sentLanguages.includes(lang);
|
|
153
|
+
const langHistory = sendHistory.filter(h => h.language === lang);
|
|
154
|
+
const lastLangSend = langHistory[0];
|
|
155
|
+
const langData = newsletter.languages?.[lang];
|
|
156
|
+
const langSubject = langData?.metadata?.subject || newsletter.title;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
key={lang}
|
|
161
|
+
className={`relative group/lang flex items-center justify-between px-3 py-2 rounded-lg border transition-all hover:border-primary/50 ${
|
|
162
|
+
hasBeenSent ? '' : 'bg-dashboard-bg border-dashboard-border'
|
|
163
|
+
}`}
|
|
164
|
+
style={{
|
|
165
|
+
backgroundColor: hasBeenSent ? 'rgba(34, 197, 94, 0.08)' : undefined,
|
|
166
|
+
borderColor: hasBeenSent ? 'rgba(34, 197, 94, 0.3)' : undefined,
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
{hasBeenSent && (
|
|
170
|
+
<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">
|
|
171
|
+
<span className="text-[10px] font-bold text-white">{langHistory.length}</span>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
176
|
+
<img
|
|
177
|
+
src={getFlagUrl(lang, 28)}
|
|
178
|
+
alt={LANGUAGE_NAMES[lang] || lang}
|
|
179
|
+
className="w-7 h-5 rounded object-cover shrink-0"
|
|
180
|
+
loading="lazy"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="flex items-center gap-4 text-xs shrink-0">
|
|
185
|
+
{hasBeenSent && lastLangSend ? (
|
|
186
|
+
<>
|
|
187
|
+
<div className="text-dashboard-text-secondary">
|
|
188
|
+
{formatDateTime(lastLangSend.sentAt)}
|
|
189
|
+
</div>
|
|
190
|
+
<div className="text-dashboard-text-secondary">
|
|
191
|
+
{lastLangSend.recipientCount} recipients
|
|
192
|
+
</div>
|
|
193
|
+
</>
|
|
194
|
+
) : (
|
|
195
|
+
<span className="text-dashboard-text-secondary">Not sent</span>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Hover tooltip with subject */}
|
|
200
|
+
<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">
|
|
201
|
+
<div className="font-bold mb-1">{LANGUAGE_NAMES[lang] || lang.toUpperCase()}</div>
|
|
202
|
+
<div className="text-neutral-300 break-words">{langSubject || 'Untitled'}</div>
|
|
203
|
+
<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" />
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
})}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Grid View Component
|
|
3
|
+
* Card-based grid layout for newsletters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { NewsletterListItem } from '../../types/newsletter';
|
|
10
|
+
import { NewsletterCard } from './NewsletterCard';
|
|
11
|
+
|
|
12
|
+
interface NewsletterGridProps {
|
|
13
|
+
newsletters: NewsletterListItem[];
|
|
14
|
+
onEdit: (id: string) => void;
|
|
15
|
+
onSend: (newsletter: NewsletterListItem) => void;
|
|
16
|
+
onDelete: (id: string, title: string) => void;
|
|
17
|
+
formatDateTime: (dateString: string | undefined) => string;
|
|
18
|
+
locale: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function NewsletterGrid({
|
|
22
|
+
newsletters,
|
|
23
|
+
onEdit,
|
|
24
|
+
onSend,
|
|
25
|
+
onDelete,
|
|
26
|
+
formatDateTime,
|
|
27
|
+
locale
|
|
28
|
+
}: NewsletterGridProps) {
|
|
29
|
+
if (newsletters.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
35
|
+
{newsletters.map((newsletter) => (
|
|
36
|
+
<NewsletterCard
|
|
37
|
+
key={newsletter.id}
|
|
38
|
+
newsletter={newsletter}
|
|
39
|
+
onEdit={onEdit}
|
|
40
|
+
onSend={onSend}
|
|
41
|
+
onDelete={onDelete}
|
|
42
|
+
formatDateTime={formatDateTime}
|
|
43
|
+
locale={locale}
|
|
44
|
+
/>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|