@jhits/plugin-newsletter 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/handlers/newsletters.d.ts.map +1 -1
- package/dist/api/handlers/newsletters.js +80 -24
- package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
- package/dist/api/handlers/send-newsletter.js +31 -4
- package/dist/api/handlers/welcome-email.d.ts.map +1 -1
- package/dist/api/handlers/welcome-email.js +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/types/newsletter.d.ts +35 -0
- package/dist/types/newsletter.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.js +28 -7
- package/dist/views/CanvasEditor/hooks/useNewsletterLoader.js +4 -4
- package/dist/views/NewsletterManager.d.ts.map +1 -1
- package/dist/views/NewsletterManager.js +96 -9
- package/dist/views/components/NewsletterCard.d.ts +16 -0
- package/dist/views/components/NewsletterCard.d.ts.map +1 -0
- package/dist/views/components/NewsletterCard.js +94 -0
- package/dist/views/components/NewsletterGrid.d.ts +16 -0
- package/dist/views/components/NewsletterGrid.d.ts.map +1 -0
- package/dist/views/components/NewsletterGrid.js +13 -0
- package/dist/views/components/SendNewsletterModal.js +1 -1
- package/package.json +1 -1
- package/src/api/handlers/newsletters.ts +94 -28
- package/src/api/handlers/send-newsletter.ts +33 -4
- package/src/api/handlers/welcome-email.ts +5 -2
- package/src/index.tsx +4 -1
- package/src/types/newsletter.ts +44 -0
- package/src/views/CanvasEditor/CanvasEditorView.tsx +28 -8
- package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +4 -4
- package/src/views/NewsletterManager.tsx +203 -20
- package/src/views/components/NewsletterCard.tsx +212 -0
- package/src/views/components/NewsletterGrid.tsx +48 -0
- package/src/views/components/SendNewsletterModal.tsx +5 -5
|
@@ -79,8 +79,22 @@ export async function POST_SEND_NEWSLETTER(
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
const
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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',
|
package/src/types/newsletter.ts
CHANGED
|
@@ -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 -
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
112
|
-
if (
|
|
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
|
-
|
|
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
|
-
//
|
|
21
|
-
if (
|
|
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
|
|
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
|
|
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
|
-
<
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
}
|