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