@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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Card Component
|
|
3
|
+
* Modern, distinctive card design for newsletters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { Trash2, Edit2, Send, Check } from 'lucide-react';
|
|
10
|
+
import { NewsletterListItem, NewsletterStatus } from '../../types/newsletter';
|
|
11
|
+
|
|
12
|
+
interface NewsletterCardProps {
|
|
13
|
+
newsletter: NewsletterListItem;
|
|
14
|
+
onEdit: (id: string) => void;
|
|
15
|
+
onSend: (newsletter: NewsletterListItem) => void;
|
|
16
|
+
onDelete: (id: string, title: string) => void;
|
|
17
|
+
formatDateTime: (dateString: string | undefined) => string;
|
|
18
|
+
locale: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getStatusConfig(status: NewsletterStatus) {
|
|
22
|
+
switch (status) {
|
|
23
|
+
case 'sent':
|
|
24
|
+
return {
|
|
25
|
+
label: 'Sent',
|
|
26
|
+
bg: 'bg-green-500',
|
|
27
|
+
text: 'text-green-600 dark:text-green-400',
|
|
28
|
+
};
|
|
29
|
+
case 'scheduled':
|
|
30
|
+
return {
|
|
31
|
+
label: 'Scheduled',
|
|
32
|
+
bg: 'bg-blue-500',
|
|
33
|
+
text: 'text-blue-600 dark:text-blue-400',
|
|
34
|
+
};
|
|
35
|
+
case 'draft':
|
|
36
|
+
return {
|
|
37
|
+
label: 'Draft',
|
|
38
|
+
bg: 'bg-gray-400',
|
|
39
|
+
text: 'text-gray-500 dark:text-gray-400',
|
|
40
|
+
};
|
|
41
|
+
case 'archived':
|
|
42
|
+
return {
|
|
43
|
+
label: 'Archived',
|
|
44
|
+
bg: 'bg-neutral-400',
|
|
45
|
+
text: 'text-neutral-500 dark:text-neutral-400',
|
|
46
|
+
};
|
|
47
|
+
default:
|
|
48
|
+
return {
|
|
49
|
+
label: status,
|
|
50
|
+
bg: 'bg-gray-400',
|
|
51
|
+
text: 'text-gray-500',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const LANGUAGE_NAMES: Record<string, string> = {
|
|
57
|
+
en: 'English',
|
|
58
|
+
nl: 'Dutch',
|
|
59
|
+
sv: 'Swedish',
|
|
60
|
+
de: 'German',
|
|
61
|
+
fr: 'French',
|
|
62
|
+
es: 'Spanish',
|
|
63
|
+
it: 'Italian',
|
|
64
|
+
pt: 'Portuguese',
|
|
65
|
+
pl: 'Polish',
|
|
66
|
+
ru: 'Russian',
|
|
67
|
+
ja: 'Japanese',
|
|
68
|
+
zh: 'Chinese',
|
|
69
|
+
ar: 'Arabic',
|
|
70
|
+
tr: 'Turkish',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const LANGUAGE_COUNTRY_CODES: Record<string, string> = {
|
|
74
|
+
en: 'gb',
|
|
75
|
+
nl: 'nl',
|
|
76
|
+
sv: 'se',
|
|
77
|
+
de: 'de',
|
|
78
|
+
fr: 'fr',
|
|
79
|
+
es: 'es',
|
|
80
|
+
it: 'it',
|
|
81
|
+
pt: 'pt',
|
|
82
|
+
pl: 'pl',
|
|
83
|
+
ru: 'ru',
|
|
84
|
+
ja: 'jp',
|
|
85
|
+
zh: 'cn',
|
|
86
|
+
ar: 'sa',
|
|
87
|
+
tr: 'tr',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const getFlagUrl = (lang: string, size: number = 32) => {
|
|
91
|
+
const countryCode = LANGUAGE_COUNTRY_CODES[lang] || lang;
|
|
92
|
+
return `https://flagcdn.com/${size}x${Math.round(size * 0.75)}/${countryCode}.png`;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export function NewsletterCard({
|
|
96
|
+
newsletter,
|
|
97
|
+
onEdit,
|
|
98
|
+
onSend,
|
|
99
|
+
onDelete,
|
|
100
|
+
formatDateTime,
|
|
101
|
+
locale
|
|
102
|
+
}: NewsletterCardProps) {
|
|
103
|
+
const statusConfig = getStatusConfig(newsletter.status);
|
|
104
|
+
const languages = newsletter.availableLanguages || [];
|
|
105
|
+
const sendHistory = newsletter.sendHistory || [];
|
|
106
|
+
|
|
107
|
+
const sentLanguages = [...new Set(sendHistory.map(h => h.language))];
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="group relative bg-white dark:bg-neutral-900 rounded-2xl border border-dashboard-border overflow-hidden hover:border-primary/40 hover:shadow-xl hover:shadow-primary/5 transition-all duration-300">
|
|
111
|
+
<div className={`h-1.5 ${statusConfig.bg}`} />
|
|
112
|
+
|
|
113
|
+
<div className="p-5">
|
|
114
|
+
<div className="flex items-center justify-between mb-4">
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
<span className={`text-[10px] font-black uppercase tracking-widest ${statusConfig.text}`}>
|
|
117
|
+
{statusConfig.label}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
122
|
+
<button
|
|
123
|
+
onClick={() => onSend(newsletter)}
|
|
124
|
+
className="p-2 rounded-lg text-neutral-400 hover:text-primary hover:bg-primary/10 transition-all"
|
|
125
|
+
title={newsletter.status === 'sent' ? 'Resend' : 'Send'}
|
|
126
|
+
>
|
|
127
|
+
<Send size={16} />
|
|
128
|
+
</button>
|
|
129
|
+
<button
|
|
130
|
+
onClick={() => onEdit(newsletter.id)}
|
|
131
|
+
className="p-2 rounded-lg text-neutral-400 hover:text-primary hover:bg-primary/10 transition-all"
|
|
132
|
+
title="Edit"
|
|
133
|
+
>
|
|
134
|
+
<Edit2 size={16} />
|
|
135
|
+
</button>
|
|
136
|
+
<button
|
|
137
|
+
onClick={() => onDelete(newsletter.id, newsletter.title)}
|
|
138
|
+
className="p-2 rounded-lg text-neutral-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all"
|
|
139
|
+
title="Delete"
|
|
140
|
+
>
|
|
141
|
+
<Trash2 size={16} />
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<h3 className="text-lg font-bold text-dashboard-text mb-4 line-clamp-2 group-hover:text-primary transition-colors">
|
|
147
|
+
{newsletter.title || 'Untitled Newsletter'}
|
|
148
|
+
</h3>
|
|
149
|
+
|
|
150
|
+
<div className="space-y-2">
|
|
151
|
+
{languages.map((lang) => {
|
|
152
|
+
const hasBeenSent = sentLanguages.includes(lang);
|
|
153
|
+
const langHistory = sendHistory.filter(h => h.language === lang);
|
|
154
|
+
const lastLangSend = langHistory[0];
|
|
155
|
+
const langData = newsletter.languages?.[lang];
|
|
156
|
+
const langSubject = langData?.metadata?.subject || newsletter.title;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
key={lang}
|
|
161
|
+
className={`relative group/lang flex items-center justify-between px-3 py-2 rounded-lg border transition-all hover:border-primary/50 ${
|
|
162
|
+
hasBeenSent ? '' : 'bg-dashboard-bg border-dashboard-border'
|
|
163
|
+
}`}
|
|
164
|
+
style={{
|
|
165
|
+
backgroundColor: hasBeenSent ? 'rgba(34, 197, 94, 0.08)' : undefined,
|
|
166
|
+
borderColor: hasBeenSent ? 'rgba(34, 197, 94, 0.3)' : undefined,
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
{hasBeenSent && (
|
|
170
|
+
<div className="absolute -top-2 -right-2 min-w-[20px] h-5 px-1.5 bg-green-500 rounded-full flex items-center justify-center shadow-sm">
|
|
171
|
+
<span className="text-[10px] font-bold text-white">{langHistory.length}</span>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
176
|
+
<img
|
|
177
|
+
src={getFlagUrl(lang, 28)}
|
|
178
|
+
alt={LANGUAGE_NAMES[lang] || lang}
|
|
179
|
+
className="w-7 h-5 rounded object-cover shrink-0"
|
|
180
|
+
loading="lazy"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="flex items-center gap-4 text-xs shrink-0">
|
|
185
|
+
{hasBeenSent && lastLangSend ? (
|
|
186
|
+
<>
|
|
187
|
+
<div className="text-dashboard-text-secondary">
|
|
188
|
+
{formatDateTime(lastLangSend.sentAt)}
|
|
189
|
+
</div>
|
|
190
|
+
<div className="text-dashboard-text-secondary">
|
|
191
|
+
{lastLangSend.recipientCount} recipients
|
|
192
|
+
</div>
|
|
193
|
+
</>
|
|
194
|
+
) : (
|
|
195
|
+
<span className="text-dashboard-text-secondary">Not sent</span>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Hover tooltip with subject */}
|
|
200
|
+
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-neutral-900 dark:bg-neutral-800 text-white text-xs rounded-lg opacity-0 group-hover/lang:opacity-100 transition-opacity pointer-events-none z-10 w-64 shadow-lg">
|
|
201
|
+
<div className="font-bold mb-1">{LANGUAGE_NAMES[lang] || lang.toUpperCase()}</div>
|
|
202
|
+
<div className="text-neutral-300 break-words">{langSubject || 'Untitled'}</div>
|
|
203
|
+
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-0 h-0 border-4 border-transparent border-t-neutral-900 dark:border-t-neutral-800" />
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
})}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Grid View Component
|
|
3
|
+
* Card-based grid layout for newsletters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { NewsletterListItem } from '../../types/newsletter';
|
|
10
|
+
import { NewsletterCard } from './NewsletterCard';
|
|
11
|
+
|
|
12
|
+
interface NewsletterGridProps {
|
|
13
|
+
newsletters: NewsletterListItem[];
|
|
14
|
+
onEdit: (id: string) => void;
|
|
15
|
+
onSend: (newsletter: NewsletterListItem) => void;
|
|
16
|
+
onDelete: (id: string, title: string) => void;
|
|
17
|
+
formatDateTime: (dateString: string | undefined) => string;
|
|
18
|
+
locale: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function NewsletterGrid({
|
|
22
|
+
newsletters,
|
|
23
|
+
onEdit,
|
|
24
|
+
onSend,
|
|
25
|
+
onDelete,
|
|
26
|
+
formatDateTime,
|
|
27
|
+
locale
|
|
28
|
+
}: NewsletterGridProps) {
|
|
29
|
+
if (newsletters.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
35
|
+
{newsletters.map((newsletter) => (
|
|
36
|
+
<NewsletterCard
|
|
37
|
+
key={newsletter.id}
|
|
38
|
+
newsletter={newsletter}
|
|
39
|
+
onEdit={onEdit}
|
|
40
|
+
onSend={onSend}
|
|
41
|
+
onDelete={onDelete}
|
|
42
|
+
formatDateTime={formatDateTime}
|
|
43
|
+
locale={locale}
|
|
44
|
+
/>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -127,7 +127,7 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
|
|
|
127
127
|
</div>
|
|
128
128
|
<div>
|
|
129
129
|
<h2 className="text-xl font-black text-dashboard-text uppercase tracking-tight">
|
|
130
|
-
Send Newsletter
|
|
130
|
+
{isAlreadySent ? 'Resend' : 'Send'} Newsletter
|
|
131
131
|
</h2>
|
|
132
132
|
<p className="text-xs text-dashboard-text-secondary truncate max-w-[200px]">
|
|
133
133
|
{newsletter.title}
|
|
@@ -146,10 +146,10 @@ export function SendNewsletterModal({ isOpen, onClose, newsletter, subscriberCou
|
|
|
146
146
|
{/* Content */}
|
|
147
147
|
<div className="p-8">
|
|
148
148
|
{isAlreadySent && (
|
|
149
|
-
<div className="mb-4 p-4 rounded-xl bg-
|
|
150
|
-
<div className="flex items-center gap-2 text-
|
|
151
|
-
<
|
|
152
|
-
<span className="text-xs font-bold uppercase">This newsletter
|
|
149
|
+
<div className="mb-4 p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
|
|
150
|
+
<div className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
|
|
151
|
+
<RefreshCw size={16} />
|
|
152
|
+
<span className="text-xs font-bold uppercase">This newsletter was already sent - sending again</span>
|
|
153
153
|
</div>
|
|
154
154
|
</div>
|
|
155
155
|
)}
|