@bernierllc/email-admin-suite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EmailAdminSuite.d.ts +11 -0
- package/dist/EmailAdminSuite.d.ts.map +1 -0
- package/dist/EmailAdminSuite.js +49 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/pages/AnalyticsPage.d.ts +8 -0
- package/dist/pages/AnalyticsPage.d.ts.map +1 -0
- package/dist/pages/AnalyticsPage.js +41 -0
- package/dist/pages/ComposePage.d.ts +8 -0
- package/dist/pages/ComposePage.d.ts.map +1 -0
- package/dist/pages/ComposePage.js +79 -0
- package/dist/pages/SchedulePage.d.ts +8 -0
- package/dist/pages/SchedulePage.d.ts.map +1 -0
- package/dist/pages/SchedulePage.js +84 -0
- package/dist/pages/TemplatesPage.d.ts +8 -0
- package/dist/pages/TemplatesPage.d.ts.map +1 -0
- package/dist/pages/TemplatesPage.js +95 -0
- package/dist/pages/index.d.ts +5 -0
- package/dist/pages/index.d.ts.map +1 -0
- package/dist/pages/index.js +11 -0
- package/package.json +86 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { EmailManager } from '@bernierllc/email-manager';
|
|
3
|
+
interface EmailAdminSuiteProps {
|
|
4
|
+
emailManager?: EmailManager;
|
|
5
|
+
basePath?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: React.CSSProperties;
|
|
8
|
+
}
|
|
9
|
+
export declare const EmailAdminSuite: React.FC<EmailAdminSuiteProps>;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=EmailAdminSuite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmailAdminSuite.d.ts","sourceRoot":"","sources":["../src/EmailAdminSuite.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAQxC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAMzD,UAAU,oBAAoB;IAC5B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B;AAWD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAkF1D,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
11
|
+
import { BrowserRouter as Router, Routes, Route, NavLink } from 'react-router-dom';
|
|
12
|
+
import { EmailManager } from '@bernierllc/email-manager';
|
|
13
|
+
import { TemplatesPage } from './pages/TemplatesPage';
|
|
14
|
+
import { SchedulePage } from './pages/SchedulePage';
|
|
15
|
+
import { AnalyticsPage } from './pages/AnalyticsPage';
|
|
16
|
+
import { ComposePage } from './pages/ComposePage';
|
|
17
|
+
const queryClient = new QueryClient({
|
|
18
|
+
defaultOptions: {
|
|
19
|
+
queries: {
|
|
20
|
+
retry: 2,
|
|
21
|
+
refetchOnWindowFocus: false,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
export const EmailAdminSuite = ({ emailManager, basePath = '/admin/email', className = '', style, }) => {
|
|
26
|
+
const [manager] = useState(() => emailManager || new EmailManager({
|
|
27
|
+
providers: [],
|
|
28
|
+
scheduling: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
checkIntervalMs: 60000,
|
|
31
|
+
maxRetries: 3,
|
|
32
|
+
retryDelayMs: 5000
|
|
33
|
+
},
|
|
34
|
+
analytics: {
|
|
35
|
+
enabled: true,
|
|
36
|
+
trackOpens: true,
|
|
37
|
+
trackClicks: true,
|
|
38
|
+
trackBounces: true,
|
|
39
|
+
maxEvents: 10000
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
const navigation = [
|
|
43
|
+
{ name: 'Compose', path: `${basePath}/compose`, icon: '✉️' },
|
|
44
|
+
{ name: 'Templates', path: `${basePath}/templates`, icon: '📄' },
|
|
45
|
+
{ name: 'Schedule', path: `${basePath}/schedule`, icon: '📅' },
|
|
46
|
+
{ name: 'Analytics', path: `${basePath}/analytics`, icon: '📊' },
|
|
47
|
+
];
|
|
48
|
+
return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(Router, { children: _jsxs("div", { className: `email-admin-suite ${className}`, style: style, children: [_jsxs("nav", { className: "email-admin-suite__nav", children: [_jsx("div", { className: "nav__brand", children: _jsx("h1", { children: "Email Admin" }) }), _jsx("div", { className: "nav__links", children: navigation.map((item) => (_jsxs(NavLink, { to: item.path, className: ({ isActive }) => `nav__link ${isActive ? 'nav__link--active' : ''}`, children: [_jsx("span", { className: "nav__icon", children: item.icon }), item.name] }, item.path))) })] }), _jsx("main", { className: "email-admin-suite__content", children: _jsxs(Routes, { children: [_jsx(Route, { path: `${basePath}/compose`, element: _jsx(ComposePage, { emailManager: manager }) }), _jsx(Route, { path: `${basePath}/templates`, element: _jsx(TemplatesPage, { emailManager: manager }) }), _jsx(Route, { path: `${basePath}/schedule`, element: _jsx(SchedulePage, { emailManager: manager }) }), _jsx(Route, { path: `${basePath}/analytics`, element: _jsx(AnalyticsPage, { emailManager: manager }) }), _jsx(Route, { path: basePath, element: _jsx(ComposePage, { emailManager: manager }) })] }) })] }) }) }));
|
|
49
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,cAAc,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
export { EmailAdminSuite } from './EmailAdminSuite';
|
|
9
|
+
export * from './pages';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { EmailManager } from '@bernierllc/email-manager';
|
|
3
|
+
interface AnalyticsPageProps {
|
|
4
|
+
emailManager: EmailManager;
|
|
5
|
+
}
|
|
6
|
+
export declare const AnalyticsPage: React.FC<AnalyticsPageProps>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=AnalyticsPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnalyticsPage.d.ts","sourceRoot":"","sources":["../../src/pages/AnalyticsPage.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,UAAU,kBAAkB;IAC1B,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA0EtD,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { useQuery } from '@tanstack/react-query';
|
|
11
|
+
import { AnalyticsDashboard } from '@bernierllc/email-ui';
|
|
12
|
+
export const AnalyticsPage = ({ emailManager }) => {
|
|
13
|
+
const [dateRange, setDateRange] = useState({
|
|
14
|
+
start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // 30 days ago
|
|
15
|
+
end: new Date(),
|
|
16
|
+
});
|
|
17
|
+
const { data: analytics, isLoading, refetch } = useQuery({
|
|
18
|
+
queryKey: ['analytics', dateRange],
|
|
19
|
+
queryFn: async () => {
|
|
20
|
+
const days = Math.ceil((dateRange.end.getTime() - dateRange.start.getTime()) / (1000 * 60 * 60 * 24));
|
|
21
|
+
return await emailManager.getAnalyticsSummary(days);
|
|
22
|
+
},
|
|
23
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
24
|
+
});
|
|
25
|
+
const handleRefresh = async () => {
|
|
26
|
+
await refetch();
|
|
27
|
+
};
|
|
28
|
+
const handleExport = async (format) => {
|
|
29
|
+
try {
|
|
30
|
+
// Export functionality would be implemented here
|
|
31
|
+
console.log(`Exporting analytics as ${format}`);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error(`Failed to export ${format}:`, error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const handleDateRangeChange = (start, end) => {
|
|
38
|
+
setDateRange({ start, end });
|
|
39
|
+
};
|
|
40
|
+
return (_jsxs("div", { className: "analytics-page", children: [_jsxs("div", { className: "page-header", children: [_jsx("h2", { children: "Email Analytics" }), _jsx("div", { className: "page-header__controls", children: _jsxs("div", { className: "date-range-picker", children: [_jsx("label", { children: "Date Range:" }), _jsx("input", { type: "date", value: dateRange.start.toISOString().split('T')[0], onChange: (e) => handleDateRangeChange(new Date(e.target.value), dateRange.end), className: "form-input" }), _jsx("span", { children: "to" }), _jsx("input", { type: "date", value: dateRange.end.toISOString().split('T')[0], onChange: (e) => handleDateRangeChange(dateRange.start, new Date(e.target.value)), className: "form-input" })] }) })] }), isLoading ? (_jsx("div", { className: "loading-state", children: "Loading analytics..." })) : analytics ? (_jsx(AnalyticsDashboard, { analytics: analytics, isLoading: isLoading, onRefresh: handleRefresh, onExport: handleExport, className: "analytics-page__dashboard" })) : (_jsxs("div", { className: "empty-state", children: [_jsx("h3", { children: "No analytics data available" }), _jsx("p", { children: "Analytics will appear here once you start sending emails." })] }))] }));
|
|
41
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { EmailManager } from '@bernierllc/email-manager';
|
|
3
|
+
interface ComposePageProps {
|
|
4
|
+
emailManager: EmailManager;
|
|
5
|
+
}
|
|
6
|
+
export declare const ComposePage: React.FC<ComposePageProps>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=ComposePage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComposePage.d.ts","sourceRoot":"","sources":["../../src/pages/ComposePage.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAIzD,UAAU,gBAAgB;IACxB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAeD,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAoFlD,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import { EmailComposer } from '@bernierllc/email-ui';
|
|
4
|
+
const convertTemplateToUI = (template) => ({
|
|
5
|
+
id: template.id,
|
|
6
|
+
name: template.name,
|
|
7
|
+
subject: template.subject,
|
|
8
|
+
content: template.htmlTemplate,
|
|
9
|
+
variables: template.variables.map(v => v.name),
|
|
10
|
+
category: template.category,
|
|
11
|
+
tags: [], // email-manager doesn't have tags field
|
|
12
|
+
createdAt: template.createdAt,
|
|
13
|
+
updatedAt: template.updatedAt,
|
|
14
|
+
isActive: template.isActive,
|
|
15
|
+
});
|
|
16
|
+
export const ComposePage = ({ emailManager }) => {
|
|
17
|
+
const { data: templates, isLoading: templatesLoading } = useQuery({
|
|
18
|
+
queryKey: ['templates'],
|
|
19
|
+
queryFn: async () => {
|
|
20
|
+
const result = await emailManager.listTemplates();
|
|
21
|
+
return result.templates.map(convertTemplateToUI);
|
|
22
|
+
},
|
|
23
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
24
|
+
});
|
|
25
|
+
const convertToEmailData = async (email) => {
|
|
26
|
+
// Convert File[] attachments to Attachment[] format
|
|
27
|
+
const attachments = email.attachments ? await Promise.all(email.attachments.map(async (file) => {
|
|
28
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
29
|
+
return {
|
|
30
|
+
filename: file.name,
|
|
31
|
+
content: buffer,
|
|
32
|
+
contentType: file.type,
|
|
33
|
+
};
|
|
34
|
+
})) : undefined;
|
|
35
|
+
return {
|
|
36
|
+
to: email.to,
|
|
37
|
+
cc: email.cc,
|
|
38
|
+
bcc: email.bcc,
|
|
39
|
+
subject: email.subject,
|
|
40
|
+
html: email.content,
|
|
41
|
+
templateId: email.templateId,
|
|
42
|
+
attachments,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const handleSend = async (email) => {
|
|
46
|
+
try {
|
|
47
|
+
const emailData = await convertToEmailData(email);
|
|
48
|
+
await emailManager.sendEmail(emailData);
|
|
49
|
+
// Show success notification
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.error('Failed to send email:', error);
|
|
53
|
+
// Show error notification
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const handleSchedule = async (email) => {
|
|
57
|
+
try {
|
|
58
|
+
if (email.scheduleTime) {
|
|
59
|
+
const emailData = await convertToEmailData(email);
|
|
60
|
+
await emailManager.scheduleEmail(emailData, email.scheduleTime);
|
|
61
|
+
// Show success notification
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error('Failed to schedule email:', error);
|
|
66
|
+
// Show error notification
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const handleSaveDraft = async (email) => {
|
|
70
|
+
try {
|
|
71
|
+
// Save draft functionality would be implemented here
|
|
72
|
+
console.log('Saving draft:', email);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error('Failed to save draft:', error);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return (_jsxs("div", { className: "compose-page", children: [_jsxs("div", { className: "page-header", children: [_jsx("h2", { children: "Compose Email" }), _jsx("p", { children: "Create and send emails using templates or compose from scratch" })] }), _jsx(EmailComposer, { templates: templates || [], onSend: handleSend, onSchedule: handleSchedule, onSaveDraft: handleSaveDraft, isLoading: templatesLoading, className: "compose-page__composer" })] }));
|
|
79
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { EmailManager } from '@bernierllc/email-manager';
|
|
3
|
+
interface SchedulePageProps {
|
|
4
|
+
emailManager: EmailManager;
|
|
5
|
+
}
|
|
6
|
+
export declare const SchedulePage: React.FC<SchedulePageProps>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=SchedulePage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SchedulePage.d.ts","sourceRoot":"","sources":["../../src/pages/SchedulePage.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,UAAU,iBAAiB;IACzB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA6MpD,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
11
|
+
import { format } from 'date-fns';
|
|
12
|
+
export const SchedulePage = ({ emailManager }) => {
|
|
13
|
+
const [statusFilter, setStatusFilter] = useState('all');
|
|
14
|
+
const queryClient = useQueryClient();
|
|
15
|
+
// Note: EmailManager doesn't currently expose getScheduledEmails() method
|
|
16
|
+
// This would need to be added to the EmailScheduler manager and exposed via EmailManager
|
|
17
|
+
const { data: scheduledEmails, isLoading } = useQuery({
|
|
18
|
+
queryKey: ['scheduled-emails', statusFilter],
|
|
19
|
+
queryFn: async () => {
|
|
20
|
+
// Stubbed for now - would need EmailManager.getScheduledEmails() implementation
|
|
21
|
+
return [];
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const cancelEmailMutation = useMutation({
|
|
25
|
+
mutationFn: (emailId) => emailManager.cancelScheduledEmail(emailId),
|
|
26
|
+
onSuccess: () => {
|
|
27
|
+
queryClient.invalidateQueries({ queryKey: ['scheduled-emails'] });
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const rescheduleEmailMutation = useMutation({
|
|
31
|
+
mutationFn: async ({ emailId, newTime }) => {
|
|
32
|
+
// Note: EmailManager doesn't have rescheduleEmail method
|
|
33
|
+
// Workaround: cancel and reschedule
|
|
34
|
+
await emailManager.cancelScheduledEmail(emailId);
|
|
35
|
+
// Would need to store original email data to reschedule
|
|
36
|
+
// This is a limitation of the current API
|
|
37
|
+
return { success: true };
|
|
38
|
+
},
|
|
39
|
+
onSuccess: () => {
|
|
40
|
+
queryClient.invalidateQueries({ queryKey: ['scheduled-emails'] });
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const handleCancelEmail = (emailId) => {
|
|
44
|
+
if (window.confirm('Are you sure you want to cancel this scheduled email?')) {
|
|
45
|
+
cancelEmailMutation.mutate(emailId);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const handleReschedule = (emailId) => {
|
|
49
|
+
const newTimeStr = prompt('Enter new date and time (YYYY-MM-DD HH:MM):');
|
|
50
|
+
if (newTimeStr) {
|
|
51
|
+
const newTime = new Date(newTimeStr);
|
|
52
|
+
if (!isNaN(newTime.getTime()) && newTime > new Date()) {
|
|
53
|
+
rescheduleEmailMutation.mutate({ emailId, newTime });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
alert('Please enter a valid future date and time.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const getStatusColor = (status) => {
|
|
61
|
+
switch (status) {
|
|
62
|
+
case 'pending': return 'text-yellow-600';
|
|
63
|
+
case 'sent': return 'text-green-600';
|
|
64
|
+
case 'failed': return 'text-red-600';
|
|
65
|
+
case 'cancelled': return 'text-gray-600';
|
|
66
|
+
default: return 'text-gray-600';
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const getStatusIcon = (status) => {
|
|
70
|
+
switch (status) {
|
|
71
|
+
case 'pending': return '⏳';
|
|
72
|
+
case 'sent': return '✅';
|
|
73
|
+
case 'failed': return '❌';
|
|
74
|
+
case 'cancelled': return '🚫';
|
|
75
|
+
default: return '❓';
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return (_jsxs("div", { className: "schedule-page", children: [_jsxs("div", { className: "page-header", children: [_jsx("h2", { children: "Scheduled Emails" }), _jsx("div", { className: "page-header__controls", children: _jsxs("div", { className: "filter-controls", children: [_jsx("label", { htmlFor: "status-filter", children: "Filter by status:" }), _jsxs("select", { id: "status-filter", value: statusFilter, onChange: (e) => setStatusFilter(e.target.value), className: "form-select", children: [_jsx("option", { value: "all", children: "All" }), _jsx("option", { value: "pending", children: "Pending" }), _jsx("option", { value: "sent", children: "Sent" }), _jsx("option", { value: "failed", children: "Failed" }), _jsx("option", { value: "cancelled", children: "Cancelled" })] })] }) })] }), isLoading ? (_jsx("div", { className: "loading-state", children: "Loading scheduled emails..." })) : scheduledEmails && scheduledEmails.length > 0 ? (_jsx("div", { className: "scheduled-emails-table", children: _jsxs("table", { className: "data-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Status" }), _jsx("th", { children: "Recipient" }), _jsx("th", { children: "Template" }), _jsx("th", { children: "Scheduled Time" }), _jsx("th", { children: "Created" }), _jsx("th", { children: "Actions" })] }) }), _jsx("tbody", { children: scheduledEmails.map((email) => (_jsxs("tr", { children: [_jsx("td", { children: _jsxs("span", { className: `status-badge ${getStatusColor(email.status)}`, children: [getStatusIcon(email.status), " ", email.status] }) }), _jsx("td", { children: email.recipientEmail }), _jsx("td", { children: email.templateId }), _jsx("td", { children: format(new Date(email.scheduledTime), 'MMM dd, yyyy HH:mm') }), _jsx("td", { children: format(new Date(email.scheduledTime), 'MMM dd, yyyy') }), _jsx("td", { children: _jsxs("div", { className: "action-buttons", children: [email.status === 'pending' && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => handleReschedule(email.id), className: "btn btn--small btn--secondary", disabled: rescheduleEmailMutation.isPending, children: "Reschedule" }), _jsx("button", { onClick: () => handleCancelEmail(email.id), className: "btn btn--small btn--danger", disabled: cancelEmailMutation.isPending, children: "Cancel" })] })), email.status === 'failed' && (_jsx("button", { onClick: () => {
|
|
79
|
+
// Retry logic would be implemented here
|
|
80
|
+
console.log('Retrying email:', email.id);
|
|
81
|
+
}, className: "btn btn--small btn--primary", children: "Retry" }))] }) })] }, email.id))) })] }) })) : (_jsxs("div", { className: "empty-state", children: [_jsx("h3", { children: "No scheduled emails" }), _jsx("p", { children: statusFilter !== 'all'
|
|
82
|
+
? `No ${statusFilter} scheduled emails found.`
|
|
83
|
+
: 'You haven\'t scheduled any emails yet.' })] })), _jsxs("div", { className: "schedule-summary", children: [_jsx("h3", { children: "Quick Statistics" }), _jsxs("div", { className: "stats-grid", children: [_jsxs("div", { className: "stat-card", children: [_jsx("div", { className: "stat-card__value", children: scheduledEmails?.filter((e) => e.status === 'pending').length || 0 }), _jsx("div", { className: "stat-card__label", children: "Pending" })] }), _jsxs("div", { className: "stat-card", children: [_jsx("div", { className: "stat-card__value", children: scheduledEmails?.filter((e) => e.status === 'sent').length || 0 }), _jsx("div", { className: "stat-card__label", children: "Sent" })] }), _jsxs("div", { className: "stat-card", children: [_jsx("div", { className: "stat-card__value", children: scheduledEmails?.filter((e) => e.status === 'failed').length || 0 }), _jsx("div", { className: "stat-card__label", children: "Failed" })] }), _jsxs("div", { className: "stat-card", children: [_jsx("div", { className: "stat-card__value", children: scheduledEmails?.filter((e) => e.status === 'cancelled').length || 0 }), _jsx("div", { className: "stat-card__label", children: "Cancelled" })] })] })] })] }));
|
|
84
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { EmailManager } from '@bernierllc/email-manager';
|
|
3
|
+
interface TemplatesPageProps {
|
|
4
|
+
emailManager: EmailManager;
|
|
5
|
+
}
|
|
6
|
+
export declare const TemplatesPage: React.FC<TemplatesPageProps>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=TemplatesPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplatesPage.d.ts","sourceRoot":"","sources":["../../src/pages/TemplatesPage.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAIzD,UAAU,kBAAkB;IAC1B,YAAY,EAAE,YAAY,CAAC;CAC5B;AAeD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAkMtD,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
11
|
+
import { TemplateEditor } from '@bernierllc/email-ui';
|
|
12
|
+
const convertTemplateToUI = (template) => ({
|
|
13
|
+
id: template.id,
|
|
14
|
+
name: template.name,
|
|
15
|
+
subject: template.subject,
|
|
16
|
+
content: template.htmlTemplate,
|
|
17
|
+
variables: template.variables.map(v => v.name),
|
|
18
|
+
category: template.category,
|
|
19
|
+
tags: [], // email-manager doesn't have tags field
|
|
20
|
+
createdAt: template.createdAt,
|
|
21
|
+
updatedAt: template.updatedAt,
|
|
22
|
+
isActive: template.isActive,
|
|
23
|
+
});
|
|
24
|
+
export const TemplatesPage = ({ emailManager }) => {
|
|
25
|
+
const [selectedTemplate, setSelectedTemplate] = useState(null);
|
|
26
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
27
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
const { data: templates, isLoading } = useQuery({
|
|
30
|
+
queryKey: ['templates'],
|
|
31
|
+
queryFn: async () => {
|
|
32
|
+
const result = await emailManager.listTemplates();
|
|
33
|
+
return result.templates.map(convertTemplateToUI);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const createTemplateMutation = useMutation({
|
|
37
|
+
mutationFn: (templateData) => emailManager.createTemplate(templateData),
|
|
38
|
+
onSuccess: () => {
|
|
39
|
+
queryClient.invalidateQueries({ queryKey: ['templates'] });
|
|
40
|
+
setIsEditing(false);
|
|
41
|
+
setSelectedTemplate(null);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const updateTemplateMutation = useMutation({
|
|
45
|
+
mutationFn: ({ id, data }) => emailManager.updateTemplate(id, data),
|
|
46
|
+
onSuccess: () => {
|
|
47
|
+
queryClient.invalidateQueries({ queryKey: ['templates'] });
|
|
48
|
+
setIsEditing(false);
|
|
49
|
+
setSelectedTemplate(null);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const deleteTemplateMutation = useMutation({
|
|
53
|
+
mutationFn: (id) => emailManager.deleteTemplate(id),
|
|
54
|
+
onSuccess: () => {
|
|
55
|
+
queryClient.invalidateQueries({ queryKey: ['templates'] });
|
|
56
|
+
setSelectedTemplate(null);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const filteredTemplates = templates?.filter((template) => template.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
60
|
+
template.subject.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
61
|
+
template.category?.toLowerCase().includes(searchTerm.toLowerCase())) || [];
|
|
62
|
+
const handleSaveTemplate = async (templateData) => {
|
|
63
|
+
if (selectedTemplate?.id) {
|
|
64
|
+
await updateTemplateMutation.mutateAsync({
|
|
65
|
+
id: selectedTemplate.id,
|
|
66
|
+
data: templateData,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
await createTemplateMutation.mutateAsync(templateData);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const handleDeleteTemplate = (id) => {
|
|
74
|
+
if (window.confirm('Are you sure you want to delete this template?')) {
|
|
75
|
+
deleteTemplateMutation.mutate(id);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const handleNewTemplate = () => {
|
|
79
|
+
setSelectedTemplate(null);
|
|
80
|
+
setIsEditing(true);
|
|
81
|
+
};
|
|
82
|
+
const handleEditTemplate = (template) => {
|
|
83
|
+
setSelectedTemplate(template);
|
|
84
|
+
setIsEditing(true);
|
|
85
|
+
};
|
|
86
|
+
if (isEditing) {
|
|
87
|
+
return (_jsxs("div", { className: "templates-page", children: [_jsx("div", { className: "page-header", children: _jsx("h2", { children: selectedTemplate ? 'Edit Template' : 'Create Template' }) }), _jsx(TemplateEditor, { template: selectedTemplate || undefined, onSave: handleSaveTemplate, onCancel: () => {
|
|
88
|
+
setIsEditing(false);
|
|
89
|
+
setSelectedTemplate(null);
|
|
90
|
+
}, isLoading: createTemplateMutation.isPending || updateTemplateMutation.isPending })] }));
|
|
91
|
+
}
|
|
92
|
+
return (_jsxs("div", { className: "templates-page", children: [_jsxs("div", { className: "page-header", children: [_jsx("h2", { children: "Email Templates" }), _jsxs("div", { className: "page-header__actions", children: [_jsx("div", { className: "search-bar", children: _jsx("input", { type: "text", placeholder: "Search templates...", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), className: "form-input" }) }), _jsx("button", { onClick: handleNewTemplate, className: "btn btn--primary", children: "New Template" })] })] }), isLoading ? (_jsx("div", { className: "loading-state", children: "Loading templates..." })) : (_jsx("div", { className: "templates-grid", children: filteredTemplates.map((template) => (_jsxs("div", { className: "template-card", children: [_jsxs("div", { className: "template-card__header", children: [_jsx("h3", { className: "template-card__title", children: template.name }), _jsxs("div", { className: "template-card__actions", children: [_jsx("button", { onClick: () => handleEditTemplate(template), className: "btn btn--small btn--secondary", children: "Edit" }), _jsx("button", { onClick: () => handleDeleteTemplate(template.id), className: "btn btn--small btn--danger", children: "Delete" })] })] }), _jsxs("div", { className: "template-card__content", children: [_jsx("p", { className: "template-card__subject", children: template.subject }), template.category && (_jsx("span", { className: "template-card__category", children: template.category })), _jsx("div", { className: "template-card__tags", children: template.tags?.map((tag) => (_jsx("span", { className: "tag", children: tag }, tag))) }), _jsxs("div", { className: "template-card__variables", children: [_jsx("strong", { children: "Variables:" }), template.variables.length > 0 ? (_jsx("span", { children: template.variables.join(', ') })) : (_jsx("span", { className: "text-muted", children: "None" }))] })] }), _jsxs("div", { className: "template-card__footer", children: [_jsx("span", { className: "template-card__status", children: template.isActive ? '✅ Active' : '❌ Inactive' }), _jsxs("span", { className: "template-card__date", children: ["Updated ", new Date(template.updatedAt).toLocaleDateString()] })] })] }, template.id))) })), filteredTemplates.length === 0 && !isLoading && (_jsxs("div", { className: "empty-state", children: [_jsx("h3", { children: "No templates found" }), _jsx("p", { children: searchTerm
|
|
93
|
+
? 'No templates match your search criteria.'
|
|
94
|
+
: 'Create your first email template to get started.' }), _jsx("button", { onClick: handleNewTemplate, className: "btn btn--primary", children: "Create Template" })] }))] }));
|
|
95
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pages/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
export { ComposePage } from './ComposePage';
|
|
9
|
+
export { TemplatesPage } from './TemplatesPage';
|
|
10
|
+
export { AnalyticsPage } from './AnalyticsPage';
|
|
11
|
+
export { SchedulePage } from './SchedulePage';
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bernierllc/email-admin-suite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Complete email administration suite with UI components, templates, scheduling, and analytics management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "tsc --watch",
|
|
11
|
+
"test": "jest",
|
|
12
|
+
"test:watch": "jest --watch",
|
|
13
|
+
"test:coverage": "jest --coverage",
|
|
14
|
+
"lint": "eslint src/**/*.{ts,tsx}",
|
|
15
|
+
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
|
|
16
|
+
"clean": "rimraf dist",
|
|
17
|
+
"prebuild": "npm run clean",
|
|
18
|
+
"storybook": "storybook dev -p 6007",
|
|
19
|
+
"build-storybook": "storybook build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"email",
|
|
23
|
+
"admin",
|
|
24
|
+
"suite",
|
|
25
|
+
"management",
|
|
26
|
+
"ui",
|
|
27
|
+
"templates",
|
|
28
|
+
"scheduling",
|
|
29
|
+
"analytics",
|
|
30
|
+
"bernierllc"
|
|
31
|
+
],
|
|
32
|
+
"author": "Bernier LLC",
|
|
33
|
+
"license": "Bernier LLC",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@bernierllc/email-manager": "^0.1.3",
|
|
36
|
+
"@bernierllc/email-ui": "^0.1.0",
|
|
37
|
+
"react": "^18.2.0",
|
|
38
|
+
"react-dom": "^18.2.0",
|
|
39
|
+
"react-router-dom": "^6.15.0",
|
|
40
|
+
"react-query": "^3.39.0",
|
|
41
|
+
"@tanstack/react-query": "^4.32.0",
|
|
42
|
+
"@headlessui/react": "^1.7.0",
|
|
43
|
+
"@heroicons/react": "^2.0.0",
|
|
44
|
+
"clsx": "^2.0.0",
|
|
45
|
+
"date-fns": "^2.30.0",
|
|
46
|
+
"zod": "^3.22.4"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": "^18.0.0",
|
|
50
|
+
"react-dom": "^18.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/react": "^18.2.0",
|
|
54
|
+
"@types/react-dom": "^18.2.0",
|
|
55
|
+
"@types/jest": "^29.5.5",
|
|
56
|
+
"@types/node": "^20.6.0",
|
|
57
|
+
"@testing-library/react": "^14.0.0",
|
|
58
|
+
"@testing-library/jest-dom": "^6.1.0",
|
|
59
|
+
"@testing-library/user-event": "^14.4.0",
|
|
60
|
+
"@storybook/react": "^7.4.0",
|
|
61
|
+
"@storybook/addon-essentials": "^7.4.0",
|
|
62
|
+
"jest": "^29.6.4",
|
|
63
|
+
"jest-environment-jsdom": "^29.6.4",
|
|
64
|
+
"ts-jest": "^29.1.1",
|
|
65
|
+
"typescript": "^5.2.2",
|
|
66
|
+
"eslint": "^8.50.0",
|
|
67
|
+
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
|
68
|
+
"@typescript-eslint/parser": "^6.7.0",
|
|
69
|
+
"eslint-plugin-react": "^7.33.0",
|
|
70
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
71
|
+
"rimraf": "^5.0.1"
|
|
72
|
+
},
|
|
73
|
+
"files": [
|
|
74
|
+
"dist/**/*",
|
|
75
|
+
"README.md",
|
|
76
|
+
"LICENSE"
|
|
77
|
+
],
|
|
78
|
+
"repository": {
|
|
79
|
+
"type": "git",
|
|
80
|
+
"url": "git+https://github.com/bernier-llc/tools.git",
|
|
81
|
+
"directory": "packages/suite/email-admin-suite"
|
|
82
|
+
},
|
|
83
|
+
"publishConfig": {
|
|
84
|
+
"access": "restricted"
|
|
85
|
+
}
|
|
86
|
+
}
|