@jhits/plugin-newsletter 0.0.7 → 0.0.8
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/package.json +2 -3
- package/src/api/handler.ts +0 -693
- package/src/api/router.ts +0 -111
- package/src/index.server.ts +0 -12
- package/src/index.tsx +0 -313
- package/src/index.tsx.patch +0 -98
- package/src/init.tsx +0 -72
- package/src/lib/blocks/BlockRenderer.tsx +0 -125
- package/src/lib/email/EmailRenderer.tsx +0 -425
- package/src/lib/email/index.ts +0 -6
- package/src/lib/mappers/apiMapper.ts +0 -57
- package/src/lib/utils/blockHelpers.ts +0 -71
- package/src/lib/utils/slugify.ts +0 -43
- package/src/registry/BlockRegistry.ts +0 -53
- package/src/registry/index.ts +0 -5
- package/src/state/EditorContext.tsx +0 -279
- package/src/state/index.ts +0 -10
- package/src/state/reducer.ts +0 -561
- package/src/state/types.ts +0 -154
- package/src/types/block.ts +0 -275
- package/src/types/newsletter.ts +0 -151
- package/src/types/registry.ts +0 -14
- package/src/views/CanvasEditor/BlockWrapper.tsx +0 -143
- package/src/views/CanvasEditor/CanvasEditorView.tsx +0 -249
- package/src/views/CanvasEditor/EditorBody.tsx +0 -95
- package/src/views/CanvasEditor/EditorHeader.tsx +0 -139
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +0 -83
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +0 -674
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +0 -120
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +0 -156
- package/src/views/CanvasEditor/components/ErrorBanner.tsx +0 -31
- package/src/views/CanvasEditor/components/LibraryItem.tsx +0 -71
- package/src/views/CanvasEditor/components/SlashCommandDetector.tsx +0 -196
- package/src/views/CanvasEditor/components/SlashCommandMenu.tsx +0 -131
- package/src/views/CanvasEditor/components/index.ts +0 -16
- package/src/views/CanvasEditor/hooks/index.ts +0 -7
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +0 -136
- package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +0 -34
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +0 -54
- package/src/views/CanvasEditor/hooks/useSlashCommand.ts +0 -106
- package/src/views/CanvasEditor/index.ts +0 -12
- package/src/views/NewsletterEditor.tsx +0 -38
- package/src/views/NewsletterManager.tsx +0 -240
- package/src/views/SettingsView.tsx +0 -216
- package/src/views/SubscribersView.tsx +0 -269
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Newsletter Subscribers View
|
|
3
|
-
* Main interface for managing newsletter subscribers
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use client';
|
|
7
|
-
|
|
8
|
-
import React, { useState, useEffect } from 'react';
|
|
9
|
-
import { Mail, Copy, Check, Trash2, Filter, Users, Globe, Calendar } from 'lucide-react';
|
|
10
|
-
import { Subscriber } from '../types/newsletter';
|
|
11
|
-
|
|
12
|
-
export interface SubscribersViewProps {
|
|
13
|
-
siteId: string;
|
|
14
|
-
locale: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function SubscribersView({ siteId, locale }: SubscribersViewProps) {
|
|
18
|
-
const [subscribers, setSubscribers] = useState<Subscriber[]>([]);
|
|
19
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
20
|
-
const [filter, setFilter] = useState<string>('all');
|
|
21
|
-
const [copyStatus, setCopyStatus] = useState(false);
|
|
22
|
-
const [deleteStatus, setDeleteStatus] = useState<string | null>(null);
|
|
23
|
-
|
|
24
|
-
// Fetch subscribers on load
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const fetchSubscribers = async () => {
|
|
27
|
-
try {
|
|
28
|
-
setIsLoading(true);
|
|
29
|
-
const response = await fetch('/api/plugin-newsletter/subscribers', {
|
|
30
|
-
credentials: 'include',
|
|
31
|
-
});
|
|
32
|
-
if (!response.ok) {
|
|
33
|
-
throw new Error('Failed to fetch subscribers');
|
|
34
|
-
}
|
|
35
|
-
const data = await response.json();
|
|
36
|
-
setSubscribers(Array.isArray(data) ? data : []);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.error('Failed to load subscribers:', error);
|
|
39
|
-
} finally {
|
|
40
|
-
setIsLoading(false);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
fetchSubscribers();
|
|
44
|
-
}, []);
|
|
45
|
-
|
|
46
|
-
// Filter subscribers
|
|
47
|
-
const filteredSubscribers = filter === 'all'
|
|
48
|
-
? subscribers
|
|
49
|
-
: subscribers.filter(s => s.language === filter);
|
|
50
|
-
|
|
51
|
-
// Get unique languages
|
|
52
|
-
const languages = Array.from(new Set(subscribers.map(s => s.language))).sort();
|
|
53
|
-
|
|
54
|
-
// Copy emails to clipboard
|
|
55
|
-
const handleCopyEmails = async () => {
|
|
56
|
-
const emails = filteredSubscribers.map(s => s.email).join(', ');
|
|
57
|
-
try {
|
|
58
|
-
await navigator.clipboard.writeText(emails);
|
|
59
|
-
setCopyStatus(true);
|
|
60
|
-
setTimeout(() => setCopyStatus(false), 2000);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.error('Failed to copy emails:', error);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Delete subscriber
|
|
67
|
-
const handleDeleteSubscriber = async (email: string) => {
|
|
68
|
-
if (!confirm(`Are you sure you want to remove ${email} from the newsletter?`)) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
setDeleteStatus(email);
|
|
74
|
-
const response = await fetch(`/api/plugin-newsletter/subscribers/${encodeURIComponent(email)}`, {
|
|
75
|
-
method: 'DELETE',
|
|
76
|
-
credentials: 'include',
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (!response.ok) {
|
|
80
|
-
const error = await response.json();
|
|
81
|
-
throw new Error(error.error || 'Failed to delete subscriber');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Remove from local state
|
|
85
|
-
setSubscribers(prev => prev.filter(sub => sub.email !== email));
|
|
86
|
-
} catch (error: any) {
|
|
87
|
-
console.error('Failed to delete subscriber:', error);
|
|
88
|
-
alert(error.message || 'Failed to delete subscriber');
|
|
89
|
-
} finally {
|
|
90
|
-
setDeleteStatus(null);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// Format date
|
|
95
|
-
const formatDate = (dateString: string | Date | undefined) => {
|
|
96
|
-
if (!dateString) return 'N/A';
|
|
97
|
-
const date = typeof dateString === 'string' ? new Date(dateString) : dateString;
|
|
98
|
-
return date.toLocaleDateString(locale, {
|
|
99
|
-
day: 'numeric',
|
|
100
|
-
month: 'short',
|
|
101
|
-
year: 'numeric',
|
|
102
|
-
});
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<div className="h-full w-full rounded-[2.5rem] bg-white dark:bg-neutral-900 p-8 overflow-y-auto">
|
|
107
|
-
<div className="max-w-7xl mx-auto">
|
|
108
|
-
{/* Header */}
|
|
109
|
-
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
|
110
|
-
<div>
|
|
111
|
-
<h1 className="text-3xl font-black text-dashboard-text uppercase tracking-tighter mb-2">
|
|
112
|
-
Newsletter Subscribers
|
|
113
|
-
</h1>
|
|
114
|
-
<p className="text-sm text-dashboard-text-secondary">
|
|
115
|
-
Manage your newsletter subscribers and their preferences
|
|
116
|
-
</p>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<div className="flex items-center gap-3">
|
|
120
|
-
{/* Language Filter */}
|
|
121
|
-
<div className="flex items-center bg-dashboard-bg border border-dashboard-border rounded-xl px-4 py-2.5">
|
|
122
|
-
<Filter size={16} className="text-primary mr-2" />
|
|
123
|
-
<select
|
|
124
|
-
value={filter}
|
|
125
|
-
onChange={(e) => setFilter(e.target.value)}
|
|
126
|
-
className="bg-transparent text-xs font-bold text-dashboard-text outline-none cursor-pointer uppercase tracking-widest"
|
|
127
|
-
>
|
|
128
|
-
<option value="all">All Languages</option>
|
|
129
|
-
{languages.map(lang => (
|
|
130
|
-
<option key={lang} value={lang}>{lang.toUpperCase()}</option>
|
|
131
|
-
))}
|
|
132
|
-
</select>
|
|
133
|
-
</div>
|
|
134
|
-
|
|
135
|
-
{/* Copy Emails Button */}
|
|
136
|
-
<button
|
|
137
|
-
onClick={handleCopyEmails}
|
|
138
|
-
disabled={filteredSubscribers.length === 0}
|
|
139
|
-
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 disabled:opacity-30 disabled:cursor-not-allowed bg-primary text-white hover:bg-primary/90"
|
|
140
|
-
>
|
|
141
|
-
{copyStatus ? (
|
|
142
|
-
<>
|
|
143
|
-
<Check size={14} />
|
|
144
|
-
Copied!
|
|
145
|
-
</>
|
|
146
|
-
) : (
|
|
147
|
-
<>
|
|
148
|
-
<Copy size={14} />
|
|
149
|
-
Copy List
|
|
150
|
-
</>
|
|
151
|
-
)}
|
|
152
|
-
</button>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
{/* Stats */}
|
|
157
|
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
158
|
-
<div className="bg-dashboard-bg p-6 rounded-3xl border border-dashboard-border">
|
|
159
|
-
<div className="flex items-center gap-3 mb-2">
|
|
160
|
-
<Users size={20} className="text-primary" />
|
|
161
|
-
<span className="text-[10px] uppercase tracking-widest text-dashboard-text-secondary font-black">
|
|
162
|
-
Total Subscribers
|
|
163
|
-
</span>
|
|
164
|
-
</div>
|
|
165
|
-
<p className="text-3xl font-black text-dashboard-text">
|
|
166
|
-
{subscribers.length}
|
|
167
|
-
</p>
|
|
168
|
-
</div>
|
|
169
|
-
<div className="bg-dashboard-bg p-6 rounded-3xl border border-dashboard-border">
|
|
170
|
-
<div className="flex items-center gap-3 mb-2">
|
|
171
|
-
<Globe size={20} className="text-primary" />
|
|
172
|
-
<span className="text-[10px] uppercase tracking-widest text-dashboard-text-secondary font-black">
|
|
173
|
-
Languages
|
|
174
|
-
</span>
|
|
175
|
-
</div>
|
|
176
|
-
<p className="text-3xl font-black text-dashboard-text">
|
|
177
|
-
{languages.length}
|
|
178
|
-
</p>
|
|
179
|
-
</div>
|
|
180
|
-
<div className="bg-dashboard-bg p-6 rounded-3xl border border-dashboard-border">
|
|
181
|
-
<div className="flex items-center gap-3 mb-2">
|
|
182
|
-
<Filter size={20} className="text-primary" />
|
|
183
|
-
<span className="text-[10px] uppercase tracking-widest text-dashboard-text-secondary font-black">
|
|
184
|
-
Filtered
|
|
185
|
-
</span>
|
|
186
|
-
</div>
|
|
187
|
-
<p className="text-3xl font-black text-dashboard-text">
|
|
188
|
-
{filteredSubscribers.length}
|
|
189
|
-
</p>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
|
|
193
|
-
{/* Subscribers Table */}
|
|
194
|
-
<div className="bg-dashboard-bg rounded-3xl border border-dashboard-border overflow-hidden">
|
|
195
|
-
{isLoading ? (
|
|
196
|
-
<div className="flex items-center justify-center py-20">
|
|
197
|
-
<div className="w-8 h-8 border-4 border-primary/20 border-t-primary rounded-full animate-spin" />
|
|
198
|
-
</div>
|
|
199
|
-
) : filteredSubscribers.length === 0 ? (
|
|
200
|
-
<div className="py-24 text-center">
|
|
201
|
-
<Users size={64} className="mx-auto text-dashboard-text-secondary mb-4" />
|
|
202
|
-
<p className="text-dashboard-text-secondary font-serif italic text-lg">
|
|
203
|
-
{filter === 'all' ? 'No subscribers yet.' : `No subscribers found for ${filter.toUpperCase()}.`}
|
|
204
|
-
</p>
|
|
205
|
-
</div>
|
|
206
|
-
) : (
|
|
207
|
-
<div className="overflow-x-auto">
|
|
208
|
-
<table className="w-full text-left border-collapse">
|
|
209
|
-
<thead>
|
|
210
|
-
<tr className="bg-dashboard-bg text-dashboard-text text-[10px] uppercase tracking-[0.2em] font-black border-b border-dashboard-border">
|
|
211
|
-
<th className="px-8 py-5">Subscriber</th>
|
|
212
|
-
<th className="px-8 py-5">Language</th>
|
|
213
|
-
<th className="px-8 py-5 text-right">Subscribed</th>
|
|
214
|
-
<th className="px-8 py-5 text-right">Actions</th>
|
|
215
|
-
</tr>
|
|
216
|
-
</thead>
|
|
217
|
-
<tbody className="divide-y divide-dashboard-border">
|
|
218
|
-
{filteredSubscribers.map((subscriber, idx) => (
|
|
219
|
-
<tr
|
|
220
|
-
key={idx}
|
|
221
|
-
className="hover:bg-dashboard-bg transition-colors group"
|
|
222
|
-
>
|
|
223
|
-
<td className="px-8 py-5">
|
|
224
|
-
<div className="flex items-center gap-4">
|
|
225
|
-
<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">
|
|
226
|
-
<Mail size={18} />
|
|
227
|
-
</div>
|
|
228
|
-
<span className="text-sm font-medium text-dashboard-text tracking-tight">
|
|
229
|
-
{subscriber.email}
|
|
230
|
-
</span>
|
|
231
|
-
</div>
|
|
232
|
-
</td>
|
|
233
|
-
<td className="px-8 py-5">
|
|
234
|
-
<span className="text-[10px] font-black px-3 py-1 bg-primary/10 rounded-full uppercase text-primary border border-primary/20">
|
|
235
|
-
{subscriber.language}
|
|
236
|
-
</span>
|
|
237
|
-
</td>
|
|
238
|
-
<td className="px-8 py-5 text-right text-xs text-dashboard-text-secondary font-medium">
|
|
239
|
-
<div className="flex items-center justify-end gap-2">
|
|
240
|
-
<Calendar size={14} />
|
|
241
|
-
{formatDate(subscriber.subscribedAt)}
|
|
242
|
-
</div>
|
|
243
|
-
</td>
|
|
244
|
-
<td className="px-8 py-5 text-right">
|
|
245
|
-
<button
|
|
246
|
-
onClick={() => handleDeleteSubscriber(subscriber.email)}
|
|
247
|
-
disabled={deleteStatus === subscriber.email}
|
|
248
|
-
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 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
249
|
-
title="Remove subscriber"
|
|
250
|
-
>
|
|
251
|
-
{deleteStatus === subscriber.email ? (
|
|
252
|
-
<div className="w-5 h-5 border-2 border-red-500 border-t-transparent rounded-full animate-spin" />
|
|
253
|
-
) : (
|
|
254
|
-
<Trash2 size={18} />
|
|
255
|
-
)}
|
|
256
|
-
</button>
|
|
257
|
-
</td>
|
|
258
|
-
</tr>
|
|
259
|
-
))}
|
|
260
|
-
</tbody>
|
|
261
|
-
</table>
|
|
262
|
-
</div>
|
|
263
|
-
)}
|
|
264
|
-
</div>
|
|
265
|
-
</div>
|
|
266
|
-
</div>
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
|