@contentgrowth/content-emailing 0.5.0 → 0.6.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/dist/TemplateManager-Db41KyPN.d.cts +77 -0
- package/dist/TemplateManager-Db41KyPN.d.ts +77 -0
- package/dist/backend/EmailService.cjs +44 -9
- package/dist/backend/EmailService.cjs.map +1 -1
- package/dist/backend/EmailService.d.cts +109 -0
- package/dist/backend/EmailService.d.ts +109 -0
- package/dist/backend/EmailService.js +44 -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 +44 -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 +44 -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 +439 -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 +440 -109
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -458,7 +458,6 @@ function createDOCacheProvider(doStub, instanceName = "global") {
|
|
|
458
458
|
const data = await response.json();
|
|
459
459
|
return data.template || null;
|
|
460
460
|
} catch (e) {
|
|
461
|
-
console.warn("[DOCacheProvider] Failed to get template:", e);
|
|
462
461
|
return null;
|
|
463
462
|
}
|
|
464
463
|
},
|
|
@@ -531,7 +530,6 @@ var EmailService = class {
|
|
|
531
530
|
* @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
|
|
532
531
|
*/
|
|
533
532
|
constructor(env, config = {}, cacheProvider = null) {
|
|
534
|
-
var _a, _b, _c;
|
|
535
533
|
this.env = env;
|
|
536
534
|
this.db = env.DB;
|
|
537
535
|
this.config = {
|
|
@@ -549,9 +547,9 @@ var EmailService = class {
|
|
|
549
547
|
settingsUpdater: config.settingsUpdater || null,
|
|
550
548
|
// Branding configuration for email templates
|
|
551
549
|
branding: {
|
|
552
|
-
brandName:
|
|
553
|
-
portalUrl:
|
|
554
|
-
primaryColor:
|
|
550
|
+
brandName: config.branding?.brandName || "Your App",
|
|
551
|
+
portalUrl: config.branding?.portalUrl || "https://app.example.com",
|
|
552
|
+
primaryColor: config.branding?.primaryColor || "#667eea",
|
|
555
553
|
...config.branding
|
|
556
554
|
},
|
|
557
555
|
...config
|
|
@@ -726,8 +724,14 @@ var EmailService = class {
|
|
|
726
724
|
const template = await this.getTemplate(templateId);
|
|
727
725
|
if (!template) throw new Error(`Template not found: ${templateId}`);
|
|
728
726
|
const subject = Mustache.render(template.subject_template, data);
|
|
729
|
-
|
|
730
|
-
|
|
727
|
+
let markdown = Mustache.render(template.body_markdown, data);
|
|
728
|
+
markdown = markdown.replace(/\\n/g, "\n");
|
|
729
|
+
marked.use({
|
|
730
|
+
mangle: false,
|
|
731
|
+
headerIds: false,
|
|
732
|
+
breaks: true
|
|
733
|
+
// Convert single line breaks to <br>
|
|
734
|
+
});
|
|
731
735
|
const htmlContent = marked.parse(markdown);
|
|
732
736
|
const html = this.wrapInBaseTemplate(htmlContent, subject, data);
|
|
733
737
|
const plainText = markdown.replace(/<[^>]*>/g, "");
|
|
@@ -743,6 +747,32 @@ var EmailService = class {
|
|
|
743
747
|
return wrapInEmailTemplate(content, subject, templateData);
|
|
744
748
|
}
|
|
745
749
|
// --- Delivery ---
|
|
750
|
+
/**
|
|
751
|
+
* Send an email using a template
|
|
752
|
+
* @param {string} templateId - Template ID
|
|
753
|
+
* @param {Object} data - Template variables
|
|
754
|
+
* @param {Object} options - Sending options (to, provider, etc.)
|
|
755
|
+
* @returns {Promise<Object>} Delivery result
|
|
756
|
+
*/
|
|
757
|
+
async sendViaTemplate(templateId, data, options) {
|
|
758
|
+
try {
|
|
759
|
+
const { subject, html, plainText } = await this.renderTemplate(templateId, data);
|
|
760
|
+
return await this.sendEmail({
|
|
761
|
+
...options,
|
|
762
|
+
subject,
|
|
763
|
+
// Can be overridden by options.subject if needed, but usually template subject is preferred
|
|
764
|
+
html,
|
|
765
|
+
text: plainText,
|
|
766
|
+
metadata: {
|
|
767
|
+
...options.metadata,
|
|
768
|
+
templateId
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
} catch (error) {
|
|
772
|
+
console.error(`[EmailService] sendViaTemplate failed for ${templateId}:`, error);
|
|
773
|
+
return { success: false, error: error.message };
|
|
774
|
+
}
|
|
775
|
+
}
|
|
746
776
|
/**
|
|
747
777
|
* Send a single email
|
|
748
778
|
* @param {Object} params - Email parameters
|
|
@@ -826,7 +856,7 @@ var EmailService = class {
|
|
|
826
856
|
return true;
|
|
827
857
|
} else {
|
|
828
858
|
const contentType = response.headers.get("content-type");
|
|
829
|
-
const errorBody =
|
|
859
|
+
const errorBody = contentType?.includes("application/json") ? await response.json() : await response.text();
|
|
830
860
|
console.error("[EmailService] MailChannels error:", response.status, errorBody);
|
|
831
861
|
return false;
|
|
832
862
|
}
|
|
@@ -940,7 +970,12 @@ var EmailService = class {
|
|
|
940
970
|
const { access_token } = tokenData;
|
|
941
971
|
const toBase64 = (str) => {
|
|
942
972
|
if (!str) return "";
|
|
943
|
-
|
|
973
|
+
try {
|
|
974
|
+
return btoa(unescape(encodeURIComponent(String(str))));
|
|
975
|
+
} catch (e) {
|
|
976
|
+
console.error("[EmailService] Base64 encoding failed:", e);
|
|
977
|
+
return "";
|
|
978
|
+
}
|
|
944
979
|
};
|
|
945
980
|
const htmlSafe = html || "";
|
|
946
981
|
const textSafe = text || (htmlSafe ? htmlSafe.replace(/<[^>]*>/g, "") : "");
|
|
@@ -1292,182 +1327,478 @@ function extractVariables(templateString) {
|
|
|
1292
1327
|
}
|
|
1293
1328
|
}
|
|
1294
1329
|
|
|
1295
|
-
// src/frontend/TemplateManager.
|
|
1296
|
-
import
|
|
1297
|
-
import {
|
|
1330
|
+
// src/frontend/TemplateManager.tsx
|
|
1331
|
+
import React3, { useState as useState3, useEffect as useEffect2, useCallback, useMemo as useMemo2 } from "react";
|
|
1332
|
+
import { marked as marked3 } from "marked";
|
|
1298
1333
|
|
|
1299
|
-
// src/frontend/TemplateEditor.
|
|
1300
|
-
import React, { useState,
|
|
1301
|
-
import {
|
|
1334
|
+
// src/frontend/TemplateEditor.tsx
|
|
1335
|
+
import React, { useState, useMemo } from "react";
|
|
1336
|
+
import { marked as marked2 } from "marked";
|
|
1337
|
+
var DEFAULT_TYPES = ["notification", "auth", "marketing", "system", "invitation", "verification"];
|
|
1338
|
+
var normalizeNewlines = (text) => {
|
|
1339
|
+
return text.replace(/\\n/g, "\n");
|
|
1340
|
+
};
|
|
1302
1341
|
var TemplateEditor = ({
|
|
1303
1342
|
initialData,
|
|
1304
1343
|
onSave,
|
|
1305
1344
|
onCancel,
|
|
1306
|
-
|
|
1307
|
-
|
|
1345
|
+
templateTypes = DEFAULT_TYPES,
|
|
1346
|
+
saving = false
|
|
1308
1347
|
}) => {
|
|
1348
|
+
const [activeTab, setActiveTab] = useState("basic");
|
|
1349
|
+
const [showPreview, setShowPreview] = useState(false);
|
|
1309
1350
|
const [formData, setFormData] = useState({
|
|
1310
|
-
template_id: "",
|
|
1311
|
-
template_name: "",
|
|
1312
|
-
template_type: "
|
|
1313
|
-
subject_template: "",
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
description: "",
|
|
1317
|
-
is_active:
|
|
1318
|
-
...initialData
|
|
1351
|
+
template_id: initialData?.template_id || "",
|
|
1352
|
+
template_name: initialData?.template_name || "",
|
|
1353
|
+
template_type: initialData?.template_type || "notification",
|
|
1354
|
+
subject_template: initialData?.subject_template || "",
|
|
1355
|
+
// Normalize newlines when loading
|
|
1356
|
+
body_markdown: normalizeNewlines(initialData?.body_markdown || ""),
|
|
1357
|
+
description: initialData?.description || "",
|
|
1358
|
+
is_active: initialData?.is_active ?? true
|
|
1319
1359
|
});
|
|
1320
|
-
const handleSubmit = (e) => {
|
|
1360
|
+
const handleSubmit = async (e) => {
|
|
1321
1361
|
e.preventDefault();
|
|
1322
|
-
onSave(formData);
|
|
1362
|
+
await onSave(formData);
|
|
1323
1363
|
};
|
|
1324
|
-
|
|
1325
|
-
|
|
1364
|
+
const isEdit = !!initialData?.template_id;
|
|
1365
|
+
const renderedPreview = useMemo(() => {
|
|
1366
|
+
let preview = formData.body_markdown;
|
|
1367
|
+
preview = preview.replace(/\{\{(\w+)\}\}/g, '<span class="bg-blue-100 text-blue-800 px-1 rounded text-sm">{{$1}}</span>');
|
|
1368
|
+
return marked2.parse(preview);
|
|
1369
|
+
}, [formData.body_markdown]);
|
|
1370
|
+
const subjectPreview = useMemo(() => {
|
|
1371
|
+
return formData.subject_template.replace(/\{\{(\w+)\}\}/g, '<span class="bg-blue-100 text-blue-800 px-1 rounded">{{$1}}</span>');
|
|
1372
|
+
}, [formData.subject_template]);
|
|
1373
|
+
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(
|
|
1374
|
+
"button",
|
|
1326
1375
|
{
|
|
1327
|
-
type: "
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1376
|
+
type: "button",
|
|
1377
|
+
onClick: () => setActiveTab("basic"),
|
|
1378
|
+
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"}`
|
|
1379
|
+
},
|
|
1380
|
+
"\u{1F4CB} Basic Info"
|
|
1381
|
+
), /* @__PURE__ */ React.createElement(
|
|
1382
|
+
"button",
|
|
1383
|
+
{
|
|
1384
|
+
type: "button",
|
|
1385
|
+
onClick: () => setActiveTab("content"),
|
|
1386
|
+
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"}`
|
|
1387
|
+
},
|
|
1388
|
+
"\u2709\uFE0F Email Content"
|
|
1389
|
+
))), /* @__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(
|
|
1335
1390
|
"input",
|
|
1336
1391
|
{
|
|
1337
1392
|
type: "text",
|
|
1338
1393
|
required: true,
|
|
1339
1394
|
value: formData.template_name,
|
|
1340
1395
|
onChange: (e) => setFormData({ ...formData, template_name: e.target.value }),
|
|
1341
|
-
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
1396
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
|
1397
|
+
placeholder: "e.g., Welcome Email"
|
|
1398
|
+
}
|
|
1399
|
+
)), /* @__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(
|
|
1400
|
+
"select",
|
|
1401
|
+
{
|
|
1402
|
+
value: formData.template_type,
|
|
1403
|
+
onChange: (e) => setFormData({ ...formData, template_type: e.target.value }),
|
|
1404
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
1405
|
+
},
|
|
1406
|
+
templateTypes.map((type) => /* @__PURE__ */ React.createElement("option", { key: type, value: type }, type.charAt(0).toUpperCase() + type.slice(1)))
|
|
1407
|
+
)), /* @__PURE__ */ React.createElement("div", { className: "flex items-center pt-6" }, /* @__PURE__ */ React.createElement("label", { className: "flex items-center cursor-pointer" }, /* @__PURE__ */ React.createElement(
|
|
1408
|
+
"input",
|
|
1409
|
+
{
|
|
1410
|
+
type: "checkbox",
|
|
1411
|
+
checked: formData.is_active,
|
|
1412
|
+
onChange: (e) => setFormData({ ...formData, is_active: e.target.checked }),
|
|
1413
|
+
className: "w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
1342
1414
|
}
|
|
1343
|
-
))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "
|
|
1415
|
+
), /* @__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(
|
|
1416
|
+
"textarea",
|
|
1417
|
+
{
|
|
1418
|
+
value: formData.description,
|
|
1419
|
+
onChange: (e) => setFormData({ ...formData, description: e.target.value }),
|
|
1420
|
+
rows: 3,
|
|
1421
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
|
1422
|
+
placeholder: "Brief description of when this template is used"
|
|
1423
|
+
}
|
|
1424
|
+
)), /* @__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(
|
|
1344
1425
|
"input",
|
|
1345
1426
|
{
|
|
1346
1427
|
type: "text",
|
|
1347
1428
|
required: true,
|
|
1348
1429
|
value: formData.subject_template,
|
|
1349
1430
|
onChange: (e) => setFormData({ ...formData, subject_template: e.target.value }),
|
|
1350
|
-
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
1431
|
+
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",
|
|
1432
|
+
placeholder: "e.g., Welcome to {{app_name}}, {{user_name}}!"
|
|
1351
1433
|
}
|
|
1352
|
-
), /* @__PURE__ */ React.createElement("
|
|
1434
|
+
), 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(
|
|
1435
|
+
"button",
|
|
1436
|
+
{
|
|
1437
|
+
type: "button",
|
|
1438
|
+
onClick: () => setShowPreview(!showPreview),
|
|
1439
|
+
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"}`
|
|
1440
|
+
},
|
|
1441
|
+
showPreview ? "\u270F\uFE0F Edit" : "\u{1F441}\uFE0F Preview"
|
|
1442
|
+
)), !showPreview ? /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(
|
|
1353
1443
|
"textarea",
|
|
1354
1444
|
{
|
|
1355
1445
|
required: true,
|
|
1356
|
-
rows: 12,
|
|
1357
1446
|
value: formData.body_markdown,
|
|
1358
1447
|
onChange: (e) => setFormData({ ...formData, body_markdown: e.target.value }),
|
|
1359
|
-
|
|
1448
|
+
rows: 14,
|
|
1449
|
+
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",
|
|
1450
|
+
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"
|
|
1360
1451
|
}
|
|
1361
|
-
)
|
|
1452
|
+
), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs text-gray-500" }, "Use ", "{{variable}}", " for dynamic content. Supports Markdown formatting.")) : /* @__PURE__ */ React.createElement(
|
|
1453
|
+
"div",
|
|
1454
|
+
{
|
|
1455
|
+
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",
|
|
1456
|
+
dangerouslySetInnerHTML: { __html: renderedPreview || '<p class="text-gray-400">No content to preview</p>' }
|
|
1457
|
+
}
|
|
1458
|
+
)))), /* @__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(
|
|
1362
1459
|
"button",
|
|
1363
1460
|
{
|
|
1364
1461
|
type: "button",
|
|
1365
|
-
onClick:
|
|
1366
|
-
className: "
|
|
1462
|
+
onClick: onCancel,
|
|
1463
|
+
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"
|
|
1367
1464
|
},
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1465
|
+
"Cancel"
|
|
1466
|
+
), /* @__PURE__ */ React.createElement(
|
|
1467
|
+
"button",
|
|
1468
|
+
{
|
|
1469
|
+
type: "submit",
|
|
1470
|
+
onClick: handleSubmit,
|
|
1471
|
+
disabled: saving || !formData.template_name || !formData.subject_template || !formData.body_markdown,
|
|
1472
|
+
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"
|
|
1473
|
+
},
|
|
1474
|
+
saving ? "Saving..." : isEdit ? "Save Changes" : "Create Template"
|
|
1475
|
+
)))));
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
// src/frontend/TemplateTester.tsx
|
|
1479
|
+
import React2, { useState as useState2, useEffect } from "react";
|
|
1480
|
+
var TemplateTester = ({
|
|
1481
|
+
template,
|
|
1482
|
+
onSendTest,
|
|
1483
|
+
onCancel,
|
|
1484
|
+
sending = false
|
|
1485
|
+
}) => {
|
|
1486
|
+
const [toEmail, setToEmail] = useState2("");
|
|
1487
|
+
const [variables, setVariables] = useState2({});
|
|
1488
|
+
const [detectedVariables, setDetectedVariables] = useState2([]);
|
|
1489
|
+
const [error, setError] = useState2(null);
|
|
1490
|
+
const [success, setSuccess] = useState2(null);
|
|
1491
|
+
useEffect(() => {
|
|
1492
|
+
const regex = /\{\{(\w+)\}\}/g;
|
|
1493
|
+
const subjectVars = Array.from(template.subject_template.matchAll(regex)).map((m) => m[1]);
|
|
1494
|
+
const bodyVars = Array.from(template.body_markdown.matchAll(regex)).map((m) => m[1]);
|
|
1495
|
+
const uniqueVars = Array.from(/* @__PURE__ */ new Set([...subjectVars, ...bodyVars]));
|
|
1496
|
+
setDetectedVariables(uniqueVars);
|
|
1497
|
+
const initialValues = {};
|
|
1498
|
+
uniqueVars.forEach((v) => {
|
|
1499
|
+
initialValues[v] = "";
|
|
1500
|
+
});
|
|
1501
|
+
setVariables(initialValues);
|
|
1502
|
+
}, [template]);
|
|
1503
|
+
const handleSubmit = async (e) => {
|
|
1504
|
+
e.preventDefault();
|
|
1505
|
+
setError(null);
|
|
1506
|
+
setSuccess(null);
|
|
1507
|
+
try {
|
|
1508
|
+
await onSendTest({
|
|
1509
|
+
template_id: template.template_id,
|
|
1510
|
+
to_email: toEmail,
|
|
1511
|
+
variables
|
|
1512
|
+
});
|
|
1513
|
+
setSuccess("Test email sent successfully!");
|
|
1514
|
+
} catch (e2) {
|
|
1515
|
+
setError(e2.message || "Failed to send test email");
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
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(
|
|
1519
|
+
"input",
|
|
1520
|
+
{
|
|
1521
|
+
type: "email",
|
|
1522
|
+
required: true,
|
|
1523
|
+
value: toEmail,
|
|
1524
|
+
onChange: (e) => setToEmail(e.target.value),
|
|
1525
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
|
1526
|
+
placeholder: "recipient@example.com"
|
|
1527
|
+
}
|
|
1528
|
+
)), 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(
|
|
1529
|
+
"input",
|
|
1530
|
+
{
|
|
1531
|
+
type: "text",
|
|
1532
|
+
value: variables[variable] || "",
|
|
1533
|
+
onChange: (e) => setVariables({ ...variables, [variable]: e.target.value }),
|
|
1534
|
+
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",
|
|
1535
|
+
placeholder: `Value for {{${variable}}}`
|
|
1536
|
+
}
|
|
1537
|
+
))))), 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(
|
|
1371
1538
|
"button",
|
|
1372
1539
|
{
|
|
1373
1540
|
type: "button",
|
|
1374
1541
|
onClick: onCancel,
|
|
1375
|
-
className: "px-4 py-2
|
|
1542
|
+
className: "px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-100"
|
|
1376
1543
|
},
|
|
1377
|
-
"
|
|
1378
|
-
), /* @__PURE__ */
|
|
1544
|
+
"Close"
|
|
1545
|
+
), /* @__PURE__ */ React2.createElement(
|
|
1379
1546
|
"button",
|
|
1380
1547
|
{
|
|
1381
1548
|
type: "submit",
|
|
1382
|
-
|
|
1549
|
+
onClick: handleSubmit,
|
|
1550
|
+
disabled: sending || !toEmail || success != null,
|
|
1551
|
+
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"
|
|
1383
1552
|
},
|
|
1384
|
-
/* @__PURE__ */
|
|
1385
|
-
|
|
1386
|
-
))))));
|
|
1553
|
+
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")
|
|
1554
|
+
))));
|
|
1387
1555
|
};
|
|
1388
1556
|
|
|
1389
|
-
// src/frontend/TemplateManager.
|
|
1557
|
+
// src/frontend/TemplateManager.tsx
|
|
1558
|
+
var TYPE_COLORS = {
|
|
1559
|
+
"auth": { bg: "bg-blue-100", text: "text-blue-800" },
|
|
1560
|
+
"notification": { bg: "bg-purple-100", text: "text-purple-800" },
|
|
1561
|
+
"marketing": { bg: "bg-green-100", text: "text-green-800" },
|
|
1562
|
+
"system": { bg: "bg-gray-100", text: "text-gray-800" },
|
|
1563
|
+
"invitation": { bg: "bg-orange-100", text: "text-orange-800" },
|
|
1564
|
+
"verification": { bg: "bg-cyan-100", text: "text-cyan-800" },
|
|
1565
|
+
"default": { bg: "bg-gray-100", text: "text-gray-600" }
|
|
1566
|
+
};
|
|
1567
|
+
var normalizeNewlines2 = (text) => {
|
|
1568
|
+
return text?.replace(/\\n/g, "\n") || "";
|
|
1569
|
+
};
|
|
1390
1570
|
var TemplateManager = ({
|
|
1391
|
-
|
|
1571
|
+
onLoadTemplates,
|
|
1572
|
+
onSaveTemplate,
|
|
1573
|
+
onDeleteTemplate,
|
|
1574
|
+
onSendTestEmail,
|
|
1392
1575
|
title = "Email Templates",
|
|
1393
|
-
description = "Manage
|
|
1576
|
+
description = "Manage system email templates",
|
|
1577
|
+
templateTypes
|
|
1394
1578
|
}) => {
|
|
1395
|
-
const [templates, setTemplates] =
|
|
1396
|
-
const [loading, setLoading] =
|
|
1397
|
-
const [
|
|
1398
|
-
const [
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
const
|
|
1579
|
+
const [templates, setTemplates] = useState3([]);
|
|
1580
|
+
const [loading, setLoading] = useState3(true);
|
|
1581
|
+
const [error, setError] = useState3(null);
|
|
1582
|
+
const [filterType, setFilterType] = useState3("all");
|
|
1583
|
+
const [previewTemplate, setPreviewTemplate] = useState3(null);
|
|
1584
|
+
const [editingTemplate, setEditingTemplate] = useState3(null);
|
|
1585
|
+
const [testingTemplate, setTestingTemplate] = useState3(null);
|
|
1586
|
+
const [showEditor, setShowEditor] = useState3(false);
|
|
1587
|
+
const [showTester, setShowTester] = useState3(false);
|
|
1588
|
+
const [saving, setSaving] = useState3(false);
|
|
1589
|
+
const [deleteConfirm, setDeleteConfirm] = useState3(null);
|
|
1590
|
+
const loadTemplates = useCallback(async () => {
|
|
1591
|
+
setLoading(true);
|
|
1592
|
+
setError(null);
|
|
1403
1593
|
try {
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
}
|
|
1409
|
-
} catch (err) {
|
|
1410
|
-
console.error("Failed to load templates:", err);
|
|
1594
|
+
const data = await onLoadTemplates();
|
|
1595
|
+
setTemplates(data);
|
|
1596
|
+
} catch (e) {
|
|
1597
|
+
setError(e.message || "Failed to load templates");
|
|
1411
1598
|
} finally {
|
|
1412
1599
|
setLoading(false);
|
|
1413
1600
|
}
|
|
1601
|
+
}, [onLoadTemplates]);
|
|
1602
|
+
useEffect2(() => {
|
|
1603
|
+
loadTemplates();
|
|
1604
|
+
}, [loadTemplates]);
|
|
1605
|
+
const openCreate = () => {
|
|
1606
|
+
setEditingTemplate(null);
|
|
1607
|
+
setShowEditor(true);
|
|
1608
|
+
};
|
|
1609
|
+
const openPreview = (template) => {
|
|
1610
|
+
setPreviewTemplate(template);
|
|
1414
1611
|
};
|
|
1415
|
-
const
|
|
1612
|
+
const openEdit = (template) => {
|
|
1613
|
+
setPreviewTemplate(null);
|
|
1614
|
+
setEditingTemplate(template);
|
|
1615
|
+
setShowEditor(true);
|
|
1616
|
+
};
|
|
1617
|
+
const openTester = (template) => {
|
|
1618
|
+
setTestingTemplate(template);
|
|
1619
|
+
setShowTester(true);
|
|
1620
|
+
};
|
|
1621
|
+
const handleSave = async (data) => {
|
|
1622
|
+
setSaving(true);
|
|
1416
1623
|
try {
|
|
1417
|
-
await
|
|
1624
|
+
await onSaveTemplate(data);
|
|
1418
1625
|
setShowEditor(false);
|
|
1419
|
-
loadTemplates();
|
|
1420
|
-
} catch (
|
|
1421
|
-
|
|
1422
|
-
|
|
1626
|
+
await loadTemplates();
|
|
1627
|
+
} catch (e) {
|
|
1628
|
+
setSaving(false);
|
|
1629
|
+
throw e;
|
|
1423
1630
|
}
|
|
1631
|
+
setSaving(false);
|
|
1424
1632
|
};
|
|
1425
1633
|
const handleDelete = async (id) => {
|
|
1426
|
-
if (!
|
|
1634
|
+
if (!onDeleteTemplate) return;
|
|
1427
1635
|
try {
|
|
1428
|
-
await
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1636
|
+
await onDeleteTemplate(id);
|
|
1637
|
+
setDeleteConfirm(null);
|
|
1638
|
+
await loadTemplates();
|
|
1639
|
+
} catch (e) {
|
|
1640
|
+
setError(e.message || "Failed to delete template");
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
const handleTest = async (data) => {
|
|
1644
|
+
if (!onSendTestEmail) return;
|
|
1645
|
+
setSaving(true);
|
|
1646
|
+
try {
|
|
1647
|
+
await onSendTestEmail(data);
|
|
1648
|
+
} finally {
|
|
1649
|
+
setSaving(false);
|
|
1433
1650
|
}
|
|
1434
1651
|
};
|
|
1435
|
-
|
|
1652
|
+
const getTypeColor = (type) => TYPE_COLORS[type] || TYPE_COLORS["default"];
|
|
1653
|
+
const uniqueTypes = ["all", ...new Set(templates.map((t) => t.template_type))];
|
|
1654
|
+
const filteredTemplates = filterType === "all" ? templates : templates.filter((t) => t.template_type === filterType);
|
|
1655
|
+
const formatDate = (timestamp) => {
|
|
1656
|
+
if (!timestamp) return "N/A";
|
|
1657
|
+
return new Date(timestamp * 1e3).toLocaleDateString("en-US", {
|
|
1658
|
+
month: "short",
|
|
1659
|
+
day: "numeric",
|
|
1660
|
+
year: "numeric"
|
|
1661
|
+
});
|
|
1662
|
+
};
|
|
1663
|
+
const previewBody = useMemo2(() => {
|
|
1664
|
+
if (!previewTemplate?.body_markdown) return "";
|
|
1665
|
+
let md = normalizeNewlines2(previewTemplate.body_markdown);
|
|
1666
|
+
md = md.replace(/\{\{(\w+)\}\}/g, '<span class="bg-blue-100 text-blue-800 px-1 rounded text-sm">{{$1}}</span>');
|
|
1667
|
+
return marked3.parse(md);
|
|
1668
|
+
}, [previewTemplate]);
|
|
1669
|
+
if (loading) {
|
|
1670
|
+
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" }));
|
|
1671
|
+
}
|
|
1672
|
+
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(
|
|
1436
1673
|
"button",
|
|
1437
1674
|
{
|
|
1438
|
-
onClick:
|
|
1439
|
-
|
|
1440
|
-
setShowEditor(true);
|
|
1441
|
-
},
|
|
1442
|
-
className: "flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
1675
|
+
onClick: openCreate,
|
|
1676
|
+
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"
|
|
1443
1677
|
},
|
|
1444
|
-
/* @__PURE__ */
|
|
1445
|
-
"
|
|
1446
|
-
)),
|
|
1678
|
+
/* @__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" })),
|
|
1679
|
+
"Create Template"
|
|
1680
|
+
)), 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(
|
|
1447
1681
|
"button",
|
|
1448
1682
|
{
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1683
|
+
key: type,
|
|
1684
|
+
onClick: () => setFilterType(type),
|
|
1685
|
+
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"}`
|
|
1686
|
+
},
|
|
1687
|
+
type.charAt(0).toUpperCase() + type.slice(1)
|
|
1688
|
+
))), 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) => {
|
|
1689
|
+
const typeColor = getTypeColor(template.template_type);
|
|
1690
|
+
return /* @__PURE__ */ React3.createElement(
|
|
1691
|
+
"div",
|
|
1692
|
+
{
|
|
1693
|
+
key: template.template_id,
|
|
1694
|
+
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",
|
|
1695
|
+
onClick: () => openPreview(template)
|
|
1452
1696
|
},
|
|
1453
|
-
className: "p-2 text-
|
|
1697
|
+
/* @__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)),
|
|
1698
|
+
/* @__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(
|
|
1699
|
+
"button",
|
|
1700
|
+
{
|
|
1701
|
+
onClick: (e) => {
|
|
1702
|
+
e.stopPropagation();
|
|
1703
|
+
openEdit(template);
|
|
1704
|
+
},
|
|
1705
|
+
className: "p-1.5 text-blue-600 hover:bg-blue-50 rounded",
|
|
1706
|
+
title: "Edit"
|
|
1707
|
+
},
|
|
1708
|
+
/* @__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" }))
|
|
1709
|
+
), onSendTestEmail && /* @__PURE__ */ React3.createElement(
|
|
1710
|
+
"button",
|
|
1711
|
+
{
|
|
1712
|
+
onClick: (e) => {
|
|
1713
|
+
e.stopPropagation();
|
|
1714
|
+
openTester(template);
|
|
1715
|
+
},
|
|
1716
|
+
className: "p-1.5 text-purple-600 hover:bg-purple-50 rounded",
|
|
1717
|
+
title: "Send Test Email"
|
|
1718
|
+
},
|
|
1719
|
+
/* @__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" }))
|
|
1720
|
+
), onDeleteTemplate && /* @__PURE__ */ React3.createElement(
|
|
1721
|
+
"button",
|
|
1722
|
+
{
|
|
1723
|
+
onClick: (e) => {
|
|
1724
|
+
e.stopPropagation();
|
|
1725
|
+
setDeleteConfirm(template.template_id);
|
|
1726
|
+
},
|
|
1727
|
+
className: "p-1.5 text-red-600 hover:bg-red-50 rounded",
|
|
1728
|
+
title: "Delete"
|
|
1729
|
+
},
|
|
1730
|
+
/* @__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" }))
|
|
1731
|
+
)))
|
|
1732
|
+
);
|
|
1733
|
+
})), 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(
|
|
1734
|
+
"div",
|
|
1735
|
+
{
|
|
1736
|
+
className: "p-4 bg-white rounded-lg border border-gray-200 prose prose-sm max-w-none",
|
|
1737
|
+
dangerouslySetInnerHTML: { __html: previewBody }
|
|
1738
|
+
}
|
|
1739
|
+
)), 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(
|
|
1740
|
+
"button",
|
|
1741
|
+
{
|
|
1742
|
+
onClick: () => setPreviewTemplate(null),
|
|
1743
|
+
className: "px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-100"
|
|
1454
1744
|
},
|
|
1455
|
-
|
|
1456
|
-
), /* @__PURE__ */
|
|
1745
|
+
"Close"
|
|
1746
|
+
), /* @__PURE__ */ React3.createElement(
|
|
1457
1747
|
"button",
|
|
1458
1748
|
{
|
|
1459
|
-
onClick: () =>
|
|
1460
|
-
className: "
|
|
1749
|
+
onClick: () => openEdit(previewTemplate),
|
|
1750
|
+
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"
|
|
1461
1751
|
},
|
|
1462
|
-
/* @__PURE__ */
|
|
1463
|
-
|
|
1752
|
+
/* @__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" })),
|
|
1753
|
+
"Edit Template"
|
|
1754
|
+
), onSendTestEmail && /* @__PURE__ */ React3.createElement(
|
|
1755
|
+
"button",
|
|
1756
|
+
{
|
|
1757
|
+
onClick: () => openTester(previewTemplate),
|
|
1758
|
+
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"
|
|
1759
|
+
},
|
|
1760
|
+
/* @__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" })),
|
|
1761
|
+
"Test"
|
|
1762
|
+
)))), showEditor && /* @__PURE__ */ React3.createElement(
|
|
1464
1763
|
TemplateEditor,
|
|
1465
1764
|
{
|
|
1466
|
-
initialData: editingTemplate
|
|
1765
|
+
initialData: editingTemplate ? {
|
|
1766
|
+
template_id: editingTemplate.template_id,
|
|
1767
|
+
template_name: editingTemplate.template_name,
|
|
1768
|
+
template_type: editingTemplate.template_type,
|
|
1769
|
+
subject_template: editingTemplate.subject_template,
|
|
1770
|
+
body_markdown: editingTemplate.body_markdown,
|
|
1771
|
+
description: editingTemplate.description || "",
|
|
1772
|
+
is_active: !!editingTemplate.is_active
|
|
1773
|
+
} : null,
|
|
1467
1774
|
onSave: handleSave,
|
|
1468
|
-
onCancel: () => setShowEditor(false)
|
|
1775
|
+
onCancel: () => setShowEditor(false),
|
|
1776
|
+
templateTypes,
|
|
1777
|
+
saving
|
|
1778
|
+
}
|
|
1779
|
+
), showTester && testingTemplate && /* @__PURE__ */ React3.createElement(
|
|
1780
|
+
TemplateTester,
|
|
1781
|
+
{
|
|
1782
|
+
template: testingTemplate,
|
|
1783
|
+
onSendTest: handleTest,
|
|
1784
|
+
onCancel: () => setShowTester(false),
|
|
1785
|
+
sending: saving
|
|
1469
1786
|
}
|
|
1470
|
-
))
|
|
1787
|
+
), 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(
|
|
1788
|
+
"button",
|
|
1789
|
+
{
|
|
1790
|
+
onClick: () => setDeleteConfirm(null),
|
|
1791
|
+
className: "px-4 py-2 border border-gray-300 rounded-lg"
|
|
1792
|
+
},
|
|
1793
|
+
"Cancel"
|
|
1794
|
+
), /* @__PURE__ */ React3.createElement(
|
|
1795
|
+
"button",
|
|
1796
|
+
{
|
|
1797
|
+
onClick: () => handleDelete(deleteConfirm),
|
|
1798
|
+
className: "px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
|
1799
|
+
},
|
|
1800
|
+
"Delete"
|
|
1801
|
+
)))));
|
|
1471
1802
|
};
|
|
1472
1803
|
export {
|
|
1473
1804
|
EmailService,
|