@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.
Files changed (37) hide show
  1. package/dist/TemplateManager-Db41KyPN.d.cts +77 -0
  2. package/dist/TemplateManager-Db41KyPN.d.ts +77 -0
  3. package/dist/backend/EmailService.cjs +44 -9
  4. package/dist/backend/EmailService.cjs.map +1 -1
  5. package/dist/backend/EmailService.d.cts +109 -0
  6. package/dist/backend/EmailService.d.ts +109 -0
  7. package/dist/backend/EmailService.js +44 -9
  8. package/dist/backend/EmailService.js.map +1 -1
  9. package/dist/backend/EmailingCacheDO.cjs +0 -1
  10. package/dist/backend/EmailingCacheDO.cjs.map +1 -1
  11. package/dist/backend/EmailingCacheDO.d.cts +66 -0
  12. package/dist/backend/EmailingCacheDO.d.ts +66 -0
  13. package/dist/backend/EmailingCacheDO.js +0 -1
  14. package/dist/backend/EmailingCacheDO.js.map +1 -1
  15. package/dist/backend/routes/index.cjs +44 -9
  16. package/dist/backend/routes/index.cjs.map +1 -1
  17. package/dist/backend/routes/index.d.cts +32 -0
  18. package/dist/backend/routes/index.d.ts +32 -0
  19. package/dist/backend/routes/index.js +44 -9
  20. package/dist/backend/routes/index.js.map +1 -1
  21. package/dist/cli.d.cts +1 -0
  22. package/dist/cli.d.ts +1 -0
  23. package/dist/common/index.d.cts +46 -0
  24. package/dist/common/index.d.ts +46 -0
  25. package/dist/frontend/index.cjs +665 -0
  26. package/dist/frontend/index.cjs.map +1 -0
  27. package/dist/frontend/index.d.cts +32 -0
  28. package/dist/frontend/index.d.ts +32 -0
  29. package/dist/frontend/index.js +626 -0
  30. package/dist/frontend/index.js.map +1 -0
  31. package/dist/index.cjs +439 -108
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +7 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.js +440 -109
  36. package/dist/index.js.map +1 -1
  37. 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: ((_a = config.branding) == null ? void 0 : _a.brandName) || "Your App",
601
- portalUrl: ((_b = config.branding) == null ? void 0 : _b.portalUrl) || "https://app.example.com",
602
- primaryColor: ((_c = config.branding) == null ? void 0 : _c.primaryColor) || "#667eea",
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
- const markdown = import_mustache.default.render(template.body_markdown, data);
778
- import_marked.marked.use({ mangle: false, headerIds: false });
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 = (contentType == null ? void 0 : contentType.includes("application/json")) ? await response.json() : await response.text();
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
- return Buffer.from(String(str)).toString("base64");
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.jsx
1344
- var import_react2 = __toESM(require("react"), 1);
1345
- var import_lucide_react2 = require("lucide-react");
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.jsx
1382
+ // src/frontend/TemplateEditor.tsx
1348
1383
  var import_react = __toESM(require("react"), 1);
