@bernierllc/email-campaign-management 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/.eslintrc.cjs +45 -0
  2. package/README.md +316 -0
  3. package/__tests__/api/campaigns.test.ts +217 -0
  4. package/__tests__/api/client.test.ts +330 -0
  5. package/__tests__/components/CampaignBuilder.test.tsx +103 -0
  6. package/__tests__/components/CampaignDashboard.test.tsx +89 -0
  7. package/__tests__/components/CampaignList.test.tsx +144 -0
  8. package/__tests__/components/MetricsOverview.test.tsx +200 -0
  9. package/__tests__/components/PerformanceChart.test.tsx +206 -0
  10. package/__tests__/hooks/useCampaignStore.test.ts +450 -0
  11. package/__tests__/hooks/useWorkflowValidation.test.ts +176 -0
  12. package/__tests__/utils/formatting.test.ts +48 -0
  13. package/__tests__/utils/validation.test.ts +199 -0
  14. package/__tests__/utils/workflow-helpers.test.ts +134 -0
  15. package/coverage/clover.xml +314 -0
  16. package/coverage/coverage-final.json +16 -0
  17. package/coverage/lcov-report/base.css +224 -0
  18. package/coverage/lcov-report/block-navigation.js +87 -0
  19. package/coverage/lcov-report/favicon.png +0 -0
  20. package/coverage/lcov-report/index.html +221 -0
  21. package/coverage/lcov-report/prettify.css +1 -0
  22. package/coverage/lcov-report/prettify.js +2 -0
  23. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  24. package/coverage/lcov-report/sorter.js +210 -0
  25. package/coverage/lcov-report/src/api/campaigns.ts.html +199 -0
  26. package/coverage/lcov-report/src/api/client.ts.html +478 -0
  27. package/coverage/lcov-report/src/api/index.html +131 -0
  28. package/coverage/lcov-report/src/components/CampaignBuilder/index.html +116 -0
  29. package/coverage/lcov-report/src/components/CampaignBuilder/index.tsx.html +454 -0
  30. package/coverage/lcov-report/src/components/CampaignDashboard/MetricsOverview.tsx.html +208 -0
  31. package/coverage/lcov-report/src/components/CampaignDashboard/PerformanceChart.tsx.html +232 -0
  32. package/coverage/lcov-report/src/components/CampaignDashboard/index.html +146 -0
  33. package/coverage/lcov-report/src/components/CampaignDashboard/index.tsx.html +241 -0
  34. package/coverage/lcov-report/src/components/CampaignList/index.html +116 -0
  35. package/coverage/lcov-report/src/components/CampaignList/index.tsx.html +244 -0
  36. package/coverage/lcov-report/src/config.ts.html +202 -0
  37. package/coverage/lcov-report/src/hooks/index.html +146 -0
  38. package/coverage/lcov-report/src/hooks/useCampaignMetrics.ts.html +208 -0
  39. package/coverage/lcov-report/src/hooks/useCampaignStore.ts.html +343 -0
  40. package/coverage/lcov-report/src/hooks/useWorkflowValidation.ts.html +136 -0
  41. package/coverage/lcov-report/src/index.html +116 -0
  42. package/coverage/lcov-report/src/types/index.html +116 -0
  43. package/coverage/lcov-report/src/types/index.ts.html +127 -0
  44. package/coverage/lcov-report/src/utils/formatting.ts.html +163 -0
  45. package/coverage/lcov-report/src/utils/index.html +146 -0
  46. package/coverage/lcov-report/src/utils/validation.ts.html +394 -0
  47. package/coverage/lcov-report/src/utils/workflow-helpers.ts.html +277 -0
  48. package/coverage/lcov.info +657 -0
  49. package/dist/api/campaigns.d.ts +9 -0
  50. package/dist/api/campaigns.js +38 -0
  51. package/dist/api/client.d.ts +14 -0
  52. package/dist/api/client.js +116 -0
  53. package/dist/components/CampaignBuilder/index.d.ts +8 -0
  54. package/dist/components/CampaignBuilder/index.js +88 -0
  55. package/dist/components/CampaignDashboard/MetricsOverview.d.ts +6 -0
  56. package/dist/components/CampaignDashboard/MetricsOverview.js +34 -0
  57. package/dist/components/CampaignDashboard/PerformanceChart.d.ts +7 -0
  58. package/dist/components/CampaignDashboard/PerformanceChart.js +45 -0
  59. package/dist/components/CampaignDashboard/index.d.ts +5 -0
  60. package/dist/components/CampaignDashboard/index.js +44 -0
  61. package/dist/components/CampaignList/index.d.ts +6 -0
  62. package/dist/components/CampaignList/index.js +68 -0
  63. package/dist/config.d.ts +12 -0
  64. package/dist/config.js +31 -0
  65. package/dist/hooks/useCampaignMetrics.d.ts +2 -0
  66. package/dist/hooks/useCampaignMetrics.js +42 -0
  67. package/dist/hooks/useCampaignStore.d.ts +14 -0
  68. package/dist/hooks/useCampaignStore.js +105 -0
  69. package/dist/hooks/useWorkflowValidation.d.ts +3 -0
  70. package/dist/hooks/useWorkflowValidation.js +17 -0
  71. package/dist/index.d.ts +13 -0
  72. package/dist/index.js +52 -0
  73. package/dist/types/abtest.d.ts +15 -0
  74. package/dist/types/abtest.js +9 -0
  75. package/dist/types/audience.d.ts +18 -0
  76. package/dist/types/audience.js +9 -0
  77. package/dist/types/campaign.d.ts +31 -0
  78. package/dist/types/campaign.js +9 -0
  79. package/dist/types/index.d.ts +6 -0
  80. package/dist/types/index.js +29 -0
  81. package/dist/types/metrics.d.ts +27 -0
  82. package/dist/types/metrics.js +9 -0
  83. package/dist/types/schedule.d.ts +15 -0
  84. package/dist/types/schedule.js +9 -0
  85. package/dist/types/workflow.d.ts +37 -0
  86. package/dist/types/workflow.js +9 -0
  87. package/dist/utils/formatting.d.ts +4 -0
  88. package/dist/utils/formatting.js +28 -0
  89. package/dist/utils/validation.d.ts +8 -0
  90. package/dist/utils/validation.js +81 -0
  91. package/dist/utils/workflow-helpers.d.ts +12 -0
  92. package/dist/utils/workflow-helpers.js +62 -0
  93. package/jest.config.cjs +33 -0
  94. package/jest.setup.cjs +9 -0
  95. package/package.json +72 -0
  96. package/src/api/campaigns.ts +38 -0
  97. package/src/api/client.ts +131 -0
  98. package/src/components/CampaignBuilder/index.tsx +123 -0
  99. package/src/components/CampaignDashboard/MetricsOverview.tsx +41 -0
  100. package/src/components/CampaignDashboard/PerformanceChart.tsx +49 -0
  101. package/src/components/CampaignDashboard/index.tsx +52 -0
  102. package/src/components/CampaignList/index.tsx +53 -0
  103. package/src/config.ts +39 -0
  104. package/src/hooks/useCampaignMetrics.ts +41 -0
  105. package/src/hooks/useCampaignStore.ts +86 -0
  106. package/src/hooks/useWorkflowValidation.ts +17 -0
  107. package/src/index.ts +32 -0
  108. package/src/types/abtest.ts +25 -0
  109. package/src/types/audience.ts +30 -0
  110. package/src/types/campaign.ts +44 -0
  111. package/src/types/index.ts +14 -0
  112. package/src/types/metrics.ts +36 -0
  113. package/src/types/schedule.ts +26 -0
  114. package/src/types/workflow.ts +53 -0
  115. package/src/utils/formatting.ts +26 -0
  116. package/src/utils/validation.ts +103 -0
  117. package/src/utils/workflow-helpers.ts +64 -0
  118. package/tsconfig.json +24 -0
