@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.
Files changed (34) hide show
  1. package/dist/api/handlers/newsletters.d.ts.map +1 -1
  2. package/dist/api/handlers/newsletters.js +80 -24
  3. package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
  4. package/dist/api/handlers/send-newsletter.js +31 -4
  5. package/dist/api/handlers/welcome-email.d.ts.map +1 -1
  6. package/dist/api/handlers/welcome-email.js +5 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -1
  9. package/dist/types/newsletter.d.ts +35 -0
  10. package/dist/types/newsletter.d.ts.map +1 -1
  11. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  12. package/dist/views/CanvasEditor/CanvasEditorView.js +28 -7
  13. package/dist/views/CanvasEditor/hooks/useNewsletterLoader.js +4 -4
  14. package/dist/views/NewsletterManager.d.ts.map +1 -1
  15. package/dist/views/NewsletterManager.js +96 -9
  16. package/dist/views/components/NewsletterCard.d.ts +16 -0
  17. package/dist/views/components/NewsletterCard.d.ts.map +1 -0
  18. package/dist/views/components/NewsletterCard.js +94 -0
  19. package/dist/views/components/NewsletterGrid.d.ts +16 -0
  20. package/dist/views/components/NewsletterGrid.d.ts.map +1 -0
  21. package/dist/views/components/NewsletterGrid.js +13 -0
  22. package/dist/views/components/SendNewsletterModal.js +1 -1
  23. package/package.json +1 -1
  24. package/src/api/handlers/newsletters.ts +94 -28
  25. package/src/api/handlers/send-newsletter.ts +33 -4
  26. package/src/api/handlers/welcome-email.ts +5 -2
  27. package/src/index.tsx +4 -1
  28. package/src/types/newsletter.ts +44 -0
  29. package/src/views/CanvasEditor/CanvasEditorView.tsx +28 -8
  30. package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +4 -4
  31. package/src/views/NewsletterManager.tsx +203 -20
  32. package/src/views/components/NewsletterCard.tsx +212 -0
  33. package/src/views/components/NewsletterGrid.tsx +48 -0
  34. 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 response = await fetch('/api/plugin-newsletter/newsletters', {
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("button", { onClick: handleCreate, className: "inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90", children: [_jsx(Plus, { size: 14 }), "New Newsletter"] })] })] }), _jsx("div", { className: "mb-6 p-6 rounded-2xl bg-gradient-to-br from-primary/5 via-primary/10 to-transparent border border-dashboard-border", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-5", children: [_jsx("div", { className: "w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Sparkles, { className: "w-6 h-6 text-primary" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-black text-dashboard-text uppercase tracking-tight mb-1", children: "Welcome Email" }), _jsx("p", { className: "text-xs text-dashboard-text-secondary mb-2", children: "The email sent automatically when someone subscribes" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [welcomeEmailStatus === 'configured' ? (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-green-600 dark:text-green-400", children: [_jsx(CheckCircle2, { size: 12 }), "Configured"] })) : (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-amber-600 dark:text-amber-400", children: [_jsx(Clock, { size: 12 }), "Not configured"] })), _jsxs("button", { onClick: () => window.location.href = '/dashboard/newsletter/subscribers', className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-primary hover:underline", children: [_jsx(Users, { size: 12 }), subscriberCount, " subscriber", subscriberCount !== 1 ? 's' : ''] }), welcomeEmailLastUpdated && (_jsxs("span", { className: "text-[10px] text-dashboard-text-secondary", children: ["Last updated: ", new Date(welcomeEmailLastUpdated).toLocaleDateString()] }))] })] })] }), _jsxs("button", { onClick: () => window.location.href = '/dashboard/newsletter/welcome', className: "inline-flex items-center gap-2 px-4 py-2.5 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors bg-primary text-white hover:bg-primary/90", children: [_jsx(Edit2, { size: 14 }), welcomeEmailStatus === 'configured' ? 'Edit' : 'Configure'] })] }) }), _jsx("div", { className: "bg-dashboard-bg rounded-3xl border border-dashboard-border overflow-hidden", children: isLoading ? (_jsx("div", { className: "flex items-center justify-center py-20", children: _jsx("div", { className: "w-8 h-8 border-4 border-primary/20 border-t-primary rounded-full animate-spin" }) })) : filteredNewsletters.length === 0 ? (_jsxs("div", { className: "py-24 text-center", children: [_jsx(Mail, { size: 64, className: "mx-auto text-dashboard-text-secondary mb-4" }), _jsx("p", { className: "text-dashboard-text-secondary font-serif italic text-lg mb-6", children: statusFilter === 'all' ? 'No newsletters yet.' : `No newsletters found with status "${statusFilter}".` }), _jsxs("button", { onClick: handleCreate, className: "inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90", children: [_jsx(Plus, { size: 14 }), "Create Your First Newsletter"] })] })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-left border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "bg-dashboard-bg text-dashboard-text text-[10px] uppercase tracking-[0.2em] font-black border-b border-dashboard-border", children: [_jsx("th", { className: "px-8 py-5", children: "Title" }), _jsx("th", { className: "px-8 py-5", children: "Subject" }), _jsx("th", { className: "px-8 py-5", children: "Status" }), _jsx("th", { className: "px-8 py-5 text-right", children: "Updated" }), _jsx("th", { className: "px-8 py-5 text-right", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-dashboard-border", children: filteredNewsletters.map((newsletter) => (_jsxs("tr", { className: "hover:bg-dashboard-bg transition-colors group", children: [_jsx("td", { className: "px-8 py-5", children: _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover:bg-primary group-hover:text-white transition-colors", children: _jsx(Mail, { size: 18 }) }), _jsx("span", { className: "text-sm font-medium text-dashboard-text tracking-tight", children: newsletter.title })] }) }), _jsx("td", { className: "px-8 py-5", children: _jsx("span", { className: "text-sm text-dashboard-text-secondary", children: newsletter.subject || 'No subject' }) }), _jsx("td", { className: "px-8 py-5", children: _jsx("span", { className: `text-[10px] font-black px-3 py-1 rounded-full uppercase border ${getStatusBadgeColor(newsletter.status)}`, children: newsletter.status }) }), _jsx("td", { className: "px-8 py-5 text-right text-xs text-dashboard-text-secondary font-medium", children: _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(Calendar, { size: 14 }), formatDate(newsletter.updatedAt)] }) }), _jsx("td", { className: "px-8 py-5 text-right", children: _jsxs("div", { className: "flex items-center justify-end gap-2", children: [newsletter.status !== 'sent' && (_jsx("button", { onClick: () => handleSend(newsletter), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors", title: "Send newsletter", children: _jsx(Send, { size: 18 }) })), _jsx("button", { onClick: () => handleEdit(newsletter.id), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors", title: "Edit newsletter", children: _jsx(Edit2, { size: 18 }) }), _jsx("button", { onClick: () => handleDelete(newsletter.id, newsletter.title), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors", title: "Delete newsletter", children: _jsx(Trash2, { size: 18 }) })] }) })] }, newsletter.id))) })] }) })) })] }) }), _jsx(SmtpSettingsModal, { isOpen: showSmtpModal, onClose: () => setShowSmtpModal(false) }), _jsx(TestEmailModal, { isOpen: showTestEmailModal, onClose: () => setShowTestEmailModal(false) }), selectedNewsletter && (_jsx(SendNewsletterModal, { isOpen: showSendModal, onClose: () => {
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: [_jsx("h2", { className: "text-xl font-black text-dashboard-text uppercase tracking-tight", children: "Send Newsletter" }), _jsx("p", { className: "text-xs text-dashboard-text-secondary truncate max-w-[200px]", children: newsletter.title })] })] }), _jsx("button", { onClick: () => !isSending && onClose(), disabled: isSending, className: "p-2 rounded-full hover:bg-dashboard-border transition-colors", children: _jsx(X, { size: 20, className: "text-dashboard-text-secondary" }) })] }), _jsxs("div", { className: "p-8", children: [isAlreadySent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800", children: _jsxs("div", { className: "flex items-center gap-2 text-amber-700 dark:text-amber-400", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter has already been sent" })] }) })), !newsletter.hasContent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800", children: _jsxs("div", { className: "flex items-center gap-2 text-red-700 dark:text-red-400", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter has no content" })] }) })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Send To" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: () => setSendMode('subscribers'), className: `flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${sendMode === 'subscribers'
85
+ return (_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [_jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-sm", onClick: () => !isSending && onClose() }), _jsxs("div", { className: "relative w-full max-w-md mx-4 bg-white dark:bg-neutral-900 rounded-3xl border border-dashboard-border shadow-2xl overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-8 py-6 border-b border-dashboard-border", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-10 h-10 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Send, { className: "w-5 h-5 text-primary" }) }), _jsxs("div", { children: [_jsxs("h2", { className: "text-xl font-black text-dashboard-text uppercase tracking-tight", children: [isAlreadySent ? 'Resend' : 'Send', " Newsletter"] }), _jsx("p", { className: "text-xs text-dashboard-text-secondary truncate max-w-[200px]", children: newsletter.title })] })] }), _jsx("button", { onClick: () => !isSending && onClose(), disabled: isSending, className: "p-2 rounded-full hover:bg-dashboard-border transition-colors", children: _jsx(X, { size: 20, className: "text-dashboard-text-secondary" }) })] }), _jsxs("div", { className: "p-8", children: [isAlreadySent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800", children: _jsxs("div", { className: "flex items-center gap-2 text-blue-700 dark:text-blue-400", children: [_jsx(RefreshCw, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter was already sent - sending again" })] }) })), !newsletter.hasContent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800", children: _jsxs("div", { className: "flex items-center gap-2 text-red-700 dark:text-red-400", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter has no content" })] }) })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Send To" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: () => setSendMode('subscribers'), className: `flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${sendMode === 'subscribers'
86
86
  ? 'bg-primary text-white'
87
87
  : subscriberCount > 0
88
88
  ? 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhits/plugin-newsletter",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Newsletter management and email delivery plugin for the JHITS ecosystem",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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: any = {};
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: any = {};
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 listItems: NewsletterListItem[] = newsletterList.map((newsletter: any) => ({
69
- id: newsletter._id?.toString() || newsletter.id,
70
- title: newsletter.title,
71
- slug: newsletter.slug,
72
- status: newsletter.publication?.status || 'draft',
73
- subject: newsletter.metadata?.subject || '',
74
- scheduledDate: newsletter.publication?.scheduledDate,
75
- sentDate: newsletter.publication?.sentDate,
76
- authorId: newsletter.publication?.authorId,
77
- updatedAt: newsletter.updatedAt || newsletter.createdAt,
78
- recipientCount: newsletter.recipientCount,
79
- hidden: newsletter.hidden,
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: newsletter.title,
165
+ title: metadata.subject || 'Untitled',
121
166
  slug: newsletter.slug,
122
- blocks: newsletter.blocks || [],
123
- metadata: newsletter.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
- const updateData: any = {
265
- title: finalTitle,
266
- blocks: blocks || [],
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: metadata.lang || 'en',
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',