@contentgrowth/content-emailing 0.1.0
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/README.md +96 -0
- package/examples/.env.example +16 -0
- package/examples/README.md +55 -0
- package/examples/mocks/MockD1.js +311 -0
- package/examples/mocks/MockEmailSender.js +64 -0
- package/examples/mocks/index.js +5 -0
- package/examples/package-lock.json +73 -0
- package/examples/package.json +18 -0
- package/examples/portal/index.html +919 -0
- package/examples/server.js +314 -0
- package/package.json +32 -0
- package/release.sh +56 -0
- package/schema.sql +63 -0
- package/src/backend/EmailService.js +474 -0
- package/src/backend/EmailTemplateCacheDO.js +363 -0
- package/src/backend/routes/index.js +30 -0
- package/src/backend/routes/templates.js +98 -0
- package/src/backend/routes/tracking.js +215 -0
- package/src/backend/routes.js +98 -0
- package/src/common/htmlWrapper.js +169 -0
- package/src/common/index.js +11 -0
- package/src/common/utils.js +117 -0
- package/src/frontend/TemplateEditor.jsx +117 -0
- package/src/frontend/TemplateManager.jsx +117 -0
- package/src/index.js +24 -0
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Email Template Portal</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--primary: #667eea;
|
|
11
|
+
--primary-dark: #5568d3;
|
|
12
|
+
--bg: #0f0f23;
|
|
13
|
+
--surface: #1a1a2e;
|
|
14
|
+
--surface-light: #252542;
|
|
15
|
+
--text: #e4e4e7;
|
|
16
|
+
--text-muted: #9ca3af;
|
|
17
|
+
--border: #374151;
|
|
18
|
+
--success: #10b981;
|
|
19
|
+
--error: #ef4444;
|
|
20
|
+
--warning: #f59e0b;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
* {
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
31
|
+
background: var(--bg);
|
|
32
|
+
color: var(--text);
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.container {
|
|
37
|
+
max-width: 1400px;
|
|
38
|
+
margin: 0 auto;
|
|
39
|
+
padding: 20px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
header {
|
|
43
|
+
display: flex;
|
|
44
|
+
justify-content: space-between;
|
|
45
|
+
align-items: center;
|
|
46
|
+
padding: 20px 0;
|
|
47
|
+
border-bottom: 1px solid var(--border);
|
|
48
|
+
margin-bottom: 30px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
header h1 {
|
|
52
|
+
font-size: 24px;
|
|
53
|
+
background: linear-gradient(135deg, var(--primary), #a78bfa);
|
|
54
|
+
-webkit-background-clip: text;
|
|
55
|
+
-webkit-text-fill-color: transparent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.tabs {
|
|
59
|
+
display: flex;
|
|
60
|
+
gap: 10px;
|
|
61
|
+
margin-bottom: 20px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.tab {
|
|
65
|
+
padding: 10px 20px;
|
|
66
|
+
background: var(--surface);
|
|
67
|
+
border: 1px solid var(--border);
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
color: var(--text-muted);
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
transition: all 0.2s;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.tab:hover {
|
|
75
|
+
background: var(--surface-light);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.tab.active {
|
|
79
|
+
background: var(--primary);
|
|
80
|
+
color: white;
|
|
81
|
+
border-color: var(--primary);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.panel {
|
|
85
|
+
display: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.panel.active {
|
|
89
|
+
display: block;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.grid {
|
|
93
|
+
display: grid;
|
|
94
|
+
grid-template-columns: 350px 1fr;
|
|
95
|
+
gap: 20px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.card {
|
|
99
|
+
background: var(--surface);
|
|
100
|
+
border: 1px solid var(--border);
|
|
101
|
+
border-radius: 12px;
|
|
102
|
+
padding: 20px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.card h2 {
|
|
106
|
+
font-size: 16px;
|
|
107
|
+
margin-bottom: 15px;
|
|
108
|
+
color: var(--text-muted);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.template-list {
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
gap: 10px;
|
|
115
|
+
max-height: 500px;
|
|
116
|
+
overflow-y: auto;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.template-item {
|
|
120
|
+
padding: 15px;
|
|
121
|
+
background: var(--surface-light);
|
|
122
|
+
border-radius: 8px;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
transition: all 0.2s;
|
|
125
|
+
border: 2px solid transparent;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.template-item:hover {
|
|
129
|
+
border-color: var(--primary);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.template-item.selected {
|
|
133
|
+
border-color: var(--primary);
|
|
134
|
+
background: rgba(102, 126, 234, 0.1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.template-item h3 {
|
|
138
|
+
font-size: 14px;
|
|
139
|
+
margin-bottom: 5px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.template-item p {
|
|
143
|
+
font-size: 12px;
|
|
144
|
+
color: var(--text-muted);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.template-item .badge {
|
|
148
|
+
display: inline-block;
|
|
149
|
+
padding: 2px 8px;
|
|
150
|
+
font-size: 10px;
|
|
151
|
+
background: var(--primary);
|
|
152
|
+
border-radius: 4px;
|
|
153
|
+
margin-top: 8px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.form-group {
|
|
157
|
+
margin-bottom: 15px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.form-group label {
|
|
161
|
+
display: block;
|
|
162
|
+
font-size: 12px;
|
|
163
|
+
color: var(--text-muted);
|
|
164
|
+
margin-bottom: 5px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
input,
|
|
168
|
+
textarea,
|
|
169
|
+
select {
|
|
170
|
+
width: 100%;
|
|
171
|
+
padding: 10px 12px;
|
|
172
|
+
background: var(--surface-light);
|
|
173
|
+
border: 1px solid var(--border);
|
|
174
|
+
border-radius: 6px;
|
|
175
|
+
color: var(--text);
|
|
176
|
+
font-size: 14px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
input:focus,
|
|
180
|
+
textarea:focus,
|
|
181
|
+
select:focus {
|
|
182
|
+
outline: none;
|
|
183
|
+
border-color: var(--primary);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
textarea {
|
|
187
|
+
resize: vertical;
|
|
188
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.btn {
|
|
192
|
+
padding: 10px 20px;
|
|
193
|
+
border: none;
|
|
194
|
+
border-radius: 6px;
|
|
195
|
+
font-size: 14px;
|
|
196
|
+
font-weight: 500;
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
transition: all 0.2s;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.btn-primary {
|
|
202
|
+
background: var(--primary);
|
|
203
|
+
color: white;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.btn-primary:hover {
|
|
207
|
+
background: var(--primary-dark);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.btn-secondary {
|
|
211
|
+
background: var(--surface-light);
|
|
212
|
+
color: var(--text);
|
|
213
|
+
border: 1px solid var(--border);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.btn-secondary:hover {
|
|
217
|
+
background: var(--border);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.btn-danger {
|
|
221
|
+
background: var(--error);
|
|
222
|
+
color: white;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.btn-danger:hover {
|
|
226
|
+
background: #dc2626;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.btn-group {
|
|
230
|
+
display: flex;
|
|
231
|
+
gap: 10px;
|
|
232
|
+
margin-top: 20px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.preview-frame {
|
|
236
|
+
width: 100%;
|
|
237
|
+
height: 500px;
|
|
238
|
+
border: none;
|
|
239
|
+
background: white;
|
|
240
|
+
border-radius: 8px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.email-list {
|
|
244
|
+
display: flex;
|
|
245
|
+
flex-direction: column;
|
|
246
|
+
gap: 10px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.email-item {
|
|
250
|
+
padding: 15px;
|
|
251
|
+
background: var(--surface-light);
|
|
252
|
+
border-radius: 8px;
|
|
253
|
+
display: flex;
|
|
254
|
+
justify-content: space-between;
|
|
255
|
+
align-items: center;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.email-item .status {
|
|
259
|
+
padding: 4px 10px;
|
|
260
|
+
border-radius: 4px;
|
|
261
|
+
font-size: 12px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.email-item .status.sent {
|
|
265
|
+
background: var(--success);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.email-item .status.failed {
|
|
269
|
+
background: var(--error);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.empty-state {
|
|
273
|
+
text-align: center;
|
|
274
|
+
padding: 40px;
|
|
275
|
+
color: var(--text-muted);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.toast {
|
|
279
|
+
position: fixed;
|
|
280
|
+
bottom: 20px;
|
|
281
|
+
right: 20px;
|
|
282
|
+
padding: 15px 25px;
|
|
283
|
+
border-radius: 8px;
|
|
284
|
+
color: white;
|
|
285
|
+
font-weight: 500;
|
|
286
|
+
animation: slideIn 0.3s ease;
|
|
287
|
+
z-index: 1000;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.toast.success {
|
|
291
|
+
background: var(--success);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.toast.error {
|
|
295
|
+
background: var(--error);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@keyframes slideIn {
|
|
299
|
+
from {
|
|
300
|
+
transform: translateX(100%);
|
|
301
|
+
opacity: 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
to {
|
|
305
|
+
transform: translateX(0);
|
|
306
|
+
opacity: 1;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.variables-section {
|
|
311
|
+
margin-top: 15px;
|
|
312
|
+
padding-top: 15px;
|
|
313
|
+
border-top: 1px solid var(--border);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.variable-input {
|
|
317
|
+
display: flex;
|
|
318
|
+
gap: 10px;
|
|
319
|
+
margin-bottom: 10px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.variable-input input:first-child {
|
|
323
|
+
flex: 1;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.variable-input input:last-child {
|
|
327
|
+
flex: 2;
|
|
328
|
+
}
|
|
329
|
+
</style>
|
|
330
|
+
</head>
|
|
331
|
+
|
|
332
|
+
<body>
|
|
333
|
+
<div class="container">
|
|
334
|
+
<header>
|
|
335
|
+
<h1>📧 Email Template Portal</h1>
|
|
336
|
+
<span id="status-badge" style="color: var(--text-muted)">Loading...</span>
|
|
337
|
+
</header>
|
|
338
|
+
|
|
339
|
+
<div class="tabs">
|
|
340
|
+
<button class="tab active" data-tab="templates">Templates</button>
|
|
341
|
+
<button class="tab" data-tab="sent">Sent Emails</button>
|
|
342
|
+
<button class="tab" data-tab="settings">Settings (Mock D1)</button>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<!-- Templates Panel -->
|
|
346
|
+
<div id="templates-panel" class="panel active">
|
|
347
|
+
<div class="grid">
|
|
348
|
+
<div class="card">
|
|
349
|
+
<h2>📋 Templates</h2>
|
|
350
|
+
<button class="btn btn-primary" style="width: 100%; margin-bottom: 15px;" onclick="createNewTemplate()">+ New
|
|
351
|
+
Template</button>
|
|
352
|
+
<div id="template-list" class="template-list">
|
|
353
|
+
<div class="empty-state">Loading...</div>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<div class="card">
|
|
358
|
+
<h2>✏️ Editor</h2>
|
|
359
|
+
<form id="template-form">
|
|
360
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
|
361
|
+
<div class="form-group">
|
|
362
|
+
<label>Template ID</label>
|
|
363
|
+
<input type="text" id="template-id" placeholder="e.g. welcome_email" required>
|
|
364
|
+
</div>
|
|
365
|
+
<div class="form-group">
|
|
366
|
+
<label>Type</label>
|
|
367
|
+
<select id="template-type">
|
|
368
|
+
<option value="transactional">Transactional</option>
|
|
369
|
+
<option value="marketing">Marketing</option>
|
|
370
|
+
<option value="onboarding">Onboarding</option>
|
|
371
|
+
<option value="notification">Notification</option>
|
|
372
|
+
</select>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div class="form-group">
|
|
377
|
+
<label>Template Name</label>
|
|
378
|
+
<input type="text" id="template-name" placeholder="e.g. Welcome Email" required>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div class="form-group">
|
|
382
|
+
<label>Subject (supports {{variables}})</label>
|
|
383
|
+
<input type="text" id="template-subject" placeholder="e.g. Welcome to {{company_name}}!" required>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<div class="form-group">
|
|
387
|
+
<label>Body (Markdown with {{variables}})</label>
|
|
388
|
+
<textarea id="template-body" rows="12"
|
|
389
|
+
placeholder="# Hello {{user_name}}! Welcome to our platform..." required></textarea>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<div class="form-group">
|
|
393
|
+
<label>Description</label>
|
|
394
|
+
<input type="text" id="template-description" placeholder="Brief description of when this is used">
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
<div class="btn-group">
|
|
398
|
+
<button type="submit" class="btn btn-primary">💾 Save Template</button>
|
|
399
|
+
<button type="button" class="btn btn-secondary" onclick="previewTemplate()">👁️ Preview</button>
|
|
400
|
+
<button type="button" class="btn btn-secondary" onclick="sendTestEmail()">📤 Send Test</button>
|
|
401
|
+
<button type="button" class="btn btn-danger" onclick="deleteTemplate()">🗑️ Delete</button>
|
|
402
|
+
</div>
|
|
403
|
+
</form>
|
|
404
|
+
|
|
405
|
+
<div class="variables-section">
|
|
406
|
+
<h3 style="font-size: 14px; margin-bottom: 10px;">📝 Preview Variables</h3>
|
|
407
|
+
<div id="variable-inputs"></div>
|
|
408
|
+
<button type="button" class="btn btn-secondary" style="margin-top: 10px;" onclick="addVariable()">+ Add
|
|
409
|
+
Variable</button>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<!-- Sent Emails Panel -->
|
|
417
|
+
<div id="sent-panel" class="panel">
|
|
418
|
+
<div class="card">
|
|
419
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
420
|
+
<h2>📬 Sent Emails</h2>
|
|
421
|
+
<button class="btn btn-secondary" onclick="clearSentEmails()">Clear All</button>
|
|
422
|
+
</div>
|
|
423
|
+
<div id="sent-list" class="email-list">
|
|
424
|
+
<div class="empty-state">No emails sent yet</div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<!-- Settings Panel -->
|
|
430
|
+
<div id="settings-panel" class="panel">
|
|
431
|
+
<div class="grid">
|
|
432
|
+
<div class="card">
|
|
433
|
+
<h2>⚙️ System Email Settings</h2>
|
|
434
|
+
<p style="font-size: 12px; color: var(--text-muted); margin-bottom: 20px;">
|
|
435
|
+
These settings are stored in the Mock D1 database. They control the default behavior if no tenant-specific
|
|
436
|
+
override is found.
|
|
437
|
+
</p>
|
|
438
|
+
<form id="settings-form">
|
|
439
|
+
<div class="form-group">
|
|
440
|
+
<label>Default Provider</label>
|
|
441
|
+
<select id="setting-provider" onchange="toggleProviderSettings()">
|
|
442
|
+
<option value="mock">Mock (Internal)</option>
|
|
443
|
+
<option value="mailchannels">MailChannels</option>
|
|
444
|
+
<option value="sendgrid">SendGrid</option>
|
|
445
|
+
<option value="resend">Resend</option>
|
|
446
|
+
<option value="sendpulse">SendPulse</option>
|
|
447
|
+
</select>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<!-- Provider Specific Settings -->
|
|
451
|
+
<div id="provider-settings"
|
|
452
|
+
style="margin-bottom: 20px; padding: 15px; background: var(--surface-light); border-radius: 8px; border: 1px solid var(--border);">
|
|
453
|
+
|
|
454
|
+
<!-- SendGrid -->
|
|
455
|
+
<div id="settings-sendgrid" class="provider-config" style="display: none;">
|
|
456
|
+
<div class="form-group">
|
|
457
|
+
<label>SendGrid API Key</label>
|
|
458
|
+
<input type="password" id="setting-sendgrid-api-key" placeholder="SG.xxxxxxxx">
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<!-- Resend -->
|
|
463
|
+
<div id="settings-resend" class="provider-config" style="display: none;">
|
|
464
|
+
<div class="form-group">
|
|
465
|
+
<label>Resend API Key</label>
|
|
466
|
+
<input type="password" id="setting-resend-api-key" placeholder="re_xxxxxxxx">
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
<!-- SendPulse -->
|
|
471
|
+
<div id="settings-sendpulse" class="provider-config" style="display: none;">
|
|
472
|
+
<div class="form-group">
|
|
473
|
+
<label>Client ID</label>
|
|
474
|
+
<input type="text" id="setting-sendpulse-client-id" placeholder="ID from SendPulse account">
|
|
475
|
+
</div>
|
|
476
|
+
<div class="form-group">
|
|
477
|
+
<label>Client Secret</label>
|
|
478
|
+
<input type="password" id="setting-sendpulse-client-secret"
|
|
479
|
+
placeholder="Secret from SendPulse account">
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
<div id="settings-mock" class="provider-config">
|
|
484
|
+
<p style="font-size: 12px; color: var(--text-muted); margin: 0;">No API keys required for Mock.</p>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
<div id="settings-mailchannels" class="provider-config" style="display: none;">
|
|
488
|
+
<p style="font-size: 12px; color: var(--text-muted); margin: 0;">No API keys required for MailChannels
|
|
489
|
+
(IP authorized).</p>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
<div class="form-group">
|
|
494
|
+
<label>From Name</label>
|
|
495
|
+
<input type="text" id="setting-from-name" placeholder="e.g. My App">
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
<div class="form-group">
|
|
499
|
+
<label>From Address</label>
|
|
500
|
+
<input type="email" id="setting-from-address" placeholder="e.g. noreply@example.com">
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<div class="btn-group">
|
|
504
|
+
<button type="submit" class="btn btn-primary">💾 Save Settings</button>
|
|
505
|
+
</div>
|
|
506
|
+
</form>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
<!-- Email Preview Modal -->
|
|
511
|
+
<div id="email-modal"
|
|
512
|
+
style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 1000; align-items: center; justify-content: center;">
|
|
513
|
+
<div class="card"
|
|
514
|
+
style="width: 90%; max-width: 1000px; height: 90%; display: flex; flex-direction: column; padding: 0; overflow: hidden;">
|
|
515
|
+
<div
|
|
516
|
+
style="padding: 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center;">
|
|
517
|
+
<h2 id="modal-subject" style="margin: 0;">Subject</h2>
|
|
518
|
+
<button class="btn btn-secondary" onclick="closeModal()">✕</button>
|
|
519
|
+
</div>
|
|
520
|
+
<div id="modal-metadata" style="padding: 15px; background: var(--surface-light); font-size: 12px;">
|
|
521
|
+
<div><strong>To:</strong> <span id="modal-to"></span></div>
|
|
522
|
+
<div><strong>From:</strong> <span id="modal-from"></span></div>
|
|
523
|
+
<div><strong>Date:</strong> <span id="modal-date"></span></div>
|
|
524
|
+
</div>
|
|
525
|
+
<div style="flex: 1; position: relative;">
|
|
526
|
+
<iframe id="modal-frame" style="width: 100%; height: 100%; border: none; background: white;"></iframe>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
<script>
|
|
533
|
+
const API = '';
|
|
534
|
+
let templates = [];
|
|
535
|
+
let sentEmails = [];
|
|
536
|
+
let selectedTemplate = null;
|
|
537
|
+
let variableValues = {};
|
|
538
|
+
|
|
539
|
+
// Tab switching
|
|
540
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
541
|
+
tab.addEventListener('click', () => {
|
|
542
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
543
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
544
|
+
tab.classList.add('active');
|
|
545
|
+
document.getElementById(`${tab.dataset.tab}-panel`).classList.add('active');
|
|
546
|
+
|
|
547
|
+
if (tab.dataset.tab === 'sent') loadSentEmails();
|
|
548
|
+
if (tab.dataset.tab === 'settings') loadSettings();
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Load templates
|
|
553
|
+
async function loadTemplates() {
|
|
554
|
+
try {
|
|
555
|
+
const res = await fetch(`${API}/api/templates`);
|
|
556
|
+
const data = await res.json();
|
|
557
|
+
templates = data.templates || [];
|
|
558
|
+
renderTemplateList();
|
|
559
|
+
} catch (err) {
|
|
560
|
+
showToast('Failed to load templates', 'error');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function loadStatus() {
|
|
565
|
+
try {
|
|
566
|
+
const res = await fetch(`${API}/api/status`);
|
|
567
|
+
const data = await res.json();
|
|
568
|
+
const badge = document.getElementById('status-badge');
|
|
569
|
+
|
|
570
|
+
if (data.useRealProvider) {
|
|
571
|
+
badge.innerHTML = `✅ <strong>REAL MODE</strong>: Sending via ${data.provider.toUpperCase()}`;
|
|
572
|
+
badge.style.color = 'var(--success)';
|
|
573
|
+
} else {
|
|
574
|
+
badge.innerHTML = `⚠️ <strong>MOCK MODE</strong>: No real emails sent`;
|
|
575
|
+
badge.style.color = 'var(--warning)';
|
|
576
|
+
}
|
|
577
|
+
} catch (err) {
|
|
578
|
+
console.error('Failed to load status', err);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function renderTemplateList() {
|
|
583
|
+
const list = document.getElementById('template-list');
|
|
584
|
+
if (templates.length === 0) {
|
|
585
|
+
list.innerHTML = '<div class="empty-state">No templates yet</div>';
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
list.innerHTML = templates.map(t => `
|
|
590
|
+
<div class="template-item ${selectedTemplate?.template_id === t.template_id ? 'selected' : ''}"
|
|
591
|
+
onclick="selectTemplate('${t.template_id}')">
|
|
592
|
+
<h3>${t.template_name}</h3>
|
|
593
|
+
<p>${t.description || 'No description'}</p>
|
|
594
|
+
<span class="badge">${t.template_type}</span>
|
|
595
|
+
</div>
|
|
596
|
+
`).join('');
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function selectTemplate(id) {
|
|
600
|
+
selectedTemplate = templates.find(t => t.template_id === id);
|
|
601
|
+
if (!selectedTemplate) return;
|
|
602
|
+
|
|
603
|
+
document.getElementById('template-id').value = selectedTemplate.template_id;
|
|
604
|
+
document.getElementById('template-id').disabled = true;
|
|
605
|
+
document.getElementById('template-name').value = selectedTemplate.template_name;
|
|
606
|
+
document.getElementById('template-type').value = selectedTemplate.template_type;
|
|
607
|
+
document.getElementById('template-subject').value = selectedTemplate.subject_template;
|
|
608
|
+
document.getElementById('template-body').value = selectedTemplate.body_markdown;
|
|
609
|
+
document.getElementById('template-description').value = selectedTemplate.description || '';
|
|
610
|
+
|
|
611
|
+
// Extract variables and create inputs
|
|
612
|
+
const vars = extractVariables(selectedTemplate.subject_template + ' ' + selectedTemplate.body_markdown);
|
|
613
|
+
renderVariableInputs(vars);
|
|
614
|
+
|
|
615
|
+
renderTemplateList();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function extractVariables(text) {
|
|
619
|
+
const matches = text.match(/\{\{([^}]+)\}\}/g) || [];
|
|
620
|
+
return [...new Set(matches.map(m => m.replace(/[{}]/g, '').trim()))];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function renderVariableInputs(vars) {
|
|
624
|
+
const container = document.getElementById('variable-inputs');
|
|
625
|
+
container.innerHTML = vars.map(v => `
|
|
626
|
+
<div class="variable-input">
|
|
627
|
+
<input type="text" value="${v}" readonly>
|
|
628
|
+
<input type="text" placeholder="Value..." onchange="variableValues['${v}'] = this.value" value="${variableValues[v] || ''}">
|
|
629
|
+
</div>
|
|
630
|
+
`).join('');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function addVariable() {
|
|
634
|
+
const container = document.getElementById('variable-inputs');
|
|
635
|
+
const div = document.createElement('div');
|
|
636
|
+
div.className = 'variable-input';
|
|
637
|
+
div.innerHTML = `
|
|
638
|
+
<input type="text" placeholder="Variable name">
|
|
639
|
+
<input type="text" placeholder="Value...">
|
|
640
|
+
`;
|
|
641
|
+
container.appendChild(div);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function createNewTemplate() {
|
|
645
|
+
selectedTemplate = null;
|
|
646
|
+
document.getElementById('template-form').reset();
|
|
647
|
+
document.getElementById('template-id').disabled = false;
|
|
648
|
+
document.getElementById('variable-inputs').innerHTML = '';
|
|
649
|
+
variableValues = {};
|
|
650
|
+
renderTemplateList();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Save template
|
|
654
|
+
document.getElementById('template-form').addEventListener('submit', async (e) => {
|
|
655
|
+
e.preventDefault();
|
|
656
|
+
|
|
657
|
+
const template = {
|
|
658
|
+
template_id: document.getElementById('template-id').value,
|
|
659
|
+
template_name: document.getElementById('template-name').value,
|
|
660
|
+
template_type: document.getElementById('template-type').value,
|
|
661
|
+
subject_template: document.getElementById('template-subject').value,
|
|
662
|
+
body_markdown: document.getElementById('template-body').value,
|
|
663
|
+
description: document.getElementById('template-description').value,
|
|
664
|
+
is_active: 1
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const res = await fetch(`${API}/api/templates`, {
|
|
669
|
+
method: 'POST',
|
|
670
|
+
headers: { 'Content-Type': 'application/json' },
|
|
671
|
+
body: JSON.stringify(template)
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const data = await res.json();
|
|
675
|
+
if (data.success) {
|
|
676
|
+
showToast('Template saved!', 'success');
|
|
677
|
+
await loadTemplates();
|
|
678
|
+
selectTemplate(template.template_id);
|
|
679
|
+
} else {
|
|
680
|
+
showToast(data.error || 'Failed to save', 'error');
|
|
681
|
+
}
|
|
682
|
+
} catch (err) {
|
|
683
|
+
showToast('Failed to save template', 'error');
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
async function deleteTemplate() {
|
|
688
|
+
if (!selectedTemplate) return;
|
|
689
|
+
if (!confirm(`Delete "${selectedTemplate.template_name}"?`)) return;
|
|
690
|
+
|
|
691
|
+
try {
|
|
692
|
+
await fetch(`${API}/api/templates/${selectedTemplate.template_id}`, { method: 'DELETE' });
|
|
693
|
+
showToast('Template deleted', 'success');
|
|
694
|
+
createNewTemplate();
|
|
695
|
+
await loadTemplates();
|
|
696
|
+
} catch (err) {
|
|
697
|
+
showToast('Failed to delete', 'error');
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function previewTemplate() {
|
|
702
|
+
const templateId = document.getElementById('template-id').value;
|
|
703
|
+
if (!templateId) {
|
|
704
|
+
showToast('Save the template first', 'error');
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Collect variable values
|
|
709
|
+
const vars = {};
|
|
710
|
+
document.querySelectorAll('.variable-input').forEach(div => {
|
|
711
|
+
const inputs = div.querySelectorAll('input');
|
|
712
|
+
if (inputs[0].value && inputs[1].value) {
|
|
713
|
+
vars[inputs[0].value] = inputs[1].value;
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
const res = await fetch(`${API}/api/templates/${templateId}/preview`, {
|
|
719
|
+
method: 'POST',
|
|
720
|
+
headers: { 'Content-Type': 'application/json' },
|
|
721
|
+
body: JSON.stringify(vars)
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
const data = await res.json();
|
|
725
|
+
if (data.success) {
|
|
726
|
+
// Open Modal
|
|
727
|
+
document.getElementById('modal-subject').textContent = 'Preview: ' + data.preview.subject;
|
|
728
|
+
document.getElementById('modal-frame').srcdoc = data.preview.html;
|
|
729
|
+
document.getElementById('modal-metadata').style.display = 'none'; // Hide metadata for preview
|
|
730
|
+
document.getElementById('email-modal').style.display = 'flex';
|
|
731
|
+
} else {
|
|
732
|
+
showToast(data.error || 'Preview failed', 'error');
|
|
733
|
+
}
|
|
734
|
+
} catch (err) {
|
|
735
|
+
showToast('Preview failed', 'error');
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function sendTestEmail() {
|
|
740
|
+
const templateId = document.getElementById('template-id').value;
|
|
741
|
+
const to = prompt('Send test email to:');
|
|
742
|
+
if (!to) return;
|
|
743
|
+
|
|
744
|
+
const vars = {};
|
|
745
|
+
document.querySelectorAll('.variable-input').forEach(div => {
|
|
746
|
+
const inputs = div.querySelectorAll('input');
|
|
747
|
+
if (inputs[0].value && inputs[1].value) {
|
|
748
|
+
vars[inputs[0].value] = inputs[1].value;
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
try {
|
|
753
|
+
const res = await fetch(`${API}/api/templates/${templateId}/test`, {
|
|
754
|
+
method: 'POST',
|
|
755
|
+
headers: { 'Content-Type': 'application/json' },
|
|
756
|
+
body: JSON.stringify({ to, variables: vars })
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
const data = await res.json();
|
|
760
|
+
if (data.success) {
|
|
761
|
+
showToast('Test email sent!', 'success');
|
|
762
|
+
} else {
|
|
763
|
+
showToast(data.error || 'Failed to send', 'error');
|
|
764
|
+
}
|
|
765
|
+
} catch (err) {
|
|
766
|
+
showToast('Failed to send test', 'error');
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function loadSentEmails() {
|
|
771
|
+
try {
|
|
772
|
+
const res = await fetch(`${API}/api/sent-emails`);
|
|
773
|
+
const data = await res.json();
|
|
774
|
+
sentEmails = data.emails || [];
|
|
775
|
+
renderSentEmails(sentEmails);
|
|
776
|
+
} catch (err) {
|
|
777
|
+
console.error('Failed to load sent emails', err);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function renderSentEmails(emails) {
|
|
782
|
+
const list = document.getElementById('sent-list');
|
|
783
|
+
if (emails.length === 0) {
|
|
784
|
+
list.innerHTML = '<div class="empty-state">No emails sent yet</div>';
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
list.innerHTML = emails.map((e, index) => `
|
|
789
|
+
<div class="email-item" onclick="openEmailModal(${index})" style="cursor: pointer;">
|
|
790
|
+
<div style="flex: 1;">
|
|
791
|
+
<strong>${e.subject}</strong>
|
|
792
|
+
<div style="font-size: 12px; color: var(--text-muted);">
|
|
793
|
+
To: ${e.to} • ${new Date(e.sentAt).toLocaleString()}
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
<span class="status ${e.status}">${e.status}</span>
|
|
797
|
+
</div>
|
|
798
|
+
`).join('');
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function openEmailModal(index) {
|
|
802
|
+
const email = sentEmails[index];
|
|
803
|
+
if (!email) return;
|
|
804
|
+
|
|
805
|
+
document.getElementById('modal-subject').textContent = email.subject;
|
|
806
|
+
document.getElementById('modal-to').textContent = email.to;
|
|
807
|
+
document.getElementById('modal-from').textContent = email.from;
|
|
808
|
+
document.getElementById('modal-date').textContent = new Date(email.sentAt).toLocaleString();
|
|
809
|
+
document.getElementById('modal-frame').srcdoc = email.html;
|
|
810
|
+
|
|
811
|
+
document.getElementById('modal-metadata').style.display = 'block'; // Show metadata for sent emails
|
|
812
|
+
document.getElementById('email-modal').style.display = 'flex';
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function closeModal() {
|
|
816
|
+
document.getElementById('email-modal').style.display = 'none';
|
|
817
|
+
document.getElementById('modal-frame').srcdoc = '';
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
document.getElementById('email-modal').addEventListener('click', (e) => {
|
|
821
|
+
if (e.target.id === 'email-modal') closeModal();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
async function clearSentEmails() {
|
|
825
|
+
await fetch(`${API}/api/sent-emails`, { method: 'DELETE' });
|
|
826
|
+
loadSentEmails();
|
|
827
|
+
showToast('Cleared', 'success');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function showToast(message, type) {
|
|
831
|
+
const toast = document.createElement('div');
|
|
832
|
+
toast.className = `toast ${type}`;
|
|
833
|
+
toast.textContent = message;
|
|
834
|
+
document.body.appendChild(toast);
|
|
835
|
+
setTimeout(() => toast.remove(), 3000);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function toggleProviderSettings() {
|
|
839
|
+
const provider = document.getElementById('setting-provider').value;
|
|
840
|
+
|
|
841
|
+
// Hide all first
|
|
842
|
+
document.querySelectorAll('.provider-config').forEach(el => el.style.display = 'none');
|
|
843
|
+
|
|
844
|
+
// Show selected
|
|
845
|
+
const configDiv = document.getElementById(`settings-${provider}`);
|
|
846
|
+
if (configDiv) {
|
|
847
|
+
configDiv.style.display = 'block';
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function loadSettings() {
|
|
852
|
+
try {
|
|
853
|
+
const res = await fetch(`${API}/api/settings`);
|
|
854
|
+
const data = await res.json();
|
|
855
|
+
if (data.success && data.settings) {
|
|
856
|
+
document.getElementById('setting-provider').value = data.settings.email_provider || 'mock';
|
|
857
|
+
document.getElementById('setting-from-name').value = data.settings.email_from_name || '';
|
|
858
|
+
document.getElementById('setting-from-address').value = data.settings.email_from_address || '';
|
|
859
|
+
|
|
860
|
+
// Load Provider Keys
|
|
861
|
+
if (data.settings.sendgrid_api_key) document.getElementById('setting-sendgrid-api-key').value = data.settings.sendgrid_api_key;
|
|
862
|
+
if (data.settings.resend_api_key) document.getElementById('setting-resend-api-key').value = data.settings.resend_api_key;
|
|
863
|
+
if (data.settings.sendpulse_client_id) document.getElementById('setting-sendpulse-client-id').value = data.settings.sendpulse_client_id;
|
|
864
|
+
if (data.settings.sendpulse_client_secret) document.getElementById('setting-sendpulse-client-secret').value = data.settings.sendpulse_client_secret;
|
|
865
|
+
|
|
866
|
+
// Trigger visibility update
|
|
867
|
+
toggleProviderSettings();
|
|
868
|
+
}
|
|
869
|
+
} catch (err) {
|
|
870
|
+
showToast('Failed to load settings', 'error');
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
document.getElementById('settings-form').addEventListener('submit', async (e) => {
|
|
875
|
+
e.preventDefault();
|
|
876
|
+
|
|
877
|
+
const settings = {
|
|
878
|
+
email_provider: document.getElementById('setting-provider').value,
|
|
879
|
+
email_from_name: document.getElementById('setting-from-name').value,
|
|
880
|
+
email_from_address: document.getElementById('setting-from-address').value
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
// Add provider-specific settings if they have values
|
|
884
|
+
const sendgridKey = document.getElementById('setting-sendgrid-api-key').value;
|
|
885
|
+
if (sendgridKey) settings.sendgrid_api_key = sendgridKey;
|
|
886
|
+
|
|
887
|
+
const resendKey = document.getElementById('setting-resend-api-key').value;
|
|
888
|
+
if (resendKey) settings.resend_api_key = resendKey;
|
|
889
|
+
|
|
890
|
+
const spId = document.getElementById('setting-sendpulse-client-id').value;
|
|
891
|
+
if (spId) settings.sendpulse_client_id = spId;
|
|
892
|
+
|
|
893
|
+
const spSecret = document.getElementById('setting-sendpulse-client-secret').value;
|
|
894
|
+
if (spSecret) settings.sendpulse_client_secret = spSecret;
|
|
895
|
+
|
|
896
|
+
try {
|
|
897
|
+
const res = await fetch(`${API}/api/settings`, {
|
|
898
|
+
method: 'POST',
|
|
899
|
+
headers: { 'Content-Type': 'application/json' },
|
|
900
|
+
body: JSON.stringify(settings)
|
|
901
|
+
});
|
|
902
|
+
const data = await res.json();
|
|
903
|
+
if (data.success) {
|
|
904
|
+
showToast('Settings saved to Mock D1', 'success');
|
|
905
|
+
} else {
|
|
906
|
+
showToast('Failed to save settings', 'error');
|
|
907
|
+
}
|
|
908
|
+
} catch (err) {
|
|
909
|
+
showToast('Error saving settings', 'error');
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// Initial load
|
|
914
|
+
loadTemplates();
|
|
915
|
+
loadStatus();
|
|
916
|
+
</script>
|
|
917
|
+
</body>
|
|
918
|
+
|
|
919
|
+
</html>
|