@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.cjs
CHANGED
|
@@ -506,7 +506,6 @@ function createDOCacheProvider(doStub, instanceName = "global") {
|
|
|
506
506
|
const data = await response.json();
|
|
507
507
|
return data.template || null;
|
|
508
508
|
} catch (e) {
|
|
509
|
-
console.warn("[DOCacheProvider] Failed to get template:", e);
|
|
510
509
|
return null;
|
|
511
510
|
}
|
|
512
511
|
},
|
|
@@ -579,7 +578,6 @@ var EmailService = class {
|
|
|
579
578
|
* @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
|
|
580
579
|
*/
|
|
581
580
|
constructor(env, config = {}, cacheProvider = null) {
|
|
582
|
-
var _a, _b, _c;
|
|
583
581
|
this.env = env;
|
|
584
582
|
this.db = env.DB;
|
|
585
583
|
this.config = {
|
|
@@ -597,9 +595,9 @@ var EmailService = class {
|
|
|
597
595
|
settingsUpdater: config.settingsUpdater || null,
|
|
598
596
|
// Branding configuration for email templates
|
|
599
597
|
branding: {
|
|
600
|
-
brandName:
|
|
601
|
-
portalUrl:
|
|
602
|
-
primaryColor:
|
|
598
|
+
brandName: config.branding?.brandName || "Your App",
|
|
599
|
+
portalUrl: config.branding?.portalUrl || "https://app.example.com",
|
|
600
|
+
primaryColor: config.branding?.primaryColor || "#667eea",
|
|
603
601
|
...config.branding
|
|
604
602
|
},
|
|
605
603
|
...config
|
|
@@ -774,8 +772,14 @@ var EmailService = class {
|
|
|
774
772
|
const template = await this.getTemplate(templateId);
|
|
775
773
|
if (!template) throw new Error(`Template not found: ${templateId}`);
|
|
776
774
|
const subject = import_mustache.default.render(template.subject_template, data);
|
|
777
|
-
|
|
778
|
-
|
|
775
|
+
let markdown = import_mustache.default.render(template.body_markdown, data);
|
|
776
|
+
markdown = markdown.replace(/\\n/g, "\n");
|
|
777
|
+
import_marked.marked.use({
|
|
778
|
+
mangle: false,
|
|
779
|
+
headerIds: false,
|
|
780
|
+
breaks: true
|
|
781
|
+
// Convert single line breaks to <br>
|
|
782
|
+
});
|
|
779
783
|
const htmlContent = import_marked.marked.parse(markdown);
|
|
780
784
|
const html = this.wrapInBaseTemplate(htmlContent, subject, data);
|
|
781
785
|
const plainText = markdown.replace(/<[^>]*>/g, "");
|
|
@@ -791,6 +795,32 @@ var EmailService = class {
|
|
|
791
795
|
return wrapInEmailTemplate(content, subject, templateData);
|
|
792
796
|
}
|
|
793
797
|
// --- Delivery ---
|
|
798
|
+
/**
|
|
799
|
+
* Send an email using a template
|
|
800
|
+
* @param {string} templateId - Template ID
|
|
801
|
+
* @param {Object} data - Template variables
|
|
802
|
+
* @param {Object} options - Sending options (to, provider, etc.)
|
|
803
|
+
* @returns {Promise<Object>} Delivery result
|
|
804
|
+
*/
|
|
805
|
+
async sendViaTemplate(templateId, data, options) {
|
|
806
|
+
try {
|
|
807
|
+
const { subject, html, plainText } = await this.renderTemplate(templateId, data);
|
|
808
|
+
return await this.sendEmail({
|
|
809
|
+
...options,
|
|
810
|
+
subject,
|
|
811
|
+
// Can be overridden by options.subject if needed, but usually template subject is preferred
|
|
812
|
+
html,
|
|
813
|
+
text: plainText,
|
|
814
|
+
metadata: {
|
|
815
|
+
...options.metadata,
|
|
816
|
+
templateId
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
} catch (error) {
|
|
820
|
+
console.error(`[EmailService] sendViaTemplate failed for ${templateId}:`, error);
|
|
821
|
+
return { success: false, error: error.message };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
794
824
|
/**
|
|
795
825
|
* Send a single email
|
|
796
826
|
* @param {Object} params - Email parameters
|
|
@@ -874,7 +904,7 @@ var EmailService = class {
|
|
|
874
904
|
return true;
|
|
875
905
|
} else {
|
|
876
906
|
const contentType = response.headers.get("content-type");
|
|
877
|
-
const errorBody =
|
|
907
|
+
const errorBody = contentType?.includes("application/json") ? await response.json() : await response.text();
|
|
878
908
|
console.error("[EmailService] MailChannels error:", response.status, errorBody);
|
|
879
909
|
return false;
|
|
880
910
|
}
|
|
@@ -988,7 +1018,12 @@ var EmailService = class {
|
|
|
988
1018
|
const { access_token } = tokenData;
|
|
989
1019
|
const toBase64 = (str) => {
|
|
990
1020
|
if (!str) return "";
|
|
991
|
-
|
|
1021
|
+
try {
|
|
1022
|
+
return btoa(unescape(encodeURIComponent(String(str))));
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
console.error("[EmailService] Base64 encoding failed:", e);
|
|
1025
|
+
return "";
|
|
1026
|
+
}
|
|
992
1027
|
};
|
|
993
1028
|
const htmlSafe = html || "";
|
|
994
1029
|
const textSafe = text || (htmlSafe ? htmlSafe.replace(/<[^>]*>/g, "") : "");
|
|
@@ -1340,182 +1375,478 @@ function extractVariables(templateString) {
|
|
|
1340
1375
|
}
|
|
1341
1376
|
}
|
|
1342
1377
|
|
|
1343
|
-
// src/frontend/TemplateManager.
|
|
1344
|
-
var
|
|
1345
|
-
var
|
|
1378
|
+
// src/frontend/TemplateManager.tsx
|
|
1379
|
+
var import_react3 = __toESM(require("react"), 1);
|
|
1380
|
+
var import_marked3 = require("marked");
|
|
1346
1381
|
|
|
1347
|
-
// src/frontend/TemplateEditor.
|
|
1382
|
+
// src/frontend/TemplateEditor.tsx
|
|
1348
1383
|
var import_react = __toESM(require("react"), 1);
|
|
1349
|
-
var
|
|
1384
|
+
var import_marked2 = require("marked");
|
|
1385
|
+
var DEFAULT_TYPES = ["notification", "auth", "marketing", "system", "invitation", "verification"];
|
|
1386
|
+
var normalizeNewlines = (text) => {
|
|
1387
|
+
return text.replace(/\\n/g, "\n");
|
|
1388
|
+
};
|
|
1350
1389
|
var TemplateEditor = ({
|
|
1351
1390
|
initialData,
|
|
1352
1391
|
onSave,
|
|
1353
1392
|
onCancel,
|
|
1354
|
-
|
|
1355
|
-
|
|
1393
|
+
templateTypes = DEFAULT_TYPES,
|
|
1394
|
+
saving = false
|
|
1356
1395
|
}) => {
|
|
1396
|
+
const [activeTab, setActiveTab] = (0, import_react.useState)("basic");
|
|
1397
|
+
const [showPreview, setShowPreview] = (0, import_react.useState)(false);
|
|
1357
1398
|
const [formData, setFormData] = (0, import_react.useState)({
|
|
1358
|
-
template_id: "",
|
|
1359
|
-
template_name: "",
|
|
1360
|
-
template_type: "
|
|
1361
|
-
subject_template: "",
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
description: "",
|
|
1365
|
-
is_active:
|
|
1366
|
-
...initialData
|
|
1399
|
+
template_id: initialData?.template_id || "",
|
|
1400
|
+
template_name: initialData?.template_name || "",
|
|
1401
|
+
template_type: initialData?.template_type || "notification",
|
|
1402
|
+
subject_template: initialData?.subject_template || "",
|
|
1403
|
+
// Normalize newlines when loading
|
|
1404
|
+
body_markdown: normalizeNewlines(initialData?.body_markdown || ""),
|
|
1405
|
+
description: initialData?.description || "",
|
|
1406
|
+
is_active: initialData?.is_active ?? true
|
|
1367
1407
|
});
|
|
1368
|
-
const handleSubmit = (e) => {
|
|
1408
|
+
const handleSubmit = async (e) => {
|
|
1369
1409
|
e.preventDefault();
|
|
1370
|
-
onSave(formData);
|
|
1410
|
+
await onSave(formData);
|
|
1371
1411
|
};
|
|
1372
|
-
|
|
1373
|
-
|
|
1412
|
+
const isEdit = !!initialData?.template_id;
|
|
1413
|
+
const renderedPreview = (0, import_react.useMemo)(() => {
|
|
1414
|
+
let preview = formData.body_markdown;
|
|
1415
|
+
preview = preview.replace(/\{\{(\w+)\}\}/g, '<span class="bg-blue-100 text-blue-800 px-1 rounded text-sm">{{$1}}</span>');
|
|
1416
|
+
return import_marked2.marked.parse(preview);
|
|
1417
|
+
}, [formData.body_markdown]);
|
|
1418
|
+
const subjectPreview = (0, import_react.useMemo)(() => {
|
|
1419
|
+
return formData.subject_template.replace(/\{\{(\w+)\}\}/g, '<span class="bg-blue-100 text-blue-800 px-1 rounded">{{$1}}</span>');
|
|
1420
|
+
}, [formData.subject_template]);
|
|
1421
|
+
return /* @__PURE__ */ import_react.default.createElement("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "bg-white rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gray-50" }, /* @__PURE__ */ import_react.default.createElement("h2", { className: "text-lg font-semibold text-gray-900" }, isEdit ? "Edit Template" : "Create New Template"), /* @__PURE__ */ import_react.default.createElement("button", { onClick: onCancel, className: "text-gray-400 hover:text-gray-600" }, /* @__PURE__ */ import_react.default.createElement("svg", { className: "w-6 h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })))), /* @__PURE__ */ import_react.default.createElement("div", { className: "border-b border-gray-200 px-6" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "flex gap-4" }, /* @__PURE__ */ import_react.default.createElement(
|
|
1422
|
+
"button",
|
|
1374
1423
|
{
|
|
1375
|
-
type: "
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1424
|
+
type: "button",
|
|
1425
|
+
onClick: () => setActiveTab("basic"),
|
|
1426
|
+
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"}`
|
|
1427
|
+
},
|
|
1428
|
+
"\u{1F4CB} Basic Info"
|
|
1429
|
+
), /* @__PURE__ */ import_react.default.createElement(
|
|
1430
|
+
"button",
|
|
1431
|
+
{
|
|
1432
|
+
type: "button",
|
|
1433
|
+
onClick: () => setActiveTab("content"),
|
|
1434
|
+
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"}`
|
|
1435
|
+
},
|
|
1436
|
+
"\u2709\uFE0F Email Content"
|
|
1437
|
+
))), /* @__PURE__ */ import_react.default.createElement("form", { onSubmit: handleSubmit, className: "flex-1 overflow-y-auto" }, activeTab === "basic" && /* @__PURE__ */ import_react.default.createElement("div", { className: "p-6 space-y-4" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "Template Name *"), /* @__PURE__ */ import_react.default.createElement(
|
|
1383
1438
|
"input",
|
|
1384
1439
|
{
|
|
1385
1440
|
type: "text",
|
|
1386
1441
|
required: true,
|
|
1387
1442
|
value: formData.template_name,
|
|
1388
1443
|
onChange: (e) => setFormData({ ...formData, template_name: e.target.value }),
|
|
1389
|
-
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
1444
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
|
1445
|
+
placeholder: "e.g., Welcome Email"
|
|
1390
1446
|
}
|
|
1391
|
-
))
|
|
1447
|
+
)), /* @__PURE__ */ import_react.default.createElement("div", { className: "grid grid-cols-2 gap-4" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "Type *"), /* @__PURE__ */ import_react.default.createElement(
|
|
1448
|
+
"select",
|
|
1449
|
+
{
|
|
1450
|
+
value: formData.template_type,
|
|
1451
|
+
onChange: (e) => setFormData({ ...formData, template_type: e.target.value }),
|
|
1452
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
1453
|
+
},
|
|
1454
|
+
templateTypes.map((type) => /* @__PURE__ */ import_react.default.createElement("option", { key: type, value: type }, type.charAt(0).toUpperCase() + type.slice(1)))
|
|
1455
|
+
)), /* @__PURE__ */ import_react.default.createElement("div", { className: "flex items-center pt-6" }, /* @__PURE__ */ import_react.default.createElement("label", { className: "flex items-center cursor-pointer" }, /* @__PURE__ */ import_react.default.createElement(
|
|
1456
|
+
"input",
|
|
1457
|
+
{
|
|
1458
|
+
type: "checkbox",
|
|
1459
|
+
checked: formData.is_active,
|
|
1460
|
+
onChange: (e) => setFormData({ ...formData, is_active: e.target.checked }),
|
|
1461
|
+
className: "w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
1462
|
+
}
|
|
1463
|
+
), /* @__PURE__ */ import_react.default.createElement("span", { className: "ml-2 text-sm text-gray-700" }, "Active")))), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "Description"), /* @__PURE__ */ import_react.default.createElement(
|
|
1464
|
+
"textarea",
|
|
1465
|
+
{
|
|
1466
|
+
value: formData.description,
|
|
1467
|
+
onChange: (e) => setFormData({ ...formData, description: e.target.value }),
|
|
1468
|
+
rows: 3,
|
|
1469
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
|
1470
|
+
placeholder: "Brief description of when this template is used"
|
|
1471
|
+
}
|
|
1472
|
+
)), /* @__PURE__ */ import_react.default.createElement("div", { className: "pt-4 border-t border-gray-200" }, /* @__PURE__ */ import_react.default.createElement("p", { className: "text-sm text-gray-500" }, "\u{1F4A1} ", /* @__PURE__ */ import_react.default.createElement("strong", null, "Tip:"), ' Use the "Email Content" tab to write your subject line and email body.'))), activeTab === "content" && /* @__PURE__ */ import_react.default.createElement("div", { className: "p-6 space-y-4" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "Subject Line *"), /* @__PURE__ */ import_react.default.createElement(
|
|
1392
1473
|
"input",
|
|
1393
1474
|
{
|
|
1394
1475
|
type: "text",
|
|
1395
1476
|
required: true,
|
|
1396
1477
|
value: formData.subject_template,
|
|
1397
1478
|
onChange: (e) => setFormData({ ...formData, subject_template: e.target.value }),
|
|
1398
|
-
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
1479
|
+
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",
|
|
1480
|
+
placeholder: "e.g., Welcome to {{app_name}}, {{user_name}}!"
|
|
1399
1481
|
}
|
|
1400
|
-
), /* @__PURE__ */ import_react.default.createElement("
|
|
1482
|
+
), formData.subject_template && /* @__PURE__ */ import_react.default.createElement("div", { className: "mt-2 text-sm text-gray-600" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "text-gray-500" }, "Preview: "), /* @__PURE__ */ import_react.default.createElement("span", { dangerouslySetInnerHTML: { __html: subjectPreview } }))), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("div", { className: "flex items-center justify-between mb-2" }, /* @__PURE__ */ import_react.default.createElement("label", { className: "block text-sm font-medium text-gray-700" }, "Email Body (Markdown) *"), /* @__PURE__ */ import_react.default.createElement(
|
|
1483
|
+
"button",
|
|
1484
|
+
{
|
|
1485
|
+
type: "button",
|
|
1486
|
+
onClick: () => setShowPreview(!showPreview),
|
|
1487
|
+
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"}`
|
|
1488
|
+
},
|
|
1489
|
+
showPreview ? "\u270F\uFE0F Edit" : "\u{1F441}\uFE0F Preview"
|
|
1490
|
+
)), !showPreview ? /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement(
|
|
1401
1491
|
"textarea",
|
|
1402
1492
|
{
|
|
1403
1493
|
required: true,
|
|
1404
|
-
rows: 12,
|
|
1405
1494
|
value: formData.body_markdown,
|
|
1406
1495
|
onChange: (e) => setFormData({ ...formData, body_markdown: e.target.value }),
|
|
1407
|
-
|
|
1496
|
+
rows: 14,
|
|
1497
|
+
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",
|
|
1498
|
+
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"
|
|
1408
1499
|
}
|
|
1409
|
-
)
|
|
1500
|
+
), /* @__PURE__ */ import_react.default.createElement("p", { className: "mt-1 text-xs text-gray-500" }, "Use ", "{{variable}}", " for dynamic content. Supports Markdown formatting.")) : /* @__PURE__ */ import_react.default.createElement(
|
|
1501
|
+
"div",
|
|
1502
|
+
{
|
|
1503
|
+
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",
|
|
1504
|
+
dangerouslySetInnerHTML: { __html: renderedPreview || '<p class="text-gray-400">No content to preview</p>' }
|
|
1505
|
+
}
|
|
1506
|
+
)))), /* @__PURE__ */ import_react.default.createElement("div", { className: "px-6 py-4 border-t border-gray-200 flex justify-between items-center bg-gray-50" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "text-sm text-gray-500" }, activeTab === "basic" && "Step 1 of 2", activeTab === "content" && "Step 2 of 2"), /* @__PURE__ */ import_react.default.createElement("div", { className: "flex gap-3" }, /* @__PURE__ */ import_react.default.createElement(
|
|
1410
1507
|
"button",
|
|
1411
1508
|
{
|
|
1412
1509
|
type: "button",
|
|
1413
|
-
onClick:
|
|
1414
|
-
className: "
|
|
1510
|
+
onClick: onCancel,
|
|
1511
|
+
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"
|
|
1415
1512
|
},
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1513
|
+
"Cancel"
|
|
1514
|
+
), /* @__PURE__ */ import_react.default.createElement(
|
|
1515
|
+
"button",
|
|
1516
|
+
{
|
|
1517
|
+
type: "submit",
|
|
1518
|
+
onClick: handleSubmit,
|
|
1519
|
+
disabled: saving || !formData.template_name || !formData.subject_template || !formData.body_markdown,
|
|
1520
|
+
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"
|
|
1521
|
+
},
|
|
1522
|
+
saving ? "Saving..." : isEdit ? "Save Changes" : "Create Template"
|
|
1523
|
+
)))));
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
// src/frontend/TemplateTester.tsx
|
|
1527
|
+
var import_react2 = __toESM(require("react"), 1);
|
|
1528
|
+
var TemplateTester = ({
|
|
1529
|
+
template,
|
|
1530
|
+
onSendTest,
|
|
1531
|
+
onCancel,
|
|
1532
|
+
sending = false
|
|
1533
|
+
}) => {
|
|
1534
|
+
const [toEmail, setToEmail] = (0, import_react2.useState)("");
|
|
1535
|
+
const [variables, setVariables] = (0, import_react2.useState)({});
|
|
1536
|
+
const [detectedVariables, setDetectedVariables] = (0, import_react2.useState)([]);
|
|
1537
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
1538
|
+
const [success, setSuccess] = (0, import_react2.useState)(null);
|
|
1539
|
+
(0, import_react2.useEffect)(() => {
|
|
1540
|
+
const regex = /\{\{(\w+)\}\}/g;
|
|
1541
|
+
const subjectVars = Array.from(template.subject_template.matchAll(regex)).map((m) => m[1]);
|
|
1542
|
+
const bodyVars = Array.from(template.body_markdown.matchAll(regex)).map((m) => m[1]);
|
|
1543
|
+
const uniqueVars = Array.from(/* @__PURE__ */ new Set([...subjectVars, ...bodyVars]));
|
|
1544
|
+
setDetectedVariables(uniqueVars);
|
|
1545
|
+
const initialValues = {};
|
|
1546
|
+
uniqueVars.forEach((v) => {
|
|
1547
|
+
initialValues[v] = "";
|
|
1548
|
+
});
|
|
1549
|
+
setVariables(initialValues);
|
|
1550
|
+
}, [template]);
|
|
1551
|
+
const handleSubmit = async (e) => {
|
|
1552
|
+
e.preventDefault();
|
|
1553
|
+
setError(null);
|
|
1554
|
+
setSuccess(null);
|
|
1555
|
+
try {
|
|
1556
|
+
await onSendTest({
|
|
1557
|
+
template_id: template.template_id,
|
|
1558
|
+
to_email: toEmail,
|
|
1559
|
+
variables
|
|
1560
|
+
});
|
|
1561
|
+
setSuccess("Test email sent successfully!");
|
|
1562
|
+
} catch (e2) {
|
|
1563
|
+
setError(e2.message || "Failed to send test email");
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
return /* @__PURE__ */ import_react2.default.createElement("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" }, /* @__PURE__ */ import_react2.default.createElement("div", { className: "bg-white rounded-xl shadow-2xl max-w-lg w-full overflow-hidden flex flex-col max-h-[90vh]" }, /* @__PURE__ */ import_react2.default.createElement("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gray-50" }, /* @__PURE__ */ import_react2.default.createElement("h2", { className: "text-lg font-semibold text-gray-900" }, "Test Template: ", template.template_name), /* @__PURE__ */ import_react2.default.createElement("button", { onClick: onCancel, className: "text-gray-400 hover:text-gray-600" }, /* @__PURE__ */ import_react2.default.createElement("svg", { className: "w-6 h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react2.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })))), /* @__PURE__ */ import_react2.default.createElement("form", { onSubmit: handleSubmit, className: "flex-1 overflow-y-auto p-6 space-y-4" }, error && /* @__PURE__ */ import_react2.default.createElement("div", { className: "p-3 rounded-md text-sm bg-red-50 text-red-800 border border-red-200" }, error), success && /* @__PURE__ */ import_react2.default.createElement("div", { className: "p-3 rounded-md text-sm bg-green-50 text-green-800 border border-green-200" }, success), /* @__PURE__ */ import_react2.default.createElement("div", null, /* @__PURE__ */ import_react2.default.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "To Email *"), /* @__PURE__ */ import_react2.default.createElement(
|
|
1567
|
+
"input",
|
|
1568
|
+
{
|
|
1569
|
+
type: "email",
|
|
1570
|
+
required: true,
|
|
1571
|
+
value: toEmail,
|
|
1572
|
+
onChange: (e) => setToEmail(e.target.value),
|
|
1573
|
+
className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
|
1574
|
+
placeholder: "recipient@example.com"
|
|
1575
|
+
}
|
|
1576
|
+
)), detectedVariables.length > 0 && /* @__PURE__ */ import_react2.default.createElement("div", { className: "pt-2" }, /* @__PURE__ */ import_react2.default.createElement("h3", { className: "text-sm font-medium text-gray-900 mb-3 pb-2 border-b border-gray-100" }, "Template Variables"), /* @__PURE__ */ import_react2.default.createElement("div", { className: "space-y-3" }, detectedVariables.map((variable) => /* @__PURE__ */ import_react2.default.createElement("div", { key: variable }, /* @__PURE__ */ import_react2.default.createElement("label", { className: "block text-xs font-medium text-gray-500 uppercase mb-1" }, variable), /* @__PURE__ */ import_react2.default.createElement(
|
|
1577
|
+
"input",
|
|
1578
|
+
{
|
|
1579
|
+
type: "text",
|
|
1580
|
+
value: variables[variable] || "",
|
|
1581
|
+
onChange: (e) => setVariables({ ...variables, [variable]: e.target.value }),
|
|
1582
|
+
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",
|
|
1583
|
+
placeholder: `Value for {{${variable}}}`
|
|
1584
|
+
}
|
|
1585
|
+
))))), detectedVariables.length === 0 && /* @__PURE__ */ import_react2.default.createElement("p", { className: "text-sm text-gray-500 italic" }, "No variables detected in this template.")), /* @__PURE__ */ import_react2.default.createElement("div", { className: "px-6 py-4 border-t border-gray-200 flex justify-end gap-3 bg-gray-50" }, /* @__PURE__ */ import_react2.default.createElement(
|
|
1419
1586
|
"button",
|
|
1420
1587
|
{
|
|
1421
1588
|
type: "button",
|
|
1422
1589
|
onClick: onCancel,
|
|
1423
|
-
className: "px-4 py-2
|
|
1590
|
+
className: "px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-100"
|
|
1424
1591
|
},
|
|
1425
|
-
"
|
|
1426
|
-
), /* @__PURE__ */
|
|
1592
|
+
"Close"
|
|
1593
|
+
), /* @__PURE__ */ import_react2.default.createElement(
|
|
1427
1594
|
"button",
|
|
1428
1595
|
{
|
|
1429
1596
|
type: "submit",
|
|
1430
|
-
|
|
1597
|
+
onClick: handleSubmit,
|
|
1598
|
+
disabled: sending || !toEmail || success != null,
|
|
1599
|
+
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"
|
|
1431
1600
|
},
|
|
1432
|
-
/* @__PURE__ */
|
|
1433
|
-
|
|
1434
|
-
))))));
|
|
1601
|
+
sending ? /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement("div", { className: "animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent" }), "Sending...") : /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react2.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8" })), "Send Test Email")
|
|
1602
|
+
))));
|
|
1435
1603
|
};
|
|
1436
1604
|
|
|
1437
|
-
// src/frontend/TemplateManager.
|
|
1605
|
+
// src/frontend/TemplateManager.tsx
|
|
1606
|
+
var TYPE_COLORS = {
|
|
1607
|
+
"auth": { bg: "bg-blue-100", text: "text-blue-800" },
|
|
1608
|
+
"notification": { bg: "bg-purple-100", text: "text-purple-800" },
|
|
1609
|
+
"marketing": { bg: "bg-green-100", text: "text-green-800" },
|
|
1610
|
+
"system": { bg: "bg-gray-100", text: "text-gray-800" },
|
|
1611
|
+
"invitation": { bg: "bg-orange-100", text: "text-orange-800" },
|
|
1612
|
+
"verification": { bg: "bg-cyan-100", text: "text-cyan-800" },
|
|
1613
|
+
"default": { bg: "bg-gray-100", text: "text-gray-600" }
|
|
1614
|
+
};
|
|
1615
|
+
var normalizeNewlines2 = (text) => {
|
|
1616
|
+
return text?.replace(/\\n/g, "\n") || "";
|
|
1617
|
+
};
|
|
1438
1618
|
var TemplateManager = ({
|
|
1439
|
-
|
|
1619
|
+
onLoadTemplates,
|
|
1620
|
+
onSaveTemplate,
|
|
1621
|
+
onDeleteTemplate,
|
|
1622
|
+
onSendTestEmail,
|
|
1440
1623
|
title = "Email Templates",
|
|
1441
|
-
description = "Manage
|
|
1624
|
+
description = "Manage system email templates",
|
|
1625
|
+
templateTypes
|
|
1442
1626
|
}) => {
|
|
1443
|
-
const [templates, setTemplates] = (0,
|
|
1444
|
-
const [loading, setLoading] = (0,
|
|
1445
|
-
const [
|
|
1446
|
-
const [
|
|
1447
|
-
(0,
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
const
|
|
1627
|
+
const [templates, setTemplates] = (0, import_react3.useState)([]);
|
|
1628
|
+
const [loading, setLoading] = (0, import_react3.useState)(true);
|
|
1629
|
+
const [error, setError] = (0, import_react3.useState)(null);
|
|
1630
|
+
const [filterType, setFilterType] = (0, import_react3.useState)("all");
|
|
1631
|
+
const [previewTemplate, setPreviewTemplate] = (0, import_react3.useState)(null);
|
|
1632
|
+
const [editingTemplate, setEditingTemplate] = (0, import_react3.useState)(null);
|
|
1633
|
+
const [testingTemplate, setTestingTemplate] = (0, import_react3.useState)(null);
|
|
1634
|
+
const [showEditor, setShowEditor] = (0, import_react3.useState)(false);
|
|
1635
|
+
const [showTester, setShowTester] = (0, import_react3.useState)(false);
|
|
1636
|
+
const [saving, setSaving] = (0, import_react3.useState)(false);
|
|
1637
|
+
const [deleteConfirm, setDeleteConfirm] = (0, import_react3.useState)(null);
|
|
1638
|
+
const loadTemplates = (0, import_react3.useCallback)(async () => {
|
|
1639
|
+
setLoading(true);
|
|
1640
|
+
setError(null);
|
|
1451
1641
|
try {
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
}
|
|
1457
|
-
} catch (err) {
|
|
1458
|
-
console.error("Failed to load templates:", err);
|
|
1642
|
+
const data = await onLoadTemplates();
|
|
1643
|
+
setTemplates(data);
|
|
1644
|
+
} catch (e) {
|
|
1645
|
+
setError(e.message || "Failed to load templates");
|
|
1459
1646
|
} finally {
|
|
1460
1647
|
setLoading(false);
|
|
1461
1648
|
}
|
|
1649
|
+
}, [onLoadTemplates]);
|
|
1650
|
+
(0, import_react3.useEffect)(() => {
|
|
1651
|
+
loadTemplates();
|
|
1652
|
+
}, [loadTemplates]);
|
|
1653
|
+
const openCreate = () => {
|
|
1654
|
+
setEditingTemplate(null);
|
|
1655
|
+
setShowEditor(true);
|
|
1656
|
+
};
|
|
1657
|
+
const openPreview = (template) => {
|
|
1658
|
+
setPreviewTemplate(template);
|
|
1462
1659
|
};
|
|
1463
|
-
const
|
|
1660
|
+
const openEdit = (template) => {
|
|
1661
|
+
setPreviewTemplate(null);
|
|
1662
|
+
setEditingTemplate(template);
|
|
1663
|
+
setShowEditor(true);
|
|
1664
|
+
};
|
|
1665
|
+
const openTester = (template) => {
|
|
1666
|
+
setTestingTemplate(template);
|
|
1667
|
+
setShowTester(true);
|
|
1668
|
+
};
|
|
1669
|
+
const handleSave = async (data) => {
|
|
1670
|
+
setSaving(true);
|
|
1464
1671
|
try {
|
|
1465
|
-
await
|
|
1672
|
+
await onSaveTemplate(data);
|
|
1466
1673
|
setShowEditor(false);
|
|
1467
|
-
loadTemplates();
|
|
1468
|
-
} catch (
|
|
1469
|
-
|
|
1470
|
-
|
|
1674
|
+
await loadTemplates();
|
|
1675
|
+
} catch (e) {
|
|
1676
|
+
setSaving(false);
|
|
1677
|
+
throw e;
|
|
1471
1678
|
}
|
|
1679
|
+
setSaving(false);
|
|
1472
1680
|
};
|
|
1473
1681
|
const handleDelete = async (id) => {
|
|
1474
|
-
if (!
|
|
1682
|
+
if (!onDeleteTemplate) return;
|
|
1475
1683
|
try {
|
|
1476
|
-
await
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1684
|
+
await onDeleteTemplate(id);
|
|
1685
|
+
setDeleteConfirm(null);
|
|
1686
|
+
await loadTemplates();
|
|
1687
|
+
} catch (e) {
|
|
1688
|
+
setError(e.message || "Failed to delete template");
|
|
1481
1689
|
}
|
|
1482
1690
|
};
|
|
1483
|
-
|
|
1691
|
+
const handleTest = async (data) => {
|
|
1692
|
+
if (!onSendTestEmail) return;
|
|
1693
|
+
setSaving(true);
|
|
1694
|
+
try {
|
|
1695
|
+
await onSendTestEmail(data);
|
|
1696
|
+
} finally {
|
|
1697
|
+
setSaving(false);
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
const getTypeColor = (type) => TYPE_COLORS[type] || TYPE_COLORS["default"];
|
|
1701
|
+
const uniqueTypes = ["all", ...new Set(templates.map((t) => t.template_type))];
|
|
1702
|
+
const filteredTemplates = filterType === "all" ? templates : templates.filter((t) => t.template_type === filterType);
|
|
1703
|
+
const formatDate = (timestamp) => {
|
|
1704
|
+
if (!timestamp) return "N/A";
|
|
1705
|
+
return new Date(timestamp * 1e3).toLocaleDateString("en-US", {
|
|
1706
|
+
month: "short",
|
|
1707
|
+
day: "numeric",
|
|
1708
|
+
year: "numeric"
|
|
1709
|
+
});
|
|
1710
|
+
};
|
|
1711
|
+
const previewBody = (0, import_react3.useMemo)(() => {
|
|
1712
|
+
if (!previewTemplate?.body_markdown) return "";
|
|
1713
|
+
let md = normalizeNewlines2(previewTemplate.body_markdown);
|
|
1714
|
+
md = md.replace(/\{\{(\w+)\}\}/g, '<span class="bg-blue-100 text-blue-800 px-1 rounded text-sm">{{$1}}</span>');
|
|
1715
|
+
return import_marked3.marked.parse(md);
|
|
1716
|
+
}, [previewTemplate]);
|
|
1717
|
+
if (loading) {
|
|
1718
|
+
return /* @__PURE__ */ import_react3.default.createElement("div", { className: "flex items-center justify-center h-64" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" }));
|
|
1719
|
+
}
|
|
1720
|
+
return /* @__PURE__ */ import_react3.default.createElement("div", { className: "template-manager" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "flex justify-between items-center mb-6" }, /* @__PURE__ */ import_react3.default.createElement("div", null, /* @__PURE__ */ import_react3.default.createElement("h1", { className: "text-2xl font-bold text-gray-900" }, title), /* @__PURE__ */ import_react3.default.createElement("p", { className: "text-sm text-gray-500 mt-1" }, description)), /* @__PURE__ */ import_react3.default.createElement(
|
|
1484
1721
|
"button",
|
|
1485
1722
|
{
|
|
1486
|
-
onClick:
|
|
1487
|
-
|
|
1488
|
-
setShowEditor(true);
|
|
1489
|
-
},
|
|
1490
|
-
className: "flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
1723
|
+
onClick: openCreate,
|
|
1724
|
+
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"
|
|
1491
1725
|
},
|
|
1492
|
-
/* @__PURE__ */
|
|
1493
|
-
"
|
|
1494
|
-
)),
|
|
1726
|
+
/* @__PURE__ */ import_react3.default.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react3.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 4v16m8-8H4" })),
|
|
1727
|
+
"Create Template"
|
|
1728
|
+
)), error && /* @__PURE__ */ import_react3.default.createElement("div", { className: "mb-4 p-3 rounded-lg bg-red-50 text-red-800 border border-red-200" }, error, /* @__PURE__ */ import_react3.default.createElement("button", { onClick: () => setError(null), className: "ml-2 underline" }, "Dismiss")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "grid grid-cols-4 gap-4 mb-6" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-gray-900" }, templates.length), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Total")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-green-600" }, templates.filter((t) => t.is_active).length), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Active")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-yellow-600" }, templates.filter((t) => !t.is_active).length), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Inactive")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-blue-600" }, new Set(templates.map((t) => t.template_type)).size), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Types"))), /* @__PURE__ */ import_react3.default.createElement("div", { className: "flex gap-2 mb-6 border-b border-gray-200 pb-3 flex-wrap" }, uniqueTypes.map((type) => /* @__PURE__ */ import_react3.default.createElement(
|
|
1495
1729
|
"button",
|
|
1496
1730
|
{
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1731
|
+
key: type,
|
|
1732
|
+
onClick: () => setFilterType(type),
|
|
1733
|
+
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"}`
|
|
1734
|
+
},
|
|
1735
|
+
type.charAt(0).toUpperCase() + type.slice(1)
|
|
1736
|
+
))), filteredTemplates.length === 0 ? /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-center py-16 bg-gray-50 rounded-lg border-2 border-dashed border-gray-200" }, /* @__PURE__ */ import_react3.default.createElement("svg", { className: "mx-auto h-12 w-12 text-gray-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react3.default.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__ */ import_react3.default.createElement("h3", { className: "mt-4 text-lg font-medium text-gray-900" }, "No templates found"), /* @__PURE__ */ import_react3.default.createElement("p", { className: "mt-2 text-sm text-gray-500" }, filterType === "all" ? "Create your first template." : `No "${filterType}" templates.`), /* @__PURE__ */ import_react3.default.createElement("button", { onClick: openCreate, className: "mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm" }, "Create Template")) : /* @__PURE__ */ import_react3.default.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" }, filteredTemplates.map((template) => {
|
|
1737
|
+
const typeColor = getTypeColor(template.template_type);
|
|
1738
|
+
return /* @__PURE__ */ import_react3.default.createElement(
|
|
1739
|
+
"div",
|
|
1740
|
+
{
|
|
1741
|
+
key: template.template_id,
|
|
1742
|
+
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",
|
|
1743
|
+
onClick: () => openPreview(template)
|
|
1500
1744
|
},
|
|
1501
|
-
className: "p-2 text-
|
|
1745
|
+
/* @__PURE__ */ import_react3.default.createElement("div", { className: "p-5 flex-1 flex flex-col" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "flex items-start justify-between mb-3" }, /* @__PURE__ */ import_react3.default.createElement("span", { className: `px-2 py-1 text-xs font-medium rounded ${typeColor.bg} ${typeColor.text}` }, template.template_type), /* @__PURE__ */ import_react3.default.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__ */ import_react3.default.createElement("h3", { className: "font-semibold text-gray-900 mb-1" }, template.template_name), /* @__PURE__ */ import_react3.default.createElement("p", { className: "text-sm text-gray-500 flex-1 line-clamp-2" }, template.description || "No description"), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-xs text-gray-400 border-t border-gray-100 pt-3 mt-3 truncate" }, "\u{1F4E7} ", template.subject_template)),
|
|
1746
|
+
/* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-gray-50 px-5 py-3 flex justify-between items-center border-t border-gray-100" }, /* @__PURE__ */ import_react3.default.createElement("span", { className: "text-xs text-gray-500" }, formatDate(template.updated_at)), /* @__PURE__ */ import_react3.default.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ import_react3.default.createElement(
|
|
1747
|
+
"button",
|
|
1748
|
+
{
|
|
1749
|
+
onClick: (e) => {
|
|
1750
|
+
e.stopPropagation();
|
|
1751
|
+
openEdit(template);
|
|
1752
|
+
},
|
|
1753
|
+
className: "p-1.5 text-blue-600 hover:bg-blue-50 rounded",
|
|
1754
|
+
title: "Edit"
|
|
1755
|
+
},
|
|
1756
|
+
/* @__PURE__ */ import_react3.default.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react3.default.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" }))
|
|
1757
|
+
), onSendTestEmail && /* @__PURE__ */ import_react3.default.createElement(
|
|
1758
|
+
"button",
|
|
1759
|
+
{
|
|
1760
|
+
onClick: (e) => {
|
|
1761
|
+
e.stopPropagation();
|
|
1762
|
+
openTester(template);
|
|
1763
|
+
},
|
|
1764
|
+
className: "p-1.5 text-purple-600 hover:bg-purple-50 rounded",
|
|
1765
|
+
title: "Send Test Email"
|
|
1766
|
+
},
|
|
1767
|
+
/* @__PURE__ */ import_react3.default.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react3.default.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" }))
|
|
1768
|
+
), onDeleteTemplate && /* @__PURE__ */ import_react3.default.createElement(
|
|
1769
|
+
"button",
|
|
1770
|
+
{
|
|
1771
|
+
onClick: (e) => {
|
|
1772
|
+
e.stopPropagation();
|
|
1773
|
+
setDeleteConfirm(template.template_id);
|
|
1774
|
+
},
|
|
1775
|
+
className: "p-1.5 text-red-600 hover:bg-red-50 rounded",
|
|
1776
|
+
title: "Delete"
|
|
1777
|
+
},
|
|
1778
|
+
/* @__PURE__ */ import_react3.default.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react3.default.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" }))
|
|
1779
|
+
)))
|
|
1780
|
+
);
|
|
1781
|
+
})), previewTemplate && /* @__PURE__ */ import_react3.default.createElement("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-hidden flex flex-col" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gray-50" }, /* @__PURE__ */ import_react3.default.createElement("div", null, /* @__PURE__ */ import_react3.default.createElement("h2", { className: "text-lg font-semibold text-gray-900" }, previewTemplate.template_name), /* @__PURE__ */ import_react3.default.createElement("p", { className: "text-sm text-gray-500" }, previewTemplate.template_type, " \u2022 ", previewTemplate.is_active ? "Active" : "Inactive")), /* @__PURE__ */ import_react3.default.createElement("button", { onClick: () => setPreviewTemplate(null), className: "text-gray-400 hover:text-gray-600" }, /* @__PURE__ */ import_react3.default.createElement("svg", { className: "w-6 h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react3.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })))), /* @__PURE__ */ import_react3.default.createElement("div", { className: "flex-1 overflow-y-auto p-6 space-y-4" }, /* @__PURE__ */ import_react3.default.createElement("div", null, /* @__PURE__ */ import_react3.default.createElement("label", { className: "block text-xs font-medium text-gray-500 uppercase mb-1" }, "Subject"), /* @__PURE__ */ import_react3.default.createElement("div", { className: "p-3 bg-gray-50 rounded-lg border border-gray-200 font-mono text-sm" }, previewTemplate.subject_template)), /* @__PURE__ */ import_react3.default.createElement("div", null, /* @__PURE__ */ import_react3.default.createElement("label", { className: "block text-xs font-medium text-gray-500 uppercase mb-1" }, "Email Body"), /* @__PURE__ */ import_react3.default.createElement(
|
|
1782
|
+
"div",
|
|
1783
|
+
{
|
|
1784
|
+
className: "p-4 bg-white rounded-lg border border-gray-200 prose prose-sm max-w-none",
|
|
1785
|
+
dangerouslySetInnerHTML: { __html: previewBody }
|
|
1786
|
+
}
|
|
1787
|
+
)), previewTemplate.description && /* @__PURE__ */ import_react3.default.createElement("div", null, /* @__PURE__ */ import_react3.default.createElement("label", { className: "block text-xs font-medium text-gray-500 uppercase mb-1" }, "Description"), /* @__PURE__ */ import_react3.default.createElement("p", { className: "text-gray-600 text-sm" }, previewTemplate.description))), /* @__PURE__ */ import_react3.default.createElement("div", { className: "px-6 py-4 border-t border-gray-200 flex justify-end gap-3 bg-gray-50" }, /* @__PURE__ */ import_react3.default.createElement(
|
|
1788
|
+
"button",
|
|
1789
|
+
{
|
|
1790
|
+
onClick: () => setPreviewTemplate(null),
|
|
1791
|
+
className: "px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-100"
|
|
1502
1792
|
},
|
|
1503
|
-
|
|
1504
|
-
), /* @__PURE__ */
|
|
1793
|
+
"Close"
|
|
1794
|
+
), /* @__PURE__ */ import_react3.default.createElement(
|
|
1505
1795
|
"button",
|
|
1506
1796
|
{
|
|
1507
|
-
onClick: () =>
|
|
1508
|
-
className: "
|
|
1797
|
+
onClick: () => openEdit(previewTemplate),
|
|
1798
|
+
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"
|
|
1509
1799
|
},
|
|
1510
|
-
/* @__PURE__ */
|
|
1511
|
-
|
|
1800
|
+
/* @__PURE__ */ import_react3.default.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react3.default.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" })),
|
|
1801
|
+
"Edit Template"
|
|
1802
|
+
), onSendTestEmail && /* @__PURE__ */ import_react3.default.createElement(
|
|
1803
|
+
"button",
|
|
1804
|
+
{
|
|
1805
|
+
onClick: () => openTester(previewTemplate),
|
|
1806
|
+
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"
|
|
1807
|
+
},
|
|
1808
|
+
/* @__PURE__ */ import_react3.default.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react3.default.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" })),
|
|
1809
|
+
"Test"
|
|
1810
|
+
)))), showEditor && /* @__PURE__ */ import_react3.default.createElement(
|
|
1512
1811
|
TemplateEditor,
|
|
1513
1812
|
{
|
|
1514
|
-
initialData: editingTemplate
|
|
1813
|
+
initialData: editingTemplate ? {
|
|
1814
|
+
template_id: editingTemplate.template_id,
|
|
1815
|
+
template_name: editingTemplate.template_name,
|
|
1816
|
+
template_type: editingTemplate.template_type,
|
|
1817
|
+
subject_template: editingTemplate.subject_template,
|
|
1818
|
+
body_markdown: editingTemplate.body_markdown,
|
|
1819
|
+
description: editingTemplate.description || "",
|
|
1820
|
+
is_active: !!editingTemplate.is_active
|
|
1821
|
+
} : null,
|
|
1515
1822
|
onSave: handleSave,
|
|
1516
|
-
onCancel: () => setShowEditor(false)
|
|
1823
|
+
onCancel: () => setShowEditor(false),
|
|
1824
|
+
templateTypes,
|
|
1825
|
+
saving
|
|
1826
|
+
}
|
|
1827
|
+
), showTester && testingTemplate && /* @__PURE__ */ import_react3.default.createElement(
|
|
1828
|
+
TemplateTester,
|
|
1829
|
+
{
|
|
1830
|
+
template: testingTemplate,
|
|
1831
|
+
onSendTest: handleTest,
|
|
1832
|
+
onCancel: () => setShowTester(false),
|
|
1833
|
+
sending: saving
|
|
1517
1834
|
}
|
|
1518
|
-
))
|
|
1835
|
+
), deleteConfirm && /* @__PURE__ */ import_react3.default.createElement("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg shadow-xl max-w-sm w-full p-6" }, /* @__PURE__ */ import_react3.default.createElement("h3", { className: "text-lg font-semibold mb-2" }, "Delete Template?"), /* @__PURE__ */ import_react3.default.createElement("p", { className: "text-gray-600 mb-4" }, "This action cannot be undone."), /* @__PURE__ */ import_react3.default.createElement("div", { className: "flex justify-end gap-3" }, /* @__PURE__ */ import_react3.default.createElement(
|
|
1836
|
+
"button",
|
|
1837
|
+
{
|
|
1838
|
+
onClick: () => setDeleteConfirm(null),
|
|
1839
|
+
className: "px-4 py-2 border border-gray-300 rounded-lg"
|
|
1840
|
+
},
|
|
1841
|
+
"Cancel"
|
|
1842
|
+
), /* @__PURE__ */ import_react3.default.createElement(
|
|
1843
|
+
"button",
|
|
1844
|
+
{
|
|
1845
|
+
onClick: () => handleDelete(deleteConfirm),
|
|
1846
|
+
className: "px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
|
1847
|
+
},
|
|
1848
|
+
"Delete"
|
|
1849
|
+
)))));
|
|
1519
1850
|
};
|
|
1520
1851
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1521
1852
|
0 && (module.exports = {
|