@jhits/plugin-newsletter 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/api/handlers/newsletters.d.ts.map +1 -1
  2. package/dist/api/handlers/newsletters.js +80 -24
  3. package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
  4. package/dist/api/handlers/send-newsletter.js +31 -4
  5. package/dist/api/handlers/welcome-email.d.ts.map +1 -1
  6. package/dist/api/handlers/welcome-email.js +5 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -1
  9. package/dist/types/newsletter.d.ts +35 -0
  10. package/dist/types/newsletter.d.ts.map +1 -1
  11. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  12. package/dist/views/CanvasEditor/CanvasEditorView.js +28 -7
  13. package/dist/views/CanvasEditor/hooks/useNewsletterLoader.js +4 -4
  14. package/dist/views/NewsletterManager.d.ts.map +1 -1
  15. package/dist/views/NewsletterManager.js +96 -9
  16. package/dist/views/components/NewsletterCard.d.ts +16 -0
  17. package/dist/views/components/NewsletterCard.d.ts.map +1 -0
  18. package/dist/views/components/NewsletterCard.js +94 -0
  19. package/dist/views/components/NewsletterGrid.d.ts +16 -0
  20. package/dist/views/components/NewsletterGrid.d.ts.map +1 -0
  21. package/dist/views/components/NewsletterGrid.js +13 -0
  22. package/dist/views/components/SendNewsletterModal.js +1 -1
  23. package/package.json +1 -1
  24. package/src/api/handlers/newsletters.ts +94 -28
  25. package/src/api/handlers/send-newsletter.ts +33 -4
  26. package/src/api/handlers/welcome-email.ts +5 -2
  27. package/src/index.tsx +4 -1
  28. package/src/types/newsletter.ts +44 -0
  29. package/src/views/CanvasEditor/CanvasEditorView.tsx +28 -8
  30. package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +4 -4
  31. package/src/views/NewsletterManager.tsx +203 -20
  32. package/src/views/components/NewsletterCard.tsx +212 -0
  33. package/src/views/components/NewsletterGrid.tsx +48 -0
  34. package/src/views/components/SendNewsletterModal.tsx +5 -5
@@ -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-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800">
150
- <div className="flex items-center gap-2 text-amber-700 dark:text-amber-400">
151
- <AlertCircle size={16} />
152
- <span className="text-xs font-bold uppercase">This newsletter has already been sent</span>
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
  )}