strapi-plugin-magic-mail 1.0.1

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 (91) hide show
  1. package/COPYRIGHT_NOTICE.txt +13 -0
  2. package/LICENSE +22 -0
  3. package/README.md +1420 -0
  4. package/admin/jsconfig.json +10 -0
  5. package/admin/src/components/AddAccountModal.jsx +1943 -0
  6. package/admin/src/components/Initializer.jsx +14 -0
  7. package/admin/src/components/LicenseGuard.jsx +475 -0
  8. package/admin/src/components/PluginIcon.jsx +5 -0
  9. package/admin/src/hooks/useAuthRefresh.js +44 -0
  10. package/admin/src/hooks/useLicense.js +158 -0
  11. package/admin/src/index.js +86 -0
  12. package/admin/src/pages/Analytics.jsx +762 -0
  13. package/admin/src/pages/App.jsx +111 -0
  14. package/admin/src/pages/EmailDesigner/EditorPage.jsx +1405 -0
  15. package/admin/src/pages/EmailDesigner/TemplateList.jsx +1807 -0
  16. package/admin/src/pages/HomePage.jsx +1233 -0
  17. package/admin/src/pages/LicensePage.jsx +424 -0
  18. package/admin/src/pages/RoutingRules.jsx +1141 -0
  19. package/admin/src/pages/Settings.jsx +603 -0
  20. package/admin/src/pluginId.js +3 -0
  21. package/admin/src/translations/de.json +71 -0
  22. package/admin/src/translations/en.json +70 -0
  23. package/admin/src/translations/es.json +71 -0
  24. package/admin/src/translations/fr.json +71 -0
  25. package/admin/src/translations/pt.json +71 -0
  26. package/admin/src/utils/fetchWithRetry.js +123 -0
  27. package/admin/src/utils/getTranslation.js +5 -0
  28. package/dist/_chunks/App-B-Gp4Vbr.js +7568 -0
  29. package/dist/_chunks/App-BymMjoGM.mjs +7543 -0
  30. package/dist/_chunks/LicensePage-Bl02myMx.mjs +342 -0
  31. package/dist/_chunks/LicensePage-CJXwPnEe.js +344 -0
  32. package/dist/_chunks/Settings-C_TmKwcz.mjs +400 -0
  33. package/dist/_chunks/Settings-zuFQ3pnn.js +402 -0
  34. package/dist/_chunks/de-CN-G9j1S.js +64 -0
  35. package/dist/_chunks/de-DS04rP54.mjs +64 -0
  36. package/dist/_chunks/en-BDc7Jk8u.js +64 -0
  37. package/dist/_chunks/en-BEFQJXvR.mjs +64 -0
  38. package/dist/_chunks/es-BpV1MIdm.js +64 -0
  39. package/dist/_chunks/es-DQHwzPpP.mjs +64 -0
  40. package/dist/_chunks/fr-BG1WfEVm.mjs +64 -0
  41. package/dist/_chunks/fr-vpziIpRp.js +64 -0
  42. package/dist/_chunks/pt-CMoGrOib.mjs +64 -0
  43. package/dist/_chunks/pt-ODpAhDNa.js +64 -0
  44. package/dist/admin/index.js +89 -0
  45. package/dist/admin/index.mjs +90 -0
  46. package/dist/server/index.js +6214 -0
  47. package/dist/server/index.mjs +6208 -0
  48. package/package.json +113 -0
  49. package/server/jsconfig.json +10 -0
  50. package/server/src/bootstrap.js +153 -0
  51. package/server/src/config/features.js +260 -0
  52. package/server/src/config/index.js +6 -0
  53. package/server/src/content-types/email-account/schema.json +93 -0
  54. package/server/src/content-types/email-event/index.js +8 -0
  55. package/server/src/content-types/email-event/schema.json +57 -0
  56. package/server/src/content-types/email-link/index.js +8 -0
  57. package/server/src/content-types/email-link/schema.json +49 -0
  58. package/server/src/content-types/email-log/index.js +8 -0
  59. package/server/src/content-types/email-log/schema.json +106 -0
  60. package/server/src/content-types/email-template/schema.json +74 -0
  61. package/server/src/content-types/email-template-version/schema.json +60 -0
  62. package/server/src/content-types/index.js +33 -0
  63. package/server/src/content-types/routing-rule/schema.json +59 -0
  64. package/server/src/controllers/accounts.js +220 -0
  65. package/server/src/controllers/analytics.js +347 -0
  66. package/server/src/controllers/controller.js +26 -0
  67. package/server/src/controllers/email-designer.js +474 -0
  68. package/server/src/controllers/index.js +21 -0
  69. package/server/src/controllers/license.js +267 -0
  70. package/server/src/controllers/oauth.js +474 -0
  71. package/server/src/controllers/routing-rules.js +122 -0
  72. package/server/src/controllers/test.js +383 -0
  73. package/server/src/destroy.js +23 -0
  74. package/server/src/index.js +25 -0
  75. package/server/src/middlewares/index.js +3 -0
  76. package/server/src/policies/index.js +3 -0
  77. package/server/src/register.js +5 -0
  78. package/server/src/routes/admin.js +469 -0
  79. package/server/src/routes/content-api.js +37 -0
  80. package/server/src/routes/index.js +9 -0
  81. package/server/src/services/account-manager.js +277 -0
  82. package/server/src/services/analytics.js +496 -0
  83. package/server/src/services/email-designer.js +870 -0
  84. package/server/src/services/email-router.js +1420 -0
  85. package/server/src/services/index.js +17 -0
  86. package/server/src/services/license-guard.js +418 -0
  87. package/server/src/services/oauth.js +515 -0
  88. package/server/src/services/service.js +7 -0
  89. package/server/src/utils/encryption.js +81 -0
  90. package/strapi-admin.js +4 -0
  91. package/strapi-server.js +4 -0
