strapi-plugin-magic-mail 2.2.4 → 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.
Files changed (70) hide show
  1. package/dist/server/index.js +1 -1
  2. package/dist/server/index.mjs +1 -1
  3. package/package.json +1 -3
  4. package/admin/jsconfig.json +0 -10
  5. package/admin/src/components/AddAccountModal.jsx +0 -1943
  6. package/admin/src/components/Initializer.jsx +0 -14
  7. package/admin/src/components/LicenseGuard.jsx +0 -475
  8. package/admin/src/components/PluginIcon.jsx +0 -5
  9. package/admin/src/hooks/useAuthRefresh.js +0 -44
  10. package/admin/src/hooks/useLicense.js +0 -158
  11. package/admin/src/index.js +0 -87
  12. package/admin/src/pages/Analytics.jsx +0 -762
  13. package/admin/src/pages/App.jsx +0 -111
  14. package/admin/src/pages/EmailDesigner/EditorPage.jsx +0 -1424
  15. package/admin/src/pages/EmailDesigner/TemplateList.jsx +0 -1807
  16. package/admin/src/pages/HomePage.jsx +0 -1170
  17. package/admin/src/pages/LicensePage.jsx +0 -430
  18. package/admin/src/pages/RoutingRules.jsx +0 -1141
  19. package/admin/src/pages/Settings.jsx +0 -603
  20. package/admin/src/pluginId.js +0 -3
  21. package/admin/src/translations/de.json +0 -71
  22. package/admin/src/translations/en.json +0 -70
  23. package/admin/src/translations/es.json +0 -71
  24. package/admin/src/translations/fr.json +0 -71
  25. package/admin/src/translations/pt.json +0 -71
  26. package/admin/src/utils/fetchWithRetry.js +0 -123
  27. package/admin/src/utils/getTranslation.js +0 -5
  28. package/admin/src/utils/theme.js +0 -85
  29. package/server/jsconfig.json +0 -10
  30. package/server/src/bootstrap.js +0 -157
  31. package/server/src/config/features.js +0 -260
  32. package/server/src/config/index.js +0 -9
  33. package/server/src/content-types/email-account/schema.json +0 -93
  34. package/server/src/content-types/email-event/index.js +0 -8
  35. package/server/src/content-types/email-event/schema.json +0 -57
  36. package/server/src/content-types/email-link/index.js +0 -8
  37. package/server/src/content-types/email-link/schema.json +0 -49
  38. package/server/src/content-types/email-log/index.js +0 -8
  39. package/server/src/content-types/email-log/schema.json +0 -106
  40. package/server/src/content-types/email-template/schema.json +0 -74
  41. package/server/src/content-types/email-template-version/schema.json +0 -60
  42. package/server/src/content-types/index.js +0 -33
  43. package/server/src/content-types/routing-rule/schema.json +0 -59
  44. package/server/src/controllers/accounts.js +0 -229
  45. package/server/src/controllers/analytics.js +0 -361
  46. package/server/src/controllers/controller.js +0 -26
  47. package/server/src/controllers/email-designer.js +0 -474
  48. package/server/src/controllers/index.js +0 -21
  49. package/server/src/controllers/license.js +0 -269
  50. package/server/src/controllers/oauth.js +0 -474
  51. package/server/src/controllers/routing-rules.js +0 -129
  52. package/server/src/controllers/test.js +0 -301
  53. package/server/src/destroy.js +0 -27
  54. package/server/src/index.js +0 -25
  55. package/server/src/middlewares/index.js +0 -3
  56. package/server/src/policies/index.js +0 -3
  57. package/server/src/register.js +0 -5
  58. package/server/src/routes/admin.js +0 -469
  59. package/server/src/routes/content-api.js +0 -37
  60. package/server/src/routes/index.js +0 -9
  61. package/server/src/services/account-manager.js +0 -329
  62. package/server/src/services/analytics.js +0 -512
  63. package/server/src/services/email-designer.js +0 -717
  64. package/server/src/services/email-router.js +0 -1446
  65. package/server/src/services/index.js +0 -17
  66. package/server/src/services/license-guard.js +0 -423
  67. package/server/src/services/oauth.js +0 -515
  68. package/server/src/services/service.js +0 -7
  69. package/server/src/utils/encryption.js +0 -81
  70. package/server/src/utils/logger.js +0 -84
