@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
@@ -79,8 +79,22 @@ export async function POST_SEND_NEWSLETTER(
79
79
  );
80
80
  }
81
81
 
82
- const blocks = newsletter.blocks || [];
83
- const metadata = newsletter.metadata || {};
82
+ // Get language-specific content if available
83
+ const languages = newsletter.languages || {};
84
+ const primaryLanguage = newsletter.metadata?.lang || 'en';
85
+
86
+ let blocks = newsletter.blocks || [];
87
+ let metadata = newsletter.metadata || {};
88
+
89
+ // If the requested language has content, use it
90
+ if (language && languages[language]) {
91
+ blocks = languages[language].blocks || [];
92
+ metadata = { ...metadata, ...languages[language].metadata };
93
+ } else if (language !== primaryLanguage && languages[primaryLanguage]) {
94
+ // Fall back to primary language content
95
+ blocks = languages[primaryLanguage].blocks || [];
96
+ metadata = { ...metadata, ...languages[primaryLanguage].metadata };
97
+ }
84
98
 
85
99
  if (!blocks || blocks.length === 0) {
86
100
  return NextResponse.json(
@@ -150,13 +164,18 @@ export async function POST_SEND_NEWSLETTER(
150
164
  recipientEmails = [testEmail];
151
165
  recipientCount = 1;
152
166
  } else {
153
- const subscriberList = await subscribers.find({ status: 'active' }).toArray();
167
+ // Filter subscribers by the selected language
168
+ const subscriberFilter: any = { status: 'active' };
169
+ if (language) {
170
+ subscriberFilter.language = language;
171
+ }
172
+ const subscriberList = await subscribers.find(subscriberFilter).toArray();
154
173
  recipientEmails = subscriberList.map((s: any) => s.email);
155
174
  recipientCount = recipientEmails.length;
156
175
 
157
176
  if (recipientCount === 0) {
158
177
  return NextResponse.json(
159
- { error: 'No active subscribers found' },
178
+ { error: `No active subscribers found for language: ${language?.toUpperCase() || 'en'}` },
160
179
  { status: 400 }
161
180
  );
162
181
  }
@@ -211,12 +230,22 @@ export async function POST_SEND_NEWSLETTER(
211
230
  }
212
231
 
213
232
  if (!isTest && successCount > 0) {
233
+ const sendHistoryEntry = {
234
+ language,
235
+ sentAt: new Date().toISOString(),
236
+ recipientCount: successCount,
237
+ authorId: userId,
238
+ };
239
+
214
240
  await newsletters.updateOne(filter, {
215
241
  $set: {
216
242
  'publication.status': 'sent',
217
243
  'publication.sentDate': new Date().toISOString(),
218
244
  'publication.recipientCount': successCount,
219
245
  updatedAt: new Date().toISOString(),
246
+ },
247
+ $push: {
248
+ sendHistory: sendHistoryEntry,
220
249
  }
221
250
  });
222
251
  }
@@ -101,7 +101,7 @@ export async function GET_WELCOME_EMAIL(
101
101
  title: 'Welcome Email',
102
102
  language,
103
103
  blocks: primaryContent.blocks,
104
- metadata: primaryContent.metadata,
104
+ metadata: { ...primaryContent.metadata, lang: language },
105
105
  languages,
106
106
  isCopy: true,
107
107
  copyFrom: primaryLanguage,
@@ -114,7 +114,10 @@ export async function GET_WELCOME_EMAIL(
114
114
  title: 'Welcome Email',
115
115
  language,
116
116
  blocks: languages[language as keyof WelcomeEmailLanguages]?.blocks || primaryContent.blocks,
117
- metadata: languages[language as keyof WelcomeEmailLanguages]?.metadata || primaryContent.metadata,
117
+ metadata: {
118
+ ...(languages[language as keyof WelcomeEmailLanguages]?.metadata || primaryContent.metadata),
119
+ lang: language
120
+ },
118
121
  languages,
119
122
  isCopy: false,
120
123
  });
package/src/index.tsx CHANGED
@@ -175,10 +175,13 @@ export default function NewsletterPlugin(props: PluginProps) {
175
175
  apiData.metadata.lang = extraData.language;
176
176
  }
177
177
 
178
+ // Build URL with language param if provided
179
+ const langParam = extraData?.language ? `?language=${extraData.language}` : '';
180
+
178
181
  // If we have an id, try to update first
179
182
  if (originalId) {
180
183
  console.log('[NewsletterPlugin] Attempting to update newsletter with id:', originalId);
181
- const updateResponse = await fetch(`/api/plugin-newsletter/newsletters/${originalId}`, {
184
+ const updateResponse = await fetch(`/api/plugin-newsletter/newsletters/${originalId}${langParam}`, {
182
185
  method: 'PUT',
183
186
  headers: { 'Content-Type': 'application/json' },
184
187
  credentials: 'include',
@@ -47,6 +47,26 @@ export interface NewsletterPublicationData {
47
47
 
48
48
  /** Last modified date */
49
49
  updatedAt?: string;
50
+
51
+ /** Recipient count for last send */
52
+ recipientCount?: number;
53
+ }
54
+
55
+ /**
56
+ * Send history entry for tracking sends per language
57
+ */
58
+ export interface SendHistoryEntry {
59
+ /** Language code sent to */
60
+ language: string;
61
+
62
+ /** When this was sent */
63
+ sentAt: string;
64
+
65
+ /** Number of recipients */
66
+ recipientCount: number;
67
+
68
+ /** Who sent it (author ID) */
69
+ authorId?: string;
50
70
  }
51
71
 
52
72
  /**
@@ -69,6 +89,21 @@ export interface NewsletterMetadata {
69
89
  };
70
90
  }
71
91
 
92
+ /**
93
+ * Newsletter language content (blocks + metadata per language)
94
+ */
95
+ export interface NewsletterLanguageContent {
96
+ blocks: Block[];
97
+ metadata: NewsletterMetadata;
98
+ }
99
+
100
+ /**
101
+ * Languages object storing content per language
102
+ */
103
+ export interface NewsletterLanguages {
104
+ [key: string]: NewsletterLanguageContent;
105
+ }
106
+
72
107
  /**
73
108
  * Complete Newsletter Structure
74
109
  * This is the headless JSON structure stored in the database
@@ -89,6 +124,12 @@ export interface Newsletter {
89
124
  /** Publication data */
90
125
  publication: NewsletterPublicationData;
91
126
 
127
+ /** Send history for tracking multiple sends per language */
128
+ sendHistory?: SendHistoryEntry[];
129
+
130
+ /** Content per language (for multi-language newsletters) */
131
+ languages?: NewsletterLanguages;
132
+
92
133
  /** Additional metadata */
93
134
  metadata: NewsletterMetadata;
94
135
 
@@ -118,6 +159,9 @@ export interface NewsletterListItem {
118
159
  updatedAt: string;
119
160
  recipientCount?: number;
120
161
  hidden?: boolean;
162
+ sendHistory?: SendHistoryEntry[];
163
+ availableLanguages?: string[];
164
+ languages?: NewsletterLanguages;
121
165
  }
122
166
 
123
167
  /**
@@ -46,15 +46,26 @@ export function CanvasEditorView({ newsletterId, darkMode, backgroundColors: pro
46
46
  // Get registered blocks
47
47
  const registeredBlocks = useRegisteredBlocks();
48
48
 
49
- // Newsletter loading - use current language for welcome email (wait until language settings are loaded)
49
+ // Newsletter loading - wait for language settings to be loaded first
50
50
  const { isLoadingNewsletter } = useNewsletterLoader(
51
51
  newsletterId,
52
52
  state.newsletterId,
53
- (newsletter) => {
53
+ (newsletter: any) => {
54
54
  helpers.loadNewsletter(newsletter);
55
- // Set current language from loaded newsletter metadata
56
- if (newsletter.metadata?.lang && !isWelcomeEmail) {
57
- setCurrentLanguage(newsletter.metadata.lang);
55
+ // Set current language from loaded newsletter (check both metadata.lang and top-level language)
56
+ const loadedLanguage = newsletter.metadata?.lang || newsletter.language;
57
+ if (loadedLanguage) {
58
+ setCurrentLanguage(loadedLanguage);
59
+ }
60
+ // Update available languages from newsletter's languages object (for regular newsletters)
61
+ if (!isWelcomeEmail && newsletter.languages && Object.keys(newsletter.languages).length > 0) {
62
+ const langs = Object.keys(newsletter.languages);
63
+ setAvailableLanguages(langs);
64
+ }
65
+ // For welcome emails, also update available languages
66
+ if (isWelcomeEmail && newsletter.languages && Object.keys(newsletter.languages).length > 0) {
67
+ const langs = Object.keys(newsletter.languages);
68
+ setAvailableLanguages(langs);
58
69
  }
59
70
  setTimeout(() => {
60
71
  dispatch({ type: 'MARK_CLEAN' });
@@ -108,8 +119,16 @@ export function CanvasEditorView({ newsletterId, darkMode, backgroundColors: pro
108
119
  setCurrentLanguage(newLanguage);
109
120
 
110
121
  // Reload with new language
111
- const response = await fetch(`/api/plugin-newsletter/welcome-email?language=${newLanguage}`);
112
- if (response.ok) {
122
+ let response;
123
+ if (isWelcomeEmail) {
124
+ response = await fetch(`/api/plugin-newsletter/welcome-email?language=${newLanguage}`);
125
+ } else if (newsletterId) {
126
+ response = await fetch(`/api/plugin-newsletter/newsletters/${newsletterId}?language=${newLanguage}`);
127
+ } else {
128
+ return;
129
+ }
130
+
131
+ if (response && response.ok) {
113
132
  const newsletter = await response.json();
114
133
  helpers.loadNewsletter(newsletter);
115
134
  dispatch({ type: 'MARK_CLEAN' });
@@ -218,7 +237,8 @@ export function CanvasEditorView({ newsletterId, darkMode, backgroundColors: pro
218
237
  setIsSaving(true);
219
238
  setSaveError(null);
220
239
  try {
221
- await helpers.save(isWelcomeEmail ? { language: currentLanguage } : undefined);
240
+ // Always pass language for saving (both welcome email and regular newsletters)
241
+ await helpers.save({ language: currentLanguage });
222
242
  setIsSaving(false);
223
243
  } catch (error: any) {
224
244
  console.error('[CanvasEditorView] Save error:', error);
@@ -17,14 +17,13 @@ export function useNewsletterLoader(
17
17
  return;
18
18
  }
19
19
 
20
- // For welcome emails, wait until language is provided
21
- if (isWelcomeEmail && !language) {
20
+ // Wait until language is provided (for both welcome emails and regular newsletters)
21
+ if (!language) {
22
22
  return;
23
23
  }
24
24
 
25
25
  // Skip if we have a regular newsletter id but no id yet, or if this is welcome email mode
26
26
  if (isWelcomeEmail) {
27
- // Load welcome email with language
28
27
  // Load welcome email with language
29
28
  const loadWelcomeEmail = async () => {
30
29
  try {
@@ -51,7 +50,8 @@ export function useNewsletterLoader(
51
50
  const loadNewsletterData = async () => {
52
51
  try {
53
52
  setIsLoadingNewsletter(true);
54
- const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletterId}`);
53
+ const langParam = language ? `?language=${language}` : '';
54
+ const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletterId}${langParam}`);
55
55
  if (!response.ok) {
56
56
  throw new Error('Failed to load newsletter');
57
57
  }
@@ -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';
10
- import { NewsletterListItem, NewsletterStatus } from '../types/newsletter';
9
+ import { Plus, Mail, Calendar, Trash2, Edit2, Settings2, Sparkles, CheckCircle2, Clock, Send, Users, Grid, List } from 'lucide-react';
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(() => {
@@ -153,10 +179,6 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
153
179
  setShowSmtpModal(true);
154
180
  return;
155
181
  }
156
- if (newsletter.status === 'sent') {
157
- alert('This newsletter has already been sent');
158
- return;
159
- }
160
182
 
161
183
  try {
162
184
  const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletter.id}/send`, {
@@ -220,6 +242,62 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
220
242
  });
221
243
  };
222
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
+
276
+ // Format last sent info
277
+ const formatLastSent = (sendHistory: SendHistoryEntry[] | undefined) => {
278
+ if (!sendHistory || sendHistory.length === 0) return null;
279
+
280
+ const lastEntry = sendHistory[sendHistory.length - 1];
281
+ const langLabel = lastEntry.language.toUpperCase();
282
+ const dateStr = formatDate(lastEntry.sentAt);
283
+
284
+ return (
285
+ <div className="flex items-center gap-2">
286
+ <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">
287
+ {langLabel}
288
+ </span>
289
+ <span className="text-xs text-dashboard-text-secondary">
290
+ {dateStr}
291
+ </span>
292
+ {sendHistory.length > 1 && (
293
+ <span className="text-[10px] text-dashboard-text-secondary" title="Total sends">
294
+ ({sendHistory.length})
295
+ </span>
296
+ )}
297
+ </div>
298
+ );
299
+ };
300
+
223
301
  return (
224
302
  <>
225
303
  <div className="h-full w-full rounded-[2.5rem] bg-white dark:bg-neutral-900 p-8 overflow-y-auto">
@@ -290,6 +368,32 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
290
368
  <option value="archived">Archived</option>
291
369
  </select>
292
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
+
293
397
  {/* Create Button */}
294
398
  <button
295
399
  onClick={handleCreate}
@@ -372,6 +476,15 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
372
476
  Create Your First Newsletter
373
477
  </button>
374
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
+ />
375
488
  ) : (
376
489
  <div className="overflow-x-auto">
377
490
  <table className="w-full text-left border-collapse">
@@ -380,6 +493,7 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
380
493
  <th className="px-8 py-5">Title</th>
381
494
  <th className="px-8 py-5">Subject</th>
382
495
  <th className="px-8 py-5">Status</th>
496
+ <th className="px-8 py-5">Last Sent</th>
383
497
  <th className="px-8 py-5 text-right">Updated</th>
384
498
  <th className="px-8 py-5 text-right">Actions</th>
385
499
  </tr>
@@ -395,9 +509,22 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
395
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">
396
510
  <Mail size={18} />
397
511
  </div>
398
- <span className="text-sm font-medium text-dashboard-text tracking-tight">
399
- {newsletter.title}
400
- </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>
401
528
  </div>
402
529
  </td>
403
530
  <td className="px-8 py-5">
@@ -410,6 +537,11 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
410
537
  {newsletter.status}
411
538
  </span>
412
539
  </td>
540
+ <td className="px-8 py-5">
541
+ {formatLastSent(newsletter.sendHistory) || (
542
+ <span className="text-xs text-dashboard-text-secondary">Never</span>
543
+ )}
544
+ </td>
413
545
  <td className="px-8 py-5 text-right text-xs text-dashboard-text-secondary font-medium">
414
546
  <div className="flex items-center justify-end gap-2">
415
547
  <Calendar size={14} />
@@ -418,15 +550,13 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
418
550
  </td>
419
551
  <td className="px-8 py-5 text-right">
420
552
  <div className="flex items-center justify-end gap-2">
421
- {newsletter.status !== 'sent' && (
422
- <button
423
- onClick={() => handleSend(newsletter)}
424
- className="p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors"
425
- title="Send newsletter"
426
- >
427
- <Send size={18} />
428
- </button>
429
- )}
553
+ <button
554
+ onClick={() => handleSend(newsletter)}
555
+ className="p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors"
556
+ title={newsletter.status === 'sent' ? 'Resend newsletter' : 'Send newsletter'}
557
+ >
558
+ <Send size={18} />
559
+ </button>
430
560
  <button
431
561
  onClick={() => handleEdit(newsletter.id)}
432
562
  className="p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors"
@@ -478,6 +608,59 @@ export function NewsletterManagerView({ siteId, locale }: NewsletterManagerViewP
478
608
  subscriberCount={subscriberCount}
479
609
  />
480
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
+ })()}
481
664
  </>
482
665
  );
483
666
  }