feat: Add email settings management and templates functionality
All checks were successful
CI / test (3.11) (push) Successful in 1m36s
CI / build-image (push) Successful in 1m27s

- Implemented email settings configuration in the admin panel, allowing for SMTP settings and notification preferences.
- Created a new template for email settings with fields for SMTP host, port, username, password, sender address, and recipients.
- Added JavaScript functionality to handle loading, saving, and validating email settings.
- Introduced email templates management, enabling the listing, editing, and saving of email templates.
- Updated navigation to include links to email settings and templates.
- Added tests for email settings and templates to ensure proper functionality and validation.
This commit is contained in:
2025-11-15 11:12:23 +01:00
parent 2629f6b25f
commit e192086833
19 changed files with 1537 additions and 192 deletions

View File

@@ -146,6 +146,162 @@ async function saveEmbedSetting(key, inputId) {
}
}
// ==================== EMAIL SETTINGS ====================
const emailSettingsFieldConfig = {
smtp_host: { selector: "#smtpHost", type: "text" },
smtp_port: { selector: "#smtpPort", type: "number" },
smtp_username: { selector: "#smtpUsername", type: "text" },
smtp_password: { selector: "#smtpPassword", type: "text" },
smtp_sender: { selector: "#smtpSender", type: "text" },
smtp_recipients: { selector: "#smtpRecipients", type: "textarea" },
smtp_use_tls: { selector: "#smtpUseTls", type: "checkbox" },
notify_contact_form: { selector: "#notifyContactForm", type: "checkbox" },
notify_newsletter_signups: {
selector: "#notifyNewsletter",
type: "checkbox",
},
};
function normalizeRecipientsInput(value) {
if (Array.isArray(value)) return value.join(", ");
if (!value) return "";
return value;
}
function applyEmailSettingsForm(settings) {
Object.entries(emailSettingsFieldConfig).forEach(([field, config]) => {
const element = document.querySelector(config.selector);
if (!element) return;
const fieldValue = settings[field];
switch (config.type) {
case "checkbox": {
const normalized =
fieldValue === true ||
fieldValue === "true" ||
fieldValue === 1 ||
fieldValue === "1";
element.checked = normalized;
break;
}
case "number": {
if (typeof fieldValue === "number" && Number.isFinite(fieldValue)) {
element.value = fieldValue;
} else {
const parsed = parseInt(fieldValue, 10);
element.value = Number.isFinite(parsed) ? parsed : "";
}
break;
}
case "textarea":
element.value = normalizeRecipientsInput(fieldValue);
break;
default:
element.value = fieldValue || "";
break;
}
});
}
function collectEmailSettingsForm() {
const payload = {};
Object.entries(emailSettingsFieldConfig).forEach(([field, config]) => {
const element = document.querySelector(config.selector);
if (!element) return;
switch (config.type) {
case "checkbox":
payload[field] = element.checked;
break;
case "number":
if (element.value === "") {
payload[field] = "";
} else {
const parsed = parseInt(element.value, 10);
payload[field] = Number.isFinite(parsed) ? parsed : element.value;
}
break;
case "textarea":
payload[field] = element.value;
break;
default:
payload[field] = element.value;
break;
}
});
return payload;
}
function clearEmailFieldErrors() {
Object.values(emailSettingsFieldConfig).forEach((config) => {
const element = document.querySelector(config.selector);
if (element) element.classList.remove("input-error");
});
}
function applyEmailFieldErrors(errors) {
Object.entries(errors || {}).forEach(([field]) => {
const config = emailSettingsFieldConfig[field];
if (!config) return;
const element = document.querySelector(config.selector);
if (element) element.classList.add("input-error");
});
}
async function loadEmailSettings() {
try {
const response = await fetch("/admin/api/email-settings");
const data = await response.json();
if (data.status === "ok" && data.settings) {
applyEmailSettingsForm(data.settings);
} else {
showMessage(data.message || "Failed to load email settings", "error");
}
} catch (error) {
console.error("Failed to load email settings", error);
showMessage("Failed to load email settings", "error");
}
}
async function submitEmailSettings(event) {
event.preventDefault();
clearEmailFieldErrors();
const payload = collectEmailSettingsForm();
try {
const response = await fetch("/admin/api/email-settings", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await response.json();
if (response.ok && data.status === "ok") {
showMessage("Email settings updated successfully.", "success");
applyEmailSettingsForm(data.settings || {});
} else {
applyEmailFieldErrors(data.errors);
const errorSummary = Array.isArray(data.errors)
? data.errors.join(" ")
: data && data.errors
? Object.values(data.errors).join(" ")
: "";
const message = [
data.message || "Failed to update email settings",
errorSummary,
]
.filter(Boolean)
.join(" ");
showMessage(message, "error");
}
} catch (error) {
console.error("Failed to update email settings", error);
showMessage("Failed to update email settings", "error");
}
}
// ==================== EMBED MANAGEMENT ====================
/**
@@ -280,56 +436,170 @@ async function loadDashboardStats() {
// ==================== EMAIL TEMPLATES ====================
/**
* Loads email template from settings
*/
function loadEmailTemplate() {
const textarea = document.getElementById("newsletterTemplate");
if (!textarea) return;
let emailTemplatesCache = [];
let activeEmailTemplateId = null;
fetch("/admin/api/settings")
.then((r) => r.json())
.then((data) => {
if (
data.status === "ok" &&
data.settings &&
data.settings.newsletter_confirmation_template
) {
textarea.value = data.settings.newsletter_confirmation_template;
}
})
.catch((err) => console.error("Failed to load template", err));
function setEmailTemplateMessage(text, type = "info") {
const messageEl = document.getElementById("templateMessage");
if (!messageEl) return;
if (!text) {
messageEl.textContent = "";
messageEl.className = "message";
messageEl.style.display = "none";
return;
}
messageEl.textContent = text;
messageEl.className = `message ${type}`;
messageEl.style.display = "block";
}
/**
* Saves email template to settings
*/
function saveEmailTemplate() {
const textarea = document.getElementById("newsletterTemplate");
const message = document.getElementById("message");
if (!textarea || !message) return;
function highlightSelectedTemplate(templateId) {
const buttons = document.querySelectorAll(
"#emailTemplatesList button[data-template-id]"
);
buttons.forEach((button) => {
if (button.dataset.templateId === templateId) {
button.classList.add("active");
} else {
button.classList.remove("active");
}
});
}
const value = textarea.value || "";
function renderEmailTemplatesList(templates) {
const listEl = document.getElementById("emailTemplatesList");
if (!listEl) return;
fetch("/admin/api/settings/newsletter_confirmation_template", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ value }),
})
.then((r) => r.json())
.then((data) => {
const isSuccess = data.status === "ok";
message.className = `message ${isSuccess ? "success" : "error"}`;
message.textContent = isSuccess
? "Template saved"
: `Failed to save template: ${data.message || ""}`;
setTimeout(() => (message.textContent = ""), 4000);
})
.catch((err) => {
console.error("Failed to save template", err);
message.className = "message error";
message.textContent = "Failed to save template";
if (!templates.length) {
listEl.innerHTML =
'<p class="empty-state">No editable templates are configured.</p>';
return;
}
listEl.innerHTML = templates
.map(
(template) => `
<button type="button" class="template-item" data-template-id="${
template.id
}">
<span class="template-name">${escapeHtml(template.name)}</span>
<span class="template-description">${escapeHtml(
template.description
)}</span>
</button>
`
)
.join("");
listEl.querySelectorAll("button[data-template-id]").forEach((button) => {
button.addEventListener("click", () => {
selectEmailTemplate(button.dataset.templateId);
});
});
}
function disableEmailTemplateEditor(disabled) {
const form = document.getElementById("emailTemplateForm");
const textarea = document.getElementById("templateContent");
const saveButton = document.getElementById("saveTemplateButton");
if (form) form.classList.toggle("disabled", disabled);
if (textarea) textarea.disabled = disabled;
if (saveButton) saveButton.disabled = disabled;
}
async function loadEmailTemplatesPage() {
setEmailTemplateMessage("Loading email templates...", "info");
disableEmailTemplateEditor(true);
try {
const response = await fetch("/admin/api/email-templates");
const data = await response.json();
if (data.status !== "ok") {
throw new Error(data.message || "Failed to load email templates");
}
emailTemplatesCache = Array.isArray(data.templates) ? data.templates : [];
renderEmailTemplatesList(emailTemplatesCache);
setEmailTemplateMessage("");
if (emailTemplatesCache.length) {
selectEmailTemplate(emailTemplatesCache[0].id);
}
} catch (error) {
console.error("Failed to load email templates", error);
setEmailTemplateMessage("Failed to load email templates", "error");
}
}
async function selectEmailTemplate(templateId) {
if (!templateId) return;
setEmailTemplateMessage("Loading template...", "info");
disableEmailTemplateEditor(true);
try {
const response = await fetch(`/admin/api/email-templates/${templateId}`);
const data = await response.json();
if (data.status !== "ok" || !data.template) {
throw new Error(data.message || "Failed to load template");
}
const { name, description, content, id } = data.template;
const titleEl = document.getElementById("templateTitle");
const descriptionEl = document.getElementById("templateDescription");
const textarea = document.getElementById("templateContent");
if (titleEl) titleEl.textContent = name;
if (descriptionEl) descriptionEl.textContent = description;
if (textarea) textarea.value = content || "";
activeEmailTemplateId = id;
highlightSelectedTemplate(id);
setEmailTemplateMessage("");
disableEmailTemplateEditor(false);
} catch (error) {
console.error("Failed to load template", error);
setEmailTemplateMessage("Failed to load template", "error");
}
}
async function saveEmailTemplate(event) {
if (event) event.preventDefault();
if (!activeEmailTemplateId) return;
const textarea = document.getElementById("templateContent");
if (!textarea) return;
const content = textarea.value || "";
setEmailTemplateMessage("Saving template...", "info");
disableEmailTemplateEditor(true);
try {
const response = await fetch(
`/admin/api/email-templates/${activeEmailTemplateId}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
}
);
const data = await response.json();
if (response.ok && data.status === "ok") {
setEmailTemplateMessage("Template saved successfully.", "success");
disableEmailTemplateEditor(false);
} else {
throw new Error(data.message || "Failed to save template");
}
} catch (error) {
console.error("Failed to save template", error);
setEmailTemplateMessage("Failed to save template", "error");
disableEmailTemplateEditor(false);
}
}
// ==================== NEWSLETTER CREATION ====================
@@ -890,8 +1160,11 @@ window.admin = {
copyNewsletterIframeCode,
loadEmbedSettingsAndInit,
loadDashboardStats,
loadEmailTemplate,
loadEmailTemplatesPage,
selectEmailTemplate,
saveEmailTemplate,
loadEmailSettings,
submitEmailSettings,
loadNewsletterStats,
previewNewsletter,
saveDraft,
@@ -928,13 +1201,14 @@ document.addEventListener("DOMContentLoaded", function () {
}
// Email templates
if (document.getElementById("newsletterTemplate")) {
loadEmailTemplate();
const form = document.getElementById("templateForm");
const emailTemplatesPage = document.getElementById("emailTemplatesPage");
if (emailTemplatesPage) {
loadEmailTemplatesPage();
const form = document.getElementById("emailTemplateForm");
if (form) {
form.addEventListener("submit", (e) => {
e.preventDefault();
saveEmailTemplate();
form.addEventListener("submit", (event) => {
event.preventDefault();
saveEmailTemplate(event);
});
}
}
@@ -983,4 +1257,11 @@ document.addEventListener("DOMContentLoaded", function () {
if (document.getElementById("settingsList")) {
loadSettingsForList();
}
// Email settings
const emailSettingsForm = document.getElementById("emailSettingsForm");
if (emailSettingsForm) {
loadEmailSettings();
emailSettingsForm.addEventListener("submit", submitEmailSettings);
}
});