@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.
- package/.eslintrc.cjs +45 -0
- package/README.md +316 -0
- package/__tests__/api/campaigns.test.ts +217 -0
- package/__tests__/api/client.test.ts +330 -0
- package/__tests__/components/CampaignBuilder.test.tsx +103 -0
- package/__tests__/components/CampaignDashboard.test.tsx +89 -0
- package/__tests__/components/CampaignList.test.tsx +144 -0
- package/__tests__/components/MetricsOverview.test.tsx +200 -0
- package/__tests__/components/PerformanceChart.test.tsx +206 -0
- package/__tests__/hooks/useCampaignStore.test.ts +450 -0
- package/__tests__/hooks/useWorkflowValidation.test.ts +176 -0
- package/__tests__/utils/formatting.test.ts +48 -0
- package/__tests__/utils/validation.test.ts +199 -0
- package/__tests__/utils/workflow-helpers.test.ts +134 -0
- package/coverage/clover.xml +314 -0
- package/coverage/coverage-final.json +16 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +221 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/api/campaigns.ts.html +199 -0
- package/coverage/lcov-report/src/api/client.ts.html +478 -0
- package/coverage/lcov-report/src/api/index.html +131 -0
- package/coverage/lcov-report/src/components/CampaignBuilder/index.html +116 -0
- package/coverage/lcov-report/src/components/CampaignBuilder/index.tsx.html +454 -0
- package/coverage/lcov-report/src/components/CampaignDashboard/MetricsOverview.tsx.html +208 -0
- package/coverage/lcov-report/src/components/CampaignDashboard/PerformanceChart.tsx.html +232 -0
- package/coverage/lcov-report/src/components/CampaignDashboard/index.html +146 -0
- package/coverage/lcov-report/src/components/CampaignDashboard/index.tsx.html +241 -0
- package/coverage/lcov-report/src/components/CampaignList/index.html +116 -0
- package/coverage/lcov-report/src/components/CampaignList/index.tsx.html +244 -0
- package/coverage/lcov-report/src/config.ts.html +202 -0
- package/coverage/lcov-report/src/hooks/index.html +146 -0
- package/coverage/lcov-report/src/hooks/useCampaignMetrics.ts.html +208 -0
- package/coverage/lcov-report/src/hooks/useCampaignStore.ts.html +343 -0
- package/coverage/lcov-report/src/hooks/useWorkflowValidation.ts.html +136 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/types/index.html +116 -0
- package/coverage/lcov-report/src/types/index.ts.html +127 -0
- package/coverage/lcov-report/src/utils/formatting.ts.html +163 -0
- package/coverage/lcov-report/src/utils/index.html +146 -0
- package/coverage/lcov-report/src/utils/validation.ts.html +394 -0
- package/coverage/lcov-report/src/utils/workflow-helpers.ts.html +277 -0
- package/coverage/lcov.info +657 -0
- package/dist/api/campaigns.d.ts +9 -0
- package/dist/api/campaigns.js +38 -0
- package/dist/api/client.d.ts +14 -0
- package/dist/api/client.js +116 -0
- package/dist/components/CampaignBuilder/index.d.ts +8 -0
- package/dist/components/CampaignBuilder/index.js +88 -0
- package/dist/components/CampaignDashboard/MetricsOverview.d.ts +6 -0
- package/dist/components/CampaignDashboard/MetricsOverview.js +34 -0
- package/dist/components/CampaignDashboard/PerformanceChart.d.ts +7 -0
- package/dist/components/CampaignDashboard/PerformanceChart.js +45 -0
- package/dist/components/CampaignDashboard/index.d.ts +5 -0
- package/dist/components/CampaignDashboard/index.js +44 -0
- package/dist/components/CampaignList/index.d.ts +6 -0
- package/dist/components/CampaignList/index.js +68 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +31 -0
- package/dist/hooks/useCampaignMetrics.d.ts +2 -0
- package/dist/hooks/useCampaignMetrics.js +42 -0
- package/dist/hooks/useCampaignStore.d.ts +14 -0
- package/dist/hooks/useCampaignStore.js +105 -0
- package/dist/hooks/useWorkflowValidation.d.ts +3 -0
- package/dist/hooks/useWorkflowValidation.js +17 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +52 -0
- package/dist/types/abtest.d.ts +15 -0
- package/dist/types/abtest.js +9 -0
- package/dist/types/audience.d.ts +18 -0
- package/dist/types/audience.js +9 -0
- package/dist/types/campaign.d.ts +31 -0
- package/dist/types/campaign.js +9 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +29 -0
- package/dist/types/metrics.d.ts +27 -0
- package/dist/types/metrics.js +9 -0
- package/dist/types/schedule.d.ts +15 -0
- package/dist/types/schedule.js +9 -0
- package/dist/types/workflow.d.ts +37 -0
- package/dist/types/workflow.js +9 -0
- package/dist/utils/formatting.d.ts +4 -0
- package/dist/utils/formatting.js +28 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.js +81 -0
- package/dist/utils/workflow-helpers.d.ts +12 -0
- package/dist/utils/workflow-helpers.js +62 -0
- package/jest.config.cjs +33 -0
- package/jest.setup.cjs +9 -0
- package/package.json +72 -0
- package/src/api/campaigns.ts +38 -0
- package/src/api/client.ts +131 -0
- package/src/components/CampaignBuilder/index.tsx +123 -0
- package/src/components/CampaignDashboard/MetricsOverview.tsx +41 -0
- package/src/components/CampaignDashboard/PerformanceChart.tsx +49 -0
- package/src/components/CampaignDashboard/index.tsx +52 -0
- package/src/components/CampaignList/index.tsx +53 -0
- package/src/config.ts +39 -0
- package/src/hooks/useCampaignMetrics.ts +41 -0
- package/src/hooks/useCampaignStore.ts +86 -0
- package/src/hooks/useWorkflowValidation.ts +17 -0
- package/src/index.ts +32 -0
- package/src/types/abtest.ts +25 -0
- package/src/types/audience.ts +30 -0
- package/src/types/campaign.ts +44 -0
- package/src/types/index.ts +14 -0
- package/src/types/metrics.ts +36 -0
- package/src/types/schedule.ts +26 -0
- package/src/types/workflow.ts +53 -0
- package/src/utils/formatting.ts +26 -0
- package/src/utils/validation.ts +103 -0
- package/src/utils/workflow-helpers.ts +64 -0
- 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
|
+
}
|