@contentgrowth/content-emailing 0.5.0 → 0.6.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/TemplateManager-Db41KyPN.d.cts +77 -0
- package/dist/TemplateManager-Db41KyPN.d.ts +77 -0
- package/dist/backend/EmailService.cjs +18 -9
- package/dist/backend/EmailService.cjs.map +1 -1
- package/dist/backend/EmailService.d.cts +101 -0
- package/dist/backend/EmailService.d.ts +101 -0
- package/dist/backend/EmailService.js +18 -9
- package/dist/backend/EmailService.js.map +1 -1
- package/dist/backend/EmailingCacheDO.cjs +0 -1
- package/dist/backend/EmailingCacheDO.cjs.map +1 -1
- package/dist/backend/EmailingCacheDO.d.cts +66 -0
- package/dist/backend/EmailingCacheDO.d.ts +66 -0
- package/dist/backend/EmailingCacheDO.js +0 -1
- package/dist/backend/EmailingCacheDO.js.map +1 -1
- package/dist/backend/routes/index.cjs +18 -9
- package/dist/backend/routes/index.cjs.map +1 -1
- package/dist/backend/routes/index.d.cts +32 -0
- package/dist/backend/routes/index.d.ts +32 -0
- package/dist/backend/routes/index.js +18 -9
- package/dist/backend/routes/index.js.map +1 -1
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/common/index.d.cts +46 -0
- package/dist/common/index.d.ts +46 -0
- package/dist/frontend/index.cjs +665 -0
- package/dist/frontend/index.cjs.map +1 -0
- package/dist/frontend/index.d.cts +32 -0
- package/dist/frontend/index.d.ts +32 -0
- package/dist/frontend/index.js +626 -0
- package/dist/frontend/index.js.map +1 -0
- package/dist/index.cjs +413 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +414 -109
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
// src/frontend/TemplateManager.tsx
|
|
2
|
+
import React3, { useState as useState3, useEffect as useEffect2, useCallback, useMemo as useMemo2 } from "react";
|
|
3
|
+
import { marked as marked2 } from "marked";
|
|
4
|
+
|
|
5
|
+
// src/frontend/TemplateEditor.tsx
|
|
6
|
+
import React, { useState, useMemo } from "react";
|
|
7
|
+
import { marked } from "marked";
|
|
8
|
+
var DEFAULT_TYPES = ["notification", "auth", "marketing", "system", "invitation", "verification"];
|
|
9
|
+
var normalizeNewlines = (text) => {
|
|
10
|
+
return text.replace(/\\n/g, "\n");
|
|
11
|
+
};
|
|
12
|
+
var TemplateEditor = ({
|
|
13
|
+
initialData,
|
|
14
|
+
onSave,
|
|
15
|
+
onCancel,
|
|
16
|
+
templateTypes = DEFAULT_TYPES,
|
|
17
|
+
saving = false
|
|
18
|
+
}) => {
|
|
19
|
+
const [activeTab, setActiveTab] = useState("basic");
|
|
20
|
+
const [showPreview, setShowPreview] = useState(false);
|
|
21
|
+
const [formData, setFormData] = useState({
|
|
22
|
+
template_id: initialData?.template_id || "",
|
|
23
|
+
template_name: initialData?.template_name || "",
|
|
24
|
+
template_type: initialData?.template_type || "notification",
|
|
25
|
+
subject_template: initialData?.subject_template || "",
|
|
26
|
+
// Normalize newlines when loading
|
|
27
|
+
body_markdown: normalizeNewlines(initialData?.body_markdown || ""),
|
|
28
|
+
description: initialData?.description || "",
|
|
29
|
+
is_active: initialData?.is_active ?? true
|
|
30
|
+
});
|
|
31
|
+
const handleSubmit = async (e) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
await onSave(formData);
|
|
34
|
+
};
|
|
35
|
+
const isEdit = !!initialData?.template_id;
|
|
36
|
+
const renderedPreview = useMemo(() => {
|
|
37
|
+
let preview = formData.body_markdown;
|
|
38
|
+
preview = preview.replace(/\{\{(\w+)\}\}/g, '<span class="bg-blue-100 text-blue-800 px-1 rounded text-sm">{{$1}}</span>');
|
|
39
|
+
return marked.parse(preview);
|
|
40
|
+
}, [formData.body_markdown]);
|
|
41
|
+
const subjectPreview = useMemo(() => {
|
|
42
|
+
return formData.subject_template.replace(/\{\{(\w+)\}\}/g, '<span class="bg-blue-100 text-blue-800 px-1 rounded">{{$1}}</span>');
|
|
43
|
+
}, [formData.subject_template]);
|
|
44
|
+
return /* @__PURE__ */ React.createElement("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "bg-white rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col" }, /* @__PURE__ */ React.createElement("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gray-50" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-gray-900" }, isEdit ? "Edit Template" : "Create New Template"), /* @__PURE__ */ React.createElement("button", { onClick: onCancel, className: "text-gray-400 hover:text-gray-600" }, /* @__PURE__ */ React.createElement("svg", { className: "w-6 h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })))), /* @__PURE__ */ React.createElement("div", { className: "border-b border-gray-200 px-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex gap-4" }, /* @__PURE__ */ React.createElement(
|
|
45
|
+
"button",
|
|
46
|
+
{
|
|
47
|
+
type: "button",
|
|
48
|
+
onClick: () => setActiveTab("basic"),
|
|
49
|
+
className: `py-3 px-1 border-b-2 text-sm font-medium transition-colors ${activeTab === "basic" ? "border-blue-600 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700"}`
|
|
50
|
+
},
|
|
51
|
+
"\u{1F4CB} Basic Info"
|
|
52
|
+
), /* @__PURE__ */ React.createElement(
|
|
53
|
+
"button",
|
|
54
|
+
{
|
|
55
|
+
type: "button",
|
|
56
|
+
onClick: () => setActiveTab("content"),
|
|
57
|
+
className: `py-3 px-1 border-b-2 text-sm font-medium transition-colors ${activeTab === "content" ? "border-blue-600 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700"}`
|
|
58
|
+
},
|
|
59
|
+
"\u2709\uFE0F Email Content"
|
|
60
|
+
))), /* @__PURE__ */ React.createElement("form", { onSubmit: handleSubmit, className: "flex-1 overflow-y-auto" }, activeTab === "basic" && /* @__PURE__ */ React.createElement("div", { className: "p-6 space-y-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "Template Name *"), /* @__PURE__ */ React.createElement(
|
|
61
|
+
"input",
|
|
62
|
+
{
|
|
63
|
+
type: "text",
|
|
64
|
+
required: true,
|
|
65
|
+
value: formData.template_name,
|
|
66
|
+
onChange: (e) => setFormData({ ...formData, template_name: e.target.value }),
|
|
67
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
|
68
|
+
placeholder: "e.g., Welcome Email"
|
|
69
|
+
}
|
|
70
|
+
)), /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-2 gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "Type *"), /* @__PURE__ */ React.createElement(
|
|
71
|
+
"select",
|
|
72
|
+
{
|
|
73
|
+
value: formData.template_type,
|
|
74
|
+
onChange: (e) => setFormData({ ...formData, template_type: e.target.value }),
|
|
75
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
76
|
+
},
|
|
77
|
+
templateTypes.map((type) => /* @__PURE__ */ React.createElement("option", { key: type, value: type }, type.charAt(0).toUpperCase() + type.slice(1)))
|
|
78
|
+
)), /* @__PURE__ */ React.createElement("div", { className: "flex items-center pt-6" }, /* @__PURE__ */ React.createElement("label", { className: "flex items-center cursor-pointer" }, /* @__PURE__ */ React.createElement(
|
|
79
|
+
"input",
|
|
80
|
+
{
|
|
81
|
+
type: "checkbox",
|
|
82
|
+
checked: formData.is_active,
|
|
83
|
+
onChange: (e) => setFormData({ ...formData, is_active: e.target.checked }),
|
|
84
|
+
className: "w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
85
|
+
}
|
|
86
|
+
), /* @__PURE__ */ React.createElement("span", { className: "ml-2 text-sm text-gray-700" }, "Active")))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "Description"), /* @__PURE__ */ React.createElement(
|
|
87
|
+
"textarea",
|
|
88
|
+
{
|
|
89
|
+
value: formData.description,
|
|
90
|
+
onChange: (e) => setFormData({ ...formData, description: e.target.value }),
|
|
91
|
+
rows: 3,
|
|
92
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
|
93
|
+
placeholder: "Brief description of when this template is used"
|
|
94
|
+
}
|
|
95
|
+
)), /* @__PURE__ */ React.createElement("div", { className: "pt-4 border-t border-gray-200" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-gray-500" }, "\u{1F4A1} ", /* @__PURE__ */ React.createElement("strong", null, "Tip:"), ' Use the "Email Content" tab to write your subject line and email body.'))), activeTab === "content" && /* @__PURE__ */ React.createElement("div", { className: "p-6 space-y-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "Subject Line *"), /* @__PURE__ */ React.createElement(
|
|
96
|
+
"input",
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
required: true,
|
|
100
|
+
value: formData.subject_template,
|
|
101
|
+
onChange: (e) => setFormData({ ...formData, subject_template: e.target.value }),
|
|
102
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm",
|
|
103
|
+
placeholder: "e.g., Welcome to {{app_name}}, {{user_name}}!"
|
|
104
|
+
}
|
|
105
|
+
), formData.subject_template && /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-gray-600" }, /* @__PURE__ */ React.createElement("span", { className: "text-gray-500" }, "Preview: "), /* @__PURE__ */ React.createElement("span", { dangerouslySetInnerHTML: { __html: subjectPreview } }))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between mb-2" }, /* @__PURE__ */ React.createElement("label", { className: "block text-sm font-medium text-gray-700" }, "Email Body (Markdown) *"), /* @__PURE__ */ React.createElement(
|
|
106
|
+
"button",
|
|
107
|
+
{
|
|
108
|
+
type: "button",
|
|
109
|
+
onClick: () => setShowPreview(!showPreview),
|
|
110
|
+
className: `px-3 py-1 text-xs font-medium rounded-full transition-colors ${showPreview ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-700 hover:bg-gray-300"}`
|
|
111
|
+
},
|
|
112
|
+
showPreview ? "\u270F\uFE0F Edit" : "\u{1F441}\uFE0F Preview"
|
|
113
|
+
)), !showPreview ? /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(
|
|
114
|
+
"textarea",
|
|
115
|
+
{
|
|
116
|
+
required: true,
|
|
117
|
+
value: formData.body_markdown,
|
|
118
|
+
onChange: (e) => setFormData({ ...formData, body_markdown: e.target.value }),
|
|
119
|
+
rows: 14,
|
|
120
|
+
className: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm",
|
|
121
|
+
placeholder: "# Welcome, {{user_name}}!\n\nThank you for signing up for **{{app_name}}**.\n\nWe're excited to have you on board!\n\n---\n\nBest regards,\nThe Team"
|
|
122
|
+
}
|
|
123
|
+
), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs text-gray-500" }, "Use ", "{{variable}}", " for dynamic content. Supports Markdown formatting.")) : /* @__PURE__ */ React.createElement(
|
|
124
|
+
"div",
|
|
125
|
+
{
|
|
126
|
+
className: "w-full min-h-[350px] p-4 border border-blue-200 rounded-lg bg-blue-50 prose prose-sm max-w-none overflow-y-auto",
|
|
127
|
+
dangerouslySetInnerHTML: { __html: renderedPreview || '<p class="text-gray-400">No content to preview</p>' }
|
|
128
|
+
}
|
|
129
|
+
)))), /* @__PURE__ */ React.createElement("div", { className: "px-6 py-4 border-t border-gray-200 flex justify-between items-center bg-gray-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm text-gray-500" }, activeTab === "basic" && "Step 1 of 2", activeTab === "content" && "Step 2 of 2"), /* @__PURE__ */ React.createElement("div", { className: "flex gap-3" }, /* @__PURE__ */ React.createElement(
|
|
130
|
+
"button",
|
|
131
|
+
{
|
|
132
|
+
type: "button",
|
|
133
|
+
onClick: onCancel,
|
|
134
|
+
className: "px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 border border-gray-300 rounded-lg hover:bg-gray-100"
|
|
135
|
+
},
|
|
136
|
+
"Cancel"
|
|
137
|
+
), /* @__PURE__ */ React.createElement(
|
|
138
|
+
"button",
|
|
139
|
+
{
|
|
140
|
+
type: "submit",
|
|
141
|
+
onClick: handleSubmit,
|
|
142
|
+
disabled: saving || !formData.template_name || !formData.subject_template || !formData.body_markdown,
|
|
143
|
+
className: "px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
144
|
+
},
|
|
145
|
+
saving ? "Saving..." : isEdit ? "Save Changes" : "Create Template"
|
|
146
|
+
)))));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/frontend/TemplateTester.tsx
|
|
150
|
+
import React2, { useState as useState2, useEffect } from "react";
|
|
151
|
+
var TemplateTester = ({
|
|
152
|
+
template,
|
|
153
|
+
onSendTest,
|
|
154
|
+
onCancel,
|
|
155
|
+
sending = false
|
|
156
|
+
}) => {
|
|
157
|
+
const [toEmail, setToEmail] = useState2("");
|
|
158
|
+
const [variables, setVariables] = useState2({});
|
|
159
|
+
const [detectedVariables, setDetectedVariables] = useState2([]);
|
|
160
|
+
const [error, setError] = useState2(null);
|
|
161
|
+
const [success, setSuccess] = useState2(null);
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const regex = /\{\{(\w+)\}\}/g;
|
|
164
|
+
const subjectVars = Array.from(template.subject_template.matchAll(regex)).map((m) => m[1]);
|
|
165
|
+
const bodyVars = Array.from(template.body_markdown.matchAll(regex)).map((m) => m[1]);
|
|
166
|
+
const uniqueVars = Array.from(/* @__PURE__ */ new Set([...subjectVars, ...bodyVars]));
|
|
167
|
+
setDetectedVariables(uniqueVars);
|
|
168
|
+
const initialValues = {};
|
|
169
|
+
uniqueVars.forEach((v) => {
|
|
170
|
+
initialValues[v] = "";
|
|
171
|
+
});
|
|
172
|
+
setVariables(initialValues);
|
|
173
|
+
}, [template]);
|
|
174
|
+
const handleSubmit = async (e) => {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
setError(null);
|
|
177
|
+
setSuccess(null);
|
|
178
|
+
try {
|
|
179
|
+
await onSendTest({
|
|
180
|
+
template_id: template.template_id,
|
|
181
|
+
to_email: toEmail,
|
|
182
|
+
variables
|
|
183
|
+
});
|
|
184
|
+
setSuccess("Test email sent successfully!");
|
|
185
|
+
} catch (e2) {
|
|
186
|
+
setError(e2.message || "Failed to send test email");
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
return /* @__PURE__ */ React2.createElement("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" }, /* @__PURE__ */ React2.createElement("div", { className: "bg-white rounded-xl shadow-2xl max-w-lg w-full overflow-hidden flex flex-col max-h-[90vh]" }, /* @__PURE__ */ React2.createElement("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gray-50" }, /* @__PURE__ */ React2.createElement("h2", { className: "text-lg font-semibold text-gray-900" }, "Test Template: ", template.template_name), /* @__PURE__ */ React2.createElement("button", { onClick: onCancel, className: "text-gray-400 hover:text-gray-600" }, /* @__PURE__ */ React2.createElement("svg", { className: "w-6 h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React2.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })))), /* @__PURE__ */ React2.createElement("form", { onSubmit: handleSubmit, className: "flex-1 overflow-y-auto p-6 space-y-4" }, error && /* @__PURE__ */ React2.createElement("div", { className: "p-3 rounded-md text-sm bg-red-50 text-red-800 border border-red-200" }, error), success && /* @__PURE__ */ React2.createElement("div", { className: "p-3 rounded-md text-sm bg-green-50 text-green-800 border border-green-200" }, success), /* @__PURE__ */ React2.createElement("div", null, /* @__PURE__ */ React2.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "To Email *"), /* @__PURE__ */ React2.createElement(
|
|
190
|
+
"input",
|
|
191
|
+
{
|
|
192
|
+
type: "email",
|
|
193
|
+
required: true,
|
|
194
|
+
value: toEmail,
|
|
195
|
+
onChange: (e) => setToEmail(e.target.value),
|
|
196
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
|
197
|
+
placeholder: "recipient@example.com"
|
|
198
|
+
}
|
|
199
|
+
)), detectedVariables.length > 0 && /* @__PURE__ */ React2.createElement("div", { className: "pt-2" }, /* @__PURE__ */ React2.createElement("h3", { className: "text-sm font-medium text-gray-900 mb-3 pb-2 border-b border-gray-100" }, "Template Variables"), /* @__PURE__ */ React2.createElement("div", { className: "space-y-3" }, detectedVariables.map((variable) => /* @__PURE__ */ React2.createElement("div", { key: variable }, /* @__PURE__ */ React2.createElement("label", { className: "block text-xs font-medium text-gray-500 uppercase mb-1" }, variable), /* @__PURE__ */ React2.createElement(
|
|
200
|
+
"input",
|
|
201
|
+
{
|
|
202
|
+
type: "text",
|
|
203
|
+
value: variables[variable] || "",
|
|
204
|
+
onChange: (e) => setVariables({ ...variables, [variable]: e.target.value }),
|
|
205
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm",
|
|
206
|
+
placeholder: `Value for {{${variable}}}`
|
|
207
|
+
}
|
|
208
|
+
))))), detectedVariables.length === 0 && /* @__PURE__ */ React2.createElement("p", { className: "text-sm text-gray-500 italic" }, "No variables detected in this template.")), /* @__PURE__ */ React2.createElement("div", { className: "px-6 py-4 border-t border-gray-200 flex justify-end gap-3 bg-gray-50" }, /* @__PURE__ */ React2.createElement(
|
|
209
|
+
"button",
|
|
210
|
+
{
|
|
211
|
+
type: "button",
|
|
212
|
+
onClick: onCancel,
|
|
213
|
+
className: "px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-100"
|
|
214
|
+
},
|
|
215
|
+
"Close"
|
|
216
|
+
), /* @__PURE__ */ React2.createElement(
|
|
217
|
+
"button",
|
|
218
|
+
{
|
|
219
|
+
type: "submit",
|
|
220
|
+
onClick: handleSubmit,
|
|
221
|
+
disabled: sending || !toEmail || success != null,
|
|
222
|
+
className: "px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
223
|
+
},
|
|
224
|
+
sending ? /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement("div", { className: "animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent" }), "Sending...") : /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React2.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8" })), "Send Test Email")
|
|
225
|
+
))));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/frontend/TemplateManager.tsx
|
|
229
|
+
var TYPE_COLORS = {
|
|
230
|
+
"auth": { bg: "bg-blue-100", text: "text-blue-800" },
|
|
231
|
+
"notification": { bg: "bg-purple-100", text: "text-purple-800" },
|
|
232
|
+
"marketing": { bg: "bg-green-100", text: "text-green-800" },
|
|
233
|
+
"system": { bg: "bg-gray-100", text: "text-gray-800" },
|
|
234
|
+
"invitation": { bg: "bg-orange-100", text: "text-orange-800" },
|
|
235
|
+
"verification": { bg: "bg-cyan-100", text: "text-cyan-800" },
|
|
236
|
+
"default": { bg: "bg-gray-100", text: "text-gray-600" }
|
|
237
|
+
};
|
|
238
|
+
var normalizeNewlines2 = (text) => {
|
|
239
|
+
return text?.replace(/\\n/g, "\n") || "";
|
|
240
|
+
};
|
|
241
|
+
var TemplateManager = ({
|
|
242
|
+
onLoadTemplates,
|
|
243
|
+
onSaveTemplate,
|
|
244
|
+
onDeleteTemplate,
|
|
245
|
+
onSendTestEmail,
|
|
246
|
+
title = "Email Templates",
|
|
247
|
+
description = "Manage system email templates",
|
|
248
|
+
templateTypes
|
|
249
|
+
}) => {
|
|
250
|
+
const [templates, setTemplates] = useState3([]);
|
|
251
|
+
const [loading, setLoading] = useState3(true);
|
|
252
|
+
const [error, setError] = useState3(null);
|
|
253
|
+
const [filterType, setFilterType] = useState3("all");
|
|
254
|
+
const [previewTemplate, setPreviewTemplate] = useState3(null);
|
|
255
|
+
const [editingTemplate, setEditingTemplate] = useState3(null);
|
|
256
|
+
const [testingTemplate, setTestingTemplate] = useState3(null);
|
|
257
|
+
const [showEditor, setShowEditor] = useState3(false);
|
|
258
|
+
const [showTester, setShowTester] = useState3(false);
|
|
259
|
+
const [saving, setSaving] = useState3(false);
|
|
260
|
+
const [deleteConfirm, setDeleteConfirm] = useState3(null);
|
|
261
|
+
const loadTemplates = useCallback(async () => {
|
|
262
|
+
setLoading(true);
|
|
263
|
+
setError(null);
|
|
264
|
+
try {
|
|
265
|
+
const data = await onLoadTemplates();
|
|
266
|
+
setTemplates(data);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
setError(e.message || "Failed to load templates");
|
|
269
|
+
} finally {
|
|
270
|
+
setLoading(false);
|
|
271
|
+
}
|
|
272
|
+
}, [onLoadTemplates]);
|
|
273
|
+
useEffect2(() => {
|
|
274
|
+
loadTemplates();
|
|
275
|
+
}, [loadTemplates]);
|
|
276
|
+
const openCreate = () => {
|
|
277
|
+
setEditingTemplate(null);
|
|
278
|
+
setShowEditor(true);
|
|
279
|
+
};
|
|
280
|
+
const openPreview = (template) => {
|
|
281
|
+
setPreviewTemplate(template);
|
|
282
|
+
};
|
|
283
|
+
const openEdit = (template) => {
|
|
284
|
+
setPreviewTemplate(null);
|
|
285
|
+
setEditingTemplate(template);
|
|
286
|
+
setShowEditor(true);
|
|
287
|
+
};
|
|
288
|
+
const openTester = (template) => {
|
|
289
|
+
setTestingTemplate(template);
|
|
290
|
+
setShowTester(true);
|
|
291
|
+
};
|
|
292
|
+
const handleSave = async (data) => {
|
|
293
|
+
setSaving(true);
|
|
294
|
+
try {
|
|
295
|
+
await onSaveTemplate(data);
|
|
296
|
+
setShowEditor(false);
|
|
297
|
+
await loadTemplates();
|
|
298
|
+
} catch (e) {
|
|
299
|
+
setSaving(false);
|
|
300
|
+
throw e;
|
|
301
|
+
}
|
|
302
|
+
setSaving(false);
|
|
303
|
+
};
|
|
304
|
+
const handleDelete = async (id) => {
|
|
305
|
+
if (!onDeleteTemplate) return;
|
|
306
|
+
try {
|
|
307
|
+
await onDeleteTemplate(id);
|
|
308
|
+
setDeleteConfirm(null);
|
|
309
|
+
await loadTemplates();
|
|
310
|
+
} catch (e) {
|
|
311
|
+
setError(e.message || "Failed to delete template");
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const handleTest = async (data) => {
|
|
315
|
+
if (!onSendTestEmail) return;
|
|
316
|
+
setSaving(true);
|
|
317
|
+
try {
|
|
318
|
+
await onSendTestEmail(data);
|
|
319
|
+
} finally {
|
|
320
|
+
setSaving(false);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
const getTypeColor = (type) => TYPE_COLORS[type] || TYPE_COLORS["default"];
|
|
324
|
+
const uniqueTypes = ["all", ...new Set(templates.map((t) => t.template_type))];
|
|
325
|
+
const filteredTemplates = filterType === "all" ? templates : templates.filter((t) => t.template_type === filterType);
|
|
326
|
+
const formatDate = (timestamp) => {
|
|
327
|
+
if (!timestamp) return "N/A";
|
|
328
|
+
return new Date(timestamp * 1e3).toLocaleDateString("en-US", {
|
|
329
|
+
month: "short",
|
|
330
|
+
day: "numeric",
|
|
331
|
+
year: "numeric"
|
|
332
|
+
});
|
|
333
|
+
};
|
|
334
|
+
const previewBody = useMemo2(() => {
|
|
335
|
+
if (!previewTemplate?.body_markdown) return "";
|
|
336
|
+
let md = normalizeNewlines2(previewTemplate.body_markdown);
|
|
337
|
+
md = md.replace(/\{\{(\w+)\}\}/g, '<span class="bg-blue-100 text-blue-800 px-1 rounded text-sm">{{$1}}</span>');
|
|
338
|
+
return marked2.parse(md);
|
|
339
|
+
}, [previewTemplate]);
|
|
340
|
+
if (loading) {
|
|
341
|
+
return /* @__PURE__ */ React3.createElement("div", { className: "flex items-center justify-center h-64" }, /* @__PURE__ */ React3.createElement("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" }));
|
|
342
|
+
}
|
|
343
|
+
return /* @__PURE__ */ React3.createElement("div", { className: "template-manager" }, /* @__PURE__ */ React3.createElement("div", { className: "flex justify-between items-center mb-6" }, /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("h1", { className: "text-2xl font-bold text-gray-900" }, title), /* @__PURE__ */ React3.createElement("p", { className: "text-sm text-gray-500 mt-1" }, description)), /* @__PURE__ */ React3.createElement(
|
|
344
|
+
"button",
|
|
345
|
+
{
|
|
346
|
+
onClick: openCreate,
|
|
347
|
+
className: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2 shadow-sm"
|
|
348
|
+
},
|
|
349
|
+
/* @__PURE__ */ React3.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React3.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 4v16m8-8H4" })),
|
|
350
|
+
"Create Template"
|
|
351
|
+
)), error && /* @__PURE__ */ React3.createElement("div", { className: "mb-4 p-3 rounded-lg bg-red-50 text-red-800 border border-red-200" }, error, /* @__PURE__ */ React3.createElement("button", { onClick: () => setError(null), className: "ml-2 underline" }, "Dismiss")), /* @__PURE__ */ React3.createElement("div", { className: "grid grid-cols-4 gap-4 mb-6" }, /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-gray-900" }, templates.length), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Total")), /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-green-600" }, templates.filter((t) => t.is_active).length), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Active")), /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-yellow-600" }, templates.filter((t) => !t.is_active).length), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Inactive")), /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-blue-600" }, new Set(templates.map((t) => t.template_type)).size), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Types"))), /* @__PURE__ */ React3.createElement("div", { className: "flex gap-2 mb-6 border-b border-gray-200 pb-3 flex-wrap" }, uniqueTypes.map((type) => /* @__PURE__ */ React3.createElement(
|
|
352
|
+
"button",
|
|
353
|
+
{
|
|
354
|
+
key: type,
|
|
355
|
+
onClick: () => setFilterType(type),
|
|
356
|
+
className: `px-4 py-2 text-sm font-medium rounded-lg transition-colors ${filterType === type ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"}`
|
|
357
|
+
},
|
|
358
|
+
type.charAt(0).toUpperCase() + type.slice(1)
|
|
359
|
+
))), filteredTemplates.length === 0 ? /* @__PURE__ */ React3.createElement("div", { className: "text-center py-16 bg-gray-50 rounded-lg border-2 border-dashed border-gray-200" }, /* @__PURE__ */ React3.createElement("svg", { className: "mx-auto h-12 w-12 text-gray-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React3.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" })), /* @__PURE__ */ React3.createElement("h3", { className: "mt-4 text-lg font-medium text-gray-900" }, "No templates found"), /* @__PURE__ */ React3.createElement("p", { className: "mt-2 text-sm text-gray-500" }, filterType === "all" ? "Create your first template." : `No "${filterType}" templates.`), /* @__PURE__ */ React3.createElement("button", { onClick: openCreate, className: "mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm" }, "Create Template")) : /* @__PURE__ */ React3.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" }, filteredTemplates.map((template) => {
|
|
360
|
+
const typeColor = getTypeColor(template.template_type);
|
|
361
|
+
return /* @__PURE__ */ React3.createElement(
|
|
362
|
+
"div",
|
|
363
|
+
{
|
|
364
|
+
key: template.template_id,
|
|
365
|
+
className: "bg-white rounded-lg border border-gray-200 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer overflow-hidden flex flex-col h-full",
|
|
366
|
+
onClick: () => openPreview(template)
|
|
367
|
+
},
|
|
368
|
+
/* @__PURE__ */ React3.createElement("div", { className: "p-5 flex-1 flex flex-col" }, /* @__PURE__ */ React3.createElement("div", { className: "flex items-start justify-between mb-3" }, /* @__PURE__ */ React3.createElement("span", { className: `px-2 py-1 text-xs font-medium rounded ${typeColor.bg} ${typeColor.text}` }, template.template_type), /* @__PURE__ */ React3.createElement("span", { className: `inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${template.is_active ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-600"}` }, template.is_active ? "\u25CF Active" : "\u25CB Inactive")), /* @__PURE__ */ React3.createElement("h3", { className: "font-semibold text-gray-900 mb-1" }, template.template_name), /* @__PURE__ */ React3.createElement("p", { className: "text-sm text-gray-500 flex-1 line-clamp-2" }, template.description || "No description"), /* @__PURE__ */ React3.createElement("div", { className: "text-xs text-gray-400 border-t border-gray-100 pt-3 mt-3 truncate" }, "\u{1F4E7} ", template.subject_template)),
|
|
369
|
+
/* @__PURE__ */ React3.createElement("div", { className: "bg-gray-50 px-5 py-3 flex justify-between items-center border-t border-gray-100" }, /* @__PURE__ */ React3.createElement("span", { className: "text-xs text-gray-500" }, formatDate(template.updated_at)), /* @__PURE__ */ React3.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React3.createElement(
|
|
370
|
+
"button",
|
|
371
|
+
{
|
|
372
|
+
onClick: (e) => {
|
|
373
|
+
e.stopPropagation();
|
|
374
|
+
openEdit(template);
|
|
375
|
+
},
|
|
376
|
+
className: "p-1.5 text-blue-600 hover:bg-blue-50 rounded",
|
|
377
|
+
title: "Edit"
|
|
378
|
+
},
|
|
379
|
+
/* @__PURE__ */ React3.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React3.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" }))
|
|
380
|
+
), onSendTestEmail && /* @__PURE__ */ React3.createElement(
|
|
381
|
+
"button",
|
|
382
|
+
{
|
|
383
|
+
onClick: (e) => {
|
|
384
|
+
e.stopPropagation();
|
|
385
|
+
openTester(template);
|
|
386
|
+
},
|
|
387
|
+
className: "p-1.5 text-purple-600 hover:bg-purple-50 rounded",
|
|
388
|
+
title: "Send Test Email"
|
|
389
|
+
},
|
|
390
|
+
/* @__PURE__ */ React3.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React3.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" }))
|
|
391
|
+
), onDeleteTemplate && /* @__PURE__ */ React3.createElement(
|
|
392
|
+
"button",
|
|
393
|
+
{
|
|
394
|
+
onClick: (e) => {
|
|
395
|
+
e.stopPropagation();
|
|
396
|
+
setDeleteConfirm(template.template_id);
|
|
397
|
+
},
|
|
398
|
+
className: "p-1.5 text-red-600 hover:bg-red-50 rounded",
|
|
399
|
+
title: "Delete"
|
|
400
|
+
},
|
|
401
|
+
/* @__PURE__ */ React3.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React3.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" }))
|
|
402
|
+
)))
|
|
403
|
+
);
|
|
404
|
+
})), previewTemplate && /* @__PURE__ */ React3.createElement("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-hidden flex flex-col" }, /* @__PURE__ */ React3.createElement("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gray-50" }, /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("h2", { className: "text-lg font-semibold text-gray-900" }, previewTemplate.template_name), /* @__PURE__ */ React3.createElement("p", { className: "text-sm text-gray-500" }, previewTemplate.template_type, " \u2022 ", previewTemplate.is_active ? "Active" : "Inactive")), /* @__PURE__ */ React3.createElement("button", { onClick: () => setPreviewTemplate(null), className: "text-gray-400 hover:text-gray-600" }, /* @__PURE__ */ React3.createElement("svg", { className: "w-6 h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React3.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })))), /* @__PURE__ */ React3.createElement("div", { className: "flex-1 overflow-y-auto p-6 space-y-4" }, /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("label", { className: "block text-xs font-medium text-gray-500 uppercase mb-1" }, "Subject"), /* @__PURE__ */ React3.createElement("div", { className: "p-3 bg-gray-50 rounded-lg border border-gray-200 font-mono text-sm" }, previewTemplate.subject_template)), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("label", { className: "block text-xs font-medium text-gray-500 uppercase mb-1" }, "Email Body"), /* @__PURE__ */ React3.createElement(
|
|
405
|
+
"div",
|
|
406
|
+
{
|
|
407
|
+
className: "p-4 bg-white rounded-lg border border-gray-200 prose prose-sm max-w-none",
|
|
408
|
+
dangerouslySetInnerHTML: { __html: previewBody }
|
|
409
|
+
}
|
|
410
|
+
)), previewTemplate.description && /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("label", { className: "block text-xs font-medium text-gray-500 uppercase mb-1" }, "Description"), /* @__PURE__ */ React3.createElement("p", { className: "text-gray-600 text-sm" }, previewTemplate.description))), /* @__PURE__ */ React3.createElement("div", { className: "px-6 py-4 border-t border-gray-200 flex justify-end gap-3 bg-gray-50" }, /* @__PURE__ */ React3.createElement(
|
|
411
|
+
"button",
|
|
412
|
+
{
|
|
413
|
+
onClick: () => setPreviewTemplate(null),
|
|
414
|
+
className: "px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-100"
|
|
415
|
+
},
|
|
416
|
+
"Close"
|
|
417
|
+
), /* @__PURE__ */ React3.createElement(
|
|
418
|
+
"button",
|
|
419
|
+
{
|
|
420
|
+
onClick: () => openEdit(previewTemplate),
|
|
421
|
+
className: "px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 flex items-center gap-2"
|
|
422
|
+
},
|
|
423
|
+
/* @__PURE__ */ React3.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React3.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" })),
|
|
424
|
+
"Edit Template"
|
|
425
|
+
), onSendTestEmail && /* @__PURE__ */ React3.createElement(
|
|
426
|
+
"button",
|
|
427
|
+
{
|
|
428
|
+
onClick: () => openTester(previewTemplate),
|
|
429
|
+
className: "px-4 py-2 text-sm font-medium text-purple-700 bg-purple-100 rounded-lg hover:bg-purple-200 flex items-center gap-2"
|
|
430
|
+
},
|
|
431
|
+
/* @__PURE__ */ React3.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React3.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" })),
|
|
432
|
+
"Test"
|
|
433
|
+
)))), showEditor && /* @__PURE__ */ React3.createElement(
|
|
434
|
+
TemplateEditor,
|
|
435
|
+
{
|
|
436
|
+
initialData: editingTemplate ? {
|
|
437
|
+
template_id: editingTemplate.template_id,
|
|
438
|
+
template_name: editingTemplate.template_name,
|
|
439
|
+
template_type: editingTemplate.template_type,
|
|
440
|
+
subject_template: editingTemplate.subject_template,
|
|
441
|
+
body_markdown: editingTemplate.body_markdown,
|
|
442
|
+
description: editingTemplate.description || "",
|
|
443
|
+
is_active: !!editingTemplate.is_active
|
|
444
|
+
} : null,
|
|
445
|
+
onSave: handleSave,
|
|
446
|
+
onCancel: () => setShowEditor(false),
|
|
447
|
+
templateTypes,
|
|
448
|
+
saving
|
|
449
|
+
}
|
|
450
|
+
), showTester && testingTemplate && /* @__PURE__ */ React3.createElement(
|
|
451
|
+
TemplateTester,
|
|
452
|
+
{
|
|
453
|
+
template: testingTemplate,
|
|
454
|
+
onSendTest: handleTest,
|
|
455
|
+
onCancel: () => setShowTester(false),
|
|
456
|
+
sending: saving
|
|
457
|
+
}
|
|
458
|
+
), deleteConfirm && /* @__PURE__ */ React3.createElement("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg shadow-xl max-w-sm w-full p-6" }, /* @__PURE__ */ React3.createElement("h3", { className: "text-lg font-semibold mb-2" }, "Delete Template?"), /* @__PURE__ */ React3.createElement("p", { className: "text-gray-600 mb-4" }, "This action cannot be undone."), /* @__PURE__ */ React3.createElement("div", { className: "flex justify-end gap-3" }, /* @__PURE__ */ React3.createElement(
|
|
459
|
+
"button",
|
|
460
|
+
{
|
|
461
|
+
onClick: () => setDeleteConfirm(null),
|
|
462
|
+
className: "px-4 py-2 border border-gray-300 rounded-lg"
|
|
463
|
+
},
|
|
464
|
+
"Cancel"
|
|
465
|
+
), /* @__PURE__ */ React3.createElement(
|
|
466
|
+
"button",
|
|
467
|
+
{
|
|
468
|
+
onClick: () => handleDelete(deleteConfirm),
|
|
469
|
+
className: "px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
|
470
|
+
},
|
|
471
|
+
"Delete"
|
|
472
|
+
)))));
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// src/frontend/EmailSettings.tsx
|
|
476
|
+
import React4, { useState as useState4, useEffect as useEffect3 } from "react";
|
|
477
|
+
var PROVIDERS = [
|
|
478
|
+
{ value: "mailchannels", label: "MailChannels (Cloudflare)" },
|
|
479
|
+
{ value: "resend", label: "Resend" },
|
|
480
|
+
{ value: "sendgrid", label: "SendGrid" },
|
|
481
|
+
{ value: "sendpulse", label: "SendPulse" }
|
|
482
|
+
];
|
|
483
|
+
var EmailSettings = ({
|
|
484
|
+
onLoadSettings,
|
|
485
|
+
onSaveSettings,
|
|
486
|
+
onTestSettings,
|
|
487
|
+
title = "Email Settings",
|
|
488
|
+
description = "Configure how the system sends transactional emails."
|
|
489
|
+
}) => {
|
|
490
|
+
const [loading, setLoading] = useState4(true);
|
|
491
|
+
const [saving, setSaving] = useState4(false);
|
|
492
|
+
const [testing, setTesting] = useState4(false);
|
|
493
|
+
const [settings, setSettings] = useState4({
|
|
494
|
+
provider: "mailchannels",
|
|
495
|
+
fromName: "",
|
|
496
|
+
fromAddress: ""
|
|
497
|
+
});
|
|
498
|
+
useEffect3(() => {
|
|
499
|
+
loadData();
|
|
500
|
+
}, []);
|
|
501
|
+
const loadData = async () => {
|
|
502
|
+
setLoading(true);
|
|
503
|
+
try {
|
|
504
|
+
const data = await onLoadSettings();
|
|
505
|
+
setSettings((prev) => ({ ...prev, ...data }));
|
|
506
|
+
} catch (e) {
|
|
507
|
+
console.error("Failed to load settings", e);
|
|
508
|
+
} finally {
|
|
509
|
+
setLoading(false);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
const handleChange = (key, value) => {
|
|
513
|
+
setSettings((prev) => ({ ...prev, [key]: value }));
|
|
514
|
+
};
|
|
515
|
+
const handleSave = async () => {
|
|
516
|
+
setSaving(true);
|
|
517
|
+
try {
|
|
518
|
+
await onSaveSettings(settings);
|
|
519
|
+
} catch (e) {
|
|
520
|
+
console.error("Failed to save settings", e);
|
|
521
|
+
throw e;
|
|
522
|
+
} finally {
|
|
523
|
+
setSaving(false);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
const handleTest = async () => {
|
|
527
|
+
if (!onTestSettings) return;
|
|
528
|
+
setTesting(true);
|
|
529
|
+
try {
|
|
530
|
+
await onTestSettings(settings);
|
|
531
|
+
} catch (e) {
|
|
532
|
+
console.error("Test failed", e);
|
|
533
|
+
throw e;
|
|
534
|
+
} finally {
|
|
535
|
+
setTesting(false);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
if (loading) {
|
|
539
|
+
return /* @__PURE__ */ React4.createElement("div", { className: "flex items-center justify-center p-12" }, /* @__PURE__ */ React4.createElement("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" }));
|
|
540
|
+
}
|
|
541
|
+
return /* @__PURE__ */ React4.createElement("div", { className: "max-w-4xl p-6 space-y-8" }, /* @__PURE__ */ React4.createElement("div", null, /* @__PURE__ */ React4.createElement("h2", { className: "text-2xl font-bold text-gray-900" }, title), /* @__PURE__ */ React4.createElement("p", { className: "text-gray-500 mt-1" }, description)), /* @__PURE__ */ React4.createElement("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden" }, /* @__PURE__ */ React4.createElement("div", { className: "px-6 py-4 border-b border-gray-100 bg-gray-50" }, /* @__PURE__ */ React4.createElement("h3", { className: "text-lg font-medium text-gray-900" }, "Global Configuration"), /* @__PURE__ */ React4.createElement("p", { className: "text-sm text-gray-500" }, "Default sender identity and delivery provider.")), /* @__PURE__ */ React4.createElement("div", { className: "p-6 space-y-6" }, /* @__PURE__ */ React4.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-6" }, /* @__PURE__ */ React4.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React4.createElement("label", { className: "block text-sm font-medium text-gray-700" }, "From Name"), /* @__PURE__ */ React4.createElement(
|
|
542
|
+
"input",
|
|
543
|
+
{
|
|
544
|
+
type: "text",
|
|
545
|
+
value: settings.fromName,
|
|
546
|
+
onChange: (e) => handleChange("fromName", e.target.value),
|
|
547
|
+
placeholder: "e.g. Acme System",
|
|
548
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
549
|
+
}
|
|
550
|
+
)), /* @__PURE__ */ React4.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React4.createElement("label", { className: "block text-sm font-medium text-gray-700" }, "From Address"), /* @__PURE__ */ React4.createElement(
|
|
551
|
+
"input",
|
|
552
|
+
{
|
|
553
|
+
type: "email",
|
|
554
|
+
value: settings.fromAddress,
|
|
555
|
+
onChange: (e) => handleChange("fromAddress", e.target.value),
|
|
556
|
+
placeholder: "e.g. noreply@acme.com",
|
|
557
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
558
|
+
}
|
|
559
|
+
))), /* @__PURE__ */ React4.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React4.createElement("label", { className: "block text-sm font-medium text-gray-700" }, "Email Provider"), /* @__PURE__ */ React4.createElement(
|
|
560
|
+
"select",
|
|
561
|
+
{
|
|
562
|
+
value: settings.provider,
|
|
563
|
+
onChange: (e) => handleChange("provider", e.target.value),
|
|
564
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
|
565
|
+
},
|
|
566
|
+
PROVIDERS.map((p) => /* @__PURE__ */ React4.createElement("option", { key: p.value, value: p.value }, p.label))
|
|
567
|
+
), /* @__PURE__ */ React4.createElement("p", { className: "text-xs text-gray-500 mt-1" }, settings.provider === "mailchannels" && "MailChannels is recommended for Cloudflare Workers (requires no API key for standard usage).", settings.provider === "resend" && "Modern email API, good for transactional data.")))), settings.provider !== "mailchannels" && /* @__PURE__ */ React4.createElement("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden" }, /* @__PURE__ */ React4.createElement("div", { className: "px-6 py-4 border-b border-gray-100 bg-gray-50" }, /* @__PURE__ */ React4.createElement("h3", { className: "text-lg font-medium text-gray-900" }, PROVIDERS.find((p) => p.value === settings.provider)?.label, " Configuration")), /* @__PURE__ */ React4.createElement("div", { className: "p-6 space-y-6" }, settings.provider === "resend" && /* @__PURE__ */ React4.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React4.createElement("label", { className: "block text-sm font-medium text-gray-700" }, "API Key"), /* @__PURE__ */ React4.createElement(
|
|
568
|
+
"input",
|
|
569
|
+
{
|
|
570
|
+
type: "password",
|
|
571
|
+
value: settings.resendApiKey || "",
|
|
572
|
+
onChange: (e) => handleChange("resendApiKey", e.target.value),
|
|
573
|
+
placeholder: "re_...",
|
|
574
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
575
|
+
}
|
|
576
|
+
)), settings.provider === "sendgrid" && /* @__PURE__ */ React4.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React4.createElement("label", { className: "block text-sm font-medium text-gray-700" }, "API Key"), /* @__PURE__ */ React4.createElement(
|
|
577
|
+
"input",
|
|
578
|
+
{
|
|
579
|
+
type: "password",
|
|
580
|
+
value: settings.sendgridApiKey || "",
|
|
581
|
+
onChange: (e) => handleChange("sendgridApiKey", e.target.value),
|
|
582
|
+
placeholder: "SG...",
|
|
583
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
584
|
+
}
|
|
585
|
+
)), settings.provider === "sendpulse" && /* @__PURE__ */ React4.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-6" }, /* @__PURE__ */ React4.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React4.createElement("label", { className: "block text-sm font-medium text-gray-700" }, "Client ID"), /* @__PURE__ */ React4.createElement(
|
|
586
|
+
"input",
|
|
587
|
+
{
|
|
588
|
+
type: "text",
|
|
589
|
+
value: settings.sendpulseClientId || "",
|
|
590
|
+
onChange: (e) => handleChange("sendpulseClientId", e.target.value),
|
|
591
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
592
|
+
}
|
|
593
|
+
)), /* @__PURE__ */ React4.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React4.createElement("label", { className: "block text-sm font-medium text-gray-700" }, "Client Secret"), /* @__PURE__ */ React4.createElement(
|
|
594
|
+
"input",
|
|
595
|
+
{
|
|
596
|
+
type: "password",
|
|
597
|
+
value: settings.sendpulseClientSecret || "",
|
|
598
|
+
onChange: (e) => handleChange("sendpulseClientSecret", e.target.value),
|
|
599
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
600
|
+
}
|
|
601
|
+
))))), /* @__PURE__ */ React4.createElement("div", { className: "flex justify-end gap-3 pt-4" }, onTestSettings && /* @__PURE__ */ React4.createElement(
|
|
602
|
+
"button",
|
|
603
|
+
{
|
|
604
|
+
type: "button",
|
|
605
|
+
onClick: handleTest,
|
|
606
|
+
disabled: saving || testing,
|
|
607
|
+
className: `px-4 py-2 bg-white text-gray-700 font-medium rounded-lg border border-gray-300 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors ${saving || testing ? "opacity-70 cursor-not-allowed" : ""}`
|
|
608
|
+
},
|
|
609
|
+
testing ? "Sending..." : "Test Configuration"
|
|
610
|
+
), /* @__PURE__ */ React4.createElement(
|
|
611
|
+
"button",
|
|
612
|
+
{
|
|
613
|
+
onClick: handleSave,
|
|
614
|
+
disabled: saving || testing,
|
|
615
|
+
className: `px-6 py-2 bg-blue-600 text-white font-medium rounded-lg shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors ${saving || testing ? "opacity-70 cursor-not-allowed" : ""}`
|
|
616
|
+
},
|
|
617
|
+
saving ? "Saving..." : "Save Changes"
|
|
618
|
+
)));
|
|
619
|
+
};
|
|
620
|
+
export {
|
|
621
|
+
EmailSettings,
|
|
622
|
+
TemplateEditor,
|
|
623
|
+
TemplateManager,
|
|
624
|
+
TemplateTester
|
|
625
|
+
};
|
|
626
|
+
//# sourceMappingURL=index.js.map
|