@@ -0,0 +1,38 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { apiClient, APIResponse } from './client';
10
+ import { Campaign, CampaignMetrics, EmailMetrics } from '../types';
11
+
12
+ export async function getCampaigns(): Promise<APIResponse<Campaign[]>> {
13
+ return apiClient.get<Campaign[]>('/campaigns');
14
+ }
15
+
16
+ export async function getCampaign(id: string): Promise<APIResponse<Campaign>> {
17
+ return apiClient.get<Campaign>(`/campaigns/${id}`);
18
+ }
19
+
20
+ export async function createCampaign(campaign: Partial<Campaign>): Promise<APIResponse<Campaign>> {
21
+ return apiClient.post<Campaign>('/campaigns', campaign);
22
+ }
23
+
24
+ export async function updateCampaign(id: string, updates: Partial<Campaign>): Promise<APIResponse<Campaign>> {
25
+ return apiClient.put<Campaign>(`/campaigns/${id}`, updates);
26
+ }
27
+
28
+ export async function deleteCampaign(id: string): Promise<APIResponse<void>> {
29
+ return apiClient.delete<void>(`/campaigns/${id}`);
30
+ }
31
+
32
+ export async function getCampaignMetrics(campaignId: string): Promise<APIResponse<CampaignMetrics>> {
33
+ return apiClient.get<CampaignMetrics>(`/campaigns/${campaignId}/metrics`);
34
+ }
35
+
36
+ export async function getEmailMetrics(campaignId: string): Promise<APIResponse<EmailMetrics[]>> {
37
+ return apiClient.get<EmailMetrics[]>(`/campaigns/${campaignId}/emails/metrics`);
38
+ }
@@ -0,0 +1,131 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { getConfig } from '../config';
10
+
11
+ export interface APIResponse<T = unknown> {
12
+ success: boolean;
13
+ data?: T;
14
+ error?: string;
15
+ }
16
+
17
+ export class APIClient {
18
+ private baseUrl: string;
19
+
20
+ constructor(baseUrl?: string) {
21
+ this.baseUrl = baseUrl || getConfig().apiUrl;
22
+ }
23
+
24
+ async get<T>(endpoint: string): Promise<APIResponse<T>> {
25
+ try {
26
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
27
+ method: 'GET',
28
+ headers: {
29
+ 'Content-Type': 'application/json'
30
+ }
31
+ });
32
+
33
+ if (!response.ok) {
34
+ return {
35
+ success: false,
36
+ error: `HTTP ${response.status}: ${response.statusText}`
37
+ };
38
+ }
39
+
40
+ const data = await response.json();
41
+ return { success: true, data };
42
+ } catch (error) {
43
+ return {
44
+ success: false,
45
+ error: error instanceof Error ? error.message : 'Unknown error'
46
+ };
47
+ }
48
+ }
49
+
50
+ async post<T>(endpoint: string, body: unknown): Promise<APIResponse<T>> {
51
+ try {
52
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json'
56
+ },
57
+ body: JSON.stringify(body)
58
+ });
59
+
60
+ if (!response.ok) {
61
+ return {
62
+ success: false,
63
+ error: `HTTP ${response.status}: ${response.statusText}`
64
+ };
65
+ }
66
+
67
+ const data = await response.json();
68
+ return { success: true, data };
69
+ } catch (error) {
70
+ return {
71
+ success: false,
72
+ error: error instanceof Error ? error.message : 'Unknown error'
73
+ };
74
+ }
75
+ }
76
+
77
+ async put<T>(endpoint: string, body: unknown): Promise<APIResponse<T>> {
78
+ try {
79
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
80
+ method: 'PUT',
81
+ headers: {
82
+ 'Content-Type': 'application/json'
83
+ },
84
+ body: JSON.stringify(body)
85
+ });
86
+
87
+ if (!response.ok) {
88
+ return {
89
+ success: false,
90
+ error: `HTTP ${response.status}: ${response.statusText}`
91
+ };
92
+ }
93
+
94
+ const data = await response.json();
95
+ return { success: true, data };
96
+ } catch (error) {
97
+ return {
98
+ success: false,
99
+ error: error instanceof Error ? error.message : 'Unknown error'
100
+ };
101
+ }
102
+ }
103
+
104
+ async delete<T>(endpoint: string): Promise<APIResponse<T>> {
105
+ try {
106
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
107
+ method: 'DELETE',
108
+ headers: {
109
+ 'Content-Type': 'application/json'
110
+ }
111
+ });
112
+
113
+ if (!response.ok) {
114
+ return {
115
+ success: false,
116
+ error: `HTTP ${response.status}: ${response.statusText}`
117
+ };
118
+ }
119
+
120
+ const data = await response.json();
121
+ return { success: true, data };
122
+ } catch (error) {
123
+ return {
124
+ success: false,
125
+ error: error instanceof Error ? error.message : 'Unknown error'
126
+ };
127
+ }
128
+ }
129
+ }
130
+
131
+ export const apiClient = new APIClient();
@@ -0,0 +1,123 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import React, { useState } from 'react';
10
+ import { Campaign, CampaignWorkflow } from '../../types';
11
+ import { getDefaultWorkflow } from '../../utils/workflow-helpers';
12
+
13
+ export interface CampaignBuilderProps {
14
+ campaign?: Campaign;
15
+ onSave: (campaign: Partial<Campaign>) => void;
16
+ onPublish?: (campaign: Campaign) => void;
17
+ }
18
+
19
+ export const CampaignBuilder: React.FC<CampaignBuilderProps> = ({
20
+ campaign,
21
+ onSave,
22
+ onPublish
23
+ }) => {
24
+ const [name, setName] = useState(campaign?.name || '');
25
+ const [description, setDescription] = useState(campaign?.description || '');
26
+ const [workflow] = useState<CampaignWorkflow>(
27
+ campaign?.workflow || getDefaultWorkflow()
28
+ );
29
+ const [activeTab, setActiveTab] = useState<'workflow' | 'audience' | 'schedule' | 'abtest'>('workflow');
30
+
31
+ const handleSave = () => {
32
+ onSave({
33
+ name,
34
+ description,
35
+ workflow,
36
+ type: 'one-time',
37
+ status: 'draft'
38
+ });
39
+ };
40
+
41
+ return (
42
+ <div className="campaign-builder">
43
+ <div className="campaign-header">
44
+ <input
45
+ type="text"
46
+ value={name}
47
+ onChange={(e) => setName(e.target.value)}
48
+ placeholder="Campaign Name"
49
+ className="campaign-name-input"
50
+ />
51
+ <textarea
52
+ value={description}
53
+ onChange={(e) => setDescription(e.target.value)}
54
+ placeholder="Campaign Description"
55
+ className="campaign-description-input"
56
+ />
57
+ </div>
58
+
59
+ <div className="campaign-tabs">
60
+ <button
61
+ className={activeTab === 'workflow' ? 'active' : ''}
62
+ onClick={() => setActiveTab('workflow')}
63
+ >
64
+ Workflow
65
+ </button>
66
+ <button
67
+ className={activeTab === 'audience' ? 'active' : ''}
68
+ onClick={() => setActiveTab('audience')}
69
+ >
70
+ Audience
71
+ </button>
72
+ <button
73
+ className={activeTab === 'schedule' ? 'active' : ''}
74
+ onClick={() => setActiveTab('schedule')}
75
+ >
76
+ Schedule
77
+ </button>
78
+ <button
79
+ className={activeTab === 'abtest' ? 'active' : ''}
80
+ onClick={() => setActiveTab('abtest')}
81
+ >
82
+ A/B Test
83
+ </button>
84
+ </div>
85
+
86
+ <div className="campaign-content">
87
+ {activeTab === 'workflow' && (
88
+ <div className="workflow-tab">
89
+ <p>Workflow editor placeholder</p>
90
+ <p>Nodes: {workflow.nodes.length}</p>
91
+ <p>Edges: {workflow.edges.length}</p>
92
+ </div>
93
+ )}
94
+ {activeTab === 'audience' && (
95
+ <div className="audience-tab">
96
+ <p>Audience selector placeholder</p>
97
+ </div>
98
+ )}
99
+ {activeTab === 'schedule' && (
100
+ <div className="schedule-tab">
101
+ <p>Schedule settings placeholder</p>
102
+ </div>
103
+ )}
104
+ {activeTab === 'abtest' && (
105
+ <div className="abtest-tab">
106
+ <p>A/B test configuration placeholder</p>
107
+ </div>
108
+ )}
109
+ </div>
110
+
111
+ <div className="campaign-actions">
112
+ <button onClick={handleSave} className="btn-save">
113
+ Save Campaign
114
+ </button>
115
+ {onPublish && campaign && (
116
+ <button onClick={() => onPublish(campaign)} className="btn-publish">
117
+ Publish Campaign
118
+ </button>
119
+ )}
120
+ </div>
121
+ </div>
122
+ );
123
+ };
@@ -0,0 +1,41 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import React from 'react';
10
+ import { CampaignMetrics } from '../../types';
11
+ import { formatNumber, formatPercentage } from '../../utils/formatting';
12
+
13
+ export interface MetricsOverviewProps {
14
+ metrics: CampaignMetrics;
15
+ }
16
+
17
+ export const MetricsOverview: React.FC<MetricsOverviewProps> = ({ metrics }) => {
18
+ return (
19
+ <div className="metrics-overview">
20
+ <div className="metric-card">
21
+ <div className="metric-value">{formatNumber(metrics.sent)}</div>
22
+ <div className="metric-label">Sent</div>
23
+ </div>
24
+ <div className="metric-card">
25
+ <div className="metric-value">{formatNumber(metrics.opened)}</div>
26
+ <div className="metric-label">Opened</div>
27
+ <div className="metric-rate">{formatPercentage(metrics.openRate)}</div>
28
+ </div>
29
+ <div className="metric-card">
30
+ <div className="metric-value">{formatNumber(metrics.clicked)}</div>
31
+ <div className="metric-label">Clicked</div>
32
+ <div className="metric-rate">{formatPercentage(metrics.clickRate)}</div>
33
+ </div>
34
+ <div className="metric-card">
35
+ <div className="metric-value">{formatNumber(metrics.converted)}</div>
36
+ <div className="metric-label">Converted</div>
37
+ <div className="metric-rate">{formatPercentage(metrics.conversionRate)}</div>
38
+ </div>
39
+ </div>
40
+ );
41
+ };
@@ -0,0 +1,49 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import React from 'react';
10
+ import { CampaignMetrics } from '../../types';
11
+
12
+ export interface PerformanceChartProps {
13
+ metrics: CampaignMetrics;
14
+ type: 'open_rate' | 'click_rate' | 'conversion_rate';
15
+ }
16
+
17
+ export const PerformanceChart: React.FC<PerformanceChartProps> = ({ metrics, type }) => {
18
+ const getTitle = () => {
19
+ switch (type) {
20
+ case 'open_rate':
21
+ return 'Open Rate';
22
+ case 'click_rate':
23
+ return 'Click Rate';
24
+ case 'conversion_rate':
25
+ return 'Conversion Rate';
26
+ }
27
+ };
28
+
29
+ const getValue = () => {
30
+ switch (type) {
31
+ case 'open_rate':
32
+ return metrics.openRate;
33
+ case 'click_rate':
34
+ return metrics.clickRate;
35
+ case 'conversion_rate':
36
+ return metrics.conversionRate;
37
+ }
38
+ };
39
+
40
+ return (
41
+ <div className="performance-chart">
42
+ <h3>{getTitle()}</h3>
43
+ <div className="chart-placeholder">
44
+ <p>Chart: {getValue().toFixed(2)}%</p>
45
+ <p>(Integration with recharts or similar library needed)</p>
46
+ </div>
47
+ </div>
48
+ );
49
+ };
@@ -0,0 +1,52 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import React from 'react';
10
+ import { useCampaignMetrics, useEmailMetrics } from '../../hooks/useCampaignMetrics';
11
+ import { MetricsOverview } from './MetricsOverview';
12
+ import { PerformanceChart } from './PerformanceChart';
13
+
14
+ export interface CampaignDashboardProps {
15
+ campaignId: string;
16
+ }
17
+
18
+ export const CampaignDashboard: React.FC<CampaignDashboardProps> = ({ campaignId }) => {
19
+ const { data: metrics, isLoading: metricsLoading, isError: metricsError } = useCampaignMetrics(campaignId);
20
+ const { data: emailMetrics, isLoading: emailLoading } = useEmailMetrics(campaignId);
21
+
22
+ if (metricsLoading || emailLoading) {
23
+ return <div className="loading">Loading campaign analytics...</div>;
24
+ }
25
+
26
+ if (metricsError || !metrics) {
27
+ return <div className="error">Failed to load campaign metrics</div>;
28
+ }
29
+
30
+ return (
31
+ <div className="campaign-dashboard">
32
+ <h2>Campaign Analytics</h2>
33
+ <MetricsOverview metrics={metrics} />
34
+ <div className="charts-grid">
35
+ <PerformanceChart metrics={metrics} type="open_rate" />
36
+ <PerformanceChart metrics={metrics} type="click_rate" />
37
+ </div>
38
+ {emailMetrics && (
39
+ <div className="email-metrics">
40
+ <h3>Email Performance</h3>
41
+ <ul>
42
+ {emailMetrics.map(email => (
43
+ <li key={email.emailId}>
44
+ Node {email.nodeId}: {email.sent} sent, {email.openRate.toFixed(2)}% open rate
45
+ </li>
46
+ ))}
47
+ </ul>
48
+ </div>
49
+ )}
50
+ </div>
51
+ );
52
+ };
@@ -0,0 +1,53 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import React, { useEffect } from 'react';
10
+ import { useCampaignStore } from '../../hooks/useCampaignStore';
11
+ import { Campaign } from '../../types';
12
+
13
+ export interface CampaignListProps {
14
+ onSelectCampaign?: (campaign: Campaign) => void;
15
+ }
16
+
17
+ export const CampaignList: React.FC<CampaignListProps> = ({ onSelectCampaign }) => {
18
+ const { campaigns, loading, error, fetchCampaigns } = useCampaignStore();
19
+
20
+ useEffect(() => {
21
+ fetchCampaigns();
22
+ }, [fetchCampaigns]);
23
+
24
+ if (loading) {
25
+ return <div className="loading">Loading campaigns...</div>;
26
+ }
27
+
28
+ if (error) {
29
+ return <div className="error">Error: {error}</div>;
30
+ }
31
+
32
+ return (
33
+ <div className="campaign-list">
34
+ <h2>Campaigns</h2>
35
+ {campaigns.length === 0 ? (
36
+ <p>No campaigns found. Create your first campaign!</p>
37
+ ) : (
38
+ <ul>
39
+ {campaigns.map(campaign => (
40
+ <li key={campaign.id} onClick={() => onSelectCampaign?.(campaign)}>
41
+ <div className="campaign-item">
42
+ <h3>{campaign.name}</h3>
43
+ <p>{campaign.description}</p>
44
+ <span className="campaign-status">{campaign.status}</span>
45
+ <span className="campaign-type">{campaign.type}</span>
46
+ </div>
47
+ </li>
48
+ ))}
49
+ </ul>
50
+ )}
51
+ </div>
52
+ );
53
+ };
package/src/config.ts ADDED
@@ -0,0 +1,39 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ export interface CampaignUIConfig {
10
+ apiUrl: string;
11
+ enableABTesting: boolean;
12
+ enableAnalytics: boolean;
13
+ metricsRefreshInterval: number;
14
+ maxWorkflowNodes: number;
15
+ defaultTimezone: string;
16
+ }
17
+
18
+ export const defaultConfig: CampaignUIConfig = {
19
+ apiUrl: process.env.REACT_APP_API_URL || 'http://localhost:3000/api',
20
+ enableABTesting: process.env.REACT_APP_ENABLE_AB_TESTING !== 'false',
21
+ enableAnalytics: process.env.REACT_APP_ENABLE_ANALYTICS !== 'false',
22
+ metricsRefreshInterval: parseInt(process.env.REACT_APP_METRICS_REFRESH_INTERVAL || '30000', 10),
23
+ maxWorkflowNodes: parseInt(process.env.REACT_APP_MAX_WORKFLOW_NODES || '50', 10),
24
+ defaultTimezone: process.env.REACT_APP_DEFAULT_TIMEZONE || 'UTC'
25
+ };
26
+
27
+ let currentConfig: CampaignUIConfig = { ...defaultConfig };
28
+
29
+ export function getConfig(): CampaignUIConfig {
30
+ return { ...currentConfig };
31
+ }
32
+
33
+ export function setConfig(config: Partial<CampaignUIConfig>): void {
34
+ currentConfig = { ...currentConfig, ...config };
35
+ }
36
+
37
+ export function resetConfig(): void {
38
+ currentConfig = { ...defaultConfig };
39
+ }
@@ -0,0 +1,41 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { useQuery } from '@tanstack/react-query';
10
+ import { getCampaignMetrics, getEmailMetrics } from '../api/campaigns';
11
+ import { getConfig } from '../config';
12
+
13
+ export function useCampaignMetrics(campaignId: string) {
14
+ return useQuery({
15
+ queryKey: ['campaign-metrics', campaignId],
16
+ queryFn: async () => {
17
+ const response = await getCampaignMetrics(campaignId);
18
+ if (!response.success) {
19
+ throw new Error(response.error || 'Failed to fetch campaign metrics');
20
+ }
21
+ return response.data;
22
+ },
23
+ refetchInterval: getConfig().metricsRefreshInterval,
24
+ enabled: !!campaignId
25
+ });
26
+ }
27
+
28
+ export function useEmailMetrics(campaignId: string) {
29
+ return useQuery({
30
+ queryKey: ['email-metrics', campaignId],
31
+ queryFn: async () => {
32
+ const response = await getEmailMetrics(campaignId);
33
+ if (!response.success) {
34
+ throw new Error(response.error || 'Failed to fetch email metrics');
35
+ }
36
+ return response.data;
37
+ },
38
+ refetchInterval: getConfig().metricsRefreshInterval,
39
+ enabled: !!campaignId
40
+ });
41
+ }
@@ -0,0 +1,86 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { create } from 'zustand';
10
+ import { Campaign } from '../types';
11
+ import * as campaignAPI from '../api/campaigns';
12
+
13
+ interface CampaignStore {
14
+ campaigns: Campaign[];
15
+ activeCampaign: Campaign | null;
16
+ loading: boolean;
17
+ error: string | null;
18
+
19
+ fetchCampaigns: () => Promise<void>;
20
+ createCampaign: (campaign: Partial<Campaign>) => Promise<Campaign | null>;
21
+ updateCampaign: (id: string, updates: Partial<Campaign>) => Promise<void>;
22
+ deleteCampaign: (id: string) => Promise<void>;
23
+ setActiveCampaign: (campaign: Campaign | null) => void;
24
+ }
25
+
26
+ export const useCampaignStore = create<CampaignStore>((set) => ({
27
+ campaigns: [],
28
+ activeCampaign: null,
29
+ loading: false,
30
+ error: null,
31
+
32
+ fetchCampaigns: async () => {
33
+ set({ loading: true, error: null });
34
+ const response = await campaignAPI.getCampaigns();
35
+ if (response.success && response.data) {
36
+ set({ campaigns: response.data, loading: false });
37
+ } else {
38
+ set({ error: response.error || 'Failed to fetch campaigns', loading: false });
39
+ }
40
+ },
41
+
42
+ createCampaign: async (campaign: Partial<Campaign>) => {
43
+ set({ loading: true, error: null });
44
+ const response = await campaignAPI.createCampaign(campaign);
45
+ if (response.success && response.data) {
46
+ set(state => ({
47
+ campaigns: [...state.campaigns, response.data as Campaign],
48
+ loading: false
49
+ }));
50
+ return response.data;
51
+ } else {
52
+ set({ error: response.error || 'Failed to create campaign', loading: false });
53
+ return null;
54
+ }
55
+ },
56
+
57
+ updateCampaign: async (id: string, updates: Partial<Campaign>) => {
58
+ set({ loading: true, error: null });
59
+ const response = await campaignAPI.updateCampaign(id, updates);
60
+ if (response.success && response.data) {
61
+ set(state => ({
62
+ campaigns: state.campaigns.map(c => c.id === id ? response.data as Campaign : c),
63
+ activeCampaign: state.activeCampaign?.id === id ? response.data : state.activeCampaign,
64
+ loading: false
65
+ }));
66
+ } else {
67
+ set({ error: response.error || 'Failed to update campaign', loading: false });
68
+ }
69
+ },
70
+
71
+ deleteCampaign: async (id: string) => {
72
+ set({ loading: true, error: null });
73
+ const response = await campaignAPI.deleteCampaign(id);
74
+ if (response.success) {
75
+ set(state => ({
76
+ campaigns: state.campaigns.filter(c => c.id !== id),
77
+ activeCampaign: state.activeCampaign?.id === id ? null : state.activeCampaign,
78
+ loading: false
79
+ }));
80
+ } else {
81
+ set({ error: response.error || 'Failed to delete campaign', loading: false });
82
+ }
83
+ },
84
+
85
+ setActiveCampaign: (campaign: Campaign | null) => set({ activeCampaign: campaign })
86
+ }));
@@ -0,0 +1,17 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { useMemo } from 'react';
10
+ import { CampaignWorkflow } from '../types';
11
+ import { validateWorkflow, ValidationResult } from '../utils/validation';
12
+
13
+ export function useWorkflowValidation(workflow: CampaignWorkflow): ValidationResult {
14
+ return useMemo(() => {
15
+ return validateWorkflow(workflow);
16
+ }, [workflow]);
17
+ }