@@ -0,0 +1,1807 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { useFetchClient, useNotification } from '@strapi/strapi/admin';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import { useAuthRefresh } from '../../hooks/useAuthRefresh';
5
+ import styled, { keyframes, css } from 'styled-components';
6
+ import {
7
+ Box,
8
+ Button,
9
+ Flex,
10
+ Typography,
11
+ Loader,
12
+ Badge,
13
+ TextInput,
14
+ Tabs,
15
+ Divider,
16
+ Modal,
17
+ } from '@strapi/design-system';
18
+ import { Table, Thead, Tbody, Tr, Th, Td } from '@strapi/design-system';
19
+ import {
20
+ PlusIcon,
21
+ PencilIcon,
22
+ TrashIcon,
23
+ ArrowDownTrayIcon,
24
+ ArrowUpTrayIcon,
25
+ DocumentTextIcon,
26
+ MagnifyingGlassIcon,
27
+ ChartBarIcon,
28
+ SparklesIcon,
29
+ CheckCircleIcon,
30
+ BoltIcon,
31
+ CodeBracketIcon,
32
+ DocumentDuplicateIcon,
33
+ DocumentArrowDownIcon,
34
+ ClipboardDocumentIcon,
35
+ CheckIcon,
36
+ PaperAirplaneIcon,
37
+ } from '@heroicons/react/24/outline';
38
+ import { useLicense } from '../../hooks/useLicense';
39
+
40
+ // ================ THEME (Exact copy from RoutingRules) ================
41
+ const theme = {
42
+ colors: {
43
+ primary: { 50: '#F0F9FF', 100: '#E0F2FE', 500: '#0EA5E9', 600: '#0284C7', 700: '#0369A1' },
44
+ secondary: { 50: '#F5F3FF', 100: '#EDE9FE', 500: '#A855F7', 600: '#9333EA' },
45
+ success: { 100: '#DCFCE7', 500: '#22C55E', 600: '#16A34A', 700: '#15803D' },
46
+ warning: { 100: '#FEF3C7', 500: '#F59E0B', 600: '#D97706' },
47
+ danger: { 100: '#FEE2E2', 500: '#EF4444', 600: '#DC2626' },
48
+ neutral: { 0: '#FFFFFF', 50: '#F9FAFB', 100: '#F3F4F6', 200: '#E5E7EB', 600: '#4B5563', 700: '#374151', 800: '#1F2937' }
49
+ },
50
+ shadows: {
51
+ sm: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
52
+ md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
53
+ lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
54
+ xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
55
+ },
56
+ transitions: { fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)', normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)', slow: '500ms cubic-bezier(0.4, 0, 0.2, 1)' },
57
+ spacing: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px', '2xl': '48px' },
58
+ borderRadius: { md: '8px', lg: '12px', xl: '16px' }
59
+ };
60
+
61
+ // ================ ANIMATIONS ================
62
+ const fadeIn = keyframes`
63
+ from { opacity: 0; transform: translateY(10px); }
64
+ to { opacity: 1; transform: translateY(0); }
65
+ `;
66
+
67
+ const shimmer = keyframes`
68
+ 0% { background-position: -200% 0; }
69
+ 100% { background-position: 200% 0; }
70
+ `;
71
+
72
+ const float = keyframes`
73
+ 0%, 100% { transform: translateY(0px); }
74
+ 50% { transform: translateY(-5px); }
75
+ `;
76
+
77
+ const FloatingEmoji = styled.div`
78
+ position: absolute;
79
+ bottom: 40px;
80
+ right: 40px;
81
+ font-size: 72px;
82
+ opacity: 0.08;
83
+ ${css`animation: ${float} 4s ease-in-out infinite;`}
84
+ `;
85
+
86
+ // Custom Scrollbar for Modal
87
+ const ScrollableDialogBody = styled(Box)`
88
+ overflow-y: auto;
89
+ max-height: calc(85vh - 160px);
90
+ padding: 0 24px 24px 24px;
91
+
92
+ /* Custom Scrollbar */
93
+ &::-webkit-scrollbar {
94
+ width: 6px;
95
+ }
96
+
97
+ &::-webkit-scrollbar-track {
98
+ background: transparent;
99
+ }
100
+
101
+ &::-webkit-scrollbar-thumb {
102
+ background: ${theme.colors.neutral[200]};
103
+ border-radius: 3px;
104
+ }
105
+
106
+ &::-webkit-scrollbar-thumb:hover {
107
+ background: ${theme.colors.neutral[300]};
108
+ }
109
+ `;
110
+
111
+ const CodeSection = styled(Box)`
112
+ margin-bottom: 32px;
113
+
114
+ &:last-child {
115
+ margin-bottom: 0;
116
+ }
117
+ `;
118
+
119
+ const CodeHeader = styled(Flex)`
120
+ align-items: center;
121
+ gap: 12px;
122
+ margin-bottom: 16px;
123
+ `;
124
+
125
+ const CodeLabel = styled(Typography)`
126
+ font-size: 15px;
127
+ font-weight: 600;
128
+ color: ${theme.colors.neutral[800]};
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 8px;
132
+ `;
133
+
134
+ const RecommendedBadge = styled(Badge)`
135
+ background: linear-gradient(135deg, ${theme.colors.success[500]}, ${theme.colors.success[600]});
136
+ color: white;
137
+ padding: 4px 12px;
138
+ font-size: 11px;
139
+ font-weight: 600;
140
+ text-transform: uppercase;
141
+ letter-spacing: 0.5px;
142
+ `;
143
+
144
+ const CodeBlockWrapper = styled(Box)`
145
+ position: relative;
146
+ background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
147
+ border-radius: 12px;
148
+ overflow: hidden;
149
+ box-shadow: ${theme.shadows.lg};
150
+ border: 1px solid rgba(255, 255, 255, 0.1);
151
+ `;
152
+
153
+ const CodeBlock = styled.pre`
154
+ margin: 0;
155
+ padding: 20px;
156
+ color: #e2e8f0;
157
+ font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
158
+ font-size: 13px;
159
+ line-height: 1.7;
160
+ overflow-x: auto;
161
+ max-height: 320px;
162
+
163
+ &::-webkit-scrollbar {
164
+ height: 6px;
165
+ width: 6px;
166
+ }
167
+
168
+ &::-webkit-scrollbar-track {
169
+ background: rgba(255, 255, 255, 0.05);
170
+ }
171
+
172
+ &::-webkit-scrollbar-thumb {
173
+ background: rgba(255, 255, 255, 0.2);
174
+ border-radius: 3px;
175
+ }
176
+
177
+ &::-webkit-scrollbar-thumb:hover {
178
+ background: rgba(255, 255, 255, 0.3);
179
+ }
180
+
181
+ /* Syntax highlighting */
182
+ .comment {
183
+ color: #94a3b8;
184
+ font-style: italic;
185
+ }
186
+
187
+ .string {
188
+ color: #86efac;
189
+ }
190
+
191
+ .keyword {
192
+ color: #c084fc;
193
+ }
194
+
195
+ .function {
196
+ color: #67e8f9;
197
+ }
198
+
199
+ .number {
200
+ color: #fbbf24;
201
+ }
202
+ `;
203
+
204
+ const CopyButton = styled(Button)`
205
+ position: absolute;
206
+ top: 12px;
207
+ right: 12px;
208
+ background: rgba(255, 255, 255, 0.1);
209
+ backdrop-filter: blur(10px);
210
+ border: 1px solid rgba(255, 255, 255, 0.2);
211
+ color: white;
212
+ padding: 6px 12px;
213
+ font-size: 12px;
214
+ transition: all 0.2s;
215
+
216
+ &:hover {
217
+ background: rgba(255, 255, 255, 0.15);
218
+ border-color: rgba(255, 255, 255, 0.3);
219
+ transform: translateY(-1px);
220
+ }
221
+
222
+ svg {
223
+ width: 14px;
224
+ height: 14px;
225
+ }
226
+ `;
227
+
228
+ const InfoBox = styled(Box)`
229
+ background: linear-gradient(135deg, ${theme.colors.primary[50]}, ${theme.colors.primary[100]});
230
+ border-left: 4px solid ${theme.colors.primary[500]};
231
+ border-radius: 8px;
232
+ padding: 16px;
233
+ margin-top: 24px;
234
+ `;
235
+
236
+ const WarningBox = styled(Box)`
237
+ background: linear-gradient(135deg, ${theme.colors.warning[50]}, ${theme.colors.warning[100]});
238
+ border-left: 4px solid ${theme.colors.warning[500]};
239
+ border-radius: 8px;
240
+ padding: 12px 16px;
241
+ margin-top: 12px;
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 8px;
245
+ `;
246
+
247
+ const LimitWarning = styled(Box)`
248
+ background: linear-gradient(135deg, ${theme.colors.warning[50]}, rgba(251, 191, 36, 0.1));
249
+ border: 1px solid ${theme.colors.warning[200]};
250
+ border-radius: 12px;
251
+ padding: 16px;
252
+ margin-bottom: 24px;
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: space-between;
256
+ `;
257
+
258
+ const UpgradeButton = styled(Button)`
259
+ background: linear-gradient(135deg, ${theme.colors.warning[500]}, ${theme.colors.warning[600]});
260
+ color: white;
261
+ font-weight: 600;
262
+ padding: 8px 16px;
263
+ font-size: 13px;
264
+ display: inline-flex;
265
+ align-items: center;
266
+ gap: 6px;
267
+
268
+ &:hover {
269
+ background: linear-gradient(135deg, ${theme.colors.warning[600]}, ${theme.colors.warning[700]});
270
+ transform: translateY(-1px);
271
+ }
272
+ `;
273
+
274
+ // ================ RESPONSIVE BREAKPOINTS ================
275
+ const breakpoints = {
276
+ mobile: '768px',
277
+ tablet: '1024px',
278
+ };
279
+
280
+ // ================ STYLED COMPONENTS ================
281
+ const Container = styled(Box)`
282
+ ${css`animation: ${fadeIn} ${theme.transitions.slow};`}
283
+ min-height: 100vh;
284
+ max-width: 1440px;
285
+ margin: 0 auto;
286
+ padding: ${theme.spacing.xl} ${theme.spacing.lg} 0;
287
+
288
+ @media screen and (max-width: ${breakpoints.mobile}) {
289
+ padding: 16px 12px 0;
290
+ }
291
+ `;
292
+
293
+ const Header = styled(Box)`
294
+ background: linear-gradient(135deg,
295
+ ${theme.colors.secondary[600]} 0%,
296
+ ${theme.colors.primary[600]} 100%
297
+ );
298
+ border-radius: ${theme.borderRadius.xl};
299
+ padding: ${theme.spacing.xl} ${theme.spacing['2xl']};
300
+ margin-bottom: ${theme.spacing.xl};
301
+ position: relative;
302
+ overflow: hidden;
303
+ box-shadow: ${theme.shadows.xl};
304
+
305
+ @media screen and (max-width: ${breakpoints.mobile}) {
306
+ padding: 24px 20px;
307
+ border-radius: 12px;
308
+ }
309
+
310
+ &::before {
311
+ content: '';
312
+ position: absolute;
313
+ top: 0;
314
+ left: -100%;
315
+ width: 200%;
316
+ height: 100%;
317
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
318
+ ${css`animation: ${shimmer} 3s infinite;`}
319
+ }
320
+
321
+ &::after {
322
+ content: '';
323
+ position: absolute;
324
+ top: 0;
325
+ right: 0;
326
+ width: 100%;
327
+ height: 100%;
328
+ background-image: radial-gradient(circle at 20% 80%, transparent 50%, rgba(255, 255, 255, 0.1) 50%);
329
+ background-size: 15px 15px;
330
+ opacity: 0.3;
331
+ }
332
+ `;
333
+
334
+ const HeaderContent = styled(Flex)`
335
+ position: relative;
336
+ z-index: 1;
337
+ `;
338
+
339
+ const Title = styled(Typography)`
340
+ color: ${theme.colors.neutral[0]};
341
+ font-size: 2rem;
342
+ font-weight: 700;
343
+ letter-spacing: -0.025em;
344
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
345
+ display: flex;
346
+ align-items: center;
347
+ gap: ${theme.spacing.sm};
348
+
349
+ svg {
350
+ width: 28px;
351
+ height: 28px;
352
+ ${css`animation: ${float} 3s ease-in-out infinite;`}
353
+ }
354
+
355
+ @media screen and (max-width: ${breakpoints.mobile}) {
356
+ font-size: 1.5rem;
357
+
358
+ svg {
359
+ width: 22px;
360
+ height: 22px;
361
+ }
362
+ }
363
+ `;
364
+
365
+ const Subtitle = styled(Typography)`
366
+ color: rgba(255, 255, 255, 0.95);
367
+ font-size: 0.95rem;
368
+ font-weight: 500;
369
+ margin-top: ${theme.spacing.xs};
370
+ letter-spacing: 0.01em;
371
+
372
+ @media screen and (max-width: ${breakpoints.mobile}) {
373
+ font-size: 0.85rem;
374
+ }
375
+ `;
376
+
377
+ const StatsGrid = styled.div`
378
+ margin-bottom: ${theme.spacing.xl};
379
+ display: grid;
380
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
381
+ gap: ${theme.spacing.lg};
382
+ justify-content: center;
383
+ max-width: 1200px;
384
+ margin-left: auto;
385
+ margin-right: auto;
386
+
387
+ @media screen and (max-width: ${breakpoints.mobile}) {
388
+ grid-template-columns: repeat(2, 1fr);
389
+ gap: 12px;
390
+ margin-bottom: 24px;
391
+ }
392
+ `;
393
+
394
+ const StatCard = styled(Box)`
395
+ background: ${theme.colors.neutral[0]};
396
+ border-radius: ${theme.borderRadius.lg};
397
+ padding: 28px ${theme.spacing.lg};
398
+ position: relative;
399
+ overflow: hidden;
400
+ transition: all ${theme.transitions.normal};
401
+ ${css`animation: ${fadeIn} ${theme.transitions.slow} backwards;`}
402
+ animation-delay: ${props => props.$delay || '0s'};
403
+ box-shadow: ${theme.shadows.sm};
404
+ border: 1px solid ${theme.colors.neutral[200]};
405
+ min-width: 200px;
406
+ flex: 1;
407
+ text-align: center;
408
+ display: flex;
409
+ flex-direction: column;
410
+ align-items: center;
411
+ justify-content: center;
412
+
413
+ @media screen and (max-width: ${breakpoints.mobile}) {
414
+ min-width: unset;
415
+ padding: 20px 12px;
416
+
417
+ &:hover {
418
+ transform: none;
419
+ }
420
+ }
421
+
422
+ &:hover {
423
+ transform: translateY(-6px);
424
+ box-shadow: ${theme.shadows.xl};
425
+ border-color: ${props => props.$color || theme.colors.primary[500]};
426
+
427
+ .stat-icon {
428
+ transform: scale(1.15) rotate(5deg);
429
+ }
430
+
431
+ .stat-value {
432
+ transform: scale(1.08);
433
+ color: ${props => props.$color || theme.colors.primary[600]};
434
+ }
435
+ }
436
+ `;
437
+
438
+ const StatIcon = styled(Box)`
439
+ width: 68px;
440
+ height: 68px;
441
+ border-radius: ${theme.borderRadius.lg};
442
+ display: flex;
443
+ align-items: center;
444
+ justify-content: center;
445
+ background: ${props => props.$bg || theme.colors.primary[100]};
446
+ transition: all ${theme.transitions.normal};
447
+ margin: 0 auto 20px;
448
+ box-shadow: ${theme.shadows.sm};
449
+
450
+ svg {
451
+ width: 34px;
452
+ height: 34px;
453
+ color: ${props => props.$color || theme.colors.primary[600]};
454
+ }
455
+
456
+ @media screen and (max-width: ${breakpoints.mobile}) {
457
+ width: 48px;
458
+ height: 48px;
459
+ margin-bottom: 12px;
460
+
461
+ svg {
462
+ width: 24px;
463
+ height: 24px;
464
+ }
465
+ }
466
+ `;
467
+
468
+ const StatValue = styled(Typography)`
469
+ font-size: 2.75rem;
470
+ font-weight: 700;
471
+ color: ${theme.colors.neutral[800]};
472
+ line-height: 1;
473
+ margin-bottom: 10px;
474
+ transition: all ${theme.transitions.normal};
475
+ text-align: center;
476
+
477
+ @media screen and (max-width: ${breakpoints.mobile}) {
478
+ font-size: 2rem;
479
+ margin-bottom: 6px;
480
+ }
481
+ `;
482
+
483
+ const StatLabel = styled(Typography)`
484
+ font-size: 0.95rem;
485
+ color: ${theme.colors.neutral[600]};
486
+ font-weight: 500;
487
+ letter-spacing: 0.025em;
488
+ text-align: center;
489
+
490
+ @media screen and (max-width: ${breakpoints.mobile}) {
491
+ font-size: 0.8rem;
492
+ }
493
+ `;
494
+
495
+ const TemplatesContainer = styled(Box)`
496
+ margin-top: ${theme.spacing.xl};
497
+ `;
498
+
499
+ const SectionHeader = styled(Box)`
500
+ margin-bottom: ${theme.spacing.md};
501
+ `;
502
+
503
+ const FilterBar = styled(Flex)`
504
+ background: ${theme.colors.neutral[0]};
505
+ padding: ${theme.spacing.md} ${theme.spacing.lg};
506
+ border-radius: ${theme.borderRadius.lg};
507
+ margin-bottom: ${theme.spacing.lg};
508
+ box-shadow: ${theme.shadows.sm};
509
+ border: 1px solid ${theme.colors.neutral[200]};
510
+ gap: ${theme.spacing.md};
511
+ align-items: center;
512
+ `;
513
+
514
+ const SearchInputWrapper = styled.div`
515
+ position: relative;
516
+ flex: 1;
517
+ display: flex;
518
+ align-items: center;
519
+ `;
520
+
521
+ const SearchIcon = styled(MagnifyingGlassIcon)`
522
+ position: absolute;
523
+ left: 12px;
524
+ width: 16px;
525
+ height: 16px;
526
+ color: ${theme.colors.neutral[600]};
527
+ pointer-events: none;
528
+ z-index: 1;
529
+ `;
530
+
531
+ const StyledSearchInput = styled(TextInput)`
532
+ width: 100%;
533
+ padding-left: 36px;
534
+ `;
535
+
536
+ const StyledTable = styled(Table)`
537
+ width: 100%;
538
+ thead {
539
+ background: ${theme.colors.neutral[50]};
540
+ border-bottom: 2px solid ${theme.colors.neutral[200]};
541
+
542
+ th {
543
+ font-weight: 600;
544
+ color: ${theme.colors.neutral[700]};
545
+ font-size: 0.875rem;
546
+ text-transform: uppercase;
547
+ letter-spacing: 0.025em;
548
+ padding: ${theme.spacing.lg} ${theme.spacing.lg};
549
+ }
550
+ }
551
+
552
+ tbody tr {
553
+ transition: all ${theme.transitions.fast};
554
+ border-bottom: 1px solid ${theme.colors.neutral[100]};
555
+
556
+ &:last-child {
557
+ border-bottom: none;
558
+ }
559
+
560
+ &:hover {
561
+ background: ${theme.colors.neutral[50]};
562
+ }
563
+
564
+ td {
565
+ padding: ${theme.spacing.lg} ${theme.spacing.lg};
566
+ color: ${theme.colors.neutral[700]};
567
+ vertical-align: middle;
568
+ }
569
+ }
570
+ `;
571
+
572
+ const EmptyState = styled(Box)`
573
+ background: ${theme.colors.neutral[0]};
574
+ border-radius: ${theme.borderRadius.xl};
575
+ border: 2px dashed ${theme.colors.neutral[200]};
576
+ padding: 80px 32px;
577
+ text-align: center;
578
+ position: relative;
579
+ overflow: hidden;
580
+ min-height: 500px;
581
+ display: flex;
582
+ align-items: center;
583
+ justify-content: center;
584
+
585
+ /* Background Gradient */
586
+ &::before {
587
+ content: '';
588
+ position: absolute;
589
+ top: 0;
590
+ left: 0;
591
+ right: 0;
592
+ bottom: 0;
593
+ background: linear-gradient(135deg, ${theme.colors.secondary[50]} 0%, ${theme.colors.primary[50]} 100%);
594
+ opacity: 0.3;
595
+ z-index: 0;
596
+ }
597
+ `;
598
+
599
+ const EmptyContent = styled.div`
600
+ position: relative;
601
+ z-index: 1;
602
+ max-width: 600px;
603
+ margin: 0 auto;
604
+ `;
605
+
606
+ const EmptyIcon = styled.div`
607
+ width: 120px;
608
+ height: 120px;
609
+ margin: 0 auto ${theme.spacing.lg};
610
+ border-radius: 50%;
611
+ background: linear-gradient(135deg, ${theme.colors.secondary[100]} 0%, ${theme.colors.primary[100]} 100%);
612
+ display: flex;
613
+ align-items: center;
614
+ justify-content: center;
615
+ box-shadow: ${theme.shadows.xl};
616
+
617
+ svg {
618
+ width: 60px;
619
+ height: 60px;
620
+ color: ${theme.colors.primary[600]};
621
+ }
622
+ `;
623
+
624
+ const EmptyFeatureList = styled.div`
625
+ margin: ${theme.spacing.xl} 0;
626
+ display: grid;
627
+ grid-template-columns: repeat(3, 1fr);
628
+ gap: ${theme.spacing.md};
629
+
630
+ @media screen and (max-width: ${breakpoints.tablet}) {
631
+ grid-template-columns: 1fr;
632
+ }
633
+ `;
634
+
635
+ const EmptyFeatureItem = styled.div`
636
+ display: flex;
637
+ flex-direction: column;
638
+ align-items: center;
639
+ text-align: center;
640
+ gap: ${theme.spacing.sm};
641
+ padding: ${theme.spacing.lg};
642
+ background: ${theme.colors.neutral[0]};
643
+ border-radius: ${theme.borderRadius.md};
644
+ box-shadow: ${theme.shadows.sm};
645
+ transition: ${theme.transitions.fast};
646
+
647
+ &:hover {
648
+ transform: translateY(-2px);
649
+ box-shadow: ${theme.shadows.md};
650
+ }
651
+
652
+ svg {
653
+ width: 28px;
654
+ height: 28px;
655
+ color: ${theme.colors.success[500]};
656
+ flex-shrink: 0;
657
+ margin-bottom: ${theme.spacing.xs};
658
+ }
659
+ `;
660
+
661
+ const EmptyButtonGroup = styled.div`
662
+ display: flex;
663
+ gap: ${theme.spacing.md};
664
+ justify-content: center;
665
+ margin-top: ${theme.spacing.xl};
666
+ flex-wrap: wrap;
667
+ `;
668
+
669
+ const HiddenFileInput = styled.input`
670
+ display: none;
671
+ `;
672
+
673
+ const TemplateList = () => {
674
+ const { get, del, post } = useFetchClient();
675
+ const { toggleNotification } = useNotification();
676
+ const navigate = useNavigate();
677
+ const { hasFeature } = useLicense();
678
+ useAuthRefresh(); // Initialize token auto-refresh
679
+
680
+ const [templates, setTemplates] = useState([]);
681
+ const [stats, setStats] = useState(null);
682
+ const [loading, setLoading] = useState(true);
683
+ const [searchTerm, setSearchTerm] = useState('');
684
+ const [activeTab, setActiveTab] = useState('customTemplates');
685
+ const [showCodeExample, setShowCodeExample] = useState(false);
686
+ const [selectedTemplate, setSelectedTemplate] = useState(null);
687
+ const [copiedCode, setCopiedCode] = useState(null); // Track which code snippet was copied
688
+ const [limits, setLimits] = useState(null);
689
+ const [showTestSendModal, setShowTestSendModal] = useState(false);
690
+ const [testEmail, setTestEmail] = useState('');
691
+ const [testAccount, setTestAccount] = useState('');
692
+ const [accounts, setAccounts] = useState([]);
693
+ const fileInputRef = useRef(null);
694
+
695
+ // Import/Export always available (no license required)
696
+ const canExport = true;
697
+ const canImport = true;
698
+
699
+ // Core email types (Strapi defaults)
700
+ const coreEmailTypes = [
701
+ {
702
+ type: 'reset-password',
703
+ name: 'Reset Password',
704
+ description: 'Email sent when user requests password reset',
705
+ },
706
+ {
707
+ type: 'email-confirmation',
708
+ name: 'Email Address Confirmation',
709
+ description: 'Email sent to confirm user email address',
710
+ },
711
+ ];
712
+
713
+ useEffect(() => {
714
+ fetchData();
715
+ fetchLimits();
716
+ fetchAccounts();
717
+ }, []);
718
+
719
+ const fetchData = async () => {
720
+ setLoading(true);
721
+ try {
722
+ // Parallel fetching for speed
723
+ const [templatesResponse, statsResponse] = await Promise.all([
724
+ get('/magic-mail/designer/templates').catch(() => ({ data: { data: [] } })),
725
+ get('/magic-mail/designer/stats').catch(() => ({ data: { data: null } })),
726
+ ]);
727
+
728
+ setTemplates(templatesResponse.data?.data || []);
729
+ setStats(statsResponse.data?.data || null);
730
+ } catch (error) {
731
+ toggleNotification({ type: 'danger', message: 'Failed to load templates' });
732
+ } finally {
733
+ setLoading(false);
734
+ }
735
+ };
736
+
737
+ const fetchLimits = async () => {
738
+ try {
739
+ const response = await get('/magic-mail/license/limits');
740
+ console.log('[DEBUG] License limits response:', response.data);
741
+
742
+ // Also fetch debug data
743
+ try {
744
+ const debugResponse = await get('/magic-mail/license/debug');
745
+ console.log('[DEBUG] License debug data:', debugResponse.data);
746
+ } catch (debugError) {
747
+ console.error('[DEBUG] Failed to fetch debug data:', debugError);
748
+ }
749
+
750
+ setLimits({
751
+ ...response.data?.limits,
752
+ tier: response.data?.tier || 'free'
753
+ });
754
+ } catch (error) {
755
+ console.error('Failed to fetch license limits:', error);
756
+ }
757
+ };
758
+
759
+ const fetchAccounts = async () => {
760
+ try {
761
+ const response = await get('/magic-mail/accounts');
762
+ setAccounts(response.data?.data || []);
763
+ } catch (error) {
764
+ console.error('Failed to fetch accounts:', error);
765
+ }
766
+ };
767
+
768
+ const handleTestSend = (template) => {
769
+ setSelectedTemplate(template);
770
+ setShowTestSendModal(true);
771
+ setTestEmail('');
772
+ setTestAccount('');
773
+ };
774
+
775
+ const sendTestEmail = async () => {
776
+ if (!testEmail) {
777
+ toggleNotification({
778
+ type: 'warning',
779
+ message: 'Please enter an email address',
780
+ });
781
+ return;
782
+ }
783
+
784
+ try {
785
+ const response = await post(`/magic-mail/designer/templates/${selectedTemplate.id}/test-send`, {
786
+ to: testEmail,
787
+ accountName: testAccount || null,
788
+ });
789
+
790
+ toggleNotification({
791
+ type: 'success',
792
+ message: `Test email sent to ${testEmail}!`,
793
+ });
794
+
795
+ setShowTestSendModal(false);
796
+ setTestEmail('');
797
+ setTestAccount('');
798
+ } catch (error) {
799
+ console.error('Failed to send test email:', error);
800
+ toggleNotification({
801
+ type: 'danger',
802
+ message: error?.response?.data?.error?.message || 'Failed to send test email',
803
+ });
804
+ }
805
+ };
806
+
807
+ const getTierInfo = () => {
808
+ const tier = limits?.tier || 'free';
809
+ const tierInfo = {
810
+ free: {
811
+ name: 'FREE',
812
+ color: 'neutral',
813
+ next: 'PREMIUM',
814
+ nextTemplates: 50,
815
+ features: ['10 Templates', '1 Account', 'Import/Export'],
816
+ },
817
+ premium: {
818
+ name: 'PREMIUM',
819
+ color: 'secondary',
820
+ next: 'ADVANCED',
821
+ nextTemplates: 200,
822
+ features: ['50 Templates', '5 Accounts', 'Versioning', 'Basic Analytics'],
823
+ },
824
+ advanced: {
825
+ name: 'ADVANCED',
826
+ color: 'primary',
827
+ next: 'ENTERPRISE',
828
+ nextTemplates: -1,
829
+ features: ['200 Templates', 'Unlimited Accounts', 'Advanced Analytics', 'API Integrations'],
830
+ },
831
+ enterprise: {
832
+ name: 'ENTERPRISE',
833
+ color: 'warning',
834
+ features: ['Unlimited Everything', 'Priority Support', 'Custom Features', 'SLA'],
835
+ },
836
+ };
837
+ return tierInfo[tier] || tierInfo.free;
838
+ };
839
+
840
+ const fetchTemplates = async () => {
841
+ try {
842
+ const response = await get('/magic-mail/designer/templates');
843
+ setTemplates(response.data?.data || []);
844
+ } catch (error) {
845
+ console.error('Failed to reload templates:', error);
846
+ }
847
+ };
848
+
849
+ const fetchStats = async () => {
850
+ try {
851
+ const response = await get('/magic-mail/designer/stats');
852
+ setStats(response.data?.data || null);
853
+ } catch (error) {
854
+ console.error('Failed to reload stats:', error);
855
+ }
856
+ };
857
+
858
+ const handleDelete = async (id, name) => {
859
+ if (!window.confirm(`Delete template "${name}"?`)) return;
860
+
861
+ try {
862
+ await del(`/magic-mail/designer/templates/${id}`);
863
+ toggleNotification({ type: 'success', message: 'Template deleted successfully' });
864
+ fetchTemplates();
865
+ fetchStats();
866
+ } catch (error) {
867
+ toggleNotification({ type: 'danger', message: 'Failed to delete template' });
868
+ }
869
+ };
870
+
871
+ const handleDownload = async (id, type) => {
872
+ try {
873
+ const response = await get(`/magic-mail/designer/templates/${id}/download?type=${type}`, {
874
+ responseType: 'blob',
875
+ });
876
+
877
+ // Create blob and download
878
+ const blob = new Blob([response.data], {
879
+ type: type === 'html' ? 'text/html' : 'application/json',
880
+ });
881
+ const url = window.URL.createObjectURL(blob);
882
+ const link = document.createElement('a');
883
+ link.href = url;
884
+ link.download = `template-${id}.${type}`;
885
+ document.body.appendChild(link);
886
+ link.click();
887
+ link.remove();
888
+ window.URL.revokeObjectURL(url);
889
+
890
+ toggleNotification({
891
+ type: 'success',
892
+ message: `Template downloaded as ${type.toUpperCase()}`,
893
+ });
894
+ } catch (error) {
895
+ toggleNotification({
896
+ type: 'danger',
897
+ message: 'Failed to download template',
898
+ });
899
+ }
900
+ };
901
+
902
+ const handleDuplicate = async (id, name) => {
903
+ try {
904
+ const response = await post(`/magic-mail/designer/templates/${id}/duplicate`);
905
+ const duplicated = response.data?.data;
906
+
907
+ toggleNotification({
908
+ type: 'success',
909
+ message: `Template "${name}" duplicated successfully`,
910
+ });
911
+
912
+ fetchTemplates();
913
+ fetchStats();
914
+
915
+ // Navigate to the duplicated template
916
+ if (duplicated?.id) {
917
+ navigate(`/plugins/magic-mail/designer/${duplicated.id}`);
918
+ }
919
+ } catch (error) {
920
+ toggleNotification({
921
+ type: 'danger',
922
+ message: 'Failed to duplicate template',
923
+ });
924
+ }
925
+ };
926
+
927
+ const handleCopyCode = (code, type) => {
928
+ navigator.clipboard.writeText(code);
929
+ setCopiedCode(type);
930
+ toggleNotification({
931
+ type: 'success',
932
+ message: 'Code copied to clipboard!',
933
+ });
934
+ setTimeout(() => setCopiedCode(null), 2000);
935
+ };
936
+
937
+ const handleCreateTemplate = () => {
938
+ // Check if we can create more templates
939
+ if (limits?.emailTemplates && !limits.emailTemplates.canCreate) {
940
+ const max = limits.emailTemplates.max;
941
+ let upgradeMessage = '';
942
+
943
+ if (max === 10) {
944
+ // Free tier
945
+ upgradeMessage = `You've reached the FREE tier limit of ${max} templates. Upgrade to PREMIUM for 50 templates, versioning, and more!`;
946
+ } else if (max === 50) {
947
+ // Premium tier
948
+ upgradeMessage = `You've reached the PREMIUM tier limit of ${max} templates. Upgrade to ADVANCED for 200 templates and advanced features!`;
949
+ } else if (max === 200) {
950
+ // Advanced tier
951
+ upgradeMessage = `You've reached the ADVANCED tier limit of ${max} templates. Upgrade to ENTERPRISE for unlimited templates!`;
952
+ }
953
+
954
+ toggleNotification({
955
+ type: 'warning',
956
+ title: '🚀 Time to Upgrade!',
957
+ message: upgradeMessage,
958
+ });
959
+ return;
960
+ }
961
+
962
+ // Navigate to create new template
963
+ navigate('/plugins/magic-mail/designer/new');
964
+ };
965
+
966
+ const handleExport = async () => {
967
+ try {
968
+ const response = await post('/magic-mail/designer/export', { templateIds: [] });
969
+ const dataStr = JSON.stringify(response.data?.data || [], null, 2);
970
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
971
+ const url = URL.createObjectURL(dataBlob);
972
+ const link = document.createElement('a');
973
+ link.href = url;
974
+ link.download = `magic-mail-templates-${new Date().toISOString().split('T')[0]}.json`;
975
+ link.click();
976
+ URL.revokeObjectURL(url);
977
+ toggleNotification({ type: 'success', message: 'Templates exported successfully' });
978
+ } catch (error) {
979
+ toggleNotification({
980
+ type: 'danger',
981
+ message: error.response?.data?.message || 'Export failed',
982
+ });
983
+ }
984
+ };
985
+
986
+ const handleImport = async (event) => {
987
+ const file = event.target.files[0];
988
+ if (!file) return;
989
+
990
+ try {
991
+ const text = await file.text();
992
+ const importedTemplates = JSON.parse(text);
993
+ const response = await post('/magic-mail/designer/import', {
994
+ templates: importedTemplates,
995
+ });
996
+ const results = response.data?.data || [];
997
+ const successful = results.filter((r) => r.success).length;
998
+ const failed = results.filter((r) => !r.success).length;
999
+
1000
+ toggleNotification({
1001
+ type: 'success',
1002
+ message: `Imported ${successful} templates${failed > 0 ? `. ${failed} failed.` : ''}`,
1003
+ });
1004
+
1005
+ fetchTemplates();
1006
+ fetchStats();
1007
+ } catch (error) {
1008
+ toggleNotification({ type: 'danger', message: 'Import failed' });
1009
+ }
1010
+ };
1011
+
1012
+ const getCategoryBadge = (category) => {
1013
+ const configs = {
1014
+ transactional: { bg: 'primary', label: 'TRANSACTIONAL' },
1015
+ marketing: { bg: 'success', label: 'MARKETING' },
1016
+ notification: { bg: 'secondary', label: 'NOTIFICATION' },
1017
+ custom: { bg: 'neutral', label: 'CUSTOM' },
1018
+ };
1019
+ const config = configs[category] || configs.custom;
1020
+ return <Badge backgroundColor={config.bg}>{config.label}</Badge>;
1021
+ };
1022
+
1023
+ const filteredTemplates = templates.filter(
1024
+ (t) =>
1025
+ t.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
1026
+ t.subject.toLowerCase().includes(searchTerm.toLowerCase()) ||
1027
+ t.templateReferenceId.toString().includes(searchTerm)
1028
+ );
1029
+
1030
+ // Optimistic UI - show skeleton while loading
1031
+ const showSkeleton = loading && templates.length === 0;
1032
+
1033
+ return (
1034
+ <Container>
1035
+ {/* Header */}
1036
+ <Header>
1037
+ <HeaderContent justifyContent="flex-start" alignItems="center">
1038
+ <div>
1039
+ <Flex alignItems="center" justifyContent="space-between" style={{ width: '100%' }}>
1040
+ <Title variant="alpha">
1041
+ <DocumentTextIcon />
1042
+ Email Templates
1043
+ </Title>
1044
+ </Flex>
1045
+ {stats && limits && (
1046
+ <Subtitle variant="epsilon">
1047
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}>
1048
+ <span>{stats.total} template{stats.total !== 1 ? 's' : ''} created</span>
1049
+ <span style={{ opacity: 0.8 }}>•</span>
1050
+ {!limits.emailTemplates.unlimited ? (
1051
+ <span style={{
1052
+ background: 'rgba(255, 255, 255, 0.2)',
1053
+ padding: '2px 10px',
1054
+ borderRadius: '12px',
1055
+ fontWeight: '600'
1056
+ }}>
1057
+ {limits.emailTemplates.max - limits.emailTemplates.current} of {limits.emailTemplates.max} slots remaining
1058
+ </span>
1059
+ ) : (
1060
+ <span style={{
1061
+ background: 'rgba(255, 255, 255, 0.2)',
1062
+ padding: '2px 10px',
1063
+ borderRadius: '12px',
1064
+ fontWeight: '600'
1065
+ }}>
1066
+ Unlimited templates
1067
+ </span>
1068
+ )}
1069
+ </span>
1070
+ </Subtitle>
1071
+ )}
1072
+ </div>
1073
+ </HeaderContent>
1074
+ </Header>
1075
+
1076
+ {/* Stats Cards */}
1077
+ <StatsGrid>
1078
+ <StatCard $delay="0.1s" $color={theme.colors.primary[500]}>
1079
+ <StatIcon className="stat-icon" $bg={theme.colors.primary[100]} $color={theme.colors.primary[600]}>
1080
+ <DocumentTextIcon />
1081
+ </StatIcon>
1082
+ <StatValue className="stat-value" variant="alpha">
1083
+ {showSkeleton ? '...' : (stats?.total || 0)}
1084
+ </StatValue>
1085
+ <StatLabel variant="pi">Total Templates</StatLabel>
1086
+ </StatCard>
1087
+
1088
+ <StatCard $delay="0.2s" $color={theme.colors.success[500]}>
1089
+ <StatIcon className="stat-icon" $bg={theme.colors.success[100]} $color={theme.colors.success[600]}>
1090
+ <ChartBarIcon />
1091
+ </StatIcon>
1092
+ <StatValue className="stat-value" variant="alpha">
1093
+ {showSkeleton ? '...' : (stats?.active || 0)}
1094
+ </StatValue>
1095
+ <StatLabel variant="pi">Active</StatLabel>
1096
+ </StatCard>
1097
+
1098
+ {(limits?.emailTemplates && !limits.emailTemplates.unlimited) && (
1099
+ <StatCard $delay="0.3s" $color={theme.colors.warning[500]}>
1100
+ <StatIcon className="stat-icon" $bg={theme.colors.warning[100]} $color={theme.colors.warning[600]}>
1101
+ <SparklesIcon />
1102
+ </StatIcon>
1103
+ <StatValue className="stat-value" variant="alpha">
1104
+ {showSkeleton ? '...' : (limits.emailTemplates.max - limits.emailTemplates.current)}
1105
+ </StatValue>
1106
+ <StatLabel variant="pi">Remaining</StatLabel>
1107
+ </StatCard>
1108
+ )}
1109
+ </StatsGrid>
1110
+
1111
+ {/* Divider */}
1112
+ <Box style={{ margin: '0 -32px 32px -32px' }}>
1113
+ <Divider />
1114
+ </Box>
1115
+
1116
+ {/* Tabs for Custom Templates vs Core Emails */}
1117
+ {/* Upgrade Warning */}
1118
+ {limits?.emailTemplates && !limits.emailTemplates.unlimited &&
1119
+ limits.emailTemplates.current >= limits.emailTemplates.max * 0.8 && (
1120
+ <LimitWarning>
1121
+ <Flex alignItems="center" gap={3}>
1122
+ <SparklesIcon style={{ width: 24, height: 24, color: theme.colors.warning[600] }} />
1123
+ <Box>
1124
+ <Typography variant="omega" fontWeight="bold" style={{ color: theme.colors.neutral[800] }}>
1125
+ {limits.emailTemplates.current >= limits.emailTemplates.max
1126
+ ? `You've reached your ${getTierInfo().name} limit!`
1127
+ : `You're approaching your ${getTierInfo().name} limit!`}
1128
+ </Typography>
1129
+ <Typography variant="pi" style={{ color: theme.colors.neutral[600], marginTop: '4px' }}>
1130
+ Using {limits.emailTemplates.current} of {limits.emailTemplates.max} templates.
1131
+ {getTierInfo().next && ` Upgrade to ${getTierInfo().next} for ${getTierInfo().nextTemplates === -1 ? 'unlimited' : getTierInfo().nextTemplates} templates!`}
1132
+ </Typography>
1133
+ </Box>
1134
+ </Flex>
1135
+ <UpgradeButton
1136
+ onClick={() => navigate('/admin/settings/magic-mail/upgrade')}
1137
+ >
1138
+ <BoltIcon style={{ width: 16, height: 16, marginRight: '6px' }} />
1139
+ Upgrade Now
1140
+ </UpgradeButton>
1141
+ </LimitWarning>
1142
+ )}
1143
+
1144
+ <Tabs.Root value={activeTab} onValueChange={setActiveTab}>
1145
+ <Tabs.List>
1146
+ <Tabs.Trigger value="customTemplates">Custom Templates</Tabs.Trigger>
1147
+ <Tabs.Trigger value="coreEmails">Core Emails</Tabs.Trigger>
1148
+ </Tabs.List>
1149
+
1150
+ {/* Custom Templates Tab */}
1151
+ <Tabs.Content value="customTemplates">
1152
+ {templates.length === 0 ? (
1153
+ <EmptyState>
1154
+
1155
+ <EmptyContent>
1156
+ <EmptyIcon>
1157
+ <SparklesIcon />
1158
+ </EmptyIcon>
1159
+
1160
+ <Typography
1161
+ variant="alpha"
1162
+ style={{
1163
+ fontSize: '1.75rem',
1164
+ fontWeight: '700',
1165
+ color: theme.colors.neutral[800],
1166
+ textAlign: 'center',
1167
+ display: 'block',
1168
+ }}
1169
+ >
1170
+ No Email Templates Yet
1171
+ </Typography>
1172
+
1173
+ <Typography
1174
+ variant="omega"
1175
+ textColor="neutral600"
1176
+ style={{
1177
+ marginTop: '24px',
1178
+ lineHeight: '1.8',
1179
+ textAlign: 'center',
1180
+ maxWidth: '500px',
1181
+ margin: '24px auto 0',
1182
+ display: 'block',
1183
+ }}
1184
+ >
1185
+ Start creating beautiful, professional email templates with our visual designer
1186
+ </Typography>
1187
+
1188
+ {/* Feature List */}
1189
+ <EmptyFeatureList>
1190
+ <EmptyFeatureItem>
1191
+ <CheckCircleIcon />
1192
+ <Typography variant="omega" fontWeight="semiBold">
1193
+ Drag & Drop Editor
1194
+ </Typography>
1195
+ <Typography variant="pi" textColor="neutral600" style={{ marginTop: '4px' }}>
1196
+ Build emails visually with Unlayer's powerful editor
1197
+ </Typography>
1198
+ </EmptyFeatureItem>
1199
+
1200
+ <EmptyFeatureItem>
1201
+ <CheckCircleIcon />
1202
+ <Typography variant="omega" fontWeight="semiBold">
1203
+ Dynamic Content
1204
+ </Typography>
1205
+ <Typography variant="pi" textColor="neutral600" style={{ marginTop: '4px' }}>
1206
+ Use Mustache variables for personalized emails
1207
+ </Typography>
1208
+ </EmptyFeatureItem>
1209
+
1210
+ <EmptyFeatureItem>
1211
+ <CheckCircleIcon />
1212
+ <Typography variant="omega" fontWeight="semiBold">
1213
+ Version Control
1214
+ </Typography>
1215
+ <Typography variant="pi" textColor="neutral600" style={{ marginTop: '4px' }}>
1216
+ Track changes and restore previous versions
1217
+ </Typography>
1218
+ </EmptyFeatureItem>
1219
+ </EmptyFeatureList>
1220
+
1221
+ {/* Action Buttons */}
1222
+ <EmptyButtonGroup>
1223
+ <Button
1224
+ startIcon={<PlusIcon style={{ width: 20, height: 20 }} />}
1225
+ onClick={handleCreateTemplate}
1226
+ size="L"
1227
+ >
1228
+ Create Your First Template
1229
+ </Button>
1230
+
1231
+ {canImport && (
1232
+ <Button
1233
+ startIcon={<ArrowUpTrayIcon style={{ width: 20, height: 20 }} />}
1234
+ onClick={() => fileInputRef.current?.click()}
1235
+ size="L"
1236
+ >
1237
+ Import Template
1238
+ </Button>
1239
+ )}
1240
+ </EmptyButtonGroup>
1241
+ </EmptyContent>
1242
+ </EmptyState>
1243
+ ) : (
1244
+ <TemplatesContainer>
1245
+ <SectionHeader>
1246
+ <Flex justifyContent="space-between" alignItems="center" marginBottom={4}>
1247
+ <Typography variant="delta" style={{ fontSize: '1.5rem', fontWeight: 600, color: theme.colors.neutral[700] }}>
1248
+ Email Templates
1249
+ </Typography>
1250
+ <Button
1251
+ startIcon={<PlusIcon style={{ width: 20, height: 20 }} />}
1252
+ onClick={handleCreateTemplate}
1253
+ size="L"
1254
+ >
1255
+ Create Template
1256
+ </Button>
1257
+ </Flex>
1258
+ </SectionHeader>
1259
+
1260
+ {/* Filter Bar */}
1261
+ <FilterBar>
1262
+ <SearchInputWrapper>
1263
+ <SearchIcon />
1264
+ <StyledSearchInput
1265
+ value={searchTerm}
1266
+ onChange={(e) => setSearchTerm(e.target.value)}
1267
+ placeholder="Search by name, subject, or ID..."
1268
+ type="text"
1269
+ />
1270
+ </SearchInputWrapper>
1271
+
1272
+ {canImport && (
1273
+ <Button
1274
+ startIcon={<ArrowUpTrayIcon style={{ width: 20, height: 20 }} />}
1275
+ onClick={() => fileInputRef.current?.click()}
1276
+ size="L"
1277
+ >
1278
+ Import
1279
+ </Button>
1280
+ )}
1281
+
1282
+ {canExport && (
1283
+ <Button
1284
+ startIcon={<ArrowDownTrayIcon style={{ width: 20, height: 20 }} />}
1285
+ onClick={handleExport}
1286
+ disabled={templates.length === 0}
1287
+ size="L"
1288
+ >
1289
+ Export
1290
+ </Button>
1291
+ )}
1292
+ </FilterBar>
1293
+
1294
+ {/* Templates Table */}
1295
+ {filteredTemplates.length > 0 ? (
1296
+ <Box>
1297
+ <StyledTable colCount={6} rowCount={filteredTemplates.length}>
1298
+ <Thead>
1299
+ <Tr>
1300
+ <Th>
1301
+ <Typography variant="sigma">ID</Typography>
1302
+ </Th>
1303
+ <Th>
1304
+ <Typography variant="sigma">Name</Typography>
1305
+ </Th>
1306
+ <Th>
1307
+ <Typography variant="sigma">Subject</Typography>
1308
+ </Th>
1309
+ <Th>
1310
+ <Typography variant="sigma">Category</Typography>
1311
+ </Th>
1312
+ <Th>
1313
+ <Typography variant="sigma">Status</Typography>
1314
+ </Th>
1315
+ <Th>
1316
+ <Box style={{ textAlign: 'right', width: '100%' }}>
1317
+ <Typography variant="sigma">Actions</Typography>
1318
+ </Box>
1319
+ </Th>
1320
+ </Tr>
1321
+ </Thead>
1322
+ <Tbody>
1323
+ {filteredTemplates.map((template) => (
1324
+ <Tr key={template.id}>
1325
+ <Td>
1326
+ <Typography variant="omega" fontWeight="bold">
1327
+ #{template.templateReferenceId}
1328
+ </Typography>
1329
+ </Td>
1330
+ <Td>
1331
+ <Typography variant="omega" fontWeight="semiBold">
1332
+ {template.name}
1333
+ </Typography>
1334
+ </Td>
1335
+ <Td>
1336
+ <Typography variant="omega" textColor="neutral600">
1337
+ {template.subject}
1338
+ </Typography>
1339
+ </Td>
1340
+ <Td>{getCategoryBadge(template.category)}</Td>
1341
+ <Td>
1342
+ <Badge backgroundColor={template.isActive ? 'success' : 'neutral'}>
1343
+ {template.isActive ? 'ACTIVE' : 'INACTIVE'}
1344
+ </Badge>
1345
+ </Td>
1346
+ <Td>
1347
+ <Flex gap={2} justifyContent="flex-end">
1348
+ <Button
1349
+ variant="secondary"
1350
+ onClick={() =>
1351
+ navigate(`/plugins/magic-mail/designer/${template.id}`)
1352
+ }
1353
+ size="S"
1354
+ aria-label="Edit Template"
1355
+ >
1356
+ <PencilIcon style={{ width: 16, height: 16 }} />
1357
+ </Button>
1358
+ <Button
1359
+ variant="secondary"
1360
+ onClick={() => handleDownload(template.id, 'html')}
1361
+ size="S"
1362
+ aria-label="Download HTML"
1363
+ title="Download as HTML"
1364
+ >
1365
+ <DocumentArrowDownIcon style={{ width: 16, height: 16 }} />
1366
+ </Button>
1367
+ <Button
1368
+ variant="secondary"
1369
+ onClick={() => handleDownload(template.id, 'json')}
1370
+ size="S"
1371
+ aria-label="Download JSON"
1372
+ title="Download as JSON"
1373
+ >
1374
+ <CodeBracketIcon style={{ width: 16, height: 16 }} />
1375
+ </Button>
1376
+ <Button
1377
+ variant="secondary"
1378
+ onClick={() => handleDuplicate(template.id, template.name)}
1379
+ size="S"
1380
+ aria-label="Duplicate Template"
1381
+ title="Duplicate Template"
1382
+ >
1383
+ <DocumentDuplicateIcon style={{ width: 16, height: 16 }} />
1384
+ </Button>
1385
+ <Button
1386
+ variant="secondary"
1387
+ onClick={() => {
1388
+ setSelectedTemplate(template);
1389
+ setShowCodeExample(true);
1390
+ }}
1391
+ size="S"
1392
+ aria-label="Code Example"
1393
+ title="View Code Example"
1394
+ >
1395
+ <BoltIcon style={{ width: 16, height: 16 }} />
1396
+ </Button>
1397
+ <Button
1398
+ variant="success-light"
1399
+ onClick={() => handleTestSend(template)}
1400
+ size="S"
1401
+ aria-label="Send Test Email"
1402
+ title="Send Test Email"
1403
+ >
1404
+ <PaperAirplaneIcon style={{ width: 16, height: 16 }} />
1405
+ </Button>
1406
+ <Button
1407
+ variant="danger-light"
1408
+ onClick={() => handleDelete(template.id, template.name)}
1409
+ size="S"
1410
+ aria-label="Delete Template"
1411
+ >
1412
+ <TrashIcon style={{ width: 16, height: 16 }} />
1413
+ </Button>
1414
+ </Flex>
1415
+ </Td>
1416
+ </Tr>
1417
+ ))}
1418
+ </Tbody>
1419
+ </StyledTable>
1420
+ </Box>
1421
+ ) : (
1422
+ <Box
1423
+ style={{
1424
+ padding: '80px 32px',
1425
+ textAlign: 'center',
1426
+ background: theme.colors.neutral[50],
1427
+ borderRadius: theme.borderRadius.lg,
1428
+ border: `1px dashed ${theme.colors.neutral[200]}`,
1429
+ }}
1430
+ >
1431
+ <MagnifyingGlassIcon style={{ width: '64px', height: '64px', margin: '0 auto 16px', color: theme.colors.neutral[400] }} />
1432
+ <Typography variant="beta" style={{ marginBottom: '8px', color: theme.colors.neutral[700] }}>
1433
+ No templates found
1434
+ </Typography>
1435
+ <Typography variant="omega" textColor="neutral600">
1436
+ Try adjusting your search or filters
1437
+ </Typography>
1438
+ <Button
1439
+ variant="secondary"
1440
+ onClick={() => {
1441
+ setSearchTerm('');
1442
+ setActiveCategory('all');
1443
+ }}
1444
+ style={{ marginTop: '20px' }}
1445
+ >
1446
+ Clear Filters
1447
+ </Button>
1448
+ </Box>
1449
+ )}
1450
+ </TemplatesContainer>
1451
+ )}
1452
+ </Tabs.Content>
1453
+
1454
+ {/* Core Emails Tab */}
1455
+ <Tabs.Content value="coreEmails">
1456
+ <Box style={{ marginTop: '24px' }}>
1457
+ <Flex direction="column" gap={2} style={{ marginBottom: '24px' }}>
1458
+ <Typography variant="delta" style={{ fontSize: '1.5rem', fontWeight: 600, color: theme.colors.neutral[700] }}>
1459
+ Core Email Templates
1460
+ </Typography>
1461
+ <Typography variant="omega" textColor="neutral600">
1462
+ Design the default Strapi system emails (Password Reset & Email Confirmation)
1463
+ </Typography>
1464
+ </Flex>
1465
+
1466
+ <Box background="neutral0" borderRadius={theme.borderRadius.lg} shadow="md" style={{ border: `1px solid ${theme.colors.neutral[200]}`, overflow: 'hidden' }}>
1467
+ <Table colCount={2} rowCount={2}>
1468
+ <Thead>
1469
+ <Tr>
1470
+ <Th>
1471
+ <Typography variant="sigma">Email Type</Typography>
1472
+ </Th>
1473
+ <Th>
1474
+ <Box style={{ textAlign: 'right', width: '100%' }}>
1475
+ <Typography variant="sigma">Actions</Typography>
1476
+ </Box>
1477
+ </Th>
1478
+ </Tr>
1479
+ </Thead>
1480
+ <Tbody>
1481
+ {coreEmailTypes.map((coreEmail) => (
1482
+ <Tr key={coreEmail.type}>
1483
+ <Td>
1484
+ <Flex direction="column" alignItems="flex-start" gap={1}>
1485
+ <Typography variant="omega" fontWeight="semiBold" style={{ fontSize: '14px' }}>
1486
+ {coreEmail.name}
1487
+ </Typography>
1488
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
1489
+ {coreEmail.description}
1490
+ </Typography>
1491
+ </Flex>
1492
+ </Td>
1493
+ <Td>
1494
+ <Flex gap={2} justifyContent="flex-end">
1495
+ <Button
1496
+ variant="secondary"
1497
+ onClick={() =>
1498
+ navigate(`/plugins/magic-mail/designer/core/${coreEmail.type}`)
1499
+ }
1500
+ size="S"
1501
+ aria-label="Edit Core Email"
1502
+ >
1503
+ <PencilIcon style={{ width: 16, height: 16 }} />
1504
+ </Button>
1505
+ </Flex>
1506
+ </Td>
1507
+ </Tr>
1508
+ ))}
1509
+ </Tbody>
1510
+ </Table>
1511
+ </Box>
1512
+ </Box>
1513
+ </Tabs.Content>
1514
+ </Tabs.Root>
1515
+
1516
+ {/* Code Example Modal */}
1517
+ {selectedTemplate && (
1518
+ <Modal.Root open={showCodeExample} onOpenChange={setShowCodeExample}>
1519
+ <Modal.Content style={{
1520
+ maxWidth: '900px',
1521
+ width: '90vw',
1522
+ maxHeight: '85vh',
1523
+ display: 'flex',
1524
+ flexDirection: 'column'
1525
+ }}>
1526
+ <Modal.Header style={{ borderBottom: `1px solid ${theme.colors.neutral[200]}`, paddingBottom: '16px' }}>
1527
+ <Flex alignItems="center" gap={2}>
1528
+ <BoltIcon style={{ width: 24, height: 24, color: theme.colors.primary[600] }} />
1529
+ <Typography variant="beta" style={{ color: theme.colors.neutral[800] }}>
1530
+ Send Template: {selectedTemplate.name}
1531
+ </Typography>
1532
+ </Flex>
1533
+ </Modal.Header>
1534
+ <ScrollableDialogBody>
1535
+ {/* Native Strapi Email Service (RECOMMENDED) */}
1536
+ <CodeSection>
1537
+ <CodeHeader>
1538
+ <CodeLabel variant="omega">
1539
+ <CheckCircleIcon style={{ width: 20, height: 20, color: theme.colors.success[600] }} />
1540
+ Native Strapi Email Service
1541
+ </CodeLabel>
1542
+ <RecommendedBadge>Empfohlen</RecommendedBadge>
1543
+ </CodeHeader>
1544
+ <Typography variant="pi" style={{ color: theme.colors.neutral[600], marginBottom: '16px' }}>
1545
+ Nutze die standard Strapi Email-Funktion. MagicMail fängt sie automatisch ab und wendet alle Features an.
1546
+ </Typography>
1547
+ <CodeBlockWrapper>
1548
+ <CodeBlock dangerouslySetInnerHTML={{ __html:
1549
+ `<span class="comment">// Überall in deinem Strapi Backend:</span>
1550
+ <span class="keyword">await</span> strapi.plugins.email.services.email.<span class="function">send</span>({
1551
+ <span class="keyword">to</span>: <span class="string">'user@example.com'</span>,
1552
+ <span class="keyword">subject</span>: <span class="string">'Dein Betreff'</span>, <span class="comment">// Optional (wird von Template überschrieben)</span>
1553
+ <span class="keyword">templateId</span>: <span class="number">${selectedTemplate.templateReferenceId}</span>, <span class="comment">// ← Template ID</span>
1554
+ <span class="keyword">data</span>: {
1555
+ <span class="keyword">name</span>: <span class="string">'John Doe'</span>,
1556
+ <span class="keyword">code</span>: <span class="string">'123456'</span>,
1557
+ <span class="comment">// ... deine dynamischen Variablen</span>
1558
+ }
1559
+ });
1560
+
1561
+ <span class="comment">// MagicMail fängt das automatisch ab und:</span>
1562
+ <span class="comment">// 1. Rendert das Template mit deinen Daten</span>
1563
+ <span class="comment">// 2. Routet über die richtige Email-Account</span>
1564
+ <span class="comment">// 3. Tracked Opens & Clicks (wenn aktiviert)</span>`
1565
+ }} />
1566
+ <CopyButton
1567
+ size="S"
1568
+ variant="ghost"
1569
+ onClick={() => handleCopyCode(
1570
+ `await strapi.plugins.email.services.email.send({
1571
+ to: 'user@example.com',
1572
+ subject: 'Dein Betreff',
1573
+ templateId: ${selectedTemplate.templateReferenceId},
1574
+ data: {
1575
+ name: 'John Doe',
1576
+ code: '123456'
1577
+ }
1578
+ });`,
1579
+ 'native'
1580
+ )}
1581
+ >
1582
+ {copiedCode === 'native' ? (
1583
+ <><CheckIcon /> Kopiert!</>
1584
+ ) : (
1585
+ <><ClipboardDocumentIcon /> Kopieren</>
1586
+ )}
1587
+ </CopyButton>
1588
+ </CodeBlockWrapper>
1589
+ </CodeSection>
1590
+
1591
+ {/* MagicMail Plugin Service (Alternative) */}
1592
+ <CodeSection>
1593
+ <CodeHeader>
1594
+ <CodeLabel variant="omega">
1595
+ <CodeBracketIcon style={{ width: 20, height: 20, color: theme.colors.primary[600] }} />
1596
+ MagicMail Plugin Service
1597
+ </CodeLabel>
1598
+ </CodeHeader>
1599
+ <Typography variant="pi" style={{ color: theme.colors.neutral[600], marginBottom: '16px' }}>
1600
+ Direkter Zugriff auf den MagicMail Service für erweiterte Optionen.
1601
+ </Typography>
1602
+ <CodeBlockWrapper>
1603
+ <CodeBlock dangerouslySetInnerHTML={{ __html:
1604
+ `<span class="comment">// Inside Strapi backend</span>
1605
+ <span class="keyword">await</span> strapi.<span class="function">plugin</span>(<span class="string">'magic-mail'</span>)
1606
+ .<span class="function">service</span>(<span class="string">'email-router'</span>)
1607
+ .<span class="function">send</span>({
1608
+ <span class="keyword">to</span>: <span class="string">'user@example.com'</span>,
1609
+ <span class="keyword">templateId</span>: <span class="number">${selectedTemplate.templateReferenceId}</span>,
1610
+ <span class="keyword">templateData</span>: {
1611
+ <span class="keyword">name</span>: <span class="string">'John Doe'</span>,
1612
+ <span class="keyword">code</span>: <span class="string">'123456'</span>
1613
+ }
1614
+ });`
1615
+ }} />
1616
+ <CopyButton
1617
+ size="S"
1618
+ variant="ghost"
1619
+ onClick={() => handleCopyCode(
1620
+ `await strapi.plugin('magic-mail')
1621
+ .service('email-router')
1622
+ .send({
1623
+ to: 'user@example.com',
1624
+ templateId: ${selectedTemplate.templateReferenceId},
1625
+ templateData: {
1626
+ name: 'John Doe',
1627
+ code: '123456'
1628
+ }
1629
+ });`,
1630
+ 'plugin'
1631
+ )}
1632
+ >
1633
+ {copiedCode === 'plugin' ? (
1634
+ <><CheckIcon /> Kopiert!</>
1635
+ ) : (
1636
+ <><ClipboardDocumentIcon /> Kopieren</>
1637
+ )}
1638
+ </CopyButton>
1639
+ </CodeBlockWrapper>
1640
+ </CodeSection>
1641
+
1642
+ {/* REST API / External */}
1643
+ <CodeSection>
1644
+ <CodeHeader>
1645
+ <CodeLabel variant="omega">
1646
+ <DocumentArrowDownIcon style={{ width: 20, height: 20, color: theme.colors.secondary[600] }} />
1647
+ REST API
1648
+ </CodeLabel>
1649
+ </CodeHeader>
1650
+ <Typography variant="pi" style={{ color: theme.colors.neutral[600], marginBottom: '16px' }}>
1651
+ Für externe Anwendungen, Frontend-Calls oder Postman Tests.
1652
+ </Typography>
1653
+ <CodeBlockWrapper>
1654
+ <CodeBlock dangerouslySetInnerHTML={{ __html:
1655
+ `curl -X POST http://localhost:1337/api/magic-mail/send \\
1656
+ -H <span class="string">"Content-Type: application/json"</span> \\
1657
+ -H <span class="string">"Authorization: Bearer YOUR_API_TOKEN"</span> \\
1658
+ -d <span class="string">'{
1659
+ "to": "user@example.com",
1660
+ "templateId": ${selectedTemplate.templateReferenceId},
1661
+ "templateData": {
1662
+ "name": "John Doe",
1663
+ "code": "123456"
1664
+ }
1665
+ }'</span>`
1666
+ }} />
1667
+ <CopyButton
1668
+ size="S"
1669
+ variant="ghost"
1670
+ onClick={() => handleCopyCode(
1671
+ `curl -X POST http://localhost:1337/api/magic-mail/send \\
1672
+ -H "Content-Type: application/json" \\
1673
+ -H "Authorization: Bearer YOUR_API_TOKEN" \\
1674
+ -d '{
1675
+ "to": "user@example.com",
1676
+ "templateId": ${selectedTemplate.templateReferenceId},
1677
+ "templateData": {
1678
+ "name": "John Doe",
1679
+ "code": "123456"
1680
+ }
1681
+ }'`,
1682
+ 'curl'
1683
+ )}
1684
+ >
1685
+ {copiedCode === 'curl' ? (
1686
+ <><CheckIcon /> Kopiert!</>
1687
+ ) : (
1688
+ <><ClipboardDocumentIcon /> Kopieren</>
1689
+ )}
1690
+ </CopyButton>
1691
+ </CodeBlockWrapper>
1692
+ </CodeSection>
1693
+
1694
+ {/* Template Info */}
1695
+ <InfoBox>
1696
+ <Flex alignItems="center" justifyContent="space-between">
1697
+ <Typography variant="pi" style={{ color: theme.colors.primary[700] }}>
1698
+ <strong>Template ID:</strong> #{selectedTemplate.templateReferenceId}
1699
+ </Typography>
1700
+ <Typography variant="pi" style={{ color: theme.colors.primary[700] }}>
1701
+ <strong>Name:</strong> {selectedTemplate.name}
1702
+ </Typography>
1703
+ </Flex>
1704
+ </InfoBox>
1705
+
1706
+ {!selectedTemplate.isActive && (
1707
+ <WarningBox>
1708
+ <SparklesIcon style={{ width: 20, height: 20, color: theme.colors.warning[600] }} />
1709
+ <Typography variant="pi" style={{ color: theme.colors.warning[700], fontWeight: 500 }}>
1710
+ Dieses Template ist derzeit <strong>INAKTIV</strong> und wird nicht versendet.
1711
+ </Typography>
1712
+ </WarningBox>
1713
+ )}
1714
+ </ScrollableDialogBody>
1715
+ <Modal.Footer>
1716
+ <Button onClick={() => setShowCodeExample(false)} variant="secondary">
1717
+ Schließen
1718
+ </Button>
1719
+ </Modal.Footer>
1720
+ </Modal.Content>
1721
+ </Modal.Root>
1722
+ )}
1723
+
1724
+ {/* Test Send Modal */}
1725
+ <Modal.Root open={showTestSendModal} onOpenChange={setShowTestSendModal}>
1726
+ <Modal.Content>
1727
+ <Modal.Header>
1728
+ <Modal.Title>Send Test Email</Modal.Title>
1729
+ </Modal.Header>
1730
+ <Modal.Body>
1731
+ <Flex direction="column" gap={4}>
1732
+ <Box>
1733
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
1734
+ Template
1735
+ </Typography>
1736
+ <Typography variant="omega" textColor="neutral600">
1737
+ {selectedTemplate?.name}
1738
+ </Typography>
1739
+ </Box>
1740
+
1741
+ <Box>
1742
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
1743
+ Recipient Email *
1744
+ </Typography>
1745
+ <TextInput
1746
+ placeholder="test@example.com"
1747
+ value={testEmail}
1748
+ onChange={(e) => setTestEmail(e.target.value)}
1749
+ type="email"
1750
+ />
1751
+ </Box>
1752
+
1753
+ <Box>
1754
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
1755
+ Send from Account (optional)
1756
+ </Typography>
1757
+ <select
1758
+ style={{
1759
+ width: '100%',
1760
+ padding: '8px 12px',
1761
+ borderRadius: '4px',
1762
+ border: '1px solid #dcdce4',
1763
+ fontSize: '14px',
1764
+ backgroundColor: 'white',
1765
+ cursor: 'pointer',
1766
+ }}
1767
+ value={testAccount}
1768
+ onChange={(e) => setTestAccount(e.target.value)}
1769
+ >
1770
+ <option value="">Auto-select best account</option>
1771
+ {accounts
1772
+ .filter(acc => acc.isActive)
1773
+ .map(account => (
1774
+ <option key={account.name} value={account.name}>
1775
+ {account.name} ({account.provider})
1776
+ </option>
1777
+ ))}
1778
+ </select>
1779
+ <Typography variant="pi" textColor="neutral600" style={{ marginTop: '8px', display: 'block' }}>
1780
+ Leave empty to use smart routing
1781
+ </Typography>
1782
+ </Box>
1783
+ </Flex>
1784
+ </Modal.Body>
1785
+ <Modal.Footer>
1786
+ <Button onClick={() => setShowTestSendModal(false)} variant="tertiary">
1787
+ Cancel
1788
+ </Button>
1789
+ <Button onClick={sendTestEmail} variant="default">
1790
+ <PaperAirplaneIcon style={{ width: 16, height: 16, marginRight: '6px' }} />
1791
+ Send Test Email
1792
+ </Button>
1793
+ </Modal.Footer>
1794
+ </Modal.Content>
1795
+ </Modal.Root>
1796
+
1797
+ <HiddenFileInput
1798
+ ref={fileInputRef}
1799
+ type="file"
1800
+ accept=".json"
1801
+ onChange={handleImport}
1802
+ />
1803
+ </Container>
1804
+ );
1805
+ };
1806
+
1807
+ export default TemplateList;