@jhits/plugin-newsletter 0.0.15 → 0.0.17
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/email-utils.d.ts.map +1 -1
- package/dist/api/email-utils.js +45 -4
- package/dist/api/handlers/newsletters.d.ts.map +1 -1
- package/dist/api/handlers/newsletters.js +33 -16
- package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
- package/dist/api/handlers/send-newsletter.js +54 -6
- package/dist/api/handlers/settings.d.ts.map +1 -1
- package/dist/api/handlers/settings.js +51 -1
- package/dist/index.d.ts +27 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -122
- package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
- package/dist/lib/blocks/BlockRenderer.js +14 -2
- package/dist/lib/email/EmailRenderer.d.ts +1 -0
- package/dist/lib/email/EmailRenderer.d.ts.map +1 -1
- package/dist/lib/email/EmailRenderer.js +31 -19
- package/dist/lib/utils/config-resolver.d.ts +33 -0
- package/dist/lib/utils/config-resolver.d.ts.map +1 -0
- package/dist/lib/utils/config-resolver.js +47 -0
- package/dist/registry/BlockRegistry.d.ts +9 -1
- package/dist/registry/BlockRegistry.d.ts.map +1 -1
- package/dist/registry/BlockRegistry.js +126 -8
- package/dist/state/EditorContext.d.ts +11 -1
- package/dist/state/EditorContext.d.ts.map +1 -1
- package/dist/state/EditorContext.js +23 -5
- package/dist/state/types.d.ts +12 -0
- package/dist/state/types.d.ts.map +1 -1
- package/dist/types/block.d.ts +9 -0
- package/dist/types/block.d.ts.map +1 -1
- package/dist/types/newsletter.d.ts +4 -0
- package/dist/types/newsletter.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.js +24 -3
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.js +77 -17
- package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
- package/dist/views/CanvasEditor/EditorBody.js +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.js +158 -100
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +3 -1
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorSidebar.js +3 -3
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +1 -1
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -1
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.js +6 -40
- package/dist/views/NewsletterManager.d.ts.map +1 -1
- package/dist/views/NewsletterManager.js +87 -5
- package/dist/views/components/DomainPromptModal.d.ts +13 -0
- package/dist/views/components/DomainPromptModal.d.ts.map +1 -0
- package/dist/views/components/DomainPromptModal.js +58 -0
- 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.d.ts.map +1 -1
- package/dist/views/components/SendNewsletterModal.js +91 -22
- package/dist/views/components/SmtpSettingsModal.d.ts.map +1 -1
- package/dist/views/components/SmtpSettingsModal.js +10 -0
- package/dist/views/components/TestEmailModal.d.ts.map +1 -1
- package/dist/views/components/TestEmailModal.js +86 -17
- package/package.json +53 -9
- package/src/api/email-utils.ts +53 -4
- package/src/api/handlers/newsletters.ts +40 -20
- package/src/api/handlers/send-newsletter.ts +65 -6
- package/src/api/handlers/settings.ts +60 -2
- package/src/index.tsx +49 -155
- package/src/lib/blocks/BlockRenderer.tsx +16 -2
- package/src/lib/email/EmailRenderer.tsx +31 -20
- package/src/lib/utils/config-resolver.ts +71 -0
- package/src/registry/BlockRegistry.tsx +255 -0
- package/src/state/EditorContext.tsx +43 -8
- package/src/state/types.ts +16 -0
- package/src/types/block.ts +10 -0
- package/src/types/newsletter.ts +5 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +27 -2
- package/src/views/CanvasEditor/CanvasEditorView.tsx +142 -61
- package/src/views/CanvasEditor/EditorBody.tsx +17 -13
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +178 -115
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +57 -2
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +6 -45
- package/src/views/NewsletterManager.tsx +164 -6
- package/src/views/components/DomainPromptModal.tsx +160 -0
- package/src/views/components/NewsletterCard.tsx +212 -0
- package/src/views/components/NewsletterGrid.tsx +48 -0
- package/src/views/components/SendNewsletterModal.tsx +270 -184
- package/src/views/components/SmtpSettingsModal.tsx +11 -0
- package/src/views/components/TestEmailModal.tsx +235 -149
- package/src/registry/BlockRegistry.ts +0 -53
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
'use client';
|
|
6
6
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
7
7
|
import { useState, useEffect } from 'react';
|
|
8
|
-
import { Plus, Mail, Calendar, Trash2, Edit2, Settings2, Sparkles, CheckCircle2, Clock, Send, Users } from 'lucide-react';
|
|
8
|
+
import { Plus, Mail, Calendar, Trash2, Edit2, Settings2, Sparkles, CheckCircle2, Clock, Send, Users, Grid, List } from 'lucide-react';
|
|
9
9
|
import { SmtpSettingsModal } from './components/SmtpSettingsModal';
|
|
10
10
|
import { TestEmailModal } from './components/TestEmailModal';
|
|
11
11
|
import { SendNewsletterModal } from './components/SendNewsletterModal';
|
|
12
|
+
import { NewsletterGrid } from './components/NewsletterGrid';
|
|
12
13
|
function getStatusBadgeColor(status) {
|
|
13
14
|
switch (status) {
|
|
14
15
|
case 'sent':
|
|
@@ -27,6 +28,8 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
27
28
|
const [newsletters, setNewsletters] = useState([]);
|
|
28
29
|
const [isLoading, setIsLoading] = useState(true);
|
|
29
30
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
31
|
+
const [primaryLanguage, setPrimaryLanguage] = useState('en');
|
|
32
|
+
const [viewMode, setViewMode] = useState('grid');
|
|
30
33
|
const [showSmtpModal, setShowSmtpModal] = useState(false);
|
|
31
34
|
const [showTestEmailModal, setShowTestEmailModal] = useState(false);
|
|
32
35
|
const [showSendModal, setShowSendModal] = useState(false);
|
|
@@ -36,12 +39,33 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
36
39
|
const [welcomeEmailStatus, setWelcomeEmailStatus] = useState('not_configured');
|
|
37
40
|
const [welcomeEmailLastUpdated, setWelcomeEmailLastUpdated] = useState(null);
|
|
38
41
|
const [subscriberCount, setSubscriberCount] = useState(0);
|
|
42
|
+
// Hover state for language tooltip
|
|
43
|
+
const [hoveredNewsletter, setHoveredNewsletter] = useState(null);
|
|
44
|
+
// Fetch primary language from SMTP settings
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const fetchPrimaryLanguage = async () => {
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch('/api/plugin-newsletter/smtp', {
|
|
49
|
+
credentials: 'include',
|
|
50
|
+
});
|
|
51
|
+
if (response.ok) {
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
setPrimaryLanguage(data.primaryLanguage || 'en');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('Failed to fetch SMTP settings:', error);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
fetchPrimaryLanguage();
|
|
61
|
+
}, []);
|
|
39
62
|
// Fetch newsletters
|
|
40
63
|
useEffect(() => {
|
|
41
64
|
const fetchNewsletters = async () => {
|
|
42
65
|
try {
|
|
43
66
|
setIsLoading(true);
|
|
44
|
-
const
|
|
67
|
+
const langParam = primaryLanguage ? `?language=${primaryLanguage}` : '';
|
|
68
|
+
const response = await fetch(`/api/plugin-newsletter/newsletters${langParam}`, {
|
|
45
69
|
credentials: 'include',
|
|
46
70
|
});
|
|
47
71
|
if (!response.ok) {
|
|
@@ -58,7 +82,7 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
58
82
|
}
|
|
59
83
|
};
|
|
60
84
|
fetchNewsletters();
|
|
61
|
-
}, []);
|
|
85
|
+
}, [primaryLanguage]);
|
|
62
86
|
// Fetch welcome email status
|
|
63
87
|
useEffect(() => {
|
|
64
88
|
const fetchWelcomeEmailStatus = async () => {
|
|
@@ -197,6 +221,37 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
197
221
|
year: 'numeric',
|
|
198
222
|
});
|
|
199
223
|
};
|
|
224
|
+
// Format date with time
|
|
225
|
+
const formatDateTime = (dateString) => {
|
|
226
|
+
if (!dateString)
|
|
227
|
+
return 'Never';
|
|
228
|
+
const date = new Date(dateString);
|
|
229
|
+
return date.toLocaleString(locale, {
|
|
230
|
+
day: 'numeric',
|
|
231
|
+
month: 'short',
|
|
232
|
+
year: 'numeric',
|
|
233
|
+
hour: '2-digit',
|
|
234
|
+
minute: '2-digit',
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
// Get send history for a specific language
|
|
238
|
+
const getSendHistoryForLanguage = (sendHistory, language) => {
|
|
239
|
+
return sendHistory
|
|
240
|
+
.filter(entry => entry.language === language)
|
|
241
|
+
.sort((a, b) => new Date(b.sentAt).getTime() - new Date(a.sentAt).getTime());
|
|
242
|
+
};
|
|
243
|
+
// Sort languages: those with send history first (most recent first), then by language code
|
|
244
|
+
const sortLanguages = (langs, sendHistory) => {
|
|
245
|
+
return [...langs].sort((a, b) => {
|
|
246
|
+
const aHasHistory = sendHistory.some(h => h.language === a);
|
|
247
|
+
const bHasHistory = sendHistory.some(h => h.language === b);
|
|
248
|
+
if (aHasHistory && !bHasHistory)
|
|
249
|
+
return -1;
|
|
250
|
+
if (!aHasHistory && bHasHistory)
|
|
251
|
+
return 1;
|
|
252
|
+
return a.localeCompare(b);
|
|
253
|
+
});
|
|
254
|
+
};
|
|
200
255
|
// Format last sent info
|
|
201
256
|
const formatLastSent = (sendHistory) => {
|
|
202
257
|
if (!sendHistory || sendHistory.length === 0)
|
|
@@ -216,7 +271,18 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
216
271
|
}
|
|
217
272
|
}, className: "inline-flex items-center gap-2 px-4 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border", children: [_jsx(Send, { size: 14 }), "Test Email"] }), _jsxs("button", { onClick: () => setShowSmtpModal(true), className: `inline-flex items-center gap-2 px-4 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors ${smtpStatus === 'not_configured'
|
|
218
273
|
? 'bg-amber-50 border-amber-300 text-amber-700 dark:bg-amber-900/20 dark:border-amber-700 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/40'
|
|
219
|
-
: 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'}`, children: [_jsx(Settings2, { size: 14 }), "SMTP", smtpStatus === 'not_configured' && (_jsx("span", { className: "w-2 h-2 rounded-full bg-amber-500" }))] }), _jsxs("select", { value: statusFilter, onChange: (e) => setStatusFilter(e.target.value), className: "bg-dashboard-bg border border-dashboard-border rounded-xl px-4 py-2.5 text-xs font-bold text-dashboard-text outline-none cursor-pointer uppercase tracking-widest", children: [_jsx("option", { value: "all", children: "All Status" }), _jsx("option", { value: "draft", children: "Draft" }), _jsx("option", { value: "scheduled", children: "Scheduled" }), _jsx("option", { value: "sent", children: "Sent" }), _jsx("option", { value: "archived", children: "Archived" })] }), _jsxs("
|
|
274
|
+
: 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'}`, children: [_jsx(Settings2, { size: 14 }), "SMTP", smtpStatus === 'not_configured' && (_jsx("span", { className: "w-2 h-2 rounded-full bg-amber-500" }))] }), _jsxs("select", { value: statusFilter, onChange: (e) => setStatusFilter(e.target.value), className: "bg-dashboard-bg border border-dashboard-border rounded-xl px-4 py-2.5 text-xs font-bold text-dashboard-text outline-none cursor-pointer uppercase tracking-widest", children: [_jsx("option", { value: "all", children: "All Status" }), _jsx("option", { value: "draft", children: "Draft" }), _jsx("option", { value: "scheduled", children: "Scheduled" }), _jsx("option", { value: "sent", children: "Sent" }), _jsx("option", { value: "archived", children: "Archived" })] }), _jsxs("div", { className: "flex items-center bg-dashboard-bg border border-dashboard-border rounded-xl p-1 gap-1", children: [_jsx("button", { onClick: () => setViewMode('grid'), className: `p-2 rounded-lg transition-colors ${viewMode === 'grid'
|
|
275
|
+
? 'bg-primary text-white'
|
|
276
|
+
: 'text-dashboard-text-secondary hover:text-dashboard-text'}`, title: "Grid view", children: _jsx(Grid, { size: 16 }) }), _jsx("button", { onClick: () => setViewMode('list'), className: `p-2 rounded-lg transition-colors ${viewMode === 'list'
|
|
277
|
+
? 'bg-primary text-white'
|
|
278
|
+
: 'text-dashboard-text-secondary hover:text-dashboard-text'}`, title: "List view", children: _jsx(List, { size: 16 }) })] }), _jsxs("button", { onClick: handleCreate, className: "inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90", children: [_jsx(Plus, { size: 14 }), "New Newsletter"] })] })] }), _jsx("div", { className: "mb-6 p-6 rounded-2xl bg-gradient-to-br from-primary/5 via-primary/10 to-transparent border border-dashboard-border", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-5", children: [_jsx("div", { className: "w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Sparkles, { className: "w-6 h-6 text-primary" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-black text-dashboard-text uppercase tracking-tight mb-1", children: "Welcome Email" }), _jsx("p", { className: "text-xs text-dashboard-text-secondary mb-2", children: "The email sent automatically when someone subscribes" }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [welcomeEmailStatus === 'configured' ? (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-green-600 dark:text-green-400", children: [_jsx(CheckCircle2, { size: 12 }), "Configured"] })) : (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-amber-600 dark:text-amber-400", children: [_jsx(Clock, { size: 12 }), "Not configured"] })), _jsxs("button", { onClick: () => window.location.href = '/dashboard/newsletter/subscribers', className: "inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-primary hover:underline", children: [_jsx(Users, { size: 12 }), subscriberCount, " subscriber", subscriberCount !== 1 ? 's' : ''] }), welcomeEmailLastUpdated && (_jsxs("span", { className: "text-[10px] text-dashboard-text-secondary", children: ["Last updated: ", new Date(welcomeEmailLastUpdated).toLocaleDateString()] }))] })] })] }), _jsxs("button", { onClick: () => window.location.href = '/dashboard/newsletter/welcome', className: "inline-flex items-center gap-2 px-4 py-2.5 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors bg-primary text-white hover:bg-primary/90", children: [_jsx(Edit2, { size: 14 }), welcomeEmailStatus === 'configured' ? 'Edit' : 'Configure'] })] }) }), _jsx("div", { className: "bg-dashboard-bg rounded-3xl border border-dashboard-border overflow-hidden", children: isLoading ? (_jsx("div", { className: "flex items-center justify-center py-20", children: _jsx("div", { className: "w-8 h-8 border-4 border-primary/20 border-t-primary rounded-full animate-spin" }) })) : filteredNewsletters.length === 0 ? (_jsxs("div", { className: "py-24 text-center", children: [_jsx(Mail, { size: 64, className: "mx-auto text-dashboard-text-secondary mb-4" }), _jsx("p", { className: "text-dashboard-text-secondary font-serif italic text-lg mb-6", children: statusFilter === 'all' ? 'No newsletters yet.' : `No newsletters found with status "${statusFilter}".` }), _jsxs("button", { onClick: handleCreate, className: "inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg shadow-primary/20 bg-primary text-white hover:bg-primary/90", children: [_jsx(Plus, { size: 14 }), "Create Your First Newsletter"] })] })) : viewMode === 'grid' ? (_jsx(NewsletterGrid, { newsletters: filteredNewsletters, onEdit: handleEdit, onSend: handleSend, onDelete: handleDelete, formatDateTime: formatDateTime, locale: locale })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-left border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "bg-dashboard-bg text-dashboard-text text-[10px] uppercase tracking-[0.2em] font-black border-b border-dashboard-border", children: [_jsx("th", { className: "px-8 py-5", children: "Title" }), _jsx("th", { className: "px-8 py-5", children: "Subject" }), _jsx("th", { className: "px-8 py-5", children: "Status" }), _jsx("th", { className: "px-8 py-5", children: "Last Sent" }), _jsx("th", { className: "px-8 py-5 text-right", children: "Updated" }), _jsx("th", { className: "px-8 py-5 text-right", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-dashboard-border", children: filteredNewsletters.map((newsletter) => (_jsxs("tr", { className: "hover:bg-dashboard-bg transition-colors group", children: [_jsx("td", { className: "px-8 py-5", children: _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover:bg-primary group-hover:text-white transition-colors", children: _jsx(Mail, { size: 18 }) }), _jsx("div", { className: "relative", children: _jsx("span", { className: "text-sm font-medium text-dashboard-text tracking-tight cursor-pointer hover:text-primary transition-colors", onMouseEnter: (e) => {
|
|
279
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
280
|
+
setHoveredNewsletter({
|
|
281
|
+
id: newsletter.id,
|
|
282
|
+
x: rect.left + rect.width / 2,
|
|
283
|
+
y: rect.bottom + 8
|
|
284
|
+
});
|
|
285
|
+
}, onMouseLeave: () => setHoveredNewsletter(null), children: newsletter.title }) })] }) }), _jsx("td", { className: "px-8 py-5", children: _jsx("span", { className: "text-sm text-dashboard-text-secondary", children: newsletter.subject || 'No subject' }) }), _jsx("td", { className: "px-8 py-5", children: _jsx("span", { className: `text-[10px] font-black px-3 py-1 rounded-full uppercase border ${getStatusBadgeColor(newsletter.status)}`, children: newsletter.status }) }), _jsx("td", { className: "px-8 py-5", children: formatLastSent(newsletter.sendHistory) || (_jsx("span", { className: "text-xs text-dashboard-text-secondary", children: "Never" })) }), _jsx("td", { className: "px-8 py-5 text-right text-xs text-dashboard-text-secondary font-medium", children: _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(Calendar, { size: 14 }), formatDate(newsletter.updatedAt)] }) }), _jsx("td", { className: "px-8 py-5 text-right", children: _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx("button", { onClick: () => handleSend(newsletter), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors", title: newsletter.status === 'sent' ? 'Resend newsletter' : 'Send newsletter', children: _jsx(Send, { size: 18 }) }), _jsx("button", { onClick: () => handleEdit(newsletter.id), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-primary hover:bg-primary/10 transition-colors", title: "Edit newsletter", children: _jsx(Edit2, { size: 18 }) }), _jsx("button", { onClick: () => handleDelete(newsletter.id, newsletter.title), className: "p-2.5 rounded-full text-dashboard-text-secondary hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors", title: "Delete newsletter", children: _jsx(Trash2, { size: 18 }) })] }) })] }, newsletter.id))) })] }) })) })] }) }), _jsx(SmtpSettingsModal, { isOpen: showSmtpModal, onClose: () => setShowSmtpModal(false) }), _jsx(TestEmailModal, { isOpen: showTestEmailModal, onClose: () => setShowTestEmailModal(false) }), selectedNewsletter && (_jsx(SendNewsletterModal, { isOpen: showSendModal, onClose: () => {
|
|
220
286
|
setShowSendModal(false);
|
|
221
287
|
setSelectedNewsletter(null);
|
|
222
288
|
}, newsletter: {
|
|
@@ -225,5 +291,21 @@ export function NewsletterManagerView({ siteId, locale }) {
|
|
|
225
291
|
subject: selectedNewsletter.subject,
|
|
226
292
|
status: selectedNewsletter.status,
|
|
227
293
|
hasContent: selectedNewsletter.hasContent,
|
|
228
|
-
}, subscriberCount: subscriberCount }))
|
|
294
|
+
}, subscriberCount: subscriberCount })), hoveredNewsletter && (() => {
|
|
295
|
+
const tooltipNewsletter = newsletters.find(n => n.id === hoveredNewsletter.id);
|
|
296
|
+
if (!tooltipNewsletter?.languages)
|
|
297
|
+
return null;
|
|
298
|
+
const tooltipLanguages = tooltipNewsletter.languages;
|
|
299
|
+
const tooltipSendHistory = tooltipNewsletter.sendHistory || [];
|
|
300
|
+
return (_jsxs("div", { className: "fixed z-50 w-72 bg-dashboard-card border border-dashboard-border rounded-xl shadow-2xl p-3 pointer-events-none", style: {
|
|
301
|
+
left: hoveredNewsletter.x,
|
|
302
|
+
top: hoveredNewsletter.y,
|
|
303
|
+
transform: 'translateX(-50%)'
|
|
304
|
+
}, children: [_jsx("div", { className: "text-[10px] uppercase tracking-wider text-dashboard-text-secondary font-bold mb-2", children: "Translations & Send History" }), sortLanguages(Object.keys(tooltipLanguages), tooltipSendHistory).map((lang) => {
|
|
305
|
+
const data = tooltipLanguages[lang];
|
|
306
|
+
const langSendHistory = getSendHistoryForLanguage(tooltipSendHistory, lang);
|
|
307
|
+
const lastSend = langSendHistory[0];
|
|
308
|
+
return (_jsxs("div", { className: "py-2 border-b border-dashboard-border last:border-0", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-[10px] font-black uppercase text-primary", children: lang }), lastSend && (_jsxs("div", { className: "text-[9px] text-green-600 dark:text-green-400 flex items-center gap-1", children: [_jsx(Send, { size: 10 }), "Sent"] }))] }), _jsx("div", { className: "text-xs text-dashboard-text truncate", children: data?.metadata?.subject || 'No subject' }), lastSend && (_jsxs("div", { className: "text-[9px] text-dashboard-text-secondary mt-1", children: [formatDateTime(lastSend.sentAt), " \u2022 ", lastSend.recipientCount, " recipients"] })), langSendHistory.length > 1 && (_jsxs("div", { className: "text-[9px] text-dashboard-text-secondary mt-0.5", children: ["+ ", langSendHistory.length - 1, " more send(s)"] }))] }, lang));
|
|
309
|
+
})] }));
|
|
310
|
+
})()] }));
|
|
229
311
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Prompt Modal Component
|
|
3
|
+
* Non-technical modal to ask for the website domain when it's missing in dev mode.
|
|
4
|
+
*/
|
|
5
|
+
interface DomainPromptModalProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onSave: (domain: string) => Promise<void>;
|
|
9
|
+
language: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function DomainPromptModal({ isOpen, onClose, onSave, language }: DomainPromptModalProps): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=DomainPromptModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DomainPromptModal.d.ts","sourceRoot":"","sources":["../../../src/views/components/DomainPromptModal.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,UAAU,sBAAsB;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,sBAAsB,kDA8I9F"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Prompt Modal Component
|
|
3
|
+
* Non-technical modal to ask for the website domain when it's missing in dev mode.
|
|
4
|
+
*/
|
|
5
|
+
'use client';
|
|
6
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
import { Globe, Save, RefreshCw, CheckCircle2, Info } from 'lucide-react';
|
|
9
|
+
export function DomainPromptModal({ isOpen, onClose, onSave, language }) {
|
|
10
|
+
const [domain, setDomain] = useState('');
|
|
11
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
12
|
+
const [isSuccess, setIsSuccess] = useState(false);
|
|
13
|
+
const [error, setError] = useState(null);
|
|
14
|
+
const languageNames = {
|
|
15
|
+
en: 'English',
|
|
16
|
+
nl: 'Dutch',
|
|
17
|
+
sv: 'Swedish',
|
|
18
|
+
};
|
|
19
|
+
const handleSave = async () => {
|
|
20
|
+
if (!domain.trim()) {
|
|
21
|
+
setError('Please enter a website address');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// Basic URL validation/cleanup
|
|
25
|
+
let cleanDomain = domain.trim().toLowerCase();
|
|
26
|
+
if (cleanDomain.startsWith('http://'))
|
|
27
|
+
cleanDomain = cleanDomain.replace('http://', '');
|
|
28
|
+
if (cleanDomain.startsWith('https://'))
|
|
29
|
+
cleanDomain = cleanDomain.replace('https://', '');
|
|
30
|
+
if (cleanDomain.endsWith('/'))
|
|
31
|
+
cleanDomain = cleanDomain.slice(0, -1);
|
|
32
|
+
try {
|
|
33
|
+
setIsSaving(true);
|
|
34
|
+
setError(null);
|
|
35
|
+
await onSave(cleanDomain);
|
|
36
|
+
setIsSuccess(true);
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
onClose();
|
|
39
|
+
}, 1500);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
setError(err.message || 'Failed to save domain configuration');
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
setIsSaving(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
if (!isOpen)
|
|
49
|
+
return null;
|
|
50
|
+
return (_jsxs("div", { className: "fixed inset-0 z-[60] flex items-center justify-center p-4", children: [_jsx("div", { className: "absolute inset-0 bg-black/60 backdrop-blur-md", onClick: () => !isSaving && onClose() }), _jsxs("div", { className: "relative w-full max-w-lg bg-white dark:bg-neutral-900 rounded-[2.5rem] border border-dashboard-border shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-300", children: [_jsxs("div", { className: "px-10 pt-10 pb-6 text-center", children: [_jsx("div", { className: "w-20 h-20 bg-primary/10 rounded-[2rem] flex items-center justify-center mx-auto mb-6", children: _jsx(Globe, { size: 40, className: "text-primary" }) }), _jsxs("h2", { className: "text-3xl font-black text-dashboard-text uppercase tracking-tighter leading-none mb-2", children: ["Website Connection", _jsx("span", { className: "text-primary", children: "." })] }), _jsxs("p", { className: "text-sm text-neutral-500 dark:text-neutral-400 font-medium", children: ["We need to know where your ", _jsx("span", { className: "font-bold text-dashboard-text", children: languageNames[language] || language }), " website is hosted to ensure links work correctly."] })] }), _jsxs("div", { className: "px-10 pb-10 space-y-6", children: [_jsxs("div", { className: "bg-blue-50/50 dark:bg-blue-900/10 border border-blue-100 dark:border-blue-900/20 rounded-2xl p-4 flex gap-3", children: [_jsx(Info, { size: 18, className: "text-blue-500 flex-shrink-0 mt-0.5" }), _jsx("p", { className: "text-xs text-blue-700 dark:text-blue-300 leading-relaxed", children: "Since you are in development mode, we couldn't automatically find the domain for this language. Please provide it below." })] }), _jsxs("div", { children: [_jsxs("label", { className: "text-[10px] font-black text-dashboard-text-secondary uppercase tracking-[0.2em] block mb-3 ml-1", children: ["Your ", languageNames[language] || language, " Website URL"] }), _jsxs("div", { className: "relative group", children: [_jsx("div", { className: "absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 group-focus-within:text-primary transition-colors font-mono text-sm", children: "https://" }), _jsx("input", { type: "text", value: domain, onChange: (e) => {
|
|
51
|
+
setDomain(e.target.value);
|
|
52
|
+
setError(null);
|
|
53
|
+
}, onKeyDown: (e) => e.key === 'Enter' && handleSave(), placeholder: "www.yourwebsite.com", className: "w-full pl-20 pr-6 py-4 bg-dashboard-bg border border-dashboard-border rounded-2xl outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all text-dashboard-text font-medium", autoFocus: true })] }), error && (_jsx("p", { className: "text-[10px] text-red-500 font-bold uppercase tracking-widest mt-3 ml-1", children: error }))] }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx("button", { onClick: onClose, disabled: isSaving, className: "flex-1 px-6 py-4 rounded-full text-[10px] font-black uppercase tracking-widest transition-all bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border active:scale-95", children: "Skip for now" }), _jsx("button", { onClick: handleSave, disabled: isSaving || isSuccess, className: `flex-[2] inline-flex items-center justify-center gap-2 px-6 py-4 rounded-full text-[10px] font-black uppercase tracking-widest transition-all shadow-lg active:scale-95 ${isSaving
|
|
54
|
+
? 'bg-neutral-400 text-white cursor-not-allowed'
|
|
55
|
+
: isSuccess
|
|
56
|
+
? 'bg-green-600 text-white'
|
|
57
|
+
: 'bg-primary text-white hover:shadow-primary/20'}`, children: isSaving ? (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { className: "w-4 h-4 animate-spin" }), "Connecting..."] })) : isSuccess ? (_jsxs(_Fragment, { children: [_jsx(CheckCircle2, { className: "w-4 h-4" }), "Saved!"] })) : (_jsxs(_Fragment, { children: [_jsx(Save, { className: "w-4 h-4" }), "Link Website"] })) })] })] })] })] }));
|
|
58
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SendNewsletterModal.d.ts","sourceRoot":"","sources":["../../../src/views/components/SendNewsletterModal.tsx"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"SendNewsletterModal.d.ts","sourceRoot":"","sources":["../../../src/views/components/SendNewsletterModal.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAQH,UAAU,wBAAwB;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,OAAO,CAAC;KACvB,CAAC;IACF,eAAe,EAAE,MAAM,CAAC;CAC3B;AAWD,wBAAgB,mBAAmB,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,EAAE,wBAAwB,kDAuX7G"}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
6
6
|
import { useState, useEffect } from 'react';
|
|
7
7
|
import { X, Send, RefreshCw, CheckCircle2, AlertCircle, Mail, Globe, Users } from 'lucide-react';
|
|
8
|
+
import { DomainPromptModal } from './DomainPromptModal';
|
|
8
9
|
const languages = [
|
|
9
10
|
{ code: 'en', label: 'EN' },
|
|
10
11
|
{ code: 'nl', label: 'NL' },
|
|
@@ -18,15 +19,19 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
|
|
|
18
19
|
const [sendSuccess, setSendSuccess] = useState(false);
|
|
19
20
|
const [sendError, setSendError] = useState(null);
|
|
20
21
|
const [resultDetails, setResultDetails] = useState(null);
|
|
22
|
+
// Domain prompt state
|
|
23
|
+
const [showDomainPrompt, setShowDomainPrompt] = useState(false);
|
|
24
|
+
const [isCheckingDomain, setIsCheckingDomain] = useState(false);
|
|
21
25
|
useEffect(() => {
|
|
22
26
|
if (isOpen) {
|
|
23
27
|
setSendMode('subscribers');
|
|
24
28
|
setSendError(null);
|
|
25
29
|
setResultDetails(null);
|
|
26
30
|
setSendSuccess(false);
|
|
31
|
+
setShowDomainPrompt(false);
|
|
27
32
|
}
|
|
28
33
|
}, [isOpen]);
|
|
29
|
-
const handleSend = async () => {
|
|
34
|
+
const handleSend = async (skipDomainCheck = false) => {
|
|
30
35
|
if (sendMode === 'test' && !testEmail.trim()) {
|
|
31
36
|
setSendError('Please enter an email address');
|
|
32
37
|
return;
|
|
@@ -38,6 +43,30 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
|
|
|
38
43
|
return;
|
|
39
44
|
}
|
|
40
45
|
}
|
|
46
|
+
// Check if we need to prompt for domain first (usually helpful in dev mode)
|
|
47
|
+
if (!skipDomainCheck) {
|
|
48
|
+
try {
|
|
49
|
+
setIsCheckingDomain(true);
|
|
50
|
+
// Check if this language has a domain configured
|
|
51
|
+
const response = await fetch('/api/plugin-website/settings');
|
|
52
|
+
const siteConfig = await response.json();
|
|
53
|
+
const hasDomain = siteConfig.domainLocaleConfig?.some((c) => c.locale === language &&
|
|
54
|
+
c.domain &&
|
|
55
|
+
c.domain !== 'undefined' &&
|
|
56
|
+
c.domain.trim() !== '');
|
|
57
|
+
if (!hasDomain) {
|
|
58
|
+
setShowDomainPrompt(true);
|
|
59
|
+
setIsCheckingDomain(false);
|
|
60
|
+
return; // Stop here, wait for domain prompt
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.warn('Failed to check domain configuration:', error);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
setIsCheckingDomain(false);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
41
70
|
try {
|
|
42
71
|
setIsSending(true);
|
|
43
72
|
setSendError(null);
|
|
@@ -52,6 +81,12 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
|
|
|
52
81
|
});
|
|
53
82
|
const data = await response.json();
|
|
54
83
|
if (!response.ok) {
|
|
84
|
+
// If server detects missing domain, trigger prompt
|
|
85
|
+
if (data.code === 'DOMAIN_MISSING') {
|
|
86
|
+
setShowDomainPrompt(true);
|
|
87
|
+
setIsSending(false);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
55
90
|
throw new Error(data.error || 'Failed to send newsletter');
|
|
56
91
|
}
|
|
57
92
|
setResultDetails(data.details);
|
|
@@ -77,31 +112,65 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
|
|
|
77
112
|
setIsSending(false);
|
|
78
113
|
}
|
|
79
114
|
};
|
|
115
|
+
const handleSaveDomain = async (domain) => {
|
|
116
|
+
try {
|
|
117
|
+
// Get current site config
|
|
118
|
+
const response = await fetch('/api/plugin-website/settings');
|
|
119
|
+
const siteConfig = await response.json();
|
|
120
|
+
const currentConfigs = siteConfig.domainLocaleConfig || [];
|
|
121
|
+
const newConfigs = [...currentConfigs];
|
|
122
|
+
const existingIndex = newConfigs.findIndex((c) => c.locale === language);
|
|
123
|
+
if (existingIndex >= 0) {
|
|
124
|
+
newConfigs[existingIndex] = { ...newConfigs[existingIndex], domain };
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
newConfigs.push({ locale: language, domain });
|
|
128
|
+
}
|
|
129
|
+
// Save updated site config
|
|
130
|
+
const saveResponse = await fetch('/api/plugin-website/settings', {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'Content-Type': 'application/json' },
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
...siteConfig,
|
|
135
|
+
domainLocaleConfig: newConfigs
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
if (!saveResponse.ok)
|
|
139
|
+
throw new Error('Failed to save website settings');
|
|
140
|
+
// Re-trigger send with domain check skipped
|
|
141
|
+
setShowDomainPrompt(false);
|
|
142
|
+
handleSend(true);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error('Failed to save domain:', error);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
80
149
|
if (!isOpen)
|
|
81
150
|
return null;
|
|
82
151
|
const canSend = newsletter.hasContent && (sendMode === 'test' || subscriberCount > 0);
|
|
83
152
|
const isAlreadySent = newsletter.status === 'sent';
|
|
84
153
|
const noSubscribersError = sendMode === 'subscribers' && subscriberCount === 0;
|
|
85
|
-
return (_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [_jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-sm", onClick: () => !isSending && onClose() }), _jsxs("div", { className: "relative w-full max-w-md mx-4 bg-white dark:bg-neutral-900 rounded-3xl border border-dashboard-border shadow-2xl overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-8 py-6 border-b border-dashboard-border", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-10 h-10 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Send, { className: "w-5 h-5 text-primary" }) }), _jsxs("div", { children: [_jsxs("h2", { className: "text-xl font-black text-dashboard-text uppercase tracking-tight", children: [isAlreadySent ? 'Resend' : 'Send', " Newsletter"] }), _jsx("p", { className: "text-xs text-dashboard-text-secondary truncate max-w-[200px]", children: newsletter.title })] })] }), _jsx("button", { onClick: () => !isSending && onClose(), disabled: isSending, className: "p-2 rounded-full hover:bg-dashboard-border transition-colors", children: _jsx(X, { size: 20, className: "text-dashboard-text-secondary" }) })] }), _jsxs("div", { className: "p-8", children: [isAlreadySent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800", children: _jsxs("div", { className: "flex items-center gap-2 text-blue-700 dark:text-blue-400", children: [_jsx(RefreshCw, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter was already sent - sending again" })] }) })), !newsletter.hasContent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800", children: _jsxs("div", { className: "flex items-center gap-2 text-red-700 dark:text-red-400", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter has no content" })] }) })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Send To" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: () => setSendMode('subscribers'), className: `flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${sendMode === 'subscribers'
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
? 'bg-neutral-400 text-white cursor-not-allowed'
|
|
102
|
-
: sendSuccess
|
|
103
|
-
? 'bg-green-600 text-white'
|
|
104
|
-
: !canSend
|
|
154
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [_jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-sm", onClick: () => !isSending && onClose() }), _jsxs("div", { className: "relative w-full max-w-md mx-4 bg-white dark:bg-neutral-900 rounded-3xl border border-dashboard-border shadow-2xl overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-8 py-6 border-b border-dashboard-border", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-10 h-10 rounded-2xl bg-primary/10 flex items-center justify-center", children: _jsx(Send, { className: "w-5 h-5 text-primary" }) }), _jsxs("div", { children: [_jsxs("h2", { className: "text-xl font-black text-dashboard-text uppercase tracking-tight", children: [isAlreadySent ? 'Resend' : 'Send', " Newsletter"] }), _jsx("p", { className: "text-xs text-dashboard-text-secondary truncate max-w-[200px]", children: newsletter.title })] })] }), _jsx("button", { onClick: () => !isSending && onClose(), disabled: isSending, className: "p-2 rounded-full hover:bg-dashboard-border transition-colors", children: _jsx(X, { size: 20, className: "text-dashboard-text-secondary" }) })] }), _jsxs("div", { className: "p-8", children: [isAlreadySent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800", children: _jsxs("div", { className: "flex items-center gap-2 text-blue-700 dark:text-blue-400", children: [_jsx(RefreshCw, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter was already sent - sending again" })] }) })), !newsletter.hasContent && (_jsx("div", { className: "mb-4 p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800", children: _jsxs("div", { className: "flex items-center gap-2 text-red-700 dark:text-red-400", children: [_jsx(AlertCircle, { size: 16 }), _jsx("span", { className: "text-xs font-bold uppercase", children: "This newsletter has no content" })] }) })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Send To" }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: () => setSendMode('subscribers'), className: `flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${sendMode === 'subscribers'
|
|
155
|
+
? 'bg-primary text-white'
|
|
156
|
+
: subscriberCount > 0
|
|
157
|
+
? 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'
|
|
158
|
+
: 'bg-dashboard-bg border border-dashboard-border text-dashboard-text-secondary opacity-50'}`, children: [_jsx(Users, { size: 14 }), "Subscribers", subscriberCount > 0 && (_jsxs("span", { className: "text-[10px] opacity-70", children: ["(", subscriberCount, ")"] }))] }), _jsxs("button", { onClick: () => setSendMode('test'), className: `flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${sendMode === 'test'
|
|
159
|
+
? 'bg-primary text-white'
|
|
160
|
+
: 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'}`, children: [_jsx(Mail, { size: 14 }), "Test"] })] })] }), sendMode === 'test' && (_jsxs("div", { children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: "Test Email Address" }), _jsx("input", { type: "email", value: testEmail, onChange: (e) => {
|
|
161
|
+
setTestEmail(e.target.value);
|
|
162
|
+
setSendError(null);
|
|
163
|
+
}, onKeyDown: (e) => e.key === 'Enter' && handleSend(), placeholder: "your@email.com", className: "w-full px-4 py-3 bg-dashboard-bg border border-dashboard-border rounded-xl outline-none focus:ring-2 focus:ring-primary transition-colors text-dashboard-text" })] })), _jsxs("div", { children: [_jsxs("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest block mb-2", children: [_jsx(Globe, { size: 12, className: "inline mr-1" }), "Language"] }), _jsx("div", { className: "flex gap-2", children: languages.map((lang) => (_jsx("button", { onClick: () => setLanguage(lang.code), className: `px-4 py-2 rounded-xl text-xs font-bold uppercase tracking-widest transition-colors ${language === lang.code
|
|
164
|
+
? 'bg-primary text-white'
|
|
165
|
+
: 'bg-dashboard-bg border border-dashboard-border text-dashboard-text hover:bg-dashboard-border'}`, children: lang.label }, lang.code))) })] }), sendError && (_jsxs("div", { className: "flex items-center gap-2 text-red-500 text-sm", children: [_jsx(AlertCircle, { size: 16 }), sendError] })), noSubscribersError && (_jsxs("div", { className: "flex items-center gap-2 text-amber-600 dark:text-amber-400 text-sm", children: [_jsx(AlertCircle, { size: 16 }), "No subscribers found. Add subscribers or use Test mode."] })), sendSuccess && resultDetails && (_jsxs("div", { className: "flex items-center gap-2 text-green-600 dark:text-green-400 text-sm", children: [_jsx(CheckCircle2, { size: 16 }), sendMode === 'test'
|
|
166
|
+
? `Test newsletter sent successfully!`
|
|
167
|
+
: `Sent to ${resultDetails.successCount} subscriber${resultDetails.successCount !== 1 ? 's' : ''}${resultDetails.failedCount > 0 ? ` (${resultDetails.failedCount} failed)` : ''}`] })), !sendSuccess && (_jsx("p", { className: "text-xs text-dashboard-text-secondary", children: sendMode === 'subscribers'
|
|
168
|
+
? `This will send the newsletter to all ${subscriberCount} active subscriber${subscriberCount !== 1 ? 's' : ''}.`
|
|
169
|
+
: 'Send a test newsletter to verify the email looks correct.' }))] })] }), _jsxs("div", { className: "flex items-center justify-end gap-3 px-8 py-6 border-t border-dashboard-border bg-dashboard-bg/50", children: [_jsx("button", { onClick: onClose, disabled: isSending, className: "px-6 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: sendSuccess ? 'Close' : 'Cancel' }), _jsx("button", { onClick: () => handleSend(), disabled: isSending || sendSuccess || !canSend || isCheckingDomain, className: `inline-flex items-center gap-2 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-colors shadow-lg ${isSending || isCheckingDomain
|
|
105
170
|
? 'bg-neutral-400 text-white cursor-not-allowed'
|
|
106
|
-
:
|
|
171
|
+
: sendSuccess
|
|
172
|
+
? 'bg-green-600 text-white'
|
|
173
|
+
: !canSend
|
|
174
|
+
? 'bg-neutral-400 text-white cursor-not-allowed'
|
|
175
|
+
: 'bg-primary text-white hover:bg-primary/90'}`, children: isSending || isCheckingDomain ? (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { className: "w-4 h-4 animate-spin" }), isCheckingDomain ? 'Checking...' : 'Sending...'] })) : sendSuccess ? (_jsxs(_Fragment, { children: [_jsx(CheckCircle2, { className: "w-4 h-4" }), "Sent!"] })) : (_jsxs(_Fragment, { children: [_jsx(Send, { className: "w-4 h-4" }), sendMode === 'subscribers' ? 'Send to Subscribers' : 'Send Test'] })) })] })] })] }), _jsx(DomainPromptModal, { isOpen: showDomainPrompt, onClose: () => setShowDomainPrompt(false), onSave: handleSaveDomain, language: language })] }));
|
|
107
176
|
}
|