1349
- var import_lucide_react = require("lucide-react");
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
- onPreview,
1355
- variablesHelpText = "Supports {{variable}}"
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: "daily_reminder",
1361
- subject_template: "",
1362
- body_markdown: "",
1363
- variables: "[]",
1364
- description: "",
1365
- is_active: 1,
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
- 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-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "p-6 border-b border-gray-200 flex justify-between items-center" }, /* @__PURE__ */ import_react.default.createElement("h2", { className: "text-xl font-bold text-gray-900" }, initialData ? "Edit Template" : "New Template"), /* @__PURE__ */ import_react.default.createElement("button", { onClick: onCancel, className: "p-2 hover:bg-gray-100 rounded-lg" }, /* @__PURE__ */ import_react.default.createElement(import_lucide_react.X, { className: "h-5 w-5" }))), /* @__PURE__ */ import_react.default.createElement("form", { onSubmit: handleSubmit, className: "flex-1 overflow-y-auto p-6 space-y-4" }, /* @__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" }, "Template ID *"), /* @__PURE__ */ import_react.default.createElement(
1373
- "input",
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: "text",
1376
- required: true,
1377
- disabled: !!initialData,
1378
- value: formData.template_id,
1379
- onChange: (e) => setFormData({ ...formData, template_id: e.target.value }),
1380
- className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
1381
- }
1382
- )), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "Name *"), /* @__PURE__ */ import_react.default.createElement(
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
- ))), /* @__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 *"), /* @__PURE__ */ import_react.default.createElement(
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("p", { className: "text-xs text-gray-500 mt-1" }, variablesHelpText)), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("label", { className: "block text-sm font-medium text-gray-700 mb-1" }, "Body (Markdown) *"), /* @__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
- className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
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
- )), /* @__PURE__ */ import_react.default.createElement("div", { className: "flex justify-between pt-4" }, /* @__PURE__ */ import_react.default.createElement(
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: () => onPreview && onPreview(formData),
1414
- className: "flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
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
- /* @__PURE__ */ import_react.default.createElement(import_lucide_react.Eye, { className: "h-4 w-4" }),
1417
- " Preview"
1418
- ), /* @__PURE__ */ import_react.default.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ import_react.default.createElement(
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 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
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
- "Cancel"
1426
- ), /* @__PURE__ */ import_react.default.createElement(
1592
+ "Close"
1593
+ ), /* @__PURE__ */ import_react2.default.createElement(
1427
1594
  "button",
1428
1595
  {
1429
1596
  type: "submit",
1430
- className: "flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
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__ */ import_react.default.createElement(import_lucide_react.Save, { className: "h-4 w-4" }),
1433
- " Save"
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.jsx
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
- apiClient,
1619
+ onLoadTemplates,
1620
+ onSaveTemplate,
1621
+ onDeleteTemplate,
1622
+ onSendTestEmail,
1440
1623
  title = "Email Templates",
1441
- description = "Manage your email templates"
1624
+ description = "Manage system email templates",
1625
+ templateTypes
1442
1626
  }) => {
1443
- const [templates, setTemplates] = (0, import_react2.useState)([]);
1444
- const [loading, setLoading] = (0, import_react2.useState)(true);
1445
- const [showEditor, setShowEditor] = (0, import_react2.useState)(false);
1446
- const [editingTemplate, setEditingTemplate] = (0, import_react2.useState)(null);
1447
- (0, import_react2.useEffect)(() => {
1448
- loadTemplates();
1449
- }, []);
1450
- const loadTemplates = async () => {
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
- setLoading(true);
1453
- const res = await apiClient.get("/templates");
1454
- if (res.data.success) {
1455
- setTemplates(res.data.templates);
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 handleSave = async (formData) => {
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 apiClient.post("/templates", formData);
1672
+ await onSaveTemplate(data);
1466
1673
  setShowEditor(false);
1467
- loadTemplates();
1468
- } catch (err) {
1469
- console.error("Failed to save template:", err);
1470
- alert("Failed to save template");
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 (!confirm("Delete this template?")) return;
1682
+ if (!onDeleteTemplate) return;
1475
1683
  try {
1476
- await apiClient.delete(`/templates/${id}`);
1477
- loadTemplates();
1478
- } catch (err) {
1479
- console.error("Failed to delete:", err);
1480
- alert("Failed to delete template");
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
- return /* @__PURE__ */ import_react2.default.createElement("div", { className: "email-template-manager" }, /* @__PURE__ */ import_react2.default.createElement("div", { className: "flex justify-between items-center mb-6" }, /* @__PURE__ */ import_react2.default.createElement("div", null, /* @__PURE__ */ import_react2.default.createElement("h1", { className: "text-2xl font-bold text-gray-900" }, title), /* @__PURE__ */ import_react2.default.createElement("p", { className: "text-gray-600 mt-1" }, description)), /* @__PURE__ */ import_react2.default.createElement(
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
- setEditingTemplate(null);
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__ */ import_react2.default.createElement(import_lucide_react2.Plus, { className: "h-4 w-4" }),
1493
- " New Template"
1494
- )), loading ? /* @__PURE__ */ import_react2.default.createElement("div", { className: "flex justify-center p-12" }, /* @__PURE__ */ import_react2.default.createElement(import_lucide_react2.Loader, { className: "animate-spin text-gray-500" })) : /* @__PURE__ */ import_react2.default.createElement("div", { className: "grid gap-4" }, templates.map((t) => /* @__PURE__ */ import_react2.default.createElement("div", { key: t.template_id, className: "bg-white border rounded-lg p-6 flex justify-between items-start hover:shadow-md transition" }, /* @__PURE__ */ import_react2.default.createElement("div", null, /* @__PURE__ */ import_react2.default.createElement("div", { className: "flex items-center gap-3 mb-2" }, /* @__PURE__ */ import_react2.default.createElement(import_lucide_react2.Mail, { className: "h-5 w-5 text-blue-600" }), /* @__PURE__ */ import_react2.default.createElement("h3", { className: "text-lg font-semibold" }, t.template_name), /* @__PURE__ */ import_react2.default.createElement("span", { className: "px-2 py-1 text-xs bg-gray-100 rounded" }, t.template_type)), /* @__PURE__ */ import_react2.default.createElement("p", { className: "text-sm text-gray-600" }, t.description)), /* @__PURE__ */ import_react2.default.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ import_react2.default.createElement(
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
- onClick: () => {
1498
- setEditingTemplate(t);
1499
- setShowEditor(true);
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-blue-600 hover:bg-blue-50 rounded"
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
- /* @__PURE__ */ import_react2.default.createElement(import_lucide_react2.Edit2, { className: "h-4 w-4" })
1504
- ), /* @__PURE__ */ import_react2.default.createElement(
1793
+ "Close"
1794
+ ), /* @__PURE__ */ import_react3.default.createElement(
1505
1795
  "button",
1506
1796
  {
1507
- onClick: () => handleDelete(t.template_id),
1508
- className: "p-2 text-red-600 hover:bg-red-50 rounded"
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__ */ import_react2.default.createElement(import_lucide_react2.Trash2, { className: "h-4 w-4" })
1511
- )))), templates.length === 0 && /* @__PURE__ */ import_react2.default.createElement("div", { className: "text-center py-12 text-gray-500 bg-gray-50 rounded border-dashed border-2" }, "No templates found. Create one to get started.")), showEditor && /* @__PURE__ */ import_react2.default.createElement(
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 = {