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,1233 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useFetchClient, useNotification } from '@strapi/strapi/admin';
3
+ import { useAuthRefresh } from '../hooks/useAuthRefresh';
4
+ import styled, { keyframes, css } from 'styled-components';
5
+ import {
6
+ Box,
7
+ Button,
8
+ Flex,
9
+ Typography,
10
+ Loader,
11
+ Badge,
12
+ SingleSelect,
13
+ SingleSelectOption,
14
+ Modal,
15
+ Field,
16
+ TextInput,
17
+ } from '@strapi/design-system';
18
+ import { Table, Thead, Tbody, Tr, Th, Td } from '@strapi/design-system';
19
+ import {
20
+ CheckIcon,
21
+ EnvelopeIcon,
22
+ ServerIcon,
23
+ SparklesIcon,
24
+ TrashIcon,
25
+ PlayIcon,
26
+ PlusIcon,
27
+ MagnifyingGlassIcon,
28
+ PencilIcon,
29
+ } from '@heroicons/react/24/outline';
30
+ import AddAccountModal from '../components/AddAccountModal';
31
+
32
+ // ================ THEME (Exact copy from magic-sessionmanager) ================
33
+ const theme = {
34
+ colors: {
35
+ primary: {
36
+ 50: '#F0F9FF',
37
+ 100: '#E0F2FE',
38
+ 500: '#0EA5E9',
39
+ 600: '#0284C7',
40
+ 700: '#0369A1',
41
+ },
42
+ secondary: {
43
+ 500: '#A855F7',
44
+ 600: '#9333EA',
45
+ },
46
+ success: {
47
+ 100: '#DCFCE7',
48
+ 500: '#22C55E',
49
+ 600: '#16A34A',
50
+ 700: '#15803D',
51
+ },
52
+ warning: {
53
+ 100: '#FEF3C7',
54
+ 500: '#F59E0B',
55
+ 600: '#D97706',
56
+ },
57
+ danger: {
58
+ 100: '#FEE2E2',
59
+ 500: '#EF4444',
60
+ 600: '#DC2626',
61
+ },
62
+ neutral: {
63
+ 0: '#FFFFFF',
64
+ 50: '#F9FAFB',
65
+ 100: '#F3F4F6',
66
+ 200: '#E5E7EB',
67
+ 600: '#4B5563',
68
+ 700: '#374151',
69
+ 800: '#1F2937',
70
+ }
71
+ },
72
+ shadows: {
73
+ sm: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
74
+ md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
75
+ lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
76
+ xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
77
+ },
78
+ transitions: {
79
+ fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
80
+ normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
81
+ slow: '500ms cubic-bezier(0.4, 0, 0.2, 1)',
82
+ },
83
+ spacing: {
84
+ xs: '4px',
85
+ sm: '8px',
86
+ md: '16px',
87
+ lg: '24px',
88
+ xl: '32px',
89
+ '2xl': '48px',
90
+ },
91
+ borderRadius: {
92
+ md: '8px',
93
+ lg: '12px',
94
+ xl: '16px',
95
+ }
96
+ };
97
+
98
+ // ================ ANIMATIONS ================
99
+ const fadeIn = keyframes`
100
+ from { opacity: 0; transform: translateY(10px); }
101
+ to { opacity: 1; transform: translateY(0); }
102
+ `;
103
+
104
+ const shimmer = keyframes`
105
+ 0% { background-position: -200% 0; }
106
+ 100% { background-position: 200% 0; }
107
+ `;
108
+
109
+ const float = keyframes`
110
+ 0%, 100% { transform: translateY(0px); }
111
+ 50% { transform: translateY(-5px); }
112
+ `;
113
+
114
+ const pulse = keyframes`
115
+ 0%, 100% { opacity: 1; }
116
+ 50% { opacity: 0.5; }
117
+ `;
118
+
119
+ const FloatingEmoji = styled.div`
120
+ position: absolute;
121
+ bottom: 40px;
122
+ right: 40px;
123
+ font-size: 72px;
124
+ opacity: 0.08;
125
+ ${css`animation: ${float} 4s ease-in-out infinite;`}
126
+ `;
127
+
128
+ // ================ RESPONSIVE BREAKPOINTS ================
129
+ const breakpoints = {
130
+ mobile: '768px',
131
+ tablet: '1024px',
132
+ };
133
+
134
+ // ================ STYLED COMPONENTS ================
135
+ const Container = styled(Box)`
136
+ ${css`animation: ${fadeIn} ${theme.transitions.slow};`}
137
+ min-height: 100vh;
138
+ max-width: 1440px;
139
+ margin: 0 auto;
140
+ padding: ${theme.spacing.xl} ${theme.spacing.lg} 0;
141
+
142
+ @media screen and (max-width: ${breakpoints.mobile}) {
143
+ padding: 16px 12px 0;
144
+ }
145
+ `;
146
+
147
+ const Header = styled(Box)`
148
+ background: linear-gradient(135deg,
149
+ ${theme.colors.primary[600]} 0%,
150
+ ${theme.colors.secondary[600]} 100%
151
+ );
152
+ border-radius: ${theme.borderRadius.xl};
153
+ padding: ${theme.spacing.xl} ${theme.spacing['2xl']};
154
+ margin-bottom: ${theme.spacing.xl};
155
+ position: relative;
156
+ overflow: hidden;
157
+ box-shadow: ${theme.shadows.xl};
158
+
159
+ @media screen and (max-width: ${breakpoints.mobile}) {
160
+ padding: 24px 20px;
161
+ border-radius: 12px;
162
+ }
163
+
164
+ &::before {
165
+ content: '';
166
+ position: absolute;
167
+ top: 0;
168
+ left: -100%;
169
+ width: 200%;
170
+ height: 100%;
171
+ background: linear-gradient(
172
+ 90deg,
173
+ transparent,
174
+ rgba(255, 255, 255, 0.15),
175
+ transparent
176
+ );
177
+ ${css`animation: ${shimmer} 3s infinite;`}
178
+ }
179
+
180
+ &::after {
181
+ content: '';
182
+ position: absolute;
183
+ top: 0;
184
+ right: 0;
185
+ width: 100%;
186
+ height: 100%;
187
+ background-image: radial-gradient(circle at 20% 80%, transparent 50%, rgba(255, 255, 255, 0.1) 50%);
188
+ background-size: 15px 15px;
189
+ opacity: 0.3;
190
+ }
191
+ `;
192
+
193
+ const HeaderContent = styled(Flex)`
194
+ position: relative;
195
+ z-index: 1;
196
+ `;
197
+
198
+ const Title = styled(Typography)`
199
+ color: ${theme.colors.neutral[0]};
200
+ font-size: 2rem;
201
+ font-weight: 700;
202
+ letter-spacing: -0.025em;
203
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
204
+ display: flex;
205
+ align-items: center;
206
+ gap: ${theme.spacing.sm};
207
+
208
+ svg {
209
+ width: 28px;
210
+ height: 28px;
211
+ ${css`animation: ${float} 3s ease-in-out infinite;`}
212
+ }
213
+
214
+ @media screen and (max-width: ${breakpoints.mobile}) {
215
+ font-size: 1.5rem;
216
+
217
+ svg {
218
+ width: 22px;
219
+ height: 22px;
220
+ }
221
+ }
222
+ `;
223
+
224
+ const Subtitle = styled(Typography)`
225
+ color: rgba(255, 255, 255, 0.95);
226
+ font-size: 0.95rem;
227
+ font-weight: 400;
228
+ margin-top: ${theme.spacing.xs};
229
+ letter-spacing: 0.01em;
230
+
231
+ @media screen and (max-width: ${breakpoints.mobile}) {
232
+ font-size: 0.85rem;
233
+ }
234
+ `;
235
+
236
+ const StatsGrid = styled.div`
237
+ margin-bottom: ${theme.spacing.xl};
238
+ display: grid;
239
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
240
+ gap: ${theme.spacing.lg};
241
+ justify-content: center;
242
+ max-width: 1200px;
243
+ margin-left: auto;
244
+ margin-right: auto;
245
+
246
+ @media screen and (max-width: ${breakpoints.mobile}) {
247
+ grid-template-columns: repeat(2, 1fr);
248
+ gap: 12px;
249
+ margin-bottom: 24px;
250
+ }
251
+ `;
252
+
253
+ const StatCard = styled(Box)`
254
+ background: ${theme.colors.neutral[0]};
255
+ border-radius: ${theme.borderRadius.lg};
256
+ padding: 28px ${theme.spacing.lg};
257
+ position: relative;
258
+ overflow: hidden;
259
+ transition: all ${theme.transitions.normal};
260
+ ${css`animation: ${fadeIn} ${theme.transitions.slow} backwards;`}
261
+ animation-delay: ${props => props.$delay || '0s'};
262
+ box-shadow: ${theme.shadows.sm};
263
+ border: 1px solid ${theme.colors.neutral[200]};
264
+ min-width: 200px;
265
+ flex: 1;
266
+ text-align: center;
267
+ display: flex;
268
+ flex-direction: column;
269
+ align-items: center;
270
+ justify-content: center;
271
+
272
+ @media screen and (max-width: ${breakpoints.mobile}) {
273
+ min-width: unset;
274
+ padding: 20px 12px;
275
+
276
+ &:hover {
277
+ transform: none;
278
+ }
279
+ }
280
+
281
+ &:hover {
282
+ transform: translateY(-6px);
283
+ box-shadow: ${theme.shadows.xl};
284
+ border-color: ${props => props.$color || theme.colors.primary[500]};
285
+
286
+ .stat-icon {
287
+ transform: scale(1.15) rotate(5deg);
288
+ }
289
+
290
+ .stat-value {
291
+ transform: scale(1.08);
292
+ color: ${props => props.$color || theme.colors.primary[600]};
293
+ }
294
+ }
295
+ `;
296
+
297
+ const StatIcon = styled(Box)`
298
+ width: 68px;
299
+ height: 68px;
300
+ border-radius: ${theme.borderRadius.lg};
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ background: ${props => props.$bg || theme.colors.primary[100]};
305
+ transition: all ${theme.transitions.normal};
306
+ margin: 0 auto 20px;
307
+ box-shadow: ${theme.shadows.sm};
308
+
309
+ svg {
310
+ width: 34px;
311
+ height: 34px;
312
+ color: ${props => props.$color || theme.colors.primary[600]};
313
+ }
314
+
315
+ @media screen and (max-width: ${breakpoints.mobile}) {
316
+ width: 48px;
317
+ height: 48px;
318
+ margin-bottom: 12px;
319
+
320
+ svg {
321
+ width: 24px;
322
+ height: 24px;
323
+ }
324
+ }
325
+ `;
326
+
327
+ const StatValue = styled(Typography)`
328
+ font-size: 2.75rem;
329
+ font-weight: 700;
330
+ color: ${theme.colors.neutral[800]};
331
+ line-height: 1;
332
+ margin-bottom: 10px;
333
+ transition: all ${theme.transitions.normal};
334
+ text-align: center;
335
+
336
+ @media screen and (max-width: ${breakpoints.mobile}) {
337
+ font-size: 2rem;
338
+ margin-bottom: 6px;
339
+ }
340
+ `;
341
+
342
+ const StatLabel = styled(Typography)`
343
+ font-size: 0.95rem;
344
+ color: ${theme.colors.neutral[600]};
345
+ font-weight: 500;
346
+ letter-spacing: 0.025em;
347
+ text-align: center;
348
+
349
+ @media screen and (max-width: ${breakpoints.mobile}) {
350
+ font-size: 0.8rem;
351
+ }
352
+ `;
353
+
354
+ const AccountsContainer = styled(Box)`
355
+ margin-top: ${theme.spacing.xl};
356
+ `;
357
+
358
+ const EmptyState = styled(Box)`
359
+ background: ${theme.colors.neutral[0]};
360
+ border-radius: ${theme.borderRadius.xl};
361
+ border: 2px dashed ${theme.colors.neutral[200]};
362
+ padding: 80px 32px;
363
+ text-align: center;
364
+ position: relative;
365
+ overflow: hidden;
366
+ min-height: 400px;
367
+ display: flex;
368
+ align-items: center;
369
+ justify-content: center;
370
+
371
+ /* Background Gradient */
372
+ &::before {
373
+ content: '';
374
+ position: absolute;
375
+ top: 0;
376
+ left: 0;
377
+ right: 0;
378
+ bottom: 0;
379
+ background: linear-gradient(135deg, ${theme.colors.primary[50]} 0%, ${theme.colors.secondary[50]} 100%);
380
+ opacity: 0.3;
381
+ z-index: 0;
382
+ }
383
+ `;
384
+
385
+ const OnlineBadge = styled.div`
386
+ width: 12px;
387
+ height: 12px;
388
+ border-radius: 50%;
389
+ background: ${props => props.$active ? theme.colors.success[500] : theme.colors.neutral[400]};
390
+ display: inline-block;
391
+ margin-right: 8px;
392
+ ${css`animation: ${props => props.$active ? pulse : 'none'} 2s ease-in-out infinite;`}
393
+ `;
394
+
395
+ const StyledTable = styled(Table)`
396
+ thead {
397
+ background: ${theme.colors.neutral[50]};
398
+ border-bottom: 2px solid ${theme.colors.neutral[200]};
399
+
400
+ th {
401
+ font-weight: 600;
402
+ color: ${theme.colors.neutral[700]};
403
+ font-size: 0.875rem;
404
+ text-transform: uppercase;
405
+ letter-spacing: 0.025em;
406
+ padding: ${theme.spacing.lg} ${theme.spacing.lg};
407
+ }
408
+ }
409
+
410
+ tbody tr {
411
+ transition: all ${theme.transitions.fast};
412
+ border-bottom: 1px solid ${theme.colors.neutral[100]};
413
+
414
+ &:last-child {
415
+ border-bottom: none;
416
+ }
417
+
418
+ &:hover {
419
+ background: ${theme.colors.neutral[50]};
420
+ }
421
+
422
+ td {
423
+ padding: ${theme.spacing.lg} ${theme.spacing.lg};
424
+ color: ${theme.colors.neutral[700]};
425
+ vertical-align: middle;
426
+ }
427
+ }
428
+ `;
429
+
430
+ const FilterBar = styled(Flex)`
431
+ background: ${theme.colors.neutral[0]};
432
+ padding: ${theme.spacing.md} ${theme.spacing.lg};
433
+ border-radius: ${theme.borderRadius.lg};
434
+ margin-bottom: ${theme.spacing.lg};
435
+ box-shadow: ${theme.shadows.sm};
436
+ border: 1px solid ${theme.colors.neutral[200]};
437
+ gap: ${theme.spacing.md};
438
+ align-items: center;
439
+ `;
440
+
441
+ const SearchInputWrapper = styled.div`
442
+ position: relative;
443
+ flex: 1;
444
+ display: flex;
445
+ align-items: center;
446
+ `;
447
+
448
+ const SearchIcon = styled(MagnifyingGlassIcon)`
449
+ position: absolute;
450
+ left: 12px;
451
+ width: 16px;
452
+ height: 16px;
453
+ color: ${theme.colors.neutral[600]};
454
+ pointer-events: none;
455
+ `;
456
+
457
+ const StyledSearchInput = styled.input`
458
+ width: 100%;
459
+ padding: 10px 12px 10px 40px;
460
+ border: 1px solid ${theme.colors.neutral[200]};
461
+ border-radius: ${theme.borderRadius.md};
462
+ font-size: 0.875rem;
463
+ transition: all ${theme.transitions.fast};
464
+
465
+ &:focus {
466
+ outline: none;
467
+ border-color: ${theme.colors.primary[500]};
468
+ box-shadow: 0 0 0 2px ${theme.colors.primary[100]};
469
+ }
470
+
471
+ &::placeholder {
472
+ color: ${theme.colors.neutral[600]};
473
+ }
474
+ `;
475
+
476
+ const HomePage = () => {
477
+ useAuthRefresh(); // Initialize token auto-refresh
478
+ const { get, post, del } = useFetchClient();
479
+ const { toggleNotification } = useNotification();
480
+ const [loading, setLoading] = useState(true);
481
+ const [accounts, setAccounts] = useState([]);
482
+ const [showAddModal, setShowAddModal] = useState(false);
483
+ const [editingAccount, setEditingAccount] = useState(null);
484
+ const [testingAccount, setTestingAccount] = useState(null);
485
+ const [searchQuery, setSearchQuery] = useState('');
486
+ const [filterStatus, setFilterStatus] = useState('all');
487
+ const [filterProvider, setFilterProvider] = useState('all');
488
+
489
+ useEffect(() => {
490
+ fetchAccounts();
491
+ }, []);
492
+
493
+ const fetchAccounts = async () => {
494
+ setLoading(true);
495
+ try {
496
+ const { data } = await get('/magic-mail/accounts');
497
+ setAccounts(data.data || []);
498
+ } catch (err) {
499
+ console.error('[magic-mail] Error fetching accounts:', err);
500
+ toggleNotification({
501
+ type: 'danger',
502
+ message: 'Failed to load email accounts',
503
+ });
504
+ } finally {
505
+ setLoading(false);
506
+ }
507
+ };
508
+
509
+ const testAccount = async (accountId, accountName, testEmail, testOptions = {}) => {
510
+ toggleNotification({
511
+ type: 'info',
512
+ message: `Testing ${accountName}...`,
513
+ });
514
+
515
+ try {
516
+ const { data } = await post(`/magic-mail/accounts/${accountId}/test`, {
517
+ testEmail: testEmail,
518
+ priority: testOptions.priority || 'normal',
519
+ type: testOptions.type || 'transactional',
520
+ unsubscribeUrl: testOptions.unsubscribeUrl || null,
521
+ });
522
+
523
+ toggleNotification({
524
+ type: data.success ? 'success' : 'danger',
525
+ message: data.message,
526
+ });
527
+ } catch (err) {
528
+ toggleNotification({
529
+ type: 'danger',
530
+ message: 'Test email failed',
531
+ });
532
+ }
533
+ };
534
+
535
+ const deleteAccount = async (accountId, accountName) => {
536
+ if (!confirm(`Delete "${accountName}"?`)) return;
537
+
538
+ try {
539
+ await del(`/magic-mail/accounts/${accountId}`);
540
+ toggleNotification({
541
+ type: 'success',
542
+ message: 'Account deleted successfully',
543
+ });
544
+ fetchAccounts();
545
+ } catch (err) {
546
+ toggleNotification({
547
+ type: 'danger',
548
+ message: 'Failed to delete account',
549
+ });
550
+ }
551
+ };
552
+
553
+ if (loading) {
554
+ return (
555
+ <Flex justifyContent="center" alignItems="center" style={{ minHeight: '400px' }}>
556
+ <Loader>Loading MagicMail...</Loader>
557
+ </Flex>
558
+ );
559
+ }
560
+
561
+ const totalSentToday = accounts.reduce((sum, acc) => sum + (acc.emailsSentToday || 0), 0);
562
+ const totalSent = accounts.reduce((sum, acc) => sum + (acc.totalEmailsSent || 0), 0);
563
+ const activeAccounts = accounts.filter(a => a.isActive).length;
564
+
565
+ // Filter and search logic
566
+ const filteredAccounts = accounts.filter(account => {
567
+ const matchesSearch =
568
+ account.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
569
+ account.fromEmail.toLowerCase().includes(searchQuery.toLowerCase()) ||
570
+ (account.provider || '').toLowerCase().includes(searchQuery.toLowerCase());
571
+
572
+ const matchesStatus =
573
+ filterStatus === 'all' ||
574
+ (filterStatus === 'active' && account.isActive) ||
575
+ (filterStatus === 'inactive' && !account.isActive) ||
576
+ (filterStatus === 'primary' && account.isPrimary);
577
+
578
+ const matchesProvider =
579
+ filterProvider === 'all' ||
580
+ account.provider === filterProvider;
581
+
582
+ return matchesSearch && matchesStatus && matchesProvider;
583
+ });
584
+
585
+ const uniqueProviders = [...new Set(accounts.map(a => a.provider))].filter(Boolean);
586
+
587
+ return (
588
+ <Container>
589
+ {/* Hero Header */}
590
+ <Header>
591
+ <HeaderContent justifyContent="space-between" alignItems="center">
592
+ <Flex direction="column" alignItems="flex-start" gap={2}>
593
+ <Title>
594
+ <EnvelopeIcon />
595
+ MagicMail - Email Business Suite
596
+ </Title>
597
+ <Subtitle>
598
+ Multi-account email management with smart routing and OAuth support
599
+ </Subtitle>
600
+ </Flex>
601
+ </HeaderContent>
602
+ </Header>
603
+
604
+ {/* Quick Stats */}
605
+ <StatsGrid>
606
+ <StatCard $delay="0.1s" $color={theme.colors.primary[600]}>
607
+ <StatIcon className="stat-icon" $bg={theme.colors.primary[100]} $color={theme.colors.primary[600]}>
608
+ <EnvelopeIcon />
609
+ </StatIcon>
610
+ <StatValue className="stat-value">{totalSentToday}</StatValue>
611
+ <StatLabel>Emails Today</StatLabel>
612
+ </StatCard>
613
+
614
+ <StatCard $delay="0.2s" $color={theme.colors.success[600]}>
615
+ <StatIcon className="stat-icon" $bg={theme.colors.success[100]} $color={theme.colors.success[600]}>
616
+ <ServerIcon />
617
+ </StatIcon>
618
+ <StatValue className="stat-value">{totalSent}</StatValue>
619
+ <StatLabel>Total Sent</StatLabel>
620
+ </StatCard>
621
+
622
+ <StatCard $delay="0.3s" $color={theme.colors.warning[600]}>
623
+ <StatIcon className="stat-icon" $bg={theme.colors.warning[100]} $color={theme.colors.warning[600]}>
624
+ <SparklesIcon />
625
+ </StatIcon>
626
+ <StatValue className="stat-value">{activeAccounts} / {accounts.length}</StatValue>
627
+ <StatLabel>Active Accounts</StatLabel>
628
+ </StatCard>
629
+ </StatsGrid>
630
+
631
+ {/* Account List or Empty State */}
632
+ {accounts.length === 0 ? (
633
+ <EmptyState>
634
+ {/* Floating Emoji */}
635
+ <FloatingEmoji>
636
+ ✉️
637
+ </FloatingEmoji>
638
+
639
+ <Flex direction="column" alignItems="center" gap={6} style={{ position: 'relative', zIndex: 1 }}>
640
+ <Box
641
+ style={{
642
+ width: '120px',
643
+ height: '120px',
644
+ borderRadius: '50%',
645
+ background: `linear-gradient(135deg, ${theme.colors.primary[100]} 0%, ${theme.colors.secondary[100]} 100%)`,
646
+ display: 'flex',
647
+ alignItems: 'center',
648
+ justifyContent: 'center',
649
+ boxShadow: theme.shadows.xl,
650
+ }}
651
+ >
652
+ <EnvelopeIcon style={{ width: '60px', height: '60px', color: theme.colors.primary[600] }} />
653
+ </Box>
654
+
655
+ <Typography
656
+ variant="alpha"
657
+ style={{
658
+ fontSize: '1.75rem',
659
+ fontWeight: '700',
660
+ color: theme.colors.neutral[800],
661
+ marginBottom: '8px',
662
+ }}
663
+ >
664
+ No Email Accounts Yet
665
+ </Typography>
666
+
667
+ <Typography
668
+ variant="omega"
669
+ textColor="neutral600"
670
+ style={{
671
+ fontSize: '1rem',
672
+ maxWidth: '500px',
673
+ lineHeight: '1.6',
674
+ }}
675
+ >
676
+ Add your first email account to start sending emails through MagicMail's multi-account routing system
677
+ </Typography>
678
+
679
+ <Button
680
+ startIcon={<PlusIcon style={{ width: 20, height: 20 }} />}
681
+ onClick={() => setShowAddModal(true)}
682
+ size="L"
683
+ >
684
+ Add First Account
685
+ </Button>
686
+ </Flex>
687
+ </EmptyState>
688
+ ) : (
689
+ <AccountsContainer>
690
+ <Box style={{ marginBottom: theme.spacing.md }}>
691
+ <Flex justifyContent="space-between" alignItems="center" marginBottom={4}>
692
+ <Typography variant="delta" style={{ fontSize: '1.5rem', fontWeight: 600, color: theme.colors.neutral[700] }}>
693
+ 📧 Email Accounts
694
+ </Typography>
695
+ <Button startIcon={<PlusIcon style={{ width: 16, height: 16 }} />} onClick={() => setShowAddModal(true)}>
696
+ Add Account
697
+ </Button>
698
+ </Flex>
699
+ </Box>
700
+
701
+ {/* Filter Bar */}
702
+ <FilterBar>
703
+ {/* Search Input */}
704
+ <SearchInputWrapper>
705
+ <SearchIcon />
706
+ <StyledSearchInput
707
+ value={searchQuery}
708
+ onChange={(e) => setSearchQuery(e.target.value)}
709
+ placeholder="Search by name, email, or provider..."
710
+ type="text"
711
+ />
712
+ </SearchInputWrapper>
713
+
714
+ {/* Status Filter */}
715
+ <Box style={{ minWidth: '160px' }}>
716
+ <SingleSelect
717
+ value={filterStatus}
718
+ onChange={setFilterStatus}
719
+ placeholder="Status"
720
+ size="S"
721
+ >
722
+ <SingleSelectOption value="all">All Accounts</SingleSelectOption>
723
+ <SingleSelectOption value="active">✅ Active</SingleSelectOption>
724
+ <SingleSelectOption value="inactive">❌ Inactive</SingleSelectOption>
725
+ <SingleSelectOption value="primary">⭐ Primary</SingleSelectOption>
726
+ </SingleSelect>
727
+ </Box>
728
+
729
+ {/* Provider Filter */}
730
+ <Box style={{ minWidth: '160px' }}>
731
+ <SingleSelect
732
+ value={filterProvider}
733
+ onChange={setFilterProvider}
734
+ placeholder="Provider"
735
+ size="S"
736
+ >
737
+ <SingleSelectOption value="all">All Providers</SingleSelectOption>
738
+ {uniqueProviders.map(provider => (
739
+ <SingleSelectOption key={provider} value={provider}>
740
+ {provider}
741
+ </SingleSelectOption>
742
+ ))}
743
+ </SingleSelect>
744
+ </Box>
745
+ </FilterBar>
746
+
747
+ {/* Accounts Table */}
748
+ {filteredAccounts.length > 0 ? (
749
+ <Box>
750
+ <StyledTable>
751
+ <Thead>
752
+ <Tr>
753
+ <Th>Status</Th>
754
+ <Th>Account</Th>
755
+ <Th>Provider</Th>
756
+ <Th title="Routing Priority (higher = preferred)">Priority</Th>
757
+ <Th>Usage Today</Th>
758
+ <Th>Total Sent</Th>
759
+ <Th>Last Used</Th>
760
+ <Th>Actions</Th>
761
+ </Tr>
762
+ </Thead>
763
+ <Tbody>
764
+ {filteredAccounts.map((account) => {
765
+ const usagePercent = account.dailyLimit > 0
766
+ ? Math.round((account.emailsSentToday / account.dailyLimit) * 100)
767
+ : 0;
768
+ const isNearLimit = usagePercent > 80;
769
+
770
+ return (
771
+ <Tr key={account.id}>
772
+ {/* Status */}
773
+ <Td>
774
+ <Flex alignItems="center" gap={2}>
775
+ <OnlineBadge $active={account.isActive} />
776
+ {account.isActive ? (
777
+ <Badge backgroundColor="success600" textColor="neutral0" size="S">
778
+ Active
779
+ </Badge>
780
+ ) : (
781
+ <Badge backgroundColor="neutral600" textColor="neutral0" size="S">
782
+ Inactive
783
+ </Badge>
784
+ )}
785
+ </Flex>
786
+ </Td>
787
+
788
+ {/* Account */}
789
+ <Td>
790
+ <Flex direction="column" alignItems="flex-start" gap={1}>
791
+ <Flex alignItems="center" gap={2}>
792
+ <Typography fontWeight="semiBold">
793
+ {account.name}
794
+ </Typography>
795
+ {account.isPrimary && (
796
+ <Badge backgroundColor="warning600" textColor="neutral0" size="S">
797
+ ⭐ Primary
798
+ </Badge>
799
+ )}
800
+ </Flex>
801
+ <Typography variant="pi" textColor="neutral600">
802
+ {account.fromEmail}
803
+ </Typography>
804
+ </Flex>
805
+ </Td>
806
+
807
+ {/* Provider */}
808
+ <Td>
809
+ <Badge size="S">
810
+ <ServerIcon style={{ width: 12, height: 12, marginRight: 4 }} />
811
+ {account.provider}
812
+ </Badge>
813
+ </Td>
814
+
815
+ {/* Priority */}
816
+ <Td>
817
+ <Badge size="S" variant="secondary">
818
+ {account.priority}/10
819
+ </Badge>
820
+ </Td>
821
+
822
+ {/* Usage Today */}
823
+ <Td>
824
+ <Flex direction="column" alignItems="flex-start" gap={1}>
825
+ <Typography fontWeight="semiBold">
826
+ {account.emailsSentToday || 0}
827
+ {account.dailyLimit > 0 && (
828
+ <Typography variant="pi" textColor="neutral500" as="span">
829
+ {' '}/ {account.dailyLimit}
830
+ </Typography>
831
+ )}
832
+ </Typography>
833
+ {account.dailyLimit > 0 && (
834
+ <Box style={{ width: '100%', minWidth: '80px' }}>
835
+ <Box
836
+ style={{
837
+ width: '100%',
838
+ height: '6px',
839
+ background: theme.colors.neutral[100],
840
+ borderRadius: '999px',
841
+ overflow: 'hidden',
842
+ }}
843
+ >
844
+ <Box
845
+ style={{
846
+ width: `${Math.min(usagePercent, 100)}%`,
847
+ height: '100%',
848
+ background: isNearLimit
849
+ ? theme.colors.danger[600]
850
+ : theme.colors.success[600],
851
+ borderRadius: '999px',
852
+ }}
853
+ />
854
+ </Box>
855
+ </Box>
856
+ )}
857
+ </Flex>
858
+ </Td>
859
+
860
+ {/* Total Sent */}
861
+ <Td>
862
+ <Typography fontWeight="semiBold">
863
+ {(account.totalEmailsSent || 0).toLocaleString()}
864
+ </Typography>
865
+ </Td>
866
+
867
+ {/* Last Used */}
868
+ <Td>
869
+ {account.lastUsed ? (
870
+ <Typography variant="pi" textColor="neutral600">
871
+ {new Date(account.lastUsed).toLocaleString('de-DE', {
872
+ day: '2-digit',
873
+ month: '2-digit',
874
+ year: 'numeric',
875
+ hour: '2-digit',
876
+ minute: '2-digit'
877
+ })}
878
+ </Typography>
879
+ ) : (
880
+ <Typography variant="pi" textColor="neutral500">
881
+ Never
882
+ </Typography>
883
+ )}
884
+ </Td>
885
+
886
+ {/* Actions */}
887
+ <Td>
888
+ <Flex gap={2}>
889
+ <Button
890
+ variant="secondary"
891
+ onClick={(e) => {
892
+ e.stopPropagation();
893
+ setEditingAccount(account);
894
+ }}
895
+ size="S"
896
+ aria-label="Edit Account"
897
+ >
898
+ <PencilIcon style={{ width: 16, height: 16 }} />
899
+ </Button>
900
+ <Button
901
+ variant="secondary"
902
+ onClick={(e) => {
903
+ e.stopPropagation();
904
+ setTestingAccount(account);
905
+ }}
906
+ size="S"
907
+ aria-label="Test Account"
908
+ >
909
+ <PlayIcon style={{ width: 16, height: 16 }} />
910
+ </Button>
911
+ <Button
912
+ variant="danger-light"
913
+ onClick={(e) => {
914
+ e.stopPropagation();
915
+ deleteAccount(account.id, account.name);
916
+ }}
917
+ size="S"
918
+ aria-label="Delete Account"
919
+ >
920
+ <TrashIcon style={{ width: 16, height: 16 }} />
921
+ </Button>
922
+ </Flex>
923
+ </Td>
924
+ </Tr>
925
+ );
926
+ })}
927
+ </Tbody>
928
+ </StyledTable>
929
+ </Box>
930
+ ) : (
931
+ <Box padding={8} style={{ textAlign: 'center' }}>
932
+ <Typography variant="beta" textColor="neutral600">
933
+ No accounts found matching your filters
934
+ </Typography>
935
+ </Box>
936
+ )}
937
+ </AccountsContainer>
938
+ )}
939
+
940
+ {/* Add Account Modal */}
941
+ <AddAccountModal
942
+ isOpen={showAddModal}
943
+ onClose={() => setShowAddModal(false)}
944
+ onAccountAdded={fetchAccounts}
945
+ />
946
+
947
+ {/* Edit Account Modal */}
948
+ <AddAccountModal
949
+ isOpen={!!editingAccount}
950
+ onClose={() => setEditingAccount(null)}
951
+ onAccountAdded={() => {
952
+ fetchAccounts();
953
+ setEditingAccount(null);
954
+ }}
955
+ editAccount={editingAccount}
956
+ />
957
+
958
+ {/* Test Email Modal */}
959
+ {testingAccount && (
960
+ <TestEmailModal
961
+ account={testingAccount}
962
+ onClose={() => setTestingAccount(null)}
963
+ onTest={(email, testOptions) => {
964
+ testAccount(testingAccount.id, testingAccount.name, email, testOptions);
965
+ setTestingAccount(null);
966
+ }}
967
+ />
968
+ )}
969
+ </Container>
970
+ );
971
+ };
972
+
973
+ // Test Email Modal Component
974
+ const TestEmailModal = ({ account, onClose, onTest }) => {
975
+ const { post } = useFetchClient();
976
+ const { toggleNotification } = useNotification();
977
+ const [testEmail, setTestEmail] = useState('');
978
+ const [priority, setPriority] = useState('normal');
979
+ const [emailType, setEmailType] = useState('transactional');
980
+ const [unsubscribeUrl, setUnsubscribeUrl] = useState('');
981
+ const [testingStrapiService, setTestingStrapiService] = useState(false);
982
+
983
+ const testStrapiService = async () => {
984
+ setTestingStrapiService(true);
985
+ try {
986
+ const { data } = await post('/magic-mail/test-strapi-service', {
987
+ testEmail,
988
+ accountName: account.name, // Force this specific account!
989
+ });
990
+
991
+ if (data.success) {
992
+ toggleNotification({
993
+ type: 'success',
994
+ message: `✅ Strapi Email Service Test: Email sent via ${account.name}!`,
995
+ });
996
+ onClose();
997
+ } else {
998
+ toggleNotification({
999
+ type: 'warning',
1000
+ message: data.message || 'Test completed with warnings',
1001
+ });
1002
+ }
1003
+ } catch (err) {
1004
+ toggleNotification({
1005
+ type: 'danger',
1006
+ message: 'Strapi Email Service test failed',
1007
+ });
1008
+ } finally {
1009
+ setTestingStrapiService(false);
1010
+ }
1011
+ };
1012
+
1013
+ // Prevent event bubbling to avoid triggering dashboard search
1014
+ const handleInputChange = (e) => {
1015
+ e.stopPropagation();
1016
+ setTestEmail(e.target.value);
1017
+ };
1018
+
1019
+ const handleKeyDown = (e) => {
1020
+ e.stopPropagation();
1021
+ };
1022
+
1023
+ return (
1024
+ <Modal.Root open={true} onOpenChange={onClose}>
1025
+ <Modal.Content size="L">
1026
+ <Modal.Header>
1027
+ <Typography variant="beta">
1028
+ <PlayIcon style={{ marginRight: 8, width: 20, height: 20 }} />
1029
+ Test Email Account
1030
+ </Typography>
1031
+ </Modal.Header>
1032
+
1033
+ <Modal.Body>
1034
+ <Flex direction="column" gap={6} style={{ width: '100%' }}>
1035
+ {/* Account Info */}
1036
+ <Box
1037
+ padding={4}
1038
+ background="neutral100"
1039
+ hasRadius
1040
+ style={{
1041
+ borderRadius: '8px',
1042
+ width: '100%',
1043
+ }}
1044
+ >
1045
+ <Flex direction="column" gap={2} style={{ width: '100%' }}>
1046
+ <Typography fontWeight="semiBold" style={{ fontSize: '14px', color: '#4B5563' }}>
1047
+ Testing Account
1048
+ </Typography>
1049
+ <Typography variant="beta" style={{ fontSize: '18px', fontWeight: 600 }}>
1050
+ {account.name}
1051
+ </Typography>
1052
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '14px' }}>
1053
+ {account.fromEmail}
1054
+ </Typography>
1055
+ </Flex>
1056
+ </Box>
1057
+
1058
+ {/* Email Input */}
1059
+ <Field.Root required style={{ width: '100%' }}>
1060
+ <Field.Label style={{ fontSize: '14px' }}>Recipient Email Address</Field.Label>
1061
+ <TextInput
1062
+ placeholder="recipient@example.com"
1063
+ value={testEmail}
1064
+ onChange={handleInputChange}
1065
+ onKeyDown={handleKeyDown}
1066
+ onClick={(e) => e.stopPropagation()}
1067
+ onFocus={(e) => e.stopPropagation()}
1068
+ onBlur={(e) => e.stopPropagation()}
1069
+ type="email"
1070
+ autoFocus
1071
+ autoComplete="off"
1072
+ name="test-email-recipient"
1073
+ style={{ width: '100%', fontSize: '14px' }}
1074
+ />
1075
+ <Field.Hint style={{ fontSize: '13px' }}>
1076
+ Enter the email address where you want to receive the test email
1077
+ </Field.Hint>
1078
+ </Field.Root>
1079
+
1080
+ {/* Test Configuration */}
1081
+ <Box style={{ width: '100%' }}>
1082
+ <Typography fontWeight="semiBold" marginBottom={3} style={{ fontSize: '14px', color: '#4B5563' }}>
1083
+ Email Configuration
1084
+ </Typography>
1085
+
1086
+ <Flex direction="column" gap={3} style={{ width: '100%' }}>
1087
+ {/* Priority */}
1088
+ <Field.Root style={{ width: '100%' }}>
1089
+ <Field.Label style={{ fontSize: '14px' }}>Priority</Field.Label>
1090
+ <SingleSelect
1091
+ value={priority}
1092
+ onChange={setPriority}
1093
+ style={{ width: '100%' }}
1094
+ >
1095
+ <SingleSelectOption value="normal">Normal Priority</SingleSelectOption>
1096
+ <SingleSelectOption value="high">High Priority</SingleSelectOption>
1097
+ </SingleSelect>
1098
+ <Field.Hint style={{ fontSize: '13px' }}>
1099
+ High priority adds X-Priority and Importance headers
1100
+ </Field.Hint>
1101
+ </Field.Root>
1102
+
1103
+ {/* Email Type */}
1104
+ <Field.Root style={{ width: '100%' }}>
1105
+ <Field.Label style={{ fontSize: '14px' }}>Email Type</Field.Label>
1106
+ <SingleSelect
1107
+ value={emailType}
1108
+ onChange={setEmailType}
1109
+ style={{ width: '100%' }}
1110
+ >
1111
+ <SingleSelectOption value="transactional">Transactional</SingleSelectOption>
1112
+ <SingleSelectOption value="marketing">Marketing</SingleSelectOption>
1113
+ <SingleSelectOption value="notification">Notification</SingleSelectOption>
1114
+ </SingleSelect>
1115
+ <Field.Hint style={{ fontSize: '13px' }}>
1116
+ Marketing emails automatically include List-Unsubscribe headers
1117
+ </Field.Hint>
1118
+ </Field.Root>
1119
+
1120
+ {/* Unsubscribe URL (nur für Marketing) */}
1121
+ {emailType === 'marketing' && (
1122
+ <Field.Root style={{ width: '100%' }}>
1123
+ <Field.Label style={{ fontSize: '14px' }}>Unsubscribe URL (Required for Marketing)</Field.Label>
1124
+ <TextInput
1125
+ placeholder="https://yoursite.com/unsubscribe"
1126
+ value={unsubscribeUrl}
1127
+ onChange={(e) => {
1128
+ e.stopPropagation();
1129
+ setUnsubscribeUrl(e.target.value);
1130
+ }}
1131
+ style={{ width: '100%', fontSize: '14px' }}
1132
+ />
1133
+ <Field.Hint style={{ fontSize: '13px' }}>
1134
+ Required for GDPR/CAN-SPAM compliance. Adds List-Unsubscribe header.
1135
+ </Field.Hint>
1136
+ </Field.Root>
1137
+ )}
1138
+ </Flex>
1139
+ </Box>
1140
+
1141
+ {/* Test Options */}
1142
+ <Box style={{ width: '100%' }}>
1143
+ <Typography fontWeight="semiBold" marginBottom={3} style={{ fontSize: '14px', color: '#4B5563' }}>
1144
+ Test Options
1145
+ </Typography>
1146
+
1147
+ <Flex direction="column" gap={3} style={{ width: '100%' }}>
1148
+ {/* Direct Test */}
1149
+ <Box
1150
+ padding={4}
1151
+ background="neutral0"
1152
+ hasRadius
1153
+ style={{
1154
+ border: '2px solid #E5E7EB',
1155
+ borderRadius: '8px',
1156
+ width: '100%',
1157
+ }}
1158
+ >
1159
+ <Flex direction="column" gap={2}>
1160
+ <Flex alignItems="center" gap={2}>
1161
+ <PlayIcon style={{ width: 18, height: 18, color: '#0EA5E9', flexShrink: 0 }} />
1162
+ <Typography fontWeight="semiBold" style={{ fontSize: '14px' }}>
1163
+ Direct Test
1164
+ </Typography>
1165
+ </Flex>
1166
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.5' }}>
1167
+ Send test email directly through this specific account
1168
+ </Typography>
1169
+ </Flex>
1170
+ </Box>
1171
+
1172
+ {/* Strapi Service Test */}
1173
+ <Box
1174
+ padding={4}
1175
+ background="primary50"
1176
+ hasRadius
1177
+ style={{
1178
+ border: '2px solid #0EA5E9',
1179
+ borderRadius: '8px',
1180
+ width: '100%',
1181
+ }}
1182
+ >
1183
+ <Flex direction="column" gap={2}>
1184
+ <Flex alignItems="center" gap={2}>
1185
+ <SparklesIcon style={{ width: 18, height: 18, color: '#0369A1', flexShrink: 0 }} />
1186
+ <Typography fontWeight="semiBold" style={{ fontSize: '14px', color: '#0369A1' }}>
1187
+ Strapi Email Service Test
1188
+ </Typography>
1189
+ </Flex>
1190
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.5' }}>
1191
+ Test if MagicMail intercepts Strapi's native email service via THIS account ({account.name})
1192
+ </Typography>
1193
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.5' }}>
1194
+ <strong style={{ color: '#0369A1' }}>Use this to verify Email Designer compatibility</strong>
1195
+ </Typography>
1196
+ </Flex>
1197
+ </Box>
1198
+ </Flex>
1199
+ </Box>
1200
+ </Flex>
1201
+ </Modal.Body>
1202
+
1203
+ <Modal.Footer>
1204
+ <Flex justifyContent="space-between" gap={2} style={{ width: '100%' }}>
1205
+ <Button onClick={onClose} variant="tertiary">
1206
+ Cancel
1207
+ </Button>
1208
+ <Flex gap={2}>
1209
+ <Button
1210
+ onClick={() => onTest(testEmail, { priority, type: emailType, unsubscribeUrl })}
1211
+ disabled={!testEmail || !testEmail.includes('@') || (emailType === 'marketing' && !unsubscribeUrl)}
1212
+ startIcon={<PlayIcon style={{ width: 16, height: 16 }} />}
1213
+ variant="secondary"
1214
+ >
1215
+ Test Direct
1216
+ </Button>
1217
+ <Button
1218
+ onClick={testStrapiService}
1219
+ disabled={!testEmail || !testEmail.includes('@')}
1220
+ loading={testingStrapiService}
1221
+ startIcon={<SparklesIcon style={{ width: 16, height: 16 }} />}
1222
+ >
1223
+ Test Strapi Service
1224
+ </Button>
1225
+ </Flex>
1226
+ </Flex>
1227
+ </Modal.Footer>
1228
+ </Modal.Content>
1229
+ </Modal.Root>
1230
+ );
1231
+ };
1232
+
1233
+ export default HomePage;