feat: Add email settings management and templates functionality
- 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:
@@ -22,6 +22,10 @@ nav a:hover {
|
||||
.nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.nav a.active {
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
@@ -172,10 +176,24 @@ nav a:hover {
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.input-error {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
.form-group textarea {
|
||||
min-height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
.checkbox-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
@@ -277,3 +295,94 @@ nav a:hover {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Email templates layout */
|
||||
.email-templates-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.email-templates-list {
|
||||
flex: 0 0 280px;
|
||||
max-width: 320px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: #f8f9fa;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.template-item:hover {
|
||||
border-color: #007bff;
|
||||
background: #eef5ff;
|
||||
}
|
||||
|
||||
.template-item.active {
|
||||
border-color: #007bff;
|
||||
background: #e2ecff;
|
||||
}
|
||||
|
||||
.template-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.template-description {
|
||||
margin: 0 0 12px 0;
|
||||
color: #555555;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.email-template-editor {
|
||||
flex: 1 1 380px;
|
||||
min-width: 300px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
padding: 16px 18px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.email-template-editor .form-group textarea {
|
||||
min-height: 360px;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
margin: 0;
|
||||
color: #555555;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.email-templates-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.email-templates-list {
|
||||
flex: 1 1 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.email-template-editor {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user