@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.
@@ -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,CAkEvB;AAED,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAuEvB;AAED,wBAAsB,eAAe,CACjC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAqEvB;AAED,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA+FvB;AAED,wBAAsB,iBAAiB,CACnC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA8BvB"}
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 listItems = newsletterList.map((newsletter) => ({
53
- id: newsletter._id?.toString() || newsletter.id,
54
- title: newsletter.title,
55
- slug: newsletter.slug,
56
- status: newsletter.publication?.status || 'draft',
57
- subject: newsletter.metadata?.subject || '',
58
- scheduledDate: newsletter.publication?.scheduledDate,
59
- sentDate: newsletter.publication?.sentDate,
60
- authorId: newsletter.publication?.authorId,
61
- updatedAt: newsletter.updatedAt || newsletter.createdAt,
62
- recipientCount: newsletter.recipientCount,
63
- hidden: newsletter.hidden,
64
- sendHistory: newsletter.sendHistory || [],
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: newsletter.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(),
@@ -128,6 +128,8 @@ export interface NewsletterListItem {
128
128
  recipientCount?: number;
129
129
  hidden?: boolean;
130
130
  sendHistory?: SendHistoryEntry[];
131
+ availableLanguages?: string[];
132
+ languages?: NewsletterLanguages;
131
133
  }
132
134
  /**
133
135
  * Newsletter Filter Options
@@ -1 +1 @@
1
- {"version":3,"file":"newsletter.d.ts","sourceRoot":"","sources":["../../src/types/newsletter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"newsletter.d.ts","sourceRoot":"","sources":["../../src/types/newsletter.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"NewsletterManager.d.ts","sourceRoot":"","sources":["../../src/views/NewsletterManager.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,WAAW,0BAA0B;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAClB;AAiBD,wBAAgB,qBAAqB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,0BAA0B,2CAydnF"}
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 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 () => {
@@ -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("button", { onClick: handleCreate, className: "inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90", children: [_jsx(Plus, { size: 14 }), "New Newsletter"] })] })] }), _jsx("div", { className: "mb-6 p-6 rounded-2xl bg-gradient-to-br from-primary/5 via-primary/10 to-transparent border border-dashboard-border", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-5", children: [_jsx("div", { className: "w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Sparkles, { className: "w-6 h-6 text-primary" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-black text-dashboard-text uppercase tracking-tight mb-1", children: "Welcome Email" }), _jsx("p", { className: "text-xs text-dashboard-text-secondary mb-2", children: "The email sent automatically when someone subscribes" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [welcomeEmailStatus === 'configured' ? (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-green-600 dark:text-green-400", children: [_jsx(CheckCircle2, { size: 12 }), "Configured"] })) : (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-amber-600 dark:text-amber-400", children: [_jsx(Clock, { size: 12 }), "Not configured"] })), _jsxs("button", { onClick: () => window.location.href = '/dashboard/newsletter/subscribers', className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-primary hover:underline", children: [_jsx(Users, { size: 12 }), subscriberCount, " subscriber", subscriberCount !== 1 ? 's' : ''] }), welcomeEmailLastUpdated && (_jsxs("span", { className: "text-[10px] text-dashboard-text-secondary", children: ["Last updated: ", new Date(welcomeEmailLastUpdated).toLocaleDateString()] }))] })] })] }), _jsxs("button", { onClick: () => window.location.href = '/dashboard/newsletter/welcome', className: "inline-flex items-center gap-2 px-4 py-2.5 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors bg-primary text-white hover:bg-primary/90", children: [_jsx(Edit2, { size: 14 }), welcomeEmailStatus === 'configured' ? 'Edit' : 'Configure'] })] }) }), _jsx("div", { className: "bg-dashboard-bg rounded-3xl border border-dashboard-border overflow-hidden", children: isLoading ? (_jsx("div", { className: "flex items-center justify-center py-20", children: _jsx("div", { className: "w-8 h-8 border-4 border-primary/20 border-t-primary rounded-full animate-spin" }) })) : filteredNewsletters.length === 0 ? (_jsxs("div", { className: "py-24 text-center", children: [_jsx(Mail, { size: 64, className: "mx-auto text-dashboard-text-secondary mb-4" }), _jsx("p", { className: "text-dashboard-text-secondary font-serif italic text-lg mb-6", children: statusFilter === 'all' ? 'No newsletters yet.' : `No newsletters found with status "${statusFilter}".` }), _jsxs("button", { onClick: handleCreate, className: "inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90", children: [_jsx(Plus, { size: 14 }), "Create Your First Newsletter"] })] })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-left border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "bg-dashboard-bg text-dashboard-text text-[10px] uppercase tracking-[0.2em] font-black border-b border-dashboard-border", children: [_jsx("th", { className: "px-8 py-5", children: "Title" }), _jsx("th", { className: "px-8 py-5", children: "Subject" }), _jsx("th", { className: "px-8 py-5", children: "Status" }), _jsx("th", { className: "px-8 py-5", children: "Last Sent" }), _jsx("th", { className: "px-8 py-5 text-right", children: "Updated" }), _jsx("th", { className: "px-8 py-5 text-right", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-dashboard-border", children: filteredNewsletters.map((newsletter) => (_jsxs("tr", { className: "hover:bg-dashboard-bg transition-colors group", children: [_jsx("td", { className: "px-8 py-5", children: _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover:bg-primary group-hover:text-white transition-colors", children: _jsx(Mail, { size: 18 }) }), _jsx("span", { className: "text-sm font-medium text-dashboard-text tracking-tight", children: newsletter.title })] }) }), _jsx("td", { className: "px-8 py-5", children: _jsx("span", { className: "text-sm text-dashboard-text-secondary", children: newsletter.subject || 'No subject' }) }), _jsx("td", { className: "px-8 py-5", children: _jsx("span", { className: `text-[10px] font-black px-3 py-1 rounded-full uppercase border ${getStatusBadgeColor(newsletter.status)}`, children: newsletter.status }) }), _jsx("td", { className: "px-8 py-5", children: formatLastSent(newsletter.sendHistory) || (_jsx("span", { className: "text-xs text-dashboard-text-secondary", children: "Never" })) }), _jsx("td", { className: "px-8 py-5 text-right text-xs text-dashboard-text-secondary font-medium", children: _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(Calendar, { size: 14 }), formatDate(newsletter.updatedAt)] }) }), _jsx("td", { className: "px-8 py-5 text-right", children: _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx("button", { onClick: () => handleSend(newsletter), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors", title: newsletter.status === 'sent' ? 'Resend newsletter' : 'Send newsletter', children: _jsx(Send, { size: 18 }) }), _jsx("button", { onClick: () => handleEdit(newsletter.id), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors", title: "Edit newsletter", children: _jsx(Edit2, { size: 18 }) }), _jsx("button", { onClick: () => handleDelete(newsletter.id, newsletter.title), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors", title: "Delete newsletter", children: _jsx(Trash2, { size: 18 }) })] }) })] }, newsletter.id))) })] }) })) })] }) }), _jsx(SmtpSettingsModal, { isOpen: showSmtpModal, onClose: () => setShowSmtpModal(false) }), _jsx(TestEmailModal, { isOpen: showTestEmailModal, onClose: () => setShowTestEmailModal(false) }), selectedNewsletter && (_jsx(SendNewsletterModal, { isOpen: showSendModal, onClose: () => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhits/plugin-newsletter",
3
- "version": "0.0.15",
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,20 +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
- sendHistory: newsletter.sendHistory || [],
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: newsletter.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: any = {
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(),
@@ -160,6 +160,8 @@ export interface NewsletterListItem {
160
160
  recipientCount?: number;
161
161
  hidden?: boolean;
162
162
  sendHistory?: SendHistoryEntry[];
163
+ availableLanguages?: string[];
164
+ languages?: NewsletterLanguages;
163
165
  }
164
166
 
165
167
  /**
@@ -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 response = await fetch('/api/plugin-newsletter/newsletters', {
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
- <span className="text-sm font-medium text-dashboard-text tracking-tight">
421
- {newsletter.title}
422
- </span>
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
+ }