strapi-plugin-magic-mail 2.2.3 → 2.2.5
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/dist/server/index.js +1 -1
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -3
- package/admin/jsconfig.json +0 -10
- package/admin/src/components/AddAccountModal.jsx +0 -1943
- package/admin/src/components/Initializer.jsx +0 -14
- package/admin/src/components/LicenseGuard.jsx +0 -475
- package/admin/src/components/PluginIcon.jsx +0 -5
- package/admin/src/hooks/useAuthRefresh.js +0 -44
- package/admin/src/hooks/useLicense.js +0 -158
- package/admin/src/index.js +0 -87
- package/admin/src/pages/Analytics.jsx +0 -762
- package/admin/src/pages/App.jsx +0 -111
- package/admin/src/pages/EmailDesigner/EditorPage.jsx +0 -1424
- package/admin/src/pages/EmailDesigner/TemplateList.jsx +0 -1807
- package/admin/src/pages/HomePage.jsx +0 -1170
- package/admin/src/pages/LicensePage.jsx +0 -430
- package/admin/src/pages/RoutingRules.jsx +0 -1141
- package/admin/src/pages/Settings.jsx +0 -603
- package/admin/src/pluginId.js +0 -3
- package/admin/src/translations/de.json +0 -71
- package/admin/src/translations/en.json +0 -70
- package/admin/src/translations/es.json +0 -71
- package/admin/src/translations/fr.json +0 -71
- package/admin/src/translations/pt.json +0 -71
- package/admin/src/utils/fetchWithRetry.js +0 -123
- package/admin/src/utils/getTranslation.js +0 -5
- package/admin/src/utils/theme.js +0 -85
- package/server/jsconfig.json +0 -10
- package/server/src/bootstrap.js +0 -157
- package/server/src/config/features.js +0 -260
- package/server/src/config/index.js +0 -9
- package/server/src/content-types/email-account/schema.json +0 -93
- package/server/src/content-types/email-event/index.js +0 -8
- package/server/src/content-types/email-event/schema.json +0 -57
- package/server/src/content-types/email-link/index.js +0 -8
- package/server/src/content-types/email-link/schema.json +0 -49
- package/server/src/content-types/email-log/index.js +0 -8
- package/server/src/content-types/email-log/schema.json +0 -106
- package/server/src/content-types/email-template/schema.json +0 -74
- package/server/src/content-types/email-template-version/schema.json +0 -60
- package/server/src/content-types/index.js +0 -33
- package/server/src/content-types/routing-rule/schema.json +0 -59
- package/server/src/controllers/accounts.js +0 -229
- package/server/src/controllers/analytics.js +0 -361
- package/server/src/controllers/controller.js +0 -26
- package/server/src/controllers/email-designer.js +0 -474
- package/server/src/controllers/index.js +0 -21
- package/server/src/controllers/license.js +0 -269
- package/server/src/controllers/oauth.js +0 -474
- package/server/src/controllers/routing-rules.js +0 -129
- package/server/src/controllers/test.js +0 -301
- package/server/src/destroy.js +0 -27
- package/server/src/index.js +0 -25
- package/server/src/middlewares/index.js +0 -3
- package/server/src/policies/index.js +0 -3
- package/server/src/register.js +0 -5
- package/server/src/routes/admin.js +0 -469
- package/server/src/routes/content-api.js +0 -37
- package/server/src/routes/index.js +0 -9
- package/server/src/services/account-manager.js +0 -329
- package/server/src/services/analytics.js +0 -512
- package/server/src/services/email-designer.js +0 -717
- package/server/src/services/email-router.js +0 -1446
- package/server/src/services/index.js +0 -17
- package/server/src/services/license-guard.js +0 -423
- package/server/src/services/oauth.js +0 -515
- package/server/src/services/service.js +0 -7
- package/server/src/utils/encryption.js +0 -81
- package/server/src/utils/logger.js +0 -84
|
@@ -1,1807 +0,0 @@
|
|
|
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: ${props => props.theme.colors.neutral200};
|
|
103
|
-
border-radius: 3px;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
&::-webkit-scrollbar-thumb:hover {
|
|
107
|
-
background: ${props => props.theme.colors.neutral300};
|
|
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: ${props => props.theme.colors.neutral800};
|
|
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: white;
|
|
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: ${props => props.theme.colors.neutral0};
|
|
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 ${props => props.theme.colors.neutral200};
|
|
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: ${props => props.theme.colors.neutral800};
|
|
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: ${props => props.theme.colors.neutral600};
|
|
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: ${props => props.theme.colors.neutral0};
|
|
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 ${props => props.theme.colors.neutral200};
|
|
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: ${props => props.theme.colors.neutral600};
|
|
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: ${props => props.theme.colors.neutral100};
|
|
540
|
-
border-bottom: 2px solid ${props => props.theme.colors.neutral200};
|
|
541
|
-
|
|
542
|
-
th {
|
|
543
|
-
font-weight: 600;
|
|
544
|
-
color: ${props => props.theme.colors.neutral800};
|
|
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 ${props => props.theme.colors.neutral150};
|
|
555
|
-
|
|
556
|
-
&:last-child {
|
|
557
|
-
border-bottom: none;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
&:hover {
|
|
561
|
-
background: ${props => props.theme.colors.primary100};
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
td {
|
|
565
|
-
padding: ${theme.spacing.lg} ${theme.spacing.lg};
|
|
566
|
-
color: ${props => props.theme.colors.neutral800};
|
|
567
|
-
vertical-align: middle;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
`;
|
|
571
|
-
|
|
572
|
-
const EmptyState = styled(Box)`
|
|
573
|
-
background: ${props => props.theme.colors.neutral0};
|
|
574
|
-
border-radius: ${theme.borderRadius.xl};
|
|
575
|
-
border: 2px dashed ${props => props.theme.colors.neutral300};
|
|
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: ${props => props.theme.colors.neutral0};
|
|
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" textColor="neutral800">
|
|
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" textColor="neutral600" style={{ 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
|
-
textColor="neutral800"
|
|
1163
|
-
style={{
|
|
1164
|
-
fontSize: '1.75rem',
|
|
1165
|
-
fontWeight: '700',
|
|
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" textColor="neutral700" style={{ fontSize: '1.5rem', fontWeight: 600 }}>
|
|
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
|
-
background="neutral100"
|
|
1424
|
-
style={{
|
|
1425
|
-
padding: '80px 32px',
|
|
1426
|
-
textAlign: 'center',
|
|
1427
|
-
borderRadius: theme.borderRadius.lg,
|
|
1428
|
-
border: '1px dashed #D1D5DB',
|
|
1429
|
-
}}
|
|
1430
|
-
>
|
|
1431
|
-
<MagnifyingGlassIcon style={{ width: '64px', height: '64px', margin: '0 auto 16px', color: '#9CA3AF' }} />
|
|
1432
|
-
<Typography variant="beta" textColor="neutral700" style={{ marginBottom: '8px' }}>
|
|
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" textColor="neutral700" style={{ fontSize: '1.5rem', fontWeight: 600 }}>
|
|
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 #E5E7EB', 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 #E5E7EB', paddingBottom: '16px' }}>
|
|
1527
|
-
<Flex alignItems="center" gap={2}>
|
|
1528
|
-
<BoltIcon style={{ width: 24, height: 24, color: theme.colors.primary[600] }} />
|
|
1529
|
-
<Typography variant="beta" textColor="neutral800">
|
|
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" textColor="neutral600" style={{ 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" textColor="neutral600" style={{ 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" textColor="neutral600" style={{ 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;
|