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.
- package/COPYRIGHT_NOTICE.txt +13 -0
- package/LICENSE +22 -0
- package/README.md +1420 -0
- package/admin/jsconfig.json +10 -0
- package/admin/src/components/AddAccountModal.jsx +1943 -0
- package/admin/src/components/Initializer.jsx +14 -0
- package/admin/src/components/LicenseGuard.jsx +475 -0
- package/admin/src/components/PluginIcon.jsx +5 -0
- package/admin/src/hooks/useAuthRefresh.js +44 -0
- package/admin/src/hooks/useLicense.js +158 -0
- package/admin/src/index.js +86 -0
- package/admin/src/pages/Analytics.jsx +762 -0
- package/admin/src/pages/App.jsx +111 -0
- package/admin/src/pages/EmailDesigner/EditorPage.jsx +1405 -0
- package/admin/src/pages/EmailDesigner/TemplateList.jsx +1807 -0
- package/admin/src/pages/HomePage.jsx +1233 -0
- package/admin/src/pages/LicensePage.jsx +424 -0
- package/admin/src/pages/RoutingRules.jsx +1141 -0
- package/admin/src/pages/Settings.jsx +603 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/translations/de.json +71 -0
- package/admin/src/translations/en.json +70 -0
- package/admin/src/translations/es.json +71 -0
- package/admin/src/translations/fr.json +71 -0
- package/admin/src/translations/pt.json +71 -0
- package/admin/src/utils/fetchWithRetry.js +123 -0
- package/admin/src/utils/getTranslation.js +5 -0
- package/dist/_chunks/App-B-Gp4Vbr.js +7568 -0
- package/dist/_chunks/App-BymMjoGM.mjs +7543 -0
- package/dist/_chunks/LicensePage-Bl02myMx.mjs +342 -0
- package/dist/_chunks/LicensePage-CJXwPnEe.js +344 -0
- package/dist/_chunks/Settings-C_TmKwcz.mjs +400 -0
- package/dist/_chunks/Settings-zuFQ3pnn.js +402 -0
- package/dist/_chunks/de-CN-G9j1S.js +64 -0
- package/dist/_chunks/de-DS04rP54.mjs +64 -0
- package/dist/_chunks/en-BDc7Jk8u.js +64 -0
- package/dist/_chunks/en-BEFQJXvR.mjs +64 -0
- package/dist/_chunks/es-BpV1MIdm.js +64 -0
- package/dist/_chunks/es-DQHwzPpP.mjs +64 -0
- package/dist/_chunks/fr-BG1WfEVm.mjs +64 -0
- package/dist/_chunks/fr-vpziIpRp.js +64 -0
- package/dist/_chunks/pt-CMoGrOib.mjs +64 -0
- package/dist/_chunks/pt-ODpAhDNa.js +64 -0
- package/dist/admin/index.js +89 -0
- package/dist/admin/index.mjs +90 -0
- package/dist/server/index.js +6214 -0
- package/dist/server/index.mjs +6208 -0
- package/package.json +113 -0
- package/server/jsconfig.json +10 -0
- package/server/src/bootstrap.js +153 -0
- package/server/src/config/features.js +260 -0
- package/server/src/config/index.js +6 -0
- package/server/src/content-types/email-account/schema.json +93 -0
- package/server/src/content-types/email-event/index.js +8 -0
- package/server/src/content-types/email-event/schema.json +57 -0
- package/server/src/content-types/email-link/index.js +8 -0
- package/server/src/content-types/email-link/schema.json +49 -0
- package/server/src/content-types/email-log/index.js +8 -0
- package/server/src/content-types/email-log/schema.json +106 -0
- package/server/src/content-types/email-template/schema.json +74 -0
- package/server/src/content-types/email-template-version/schema.json +60 -0
- package/server/src/content-types/index.js +33 -0
- package/server/src/content-types/routing-rule/schema.json +59 -0
- package/server/src/controllers/accounts.js +220 -0
- package/server/src/controllers/analytics.js +347 -0
- package/server/src/controllers/controller.js +26 -0
- package/server/src/controllers/email-designer.js +474 -0
- package/server/src/controllers/index.js +21 -0
- package/server/src/controllers/license.js +267 -0
- package/server/src/controllers/oauth.js +474 -0
- package/server/src/controllers/routing-rules.js +122 -0
- package/server/src/controllers/test.js +383 -0
- package/server/src/destroy.js +23 -0
- package/server/src/index.js +25 -0
- package/server/src/middlewares/index.js +3 -0
- package/server/src/policies/index.js +3 -0
- package/server/src/register.js +5 -0
- package/server/src/routes/admin.js +469 -0
- package/server/src/routes/content-api.js +37 -0
- package/server/src/routes/index.js +9 -0
- package/server/src/services/account-manager.js +277 -0
- package/server/src/services/analytics.js +496 -0
- package/server/src/services/email-designer.js +870 -0
- package/server/src/services/email-router.js +1420 -0
- package/server/src/services/index.js +17 -0
- package/server/src/services/license-guard.js +418 -0
- package/server/src/services/oauth.js +515 -0
- package/server/src/services/service.js +7 -0
- package/server/src/utils/encryption.js +81 -0
- package/strapi-admin.js +4 -0
- 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;
|