@@ -1,1141 +0,0 @@
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
- Modal,
13
- Field,
14
- TextInput,
15
- Textarea,
16
- NumberInput,
17
- Toggle,
18
- SingleSelect,
19
- SingleSelectOption,
20
- } from '@strapi/design-system';
21
- import { Table, Thead, Tbody, Tr, Th, Td } from '@strapi/design-system';
22
- import {
23
- PlusIcon,
24
- PencilIcon,
25
- TrashIcon,
26
- CheckIcon,
27
- Cog6ToothIcon,
28
- SparklesIcon,
29
- FunnelIcon,
30
- MagnifyingGlassIcon,
31
- } from '@heroicons/react/24/outline';
32
-
33
- // ================ THEME (Exact copy from Email Accounts) ================
34
- const theme = {
35
- colors: {
36
- primary: {
37
- 50: '#F0F9FF',
38
- 100: '#E0F2FE',
39
- 500: '#0EA5E9',
40
- 600: '#0284C7',
41
- 700: '#0369A1',
42
- },
43
- secondary: {
44
- 50: '#F5F3FF',
45
- 100: '#EDE9FE',
46
- 500: '#A855F7',
47
- 600: '#9333EA',
48
- },
49
- success: {
50
- 100: '#DCFCE7',
51
- 500: '#22C55E',
52
- 600: '#16A34A',
53
- 700: '#15803D',
54
- },
55
- warning: {
56
- 100: '#FEF3C7',
57
- 500: '#F59E0B',
58
- 600: '#D97706',
59
- },
60
- danger: {
61
- 100: '#FEE2E2',
62
- 500: '#EF4444',
63
- 600: '#DC2626',
64
- },
65
- neutral: {
66
- 0: '#FFFFFF',
67
- 50: '#F9FAFB',
68
- 100: '#F3F4F6',
69
- 200: '#E5E7EB',
70
- 600: '#4B5563',
71
- 700: '#374151',
72
- 800: '#1F2937',
73
- }
74
- },
75
- shadows: {
76
- sm: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
77
- md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
78
- lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
79
- xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
80
- },
81
- transitions: {
82
- fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
83
- normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
84
- slow: '500ms cubic-bezier(0.4, 0, 0.2, 1)',
85
- },
86
- spacing: {
87
- xs: '4px',
88
- sm: '8px',
89
- md: '16px',
90
- lg: '24px',
91
- xl: '32px',
92
- '2xl': '48px',
93
- },
94
- borderRadius: {
95
- md: '8px',
96
- lg: '12px',
97
- xl: '16px',
98
- }
99
- };
100
-
101
- // ================ ANIMATIONS ================
102
- const fadeIn = keyframes`
103
- from { opacity: 0; transform: translateY(10px); }
104
- to { opacity: 1; transform: translateY(0); }
105
- `;
106
-
107
- const shimmer = keyframes`
108
- 0% { background-position: -200% 0; }
109
- 100% { background-position: 200% 0; }
110
- `;
111
-
112
- const float = keyframes`
113
- 0%, 100% { transform: translateY(0px); }
114
- 50% { transform: translateY(-5px); }
115
- `;
116
-
117
- const pulse = keyframes`
118
- 0%, 100% { opacity: 1; }
119
- 50% { opacity: 0.5; }
120
- `;
121
-
122
- const FloatingEmoji = styled.div`
123
- position: absolute;
124
- bottom: 40px;
125
- right: 40px;
126
- font-size: 72px;
127
- opacity: 0.08;
128
- ${css`animation: ${float} 4s ease-in-out infinite;`}
129
- `;
130
-
131
- // ================ RESPONSIVE BREAKPOINTS ================
132
- const breakpoints = {
133
- mobile: '768px',
134
- tablet: '1024px',
135
- };
136
-
137
- // ================ STYLED COMPONENTS ================
138
- const Container = styled(Box)`
139
- ${css`animation: ${fadeIn} ${theme.transitions.slow};`}
140
- min-height: 100vh;
141
- max-width: 1440px;
142
- margin: 0 auto;
143
- padding: ${theme.spacing.xl} ${theme.spacing.lg} 0;
144
-
145
- @media screen and (max-width: ${breakpoints.mobile}) {
146
- padding: 16px 12px 0;
147
- }
148
- `;
149
-
150
- const Header = styled(Box)`
151
- background: linear-gradient(135deg,
152
- ${theme.colors.secondary[600]} 0%,
153
- ${theme.colors.primary[600]} 100%
154
- );
155
- border-radius: ${theme.borderRadius.xl};
156
- padding: ${theme.spacing.xl} ${theme.spacing['2xl']};
157
- margin-bottom: ${theme.spacing.xl};
158
- position: relative;
159
- overflow: hidden;
160
- box-shadow: ${theme.shadows.xl};
161
-
162
- @media screen and (max-width: ${breakpoints.mobile}) {
163
- padding: 24px 20px;
164
- border-radius: 12px;
165
- }
166
-
167
- &::before {
168
- content: '';
169
- position: absolute;
170
- top: 0;
171
- left: -100%;
172
- width: 200%;
173
- height: 100%;
174
- background: linear-gradient(
175
- 90deg,
176
- transparent,
177
- rgba(255, 255, 255, 0.15),
178
- transparent
179
- );
180
- ${css`animation: ${shimmer} 3s infinite;`}
181
- }
182
-
183
- &::after {
184
- content: '';
185
- position: absolute;
186
- top: 0;
187
- right: 0;
188
- width: 100%;
189
- height: 100%;
190
- background-image: radial-gradient(circle at 20% 80%, transparent 50%, rgba(255, 255, 255, 0.1) 50%);
191
- background-size: 15px 15px;
192
- opacity: 0.3;
193
- }
194
- `;
195
-
196
- const HeaderContent = styled(Flex)`
197
- position: relative;
198
- z-index: 1;
199
- `;
200
-
201
- const Title = styled(Typography)`
202
- color: white;
203
- font-size: 2rem;
204
- font-weight: 700;
205
- letter-spacing: -0.025em;
206
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
207
- display: flex;
208
- align-items: center;
209
- gap: ${theme.spacing.sm};
210
-
211
- svg {
212
- width: 28px;
213
- height: 28px;
214
- ${css`animation: ${float} 3s ease-in-out infinite;`}
215
- }
216
-
217
- @media screen and (max-width: ${breakpoints.mobile}) {
218
- font-size: 1.5rem;
219
-
220
- svg {
221
- width: 22px;
222
- height: 22px;
223
- }
224
- }
225
- `;
226
-
227
- const Subtitle = styled(Typography)`
228
- color: rgba(255, 255, 255, 0.95);
229
- font-size: 0.95rem;
230
- font-weight: 400;
231
- margin-top: ${theme.spacing.xs};
232
- letter-spacing: 0.01em;
233
-
234
- @media screen and (max-width: ${breakpoints.mobile}) {
235
- font-size: 0.85rem;
236
- }
237
- `;
238
-
239
- const StatsGrid = styled.div`
240
- margin-bottom: ${theme.spacing.xl};
241
- display: grid;
242
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
243
- gap: ${theme.spacing.lg};
244
- justify-content: center;
245
- max-width: 1200px;
246
- margin-left: auto;
247
- margin-right: auto;
248
-
249
- @media screen and (max-width: ${breakpoints.mobile}) {
250
- grid-template-columns: repeat(2, 1fr);
251
- gap: 12px;
252
- margin-bottom: 24px;
253
- }
254
- `;
255
-
256
- const StatCard = styled(Box)`
257
- background: ${props => props.theme.colors.neutral0};
258
- border-radius: ${theme.borderRadius.lg};
259
- padding: 28px ${theme.spacing.lg};
260
- position: relative;
261
- overflow: hidden;
262
- transition: all ${theme.transitions.normal};
263
- ${css`animation: ${fadeIn} ${theme.transitions.slow} backwards;`}
264
- animation-delay: ${props => props.$delay || '0s'};
265
- box-shadow: ${theme.shadows.sm};
266
- border: 1px solid ${props => props.theme.colors.neutral200};
267
- min-width: 200px;
268
- flex: 1;
269
- text-align: center;
270
- display: flex;
271
- flex-direction: column;
272
- align-items: center;
273
- justify-content: center;
274
-
275
- @media screen and (max-width: ${breakpoints.mobile}) {
276
- min-width: unset;
277
- padding: 20px 12px;
278
-
279
- &:hover {
280
- transform: none;
281
- }
282
- }
283
-
284
- &:hover {
285
- transform: translateY(-6px);
286
- box-shadow: ${theme.shadows.xl};
287
- border-color: ${props => props.$color || theme.colors.primary[500]};
288
-
289
- .stat-icon {
290
- transform: scale(1.15) rotate(5deg);
291
- }
292
-
293
- .stat-value {
294
- transform: scale(1.08);
295
- color: ${props => props.$color || theme.colors.primary[600]};
296
- }
297
- }
298
- `;
299
-
300
- const StatIcon = styled(Box)`
301
- width: 68px;
302
- height: 68px;
303
- border-radius: ${theme.borderRadius.lg};
304
- display: flex;
305
- align-items: center;
306
- justify-content: center;
307
- background: ${props => props.$bg || theme.colors.primary[100]};
308
- transition: all ${theme.transitions.normal};
309
- margin: 0 auto 20px;
310
- box-shadow: ${theme.shadows.sm};
311
-
312
- svg {
313
- width: 34px;
314
- height: 34px;
315
- color: ${props => props.$color || theme.colors.primary[600]};
316
- }
317
-
318
- @media screen and (max-width: ${breakpoints.mobile}) {
319
- width: 48px;
320
- height: 48px;
321
- margin-bottom: 12px;
322
-
323
- svg {
324
- width: 24px;
325
- height: 24px;
326
- }
327
- }
328
- `;
329
-
330
- const StatValue = styled(Typography)`
331
- font-size: 2.75rem;
332
- font-weight: 700;
333
- color: ${props => props.theme.colors.neutral800};
334
- line-height: 1;
335
- margin-bottom: 10px;
336
- transition: all ${theme.transitions.normal};
337
- text-align: center;
338
-
339
- @media screen and (max-width: ${breakpoints.mobile}) {
340
- font-size: 2rem;
341
- margin-bottom: 6px;
342
- }
343
- `;
344
-
345
- const StatLabel = styled(Typography)`
346
- font-size: 0.95rem;
347
- color: ${props => props.theme.colors.neutral600};
348
- font-weight: 500;
349
- letter-spacing: 0.025em;
350
- text-align: center;
351
-
352
- @media screen and (max-width: ${breakpoints.mobile}) {
353
- font-size: 0.8rem;
354
- }
355
- `;
356
-
357
- const RulesContainer = styled(Box)`
358
- margin-top: ${theme.spacing.xl};
359
- `;
360
-
361
- const EmptyState = styled(Box)`
362
- background: ${props => props.theme.colors.neutral0};
363
- border-radius: ${theme.borderRadius.xl};
364
- border: 2px dashed ${props => props.theme.colors.neutral300};
365
- padding: 80px 32px;
366
- text-align: center;
367
- position: relative;
368
- overflow: hidden;
369
- min-height: 400px;
370
- display: flex;
371
- align-items: center;
372
- justify-content: center;
373
-
374
- /* Background Gradient */
375
- &::before {
376
- content: '';
377
- position: absolute;
378
- top: 0;
379
- left: 0;
380
- right: 0;
381
- bottom: 0;
382
- background: linear-gradient(135deg, ${theme.colors.secondary[50]} 0%, ${theme.colors.primary[50]} 100%);
383
- opacity: 0.3;
384
- z-index: 0;
385
- }
386
- `;
387
-
388
- const OnlineBadge = styled.div`
389
- width: 12px;
390
- height: 12px;
391
- border-radius: 50%;
392
- background: ${props => props.$active ? theme.colors.success[500] : props.theme.colors.neutral400};
393
- display: inline-block;
394
- margin-right: 8px;
395
- ${css`animation: ${props => props.$active ? pulse : 'none'} 2s ease-in-out infinite;`}
396
- `;
397
-
398
- const StyledTable = styled(Table)`
399
- thead {
400
- background: ${props => props.theme.colors.neutral100};
401
- border-bottom: 2px solid ${props => props.theme.colors.neutral200};
402
-
403
- th {
404
- font-weight: 600;
405
- color: ${props => props.theme.colors.neutral800};
406
- font-size: 0.875rem;
407
- text-transform: uppercase;
408
- letter-spacing: 0.025em;
409
- padding: ${theme.spacing.lg} ${theme.spacing.lg};
410
- }
411
- }
412
-
413
- tbody tr {
414
- transition: all ${theme.transitions.fast};
415
- border-bottom: 1px solid ${props => props.theme.colors.neutral150};
416
-
417
- &:last-child {
418
- border-bottom: none;
419
- }
420
-
421
- &:hover {
422
- background: ${props => props.theme.colors.primary100};
423
- }
424
-
425
- td {
426
- padding: ${theme.spacing.lg} ${theme.spacing.lg};
427
- color: ${props => props.theme.colors.neutral800};
428
- vertical-align: middle;
429
- }
430
- }
431
- `;
432
-
433
- const FilterBar = styled(Flex)`
434
- background: ${props => props.theme.colors.neutral0};
435
- padding: ${theme.spacing.md} ${theme.spacing.lg};
436
- border-radius: ${theme.borderRadius.lg};
437
- margin-bottom: ${theme.spacing.lg};
438
- box-shadow: ${theme.shadows.sm};
439
- border: 1px solid ${props => props.theme.colors.neutral200};
440
- gap: ${theme.spacing.md};
441
- align-items: center;
442
- `;
443
-
444
- const SearchInputWrapper = styled.div`
445
- position: relative;
446
- flex: 1;
447
- display: flex;
448
- align-items: center;
449
- `;
450
-
451
- const SearchIcon = styled(MagnifyingGlassIcon)`
452
- position: absolute;
453
- left: 12px;
454
- width: 16px;
455
- height: 16px;
456
- color: ${props => props.theme.colors.neutral600};
457
- pointer-events: none;
458
- `;
459
-
460
- const StyledSearchInput = styled.input`
461
- width: 100%;
462
- padding: 10px 12px 10px 40px;
463
- border: 1px solid ${props => props.theme.colors.neutral200};
464
- border-radius: ${theme.borderRadius.md};
465
- font-size: 0.875rem;
466
- transition: all ${theme.transitions.fast};
467
-
468
- &:focus {
469
- outline: none;
470
- border-color: ${theme.colors.primary[500]};
471
- box-shadow: 0 0 0 2px ${theme.colors.primary[100]};
472
- }
473
-
474
- &::placeholder {
475
- color: ${props => props.theme.colors.neutral500};
476
- }
477
- `;
478
-
479
- const RoutingRulesPage = () => {
480
- useAuthRefresh(); // Initialize token auto-refresh
481
- const { get, post, put, del } = useFetchClient();
482
- const { toggleNotification } = useNotification();
483
- const [loading, setLoading] = useState(true);
484
- const [rules, setRules] = useState([]);
485
- const [accounts, setAccounts] = useState([]);
486
- const [showModal, setShowModal] = useState(false);
487
- const [editingRule, setEditingRule] = useState(null);
488
- const [searchQuery, setSearchQuery] = useState('');
489
- const [filterStatus, setFilterStatus] = useState('all');
490
- const [filterMatchType, setFilterMatchType] = useState('all');
491
-
492
- useEffect(() => {
493
- fetchData();
494
- }, []);
495
-
496
- const fetchData = async () => {
497
- setLoading(true);
498
- try {
499
- const [rulesRes, accountsRes] = await Promise.all([
500
- get('/magic-mail/routing-rules'),
501
- get('/magic-mail/accounts'),
502
- ]);
503
- setRules(rulesRes.data.data || []);
504
- setAccounts(accountsRes.data.data || []);
505
- } catch (err) {
506
- console.error('[magic-mail] Error fetching data:', err);
507
- toggleNotification({
508
- type: 'danger',
509
- message: 'Failed to load routing rules',
510
- });
511
- } finally {
512
- setLoading(false);
513
- }
514
- };
515
-
516
- const deleteRule = async (ruleId, ruleName) => {
517
- if (!confirm(`Delete routing rule "${ruleName}"?`)) return;
518
-
519
- try {
520
- await del(`/magic-mail/routing-rules/${ruleId}`);
521
- toggleNotification({
522
- type: 'success',
523
- message: 'Routing rule deleted successfully',
524
- });
525
- fetchData();
526
- } catch (err) {
527
- toggleNotification({
528
- type: 'danger',
529
- message: 'Failed to delete routing rule',
530
- });
531
- }
532
- };
533
-
534
- if (loading) {
535
- return (
536
- <Flex justifyContent="center" alignItems="center" style={{ minHeight: '400px' }}>
537
- <Loader>Loading Routing Rules...</Loader>
538
- </Flex>
539
- );
540
- }
541
-
542
- // Calculate stats
543
- const totalRules = rules.length;
544
- const activeRules = rules.filter(r => r.isActive).length;
545
- const highPriorityRules = rules.filter(r => r.priority >= 5).length;
546
-
547
- // Filter and search logic
548
- const filteredRules = rules.filter(rule => {
549
- const matchesSearch =
550
- rule.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
551
- (rule.description || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
552
- rule.matchValue.toLowerCase().includes(searchQuery.toLowerCase()) ||
553
- (rule.accountName || '').toLowerCase().includes(searchQuery.toLowerCase());
554
-
555
- const matchesStatus =
556
- filterStatus === 'all' ||
557
- (filterStatus === 'active' && rule.isActive) ||
558
- (filterStatus === 'inactive' && !rule.isActive);
559
-
560
- const matchesType =
561
- filterMatchType === 'all' ||
562
- rule.matchType === filterMatchType;
563
-
564
- return matchesSearch && matchesStatus && matchesType;
565
- });
566
-
567
- const uniqueMatchTypes = [...new Set(rules.map(r => r.matchType))].filter(Boolean);
568
-
569
- return (
570
- <Container>
571
- {/* Hero Header */}
572
- <Header>
573
- <HeaderContent justifyContent="space-between" alignItems="center">
574
- <Flex direction="column" alignItems="flex-start" gap={2}>
575
- <Title>
576
- <FunnelIcon />
577
- Email Routing Rules
578
- </Title>
579
- <Subtitle>
580
- Define intelligent routing rules to send emails through specific accounts based on conditions
581
- </Subtitle>
582
- </Flex>
583
- </HeaderContent>
584
- </Header>
585
-
586
- {/* Quick Stats */}
587
- <StatsGrid>
588
- <StatCard $delay="0.1s" $color={theme.colors.secondary[600]}>
589
- <StatIcon className="stat-icon" $bg={theme.colors.secondary[100]} $color={theme.colors.secondary[600]}>
590
- <FunnelIcon />
591
- </StatIcon>
592
- <StatValue className="stat-value">{totalRules}</StatValue>
593
- <StatLabel>Total Rules</StatLabel>
594
- </StatCard>
595
-
596
- <StatCard $delay="0.2s" $color={theme.colors.success[600]}>
597
- <StatIcon className="stat-icon" $bg={theme.colors.success[100]} $color={theme.colors.success[600]}>
598
- <CheckIcon />
599
- </StatIcon>
600
- <StatValue className="stat-value">{activeRules}</StatValue>
601
- <StatLabel>Active Rules</StatLabel>
602
- </StatCard>
603
-
604
- <StatCard $delay="0.3s" $color={theme.colors.warning[600]}>
605
- <StatIcon className="stat-icon" $bg={theme.colors.warning[100]} $color={theme.colors.warning[600]}>
606
- <SparklesIcon />
607
- </StatIcon>
608
- <StatValue className="stat-value">{highPriorityRules}</StatValue>
609
- <StatLabel>High Priority</StatLabel>
610
- </StatCard>
611
- </StatsGrid>
612
-
613
- {/* Rules List or Empty State */}
614
- {rules.length === 0 ? (
615
- <EmptyState>
616
- {/* Floating Emoji */}
617
- <FloatingEmoji>
618
- 🎯
619
- </FloatingEmoji>
620
-
621
- <Flex direction="column" alignItems="center" gap={6} style={{ position: 'relative', zIndex: 1 }}>
622
- <Box
623
- style={{
624
- width: '120px',
625
- height: '120px',
626
- borderRadius: '50%',
627
- background: `linear-gradient(135deg, ${theme.colors.secondary[100]} 0%, ${theme.colors.primary[100]} 100%)`,
628
- display: 'flex',
629
- alignItems: 'center',
630
- justifyContent: 'center',
631
- boxShadow: theme.shadows.xl,
632
- }}
633
- >
634
- <FunnelIcon style={{ width: '60px', height: '60px', color: theme.colors.secondary[600] }} />
635
- </Box>
636
-
637
- <Typography
638
- variant="alpha"
639
- textColor="neutral800"
640
- style={{
641
- fontSize: '1.75rem',
642
- fontWeight: '700',
643
- marginBottom: '8px',
644
- }}
645
- >
646
- No Routing Rules Yet
647
- </Typography>
648
-
649
- <Typography
650
- variant="omega"
651
- textColor="neutral600"
652
- style={{
653
- fontSize: '1rem',
654
- maxWidth: '500px',
655
- lineHeight: '1.6',
656
- }}
657
- >
658
- Create your first routing rule to intelligently route emails based on type, recipient, subject, or custom conditions
659
- </Typography>
660
-
661
- <Button
662
- startIcon={<PlusIcon style={{ width: 20, height: 20 }} />}
663
- onClick={() => setShowModal(true)}
664
- size="L"
665
- >
666
- Create First Rule
667
- </Button>
668
- </Flex>
669
- </EmptyState>
670
- ) : (
671
- <RulesContainer>
672
- <Box style={{ marginBottom: theme.spacing.md }}>
673
- <Flex justifyContent="space-between" alignItems="center" marginBottom={4}>
674
- <Typography variant="delta" textColor="neutral700" style={{ fontSize: '1.5rem', fontWeight: 600 }}>
675
- 🎯 Routing Rules
676
- </Typography>
677
- <Button startIcon={<PlusIcon style={{ width: 16, height: 16 }} />} onClick={() => setShowModal(true)}>
678
- Create Rule
679
- </Button>
680
- </Flex>
681
- </Box>
682
-
683
- {/* Filter Bar */}
684
- <FilterBar>
685
- {/* Search Input */}
686
- <SearchInputWrapper>
687
- <SearchIcon />
688
- <StyledSearchInput
689
- value={searchQuery}
690
- onChange={(e) => setSearchQuery(e.target.value)}
691
- placeholder="Search by name, description, or value..."
692
- type="text"
693
- />
694
- </SearchInputWrapper>
695
-
696
- {/* Status Filter */}
697
- <Box style={{ minWidth: '160px' }}>
698
- <SingleSelect
699
- value={filterStatus}
700
- onChange={setFilterStatus}
701
- placeholder="Status"
702
- size="S"
703
- >
704
- <SingleSelectOption value="all">All Rules</SingleSelectOption>
705
- <SingleSelectOption value="active">✅ Active</SingleSelectOption>
706
- <SingleSelectOption value="inactive">❌ Inactive</SingleSelectOption>
707
- </SingleSelect>
708
- </Box>
709
-
710
- {/* Match Type Filter */}
711
- <Box style={{ minWidth: '160px' }}>
712
- <SingleSelect
713
- value={filterMatchType}
714
- onChange={setFilterMatchType}
715
- placeholder="Match Type"
716
- size="S"
717
- >
718
- <SingleSelectOption value="all">All Types</SingleSelectOption>
719
- {uniqueMatchTypes.map(type => (
720
- <SingleSelectOption key={type} value={type}>
721
- {type}
722
- </SingleSelectOption>
723
- ))}
724
- </SingleSelect>
725
- </Box>
726
- </FilterBar>
727
-
728
- {/* Rules Table */}
729
- {filteredRules.length > 0 ? (
730
- <Box>
731
- <StyledTable>
732
- <Thead>
733
- <Tr>
734
- <Th>Status</Th>
735
- <Th>Rule Name</Th>
736
- <Th>Match Type</Th>
737
- <Th>Match Value</Th>
738
- <Th>Target Account</Th>
739
- <Th>Priority</Th>
740
- <Th>Actions</Th>
741
- </Tr>
742
- </Thead>
743
- <Tbody>
744
- {filteredRules.map((rule) => (
745
- <Tr key={rule.id}>
746
- {/* Status */}
747
- <Td>
748
- <Flex alignItems="center" gap={2}>
749
- <OnlineBadge $active={rule.isActive} />
750
- {rule.isActive ? (
751
- <Badge backgroundColor="success600" textColor="neutral0" size="S">
752
- Active
753
- </Badge>
754
- ) : (
755
- <Badge backgroundColor="neutral600" textColor="neutral0" size="S">
756
- Inactive
757
- </Badge>
758
- )}
759
- </Flex>
760
- </Td>
761
-
762
- {/* Name */}
763
- <Td>
764
- <Flex direction="column" alignItems="flex-start" gap={1}>
765
- <Typography fontWeight="semiBold">
766
- {rule.name}
767
- </Typography>
768
- {rule.description && (
769
- <Typography variant="pi" textColor="neutral600">
770
- {rule.description}
771
- </Typography>
772
- )}
773
- </Flex>
774
- </Td>
775
-
776
- {/* Match Type */}
777
- <Td>
778
- <Badge size="S" variant="secondary">
779
- {rule.matchType === 'emailType' && '📧 Email Type'}
780
- {rule.matchType === 'recipient' && '👤 Recipient'}
781
- {rule.matchType === 'subject' && '📝 Subject'}
782
- {rule.matchType === 'template' && '🎨 Template'}
783
- {rule.matchType === 'custom' && '⚙️ Custom'}
784
- </Badge>
785
- </Td>
786
-
787
- {/* Match Value */}
788
- <Td>
789
- <Typography variant="pi" style={{ fontFamily: 'monospace', fontSize: '0.85rem' }}>
790
- {rule.matchValue}
791
- </Typography>
792
- </Td>
793
-
794
- {/* Target Account */}
795
- <Td>
796
- <Flex direction="column" alignItems="flex-start" gap={1}>
797
- <Typography fontWeight="semiBold">
798
- {rule.accountName}
799
- </Typography>
800
- {rule.fallbackAccountName && (
801
- <Typography variant="pi" textColor="neutral600">
802
- Fallback: {rule.fallbackAccountName}
803
- </Typography>
804
- )}
805
- </Flex>
806
- </Td>
807
-
808
- {/* Priority */}
809
- <Td>
810
- <Badge
811
- size="S"
812
- variant="secondary"
813
- backgroundColor={rule.priority >= 5 ? 'warning100' : 'neutral100'}
814
- textColor={rule.priority >= 5 ? 'warning700' : 'neutral700'}
815
- >
816
- {rule.priority >= 5 && '⭐ '}
817
- {rule.priority}
818
- </Badge>
819
- </Td>
820
-
821
- {/* Actions */}
822
- <Td>
823
- <Flex gap={2}>
824
- <Button
825
- variant="secondary"
826
- onClick={() => setEditingRule(rule)}
827
- size="S"
828
- aria-label="Edit Rule"
829
- >
830
- <PencilIcon style={{ width: 16, height: 16 }} />
831
- </Button>
832
- <Button
833
- variant="danger-light"
834
- onClick={() => deleteRule(rule.id, rule.name)}
835
- size="S"
836
- aria-label="Delete Rule"
837
- >
838
- <TrashIcon style={{ width: 16, height: 16 }} />
839
- </Button>
840
- </Flex>
841
- </Td>
842
- </Tr>
843
- ))}
844
- </Tbody>
845
- </StyledTable>
846
- </Box>
847
- ) : (
848
- <Box padding={8} style={{ textAlign: 'center' }}>
849
- <Typography variant="beta" textColor="neutral600">
850
- No rules found matching your filters
851
- </Typography>
852
- </Box>
853
- )}
854
- </RulesContainer>
855
- )}
856
-
857
- {/* Create/Edit Modal */}
858
- {(showModal || editingRule) && (
859
- <RuleModal
860
- rule={editingRule}
861
- accounts={accounts}
862
- onClose={() => {
863
- setShowModal(false);
864
- setEditingRule(null);
865
- }}
866
- onSave={fetchData}
867
- />
868
- )}
869
- </Container>
870
- );
871
- };
872
-
873
- // ================ RULE MODAL COMPONENT ================
874
- const RuleModal = ({ rule, accounts, onClose, onSave }) => {
875
- const { post, put } = useFetchClient();
876
- const { toggleNotification } = useNotification();
877
- const [loading, setLoading] = useState(false);
878
- const isEditMode = !!rule;
879
-
880
- const [formData, setFormData] = useState({
881
- name: rule?.name || '',
882
- description: rule?.description || '',
883
- isActive: rule?.isActive !== undefined ? rule.isActive : true,
884
- priority: rule?.priority || 1,
885
- matchType: rule?.matchType || 'emailType',
886
- matchValue: rule?.matchValue || '',
887
- accountName: rule?.accountName || '',
888
- fallbackAccountName: rule?.fallbackAccountName || '',
889
- });
890
-
891
- const handleChange = (field, value) => {
892
- setFormData(prev => ({ ...prev, [field]: value }));
893
- };
894
-
895
- const handleSubmit = async () => {
896
- setLoading(true);
897
- try {
898
- if (isEditMode) {
899
- await put(`/magic-mail/routing-rules/${rule.id}`, formData);
900
- toggleNotification({
901
- type: 'success',
902
- message: 'Routing rule updated successfully',
903
- });
904
- } else {
905
- await post('/magic-mail/routing-rules', formData);
906
- toggleNotification({
907
- type: 'success',
908
- message: 'Routing rule created successfully',
909
- });
910
- }
911
- onSave();
912
- onClose();
913
- } catch (err) {
914
- toggleNotification({
915
- type: 'danger',
916
- message: err.response?.data?.error?.message || `Failed to ${isEditMode ? 'update' : 'create'} routing rule`,
917
- });
918
- } finally {
919
- setLoading(false);
920
- }
921
- };
922
-
923
- const canSubmit = formData.name && formData.matchType && formData.matchValue && formData.accountName;
924
-
925
- const getMatchTypeHelp = () => {
926
- switch (formData.matchType) {
927
- case 'emailType':
928
- return 'Match based on email type (e.g., "transactional", "marketing", "notification")';
929
- case 'recipient':
930
- return 'Match if recipient email contains this value (e.g., "@vip-customers.com")';
931
- case 'subject':
932
- return 'Match if subject line contains this value (e.g., "Invoice", "Password Reset")';
933
- case 'template':
934
- return 'Match if email uses this template name (exact match)';
935
- case 'custom':
936
- return 'Match based on custom field value passed in emailData.customField';
937
- default:
938
- return '';
939
- }
940
- };
941
-
942
- return (
943
- <Modal.Root open={true} onOpenChange={onClose}>
944
- <Modal.Content size="L">
945
- <Modal.Header>
946
- <Typography variant="beta">
947
- <Cog6ToothIcon style={{ marginRight: 8, width: 24, height: 24 }} />
948
- {isEditMode ? 'Edit Routing Rule' : 'Create Routing Rule'}
949
- </Typography>
950
- </Modal.Header>
951
-
952
- <Modal.Body>
953
- <Box style={{ width: '100%' }}>
954
- <Flex direction="column" gap={6} style={{ width: '100%' }}>
955
-
956
- {/* Rule Name */}
957
- <Field.Root required style={{ width: '100%' }}>
958
- <Field.Label>Rule Name</Field.Label>
959
- <TextInput
960
- placeholder="Marketing emails via SendGrid"
961
- value={formData.name}
962
- onChange={(e) => handleChange('name', e.target.value)}
963
- style={{ width: '100%' }}
964
- />
965
- <Field.Hint>
966
- A descriptive name for this routing rule
967
- </Field.Hint>
968
- </Field.Root>
969
-
970
- {/* Description */}
971
- <Field.Root style={{ width: '100%' }}>
972
- <Field.Label>Description (Optional)</Field.Label>
973
- <Textarea
974
- placeholder="Route all marketing emails through SendGrid for better deliverability..."
975
- value={formData.description}
976
- onChange={(e) => handleChange('description', e.target.value)}
977
- rows={3}
978
- style={{ width: '100%' }}
979
- />
980
- </Field.Root>
981
-
982
- {/* Match Type */}
983
- <Field.Root required style={{ width: '100%' }}>
984
- <Field.Label>Match Type</Field.Label>
985
- <SingleSelect
986
- value={formData.matchType}
987
- onChange={(value) => handleChange('matchType', value)}
988
- style={{ width: '100%' }}
989
- >
990
- <SingleSelectOption value="emailType">📧 Email Type</SingleSelectOption>
991
- <SingleSelectOption value="recipient">👤 Recipient</SingleSelectOption>
992
- <SingleSelectOption value="subject">📝 Subject</SingleSelectOption>
993
- <SingleSelectOption value="template">🎨 Template</SingleSelectOption>
994
- <SingleSelectOption value="custom">⚙️ Custom Field</SingleSelectOption>
995
- </SingleSelect>
996
- <Field.Hint>
997
- {getMatchTypeHelp()}
998
- </Field.Hint>
999
- </Field.Root>
1000
-
1001
- {/* Match Value */}
1002
- <Field.Root required style={{ width: '100%' }}>
1003
- <Field.Label>Match Value</Field.Label>
1004
- <TextInput
1005
- placeholder={
1006
- formData.matchType === 'emailType' ? 'marketing' :
1007
- formData.matchType === 'recipient' ? '@vip-customers.com' :
1008
- formData.matchType === 'subject' ? 'Invoice' :
1009
- formData.matchType === 'template' ? 'welcome-email' :
1010
- 'custom-value'
1011
- }
1012
- value={formData.matchValue}
1013
- onChange={(e) => handleChange('matchValue', e.target.value)}
1014
- style={{ width: '100%' }}
1015
- />
1016
- <Field.Hint>
1017
- The value to match against. Case-insensitive for recipient and subject.
1018
- </Field.Hint>
1019
- </Field.Root>
1020
-
1021
- {/* Target Account */}
1022
- <Field.Root required style={{ width: '100%' }}>
1023
- <Field.Label>Target Account</Field.Label>
1024
- <SingleSelect
1025
- value={formData.accountName}
1026
- onChange={(value) => handleChange('accountName', value)}
1027
- style={{ width: '100%' }}
1028
- >
1029
- <SingleSelectOption value="">Select account...</SingleSelectOption>
1030
- {accounts.filter(a => a.isActive).map(account => (
1031
- <SingleSelectOption key={account.name} value={account.name}>
1032
- {account.name} ({account.provider})
1033
- </SingleSelectOption>
1034
- ))}
1035
- </SingleSelect>
1036
- <Field.Hint>
1037
- The email account to use when this rule matches
1038
- </Field.Hint>
1039
- </Field.Root>
1040
-
1041
- {/* Fallback Account */}
1042
- <Field.Root style={{ width: '100%' }}>
1043
- <Field.Label>Fallback Account (Optional)</Field.Label>
1044
- <SingleSelect
1045
- value={formData.fallbackAccountName}
1046
- onChange={(value) => handleChange('fallbackAccountName', value)}
1047
- style={{ width: '100%' }}
1048
- >
1049
- <SingleSelectOption value="">No fallback</SingleSelectOption>
1050
- {accounts.filter(a => a.isActive && a.name !== formData.accountName).map(account => (
1051
- <SingleSelectOption key={account.name} value={account.name}>
1052
- {account.name} ({account.provider})
1053
- </SingleSelectOption>
1054
- ))}
1055
- </SingleSelect>
1056
- <Field.Hint>
1057
- Use this account if the target account is unavailable or rate-limited
1058
- </Field.Hint>
1059
- </Field.Root>
1060
-
1061
- {/* Priority */}
1062
- <Field.Root style={{ width: '100%' }}>
1063
- <Field.Label>Rule Priority</Field.Label>
1064
- <NumberInput
1065
- value={formData.priority}
1066
- onValueChange={(value) => handleChange('priority', value)}
1067
- min={1}
1068
- max={10}
1069
- style={{ width: '100%' }}
1070
- />
1071
- <Field.Hint>
1072
- Higher priority rules are evaluated first (1-10). Use high priority for more specific rules.
1073
- </Field.Hint>
1074
- </Field.Root>
1075
-
1076
- {/* Active Toggle */}
1077
- <Box
1078
- padding={4}
1079
- background={formData.isActive ? theme.colors.success[100] : theme.colors.danger[100]}
1080
- hasRadius
1081
- style={{
1082
- width: '100%',
1083
- border: formData.isActive ? `2px solid ${theme.colors.success[600]}` : `2px solid ${theme.colors.danger[600]}`,
1084
- borderRadius: theme.borderRadius.md,
1085
- transition: 'all 0.2s ease'
1086
- }}
1087
- >
1088
- <Flex gap={3} alignItems="center">
1089
- <Toggle
1090
- checked={formData.isActive}
1091
- onChange={() => handleChange('isActive', !formData.isActive)}
1092
- />
1093
- <Box style={{ flex: 1 }}>
1094
- <Flex alignItems="center" gap={2}>
1095
- <Typography fontWeight="semiBold">
1096
- {formData.isActive ? '✅ Rule Active' : '❌ Rule Inactive'}
1097
- </Typography>
1098
- <Badge
1099
- backgroundColor={formData.isActive ? 'success600' : 'danger600'}
1100
- textColor="neutral0"
1101
- size="S"
1102
- >
1103
- {formData.isActive ? 'ENABLED' : 'DISABLED'}
1104
- </Badge>
1105
- </Flex>
1106
- <Typography variant="pi" textColor="neutral600" marginTop={1}>
1107
- {formData.isActive
1108
- ? 'This rule is active and will be used for email routing'
1109
- : 'This rule is disabled and will be ignored'
1110
- }
1111
- </Typography>
1112
- </Box>
1113
- </Flex>
1114
- </Box>
1115
-
1116
- </Flex>
1117
- </Box>
1118
- </Modal.Body>
1119
-
1120
- <Modal.Footer>
1121
- <Flex justifyContent="flex-end" gap={2} style={{ width: '100%' }}>
1122
- <Button onClick={onClose} variant="tertiary">
1123
- Cancel
1124
- </Button>
1125
- <Button
1126
- onClick={handleSubmit}
1127
- loading={loading}
1128
- disabled={!canSubmit}
1129
- startIcon={<CheckIcon style={{ width: 16, height: 16 }} />}
1130
- >
1131
- {isEditMode ? 'Update Rule' : 'Create Rule'}
1132
- </Button>
1133
- </Flex>
1134
- </Modal.Footer>
1135
- </Modal.Content>
1136
- </Modal.Root>
1137
- );
1138
- };
1139
-
1140
- export default RoutingRulesPage;
1141
-