- 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.
1268 lines
36 KiB
JavaScript
1268 lines
36 KiB
JavaScript
/**
|
|
* Admin JavaScript - Consolidated functionality for all admin pages
|
|
* Provides shared utilities, API interactions, and page-specific features
|
|
*/
|
|
|
|
// ==================== UTILITY FUNCTIONS ====================
|
|
|
|
/**
|
|
* Displays a message to the user with auto-hide functionality
|
|
* @param {string} text - The message text to display
|
|
* @param {string} type - Message type ('success', 'error', 'info', etc.)
|
|
*/
|
|
function showMessage(text, type) {
|
|
let messageDiv = document.getElementById("message");
|
|
if (!messageDiv) {
|
|
messageDiv = document.createElement("div");
|
|
messageDiv.id = "message";
|
|
document.body.insertBefore(messageDiv, document.body.firstChild);
|
|
}
|
|
messageDiv.className = `message ${type}`;
|
|
messageDiv.textContent = text;
|
|
messageDiv.style.display = "block";
|
|
setTimeout(() => (messageDiv.style.display = "none"), 5000);
|
|
}
|
|
|
|
/**
|
|
* Escapes HTML characters to prevent XSS
|
|
* @param {string} text - Text to escape
|
|
* @returns {string} Escaped HTML string
|
|
*/
|
|
const escapeHtml = (text) => {
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
};
|
|
|
|
/**
|
|
* Copies text to clipboard with fallback for older browsers
|
|
* @param {string} text - Text to copy
|
|
* @returns {Promise<boolean>} Success status
|
|
*/
|
|
async function copyToClipboard(text) {
|
|
if (navigator.clipboard?.writeText) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
return true;
|
|
} catch (e) {
|
|
// Fall through to fallback
|
|
}
|
|
}
|
|
|
|
// Fallback method
|
|
const tmp = document.createElement("textarea");
|
|
tmp.value = text;
|
|
tmp.setAttribute("readonly", "");
|
|
tmp.style.position = "absolute";
|
|
tmp.style.left = "-9999px";
|
|
document.body.appendChild(tmp);
|
|
tmp.select();
|
|
|
|
try {
|
|
const ok = document.execCommand("copy");
|
|
document.body.removeChild(tmp);
|
|
return ok;
|
|
} catch (err) {
|
|
document.body.removeChild(tmp);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ==================== SETTINGS MANAGEMENT ====================
|
|
|
|
/**
|
|
* Loads settings from API and displays them
|
|
*/
|
|
function loadSettingsForList() {
|
|
fetch("/admin/api/settings")
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (data.status === "ok") {
|
|
appSettings = data.settings || {};
|
|
displaySettings();
|
|
} else {
|
|
showMessage(
|
|
"Error loading settings: " + (data.message || "Unknown error"),
|
|
"error"
|
|
);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error:", error);
|
|
showMessage("Error loading settings", "error");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches embed settings from API
|
|
* @returns {Promise<Object>} Settings object or empty object on error
|
|
*/
|
|
async function fetchEmbedSettings() {
|
|
try {
|
|
const response = await fetch("/admin/api/settings");
|
|
const data = await response.json();
|
|
return data.status === "ok" && data.settings ? data.settings : {};
|
|
} catch (err) {
|
|
console.error("Failed to load embed settings", err);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves a setting via API
|
|
* @param {string} key - Setting key
|
|
* @param {string} inputId - Input element ID
|
|
*/
|
|
async function saveEmbedSetting(key, inputId) {
|
|
const input = document.getElementById(inputId);
|
|
if (!input) return showMessage("Input not found", "error");
|
|
|
|
const value = (input.value || "").toString().trim();
|
|
if (!value) return showMessage("Value cannot be empty", "error");
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`/admin/api/settings/${encodeURIComponent(key)}`,
|
|
{
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ value }),
|
|
}
|
|
);
|
|
|
|
const data = await response.json();
|
|
if (data.status === "ok") {
|
|
showMessage("Setting saved", "success");
|
|
// Rebuild textarea values to reflect the new size
|
|
if (typeof loadEmbedSettingsAndInit === "function") {
|
|
loadEmbedSettingsAndInit();
|
|
}
|
|
} else {
|
|
showMessage("Failed to save setting: " + (data.message || ""), "error");
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to save setting", err);
|
|
showMessage("Failed to save setting", "error");
|
|
}
|
|
}
|
|
|
|
// ==================== 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 ====================
|
|
|
|
/**
|
|
* Initializes embed settings and populates form fields
|
|
*/
|
|
async function loadEmbedSettingsAndInit() {
|
|
const origin =
|
|
window.location && window.location.origin
|
|
? window.location.origin
|
|
: "http://your-server-domain";
|
|
|
|
// Default dimensions
|
|
let contactWidth = "600",
|
|
contactHeight = "400";
|
|
let newsletterWidth = "600",
|
|
newsletterHeight = "300";
|
|
|
|
try {
|
|
const settings = await fetchEmbedSettings();
|
|
contactWidth = settings.embed_contact_width || contactWidth;
|
|
contactHeight = settings.embed_contact_height || contactHeight;
|
|
newsletterWidth = settings.embed_newsletter_width || newsletterWidth;
|
|
newsletterHeight = settings.embed_newsletter_height || newsletterHeight;
|
|
} catch (err) {
|
|
console.error("Error loading embed settings", err);
|
|
}
|
|
|
|
// Update input fields
|
|
const inputs = {
|
|
contactWidth,
|
|
contactHeight,
|
|
newsletterWidth,
|
|
newsletterHeight,
|
|
};
|
|
|
|
Object.entries(inputs).forEach(([id, value]) => {
|
|
const element = document.getElementById(id);
|
|
if (element) element.value = value;
|
|
});
|
|
|
|
// Update iframe code textareas
|
|
const contactTextarea = document.getElementById("iframeCode");
|
|
if (contactTextarea) {
|
|
contactTextarea.value = `<iframe src="${origin}/embed/contact" width="${contactWidth}" height="${contactHeight}" frameborder="0" allowfullscreen></iframe>`;
|
|
}
|
|
|
|
const newsletterTextarea = document.getElementById("iframeNewsletterCode");
|
|
if (newsletterTextarea) {
|
|
newsletterTextarea.value = `<iframe src="${origin}/embed/newsletter" width="${newsletterWidth}" height="${newsletterHeight}" frameborder="0" allowfullscreen></iframe>`;
|
|
}
|
|
|
|
const contactPreview = document.getElementById("contactFormPreview");
|
|
if (contactPreview) {
|
|
contactPreview.innerHTML = `<iframe src="${origin}/embed/contact" width="${contactWidth}" height="${contactHeight}" frameborder="0" allowfullscreen></iframe>`;
|
|
}
|
|
|
|
const newsletterPreview = document.getElementById("newsletterFormPreview");
|
|
if (newsletterPreview) {
|
|
newsletterPreview.innerHTML = `<iframe src="${origin}/embed/newsletter" width="${newsletterWidth}" height="${newsletterHeight}" frameborder="0" allowfullscreen></iframe>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copies contact iframe code to clipboard
|
|
*/
|
|
function copyIframeCode() {
|
|
const textarea = document.getElementById("iframeCode");
|
|
if (!textarea) return showMessage("Contact iframe not found", "error");
|
|
|
|
copyToClipboard(textarea.value).then((ok) =>
|
|
showMessage(
|
|
ok
|
|
? "Contact iframe code copied to clipboard!"
|
|
: "Failed to copy contact iframe code",
|
|
ok ? "success" : "error"
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Copies newsletter iframe code to clipboard
|
|
*/
|
|
function copyNewsletterIframeCode() {
|
|
const textarea = document.getElementById("iframeNewsletterCode");
|
|
if (!textarea) return showMessage("Newsletter iframe not found", "error");
|
|
|
|
copyToClipboard(textarea.value).then((ok) =>
|
|
showMessage(
|
|
ok
|
|
? "Newsletter iframe code copied to clipboard!"
|
|
: "Failed to copy newsletter iframe code",
|
|
ok ? "success" : "error"
|
|
)
|
|
);
|
|
}
|
|
|
|
// ==================== DASHBOARD ====================
|
|
|
|
/**
|
|
* Loads and displays dashboard statistics
|
|
*/
|
|
async function loadDashboardStats() {
|
|
try {
|
|
const [contactRes, newsletterRes, settingsRes] = await Promise.all([
|
|
fetch("/admin/api/contact?page=1&per_page=1"),
|
|
fetch("/admin/api/newsletter?page=1&per_page=1"),
|
|
fetch("/admin/api/settings"),
|
|
]);
|
|
|
|
if (contactRes.ok) {
|
|
const data = await contactRes.json();
|
|
document.getElementById("contact-count").textContent =
|
|
data.pagination.total;
|
|
}
|
|
|
|
if (newsletterRes.ok) {
|
|
const data = await newsletterRes.json();
|
|
document.getElementById("newsletter-count").textContent =
|
|
data.pagination.total;
|
|
}
|
|
|
|
if (settingsRes.ok) {
|
|
const data = await settingsRes.json();
|
|
document.getElementById("settings-count").textContent = Object.keys(
|
|
data.settings || {}
|
|
).length;
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load dashboard stats:", error);
|
|
}
|
|
}
|
|
|
|
// ==================== EMAIL TEMPLATES ====================
|
|
|
|
let emailTemplatesCache = [];
|
|
let activeEmailTemplateId = null;
|
|
|
|
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";
|
|
}
|
|
|
|
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");
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderEmailTemplatesList(templates) {
|
|
const listEl = document.getElementById("emailTemplatesList");
|
|
if (!listEl) return;
|
|
|
|
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 ====================
|
|
|
|
let newsletterStats = {};
|
|
|
|
/**
|
|
* Loads newsletter statistics
|
|
*/
|
|
function loadNewsletterStats() {
|
|
fetch("/admin/api/newsletter?page=1&per_page=1")
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (data.status === "ok") {
|
|
const total = data.pagination.total;
|
|
document.getElementById("totalSubscribers").textContent = total;
|
|
document.getElementById("activeSubscribers").textContent = total; // Assume all active
|
|
newsletterStats.totalSubscribers = total;
|
|
}
|
|
})
|
|
.catch((error) => console.error("Error loading subscriber stats:", error))
|
|
.finally(() => {
|
|
const lastSentEl = document.getElementById("lastSent");
|
|
if (lastSentEl) lastSentEl.textContent = "N/A";
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generates newsletter preview
|
|
*/
|
|
function previewNewsletter() {
|
|
const subject = document.getElementById("subject").value.trim();
|
|
const content = document.getElementById("content").value.trim();
|
|
const senderName = document.getElementById("senderName").value.trim();
|
|
|
|
if (!subject || !content) {
|
|
return showMessage(
|
|
"Subject and content are required for preview.",
|
|
"error"
|
|
);
|
|
}
|
|
|
|
const previewContent = document.getElementById("previewContent");
|
|
if (previewContent) {
|
|
previewContent.innerHTML = `
|
|
<h2>${escapeHtml(subject)}</h2>
|
|
${
|
|
senderName
|
|
? `<p><strong>From:</strong> ${escapeHtml(senderName)}</p>`
|
|
: ""
|
|
}
|
|
<div style="margin-top: 20px; line-height: 1.6;">
|
|
${content.replace(/\n/g, "<br>")}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const previewSection = document.getElementById("previewSection");
|
|
if (previewSection) previewSection.classList.remove("hidden");
|
|
|
|
showMessage("Newsletter preview generated.", "info");
|
|
}
|
|
|
|
/**
|
|
* Saves newsletter as draft
|
|
*/
|
|
function saveDraft() {
|
|
const form = document.getElementById("newsletterForm");
|
|
if (!form) return;
|
|
|
|
const formData = new FormData(form);
|
|
const newsletterData = {
|
|
subject: formData.get("subject"),
|
|
content: formData.get("content"),
|
|
sender_name: formData.get("sender_name"),
|
|
send_date: formData.get("send_date"),
|
|
status: "draft",
|
|
};
|
|
|
|
if (!newsletterData.subject || !newsletterData.content) {
|
|
return showMessage("Subject and content are required.", "error");
|
|
}
|
|
|
|
fetch("/admin/api/newsletters", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(newsletterData),
|
|
})
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
showMessage(
|
|
data.status === "ok"
|
|
? "Newsletter draft saved successfully!"
|
|
: data.message || "Failed to save draft.",
|
|
data.status === "ok" ? "success" : "error"
|
|
);
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error saving draft:", error);
|
|
showMessage("Failed to save draft.", "error");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sends newsletter to subscribers
|
|
*/
|
|
function sendNewsletter() {
|
|
const form = document.getElementById("newsletterForm");
|
|
if (!form) return;
|
|
|
|
const formData = new FormData(form);
|
|
const newsletterData = {
|
|
subject: formData.get("subject"),
|
|
content: formData.get("content"),
|
|
sender_name: formData.get("sender_name"),
|
|
send_date: formData.get("send_date"),
|
|
status: "sent",
|
|
};
|
|
|
|
if (!newsletterData.subject || !newsletterData.content) {
|
|
return showMessage("Subject and content are required.", "error");
|
|
}
|
|
|
|
if (
|
|
!confirm(
|
|
`Are you sure you want to send this newsletter to ${
|
|
newsletterStats.totalSubscribers || 0
|
|
} subscribers?`
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Save and send
|
|
fetch("/admin/api/newsletters", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(newsletterData),
|
|
})
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (data.status === "ok") {
|
|
const newsletterId = data.newsletter_id;
|
|
return fetch(`/admin/api/newsletters/${newsletterId}/send`, {
|
|
method: "POST",
|
|
});
|
|
} else {
|
|
throw new Error(data.message || "Failed to save newsletter.");
|
|
}
|
|
})
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
showMessage(
|
|
data.status === "ok"
|
|
? `Newsletter sent successfully to ${data.sent_count} subscribers!`
|
|
: data.message || "Failed to send newsletter.",
|
|
data.status === "ok" ? "success" : "error"
|
|
);
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error sending newsletter:", error);
|
|
showMessage("Failed to send newsletter.", "error");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clears newsletter form with confirmation
|
|
*/
|
|
function clearForm() {
|
|
if (
|
|
!confirm(
|
|
"Are you sure you want to clear the form? All unsaved changes will be lost."
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const form = document.getElementById("newsletterForm");
|
|
if (form) form.reset();
|
|
|
|
const previewSection = document.getElementById("previewSection");
|
|
if (previewSection) previewSection.classList.add("hidden");
|
|
|
|
showMessage("Form cleared.", "info");
|
|
}
|
|
|
|
// ==================== NEWSLETTER SUBSCRIBERS ====================
|
|
|
|
let currentPage = 1;
|
|
let currentFilters = {
|
|
email: "",
|
|
sort_by: "subscribed_at",
|
|
sort_order: "desc",
|
|
};
|
|
|
|
/**
|
|
* Applies filters to subscriber list
|
|
*/
|
|
function applyFilters() {
|
|
const emailFilter = document.getElementById("emailFilter");
|
|
const sortBy = document.getElementById("sortBy");
|
|
const sortOrder = document.getElementById("sortOrder");
|
|
|
|
if (emailFilter) currentFilters.email = emailFilter.value.trim();
|
|
if (sortBy) currentFilters.sort_by = sortBy.value;
|
|
if (sortOrder) currentFilters.sort_order = sortOrder.value;
|
|
|
|
currentPage = 1;
|
|
loadSubscribers();
|
|
}
|
|
|
|
/**
|
|
* Clears all filters
|
|
*/
|
|
function clearFilters() {
|
|
const emailFilter = document.getElementById("emailFilter");
|
|
const sortBy = document.getElementById("sortBy");
|
|
const sortOrder = document.getElementById("sortOrder");
|
|
|
|
if (emailFilter) emailFilter.value = "";
|
|
if (sortBy) sortBy.value = "subscribed_at";
|
|
if (sortOrder) sortOrder.value = "desc";
|
|
|
|
currentFilters = { email: "", sort_by: "subscribed_at", sort_order: "desc" };
|
|
currentPage = 1;
|
|
loadSubscribers();
|
|
}
|
|
|
|
/**
|
|
* Loads subscribers with current filters and pagination
|
|
*/
|
|
function loadSubscribers() {
|
|
const loading = document.getElementById("loading");
|
|
const table = document.getElementById("subscribersTable");
|
|
const pagination = document.getElementById("pagination");
|
|
|
|
if (loading) loading.style.display = "block";
|
|
if (table) table.style.display = "none";
|
|
if (pagination) pagination.style.display = "none";
|
|
|
|
const params = new URLSearchParams({
|
|
page: currentPage,
|
|
per_page: 50,
|
|
sort_by: currentFilters.sort_by,
|
|
sort_order: currentFilters.sort_order,
|
|
});
|
|
|
|
if (currentFilters.email) params.append("email", currentFilters.email);
|
|
|
|
fetch(`/admin/api/newsletter?${params}`)
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (data.status === "ok") {
|
|
displaySubscribers(data.subscribers);
|
|
updatePagination(data.pagination);
|
|
} else {
|
|
showMessage(
|
|
"Error loading subscribers: " + (data.message || "Unknown error"),
|
|
"error"
|
|
);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error:", error);
|
|
showMessage("Error loading subscribers", "error");
|
|
})
|
|
.finally(() => {
|
|
if (loading) loading.style.display = "none";
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Displays subscribers in table
|
|
* @param {Array} subscribers - Array of subscriber objects
|
|
*/
|
|
function displaySubscribers(subscribers) {
|
|
const tbody = document.getElementById("subscribersBody");
|
|
if (!tbody) return;
|
|
|
|
tbody.innerHTML = "";
|
|
|
|
if (subscribers.length === 0) {
|
|
tbody.innerHTML =
|
|
'<tr><td colspan="3" style="text-align: center; padding: 40px; color: #666;">No subscribers found</td></tr>';
|
|
} else {
|
|
subscribers.forEach((subscriber) => {
|
|
const row = document.createElement("tr");
|
|
row.innerHTML = `
|
|
<td>${escapeHtml(subscriber.email)}</td>
|
|
<td>${new Date(subscriber.subscribed_at).toLocaleDateString()}</td>
|
|
<td class="actions">
|
|
<button class="btn btn-danger" onclick="unsubscribe('${escapeHtml(
|
|
subscriber.email
|
|
)}')">Unsubscribe</button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
const table = document.getElementById("subscribersTable");
|
|
if (table) table.style.display = "table";
|
|
}
|
|
|
|
/**
|
|
* Updates pagination controls
|
|
* @param {Object} pagination - Pagination data
|
|
*/
|
|
function updatePagination(pagination) {
|
|
const pageInfo = document.getElementById("pageInfo");
|
|
const prevBtn = document.getElementById("prevBtn");
|
|
const nextBtn = document.getElementById("nextBtn");
|
|
|
|
if (pageInfo)
|
|
pageInfo.textContent = `Page ${pagination.page} of ${pagination.pages} (${pagination.total} total)`;
|
|
if (prevBtn) prevBtn.disabled = pagination.page <= 1;
|
|
if (nextBtn) nextBtn.disabled = pagination.page >= pagination.pages;
|
|
|
|
const paginationDiv = document.getElementById("pagination");
|
|
if (paginationDiv) paginationDiv.style.display = "flex";
|
|
}
|
|
|
|
/**
|
|
* Changes to a different page
|
|
* @param {number} page - Page number to navigate to
|
|
*/
|
|
function changePage(page) {
|
|
currentPage = page;
|
|
loadSubscribers();
|
|
}
|
|
|
|
/**
|
|
* Unsubscribes a user from newsletter
|
|
* @param {string} email - Email address to unsubscribe
|
|
*/
|
|
function unsubscribe(email) {
|
|
if (!confirm(`Are you sure you want to unsubscribe ${email}?`)) return;
|
|
|
|
fetch("/api/newsletter", {
|
|
method: "DELETE",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email }),
|
|
})
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (data.status === "ok") {
|
|
showMessage("Subscriber unsubscribed successfully", "success");
|
|
loadSubscribers();
|
|
} else {
|
|
showMessage(
|
|
"Error unsubscribing: " + (data.message || "Unknown error"),
|
|
"error"
|
|
);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error:", error);
|
|
showMessage("Error unsubscribing subscriber", "error");
|
|
});
|
|
}
|
|
|
|
// ==================== CONTACT SUBMISSIONS ====================
|
|
|
|
let submissionsCurrentPage = 1;
|
|
let submissionsSortBy = "created_at";
|
|
let submissionsSortOrder = "desc";
|
|
|
|
/**
|
|
* Clears contact submission filters
|
|
*/
|
|
function clearSubmissionFilters() {
|
|
const email = document.getElementById("email");
|
|
const dateFrom = document.getElementById("date_from");
|
|
const dateTo = document.getElementById("date_to");
|
|
const perPage = document.getElementById("per_page");
|
|
|
|
if (email) email.value = "";
|
|
if (dateFrom) dateFrom.value = "";
|
|
if (dateTo) dateTo.value = "";
|
|
if (perPage) perPage.value = "50";
|
|
|
|
submissionsCurrentPage = 1;
|
|
submissionsSortBy = "created_at";
|
|
submissionsSortOrder = "desc";
|
|
loadSubmissions();
|
|
}
|
|
|
|
/**
|
|
* Loads contact submissions with filters
|
|
*/
|
|
function loadSubmissions() {
|
|
const loading = document.getElementById("loading");
|
|
const table = document.getElementById("submissionsTable");
|
|
|
|
if (loading) loading.style.display = "block";
|
|
if (table) table.style.opacity = "0.5";
|
|
|
|
const perPage = document.getElementById("per_page");
|
|
const email = document.getElementById("email");
|
|
const dateFrom = document.getElementById("date_from");
|
|
const dateTo = document.getElementById("date_to");
|
|
|
|
const params = new URLSearchParams({
|
|
page: submissionsCurrentPage,
|
|
per_page: perPage?.value || "50",
|
|
sort_by: submissionsSortBy,
|
|
sort_order: submissionsSortOrder,
|
|
email: email?.value || "",
|
|
date_from: dateFrom?.value || "",
|
|
date_to: dateTo?.value || "",
|
|
});
|
|
|
|
fetch(`/api/contact?${params}`)
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (data.status === "ok") {
|
|
displaySubmissions(data.submissions);
|
|
displaySubmissionPagination(data.pagination);
|
|
} else {
|
|
showMessage(
|
|
"Error loading submissions: " + (data.message || "Unknown error"),
|
|
"error"
|
|
);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error:", error);
|
|
showMessage("Error loading submissions", "error");
|
|
})
|
|
.finally(() => {
|
|
if (loading) loading.style.display = "none";
|
|
if (table) table.style.opacity = "1";
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Displays contact submissions in table
|
|
* @param {Array} submissions - Array of submission objects
|
|
*/
|
|
function displaySubmissions(submissions) {
|
|
const tbody = document.getElementById("submissionsBody");
|
|
if (!tbody) return;
|
|
|
|
if (submissions.length === 0) {
|
|
tbody.innerHTML =
|
|
'<tr><td colspan="7" class="no-data">No submissions found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = submissions
|
|
.map(
|
|
(submission) => `
|
|
<tr>
|
|
<td>${submission.id}</td>
|
|
<td>${escapeHtml(submission.name)}</td>
|
|
<td>${escapeHtml(submission.email)}</td>
|
|
<td>${escapeHtml(submission.company || "")}</td>
|
|
<td class="submission-details">${escapeHtml(submission.message)}</td>
|
|
<td>${new Date(submission.created_at).toLocaleString()}</td>
|
|
<td><button class="delete-btn" onclick="deleteSubmission(${
|
|
submission.id
|
|
})">Delete</button></td>
|
|
</tr>
|
|
`
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
/**
|
|
* Updates submission pagination controls
|
|
* @param {Object} pagination - Pagination data
|
|
*/
|
|
function displaySubmissionPagination(pagination) {
|
|
const paginationDiv = document.getElementById("pagination");
|
|
if (!paginationDiv) return;
|
|
|
|
if (pagination.pages <= 1) {
|
|
paginationDiv.innerHTML = "";
|
|
return;
|
|
}
|
|
|
|
let buttons = [];
|
|
|
|
// Previous button
|
|
buttons.push(
|
|
`<button ${
|
|
pagination.page <= 1 ? "disabled" : ""
|
|
} onclick="changeSubmissionPage(${pagination.page - 1})">Previous</button>`
|
|
);
|
|
|
|
// Page numbers
|
|
const startPage = Math.max(1, pagination.page - 2);
|
|
const endPage = Math.min(pagination.pages, pagination.page + 2);
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
buttons.push(
|
|
`<button class="${
|
|
i === pagination.page ? "active" : ""
|
|
}" onclick="changeSubmissionPage(${i})">${i}</button>`
|
|
);
|
|
}
|
|
|
|
// Next button
|
|
buttons.push(
|
|
`<button ${
|
|
pagination.page >= pagination.pages ? "disabled" : ""
|
|
} onclick="changeSubmissionPage(${pagination.page + 1})">Next</button>`
|
|
);
|
|
|
|
paginationDiv.innerHTML = buttons.join("");
|
|
}
|
|
|
|
/**
|
|
* Changes to a different submission page
|
|
* @param {number} page - Page number to navigate to
|
|
*/
|
|
function changeSubmissionPage(page) {
|
|
submissionsCurrentPage = page;
|
|
loadSubmissions();
|
|
window.scrollTo(0, 0);
|
|
}
|
|
|
|
/**
|
|
* Deletes a contact submission
|
|
* @param {number} id - Submission ID to delete
|
|
*/
|
|
function deleteSubmission(id) {
|
|
if (!confirm("Are you sure you want to delete this submission?")) return;
|
|
|
|
fetch(`/api/contact/${id}`, { method: "DELETE" })
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (data.status === "ok") {
|
|
showMessage("Submission deleted successfully", "success");
|
|
loadSubmissions();
|
|
} else {
|
|
showMessage(
|
|
"Error deleting submission: " + (data.message || "Unknown error"),
|
|
"error"
|
|
);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error:", error);
|
|
showMessage("Error deleting submission", "error");
|
|
});
|
|
}
|
|
|
|
// ==================== INITIALIZATION ====================
|
|
|
|
// Global admin object for external access
|
|
window.admin = {
|
|
showMessage,
|
|
escapeHtml,
|
|
fetchEmbedSettings,
|
|
saveEmbedSetting,
|
|
copyIframeCode,
|
|
copyNewsletterIframeCode,
|
|
loadEmbedSettingsAndInit,
|
|
loadDashboardStats,
|
|
loadEmailTemplatesPage,
|
|
selectEmailTemplate,
|
|
saveEmailTemplate,
|
|
loadEmailSettings,
|
|
submitEmailSettings,
|
|
loadNewsletterStats,
|
|
previewNewsletter,
|
|
saveDraft,
|
|
sendNewsletter,
|
|
clearForm,
|
|
applyFilters,
|
|
clearFilters,
|
|
loadSubscribers,
|
|
displaySubscribers,
|
|
updatePagination,
|
|
changePage,
|
|
unsubscribe,
|
|
clearSubmissionFilters,
|
|
loadSubmissions,
|
|
displaySubmissions,
|
|
displaySubmissionPagination,
|
|
changeSubmissionPage,
|
|
deleteSubmission,
|
|
};
|
|
|
|
// Auto-initialize based on page content
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
// Embed page
|
|
if (
|
|
document.getElementById("iframeCode") ||
|
|
document.getElementById("iframeNewsletterCode")
|
|
) {
|
|
loadEmbedSettingsAndInit();
|
|
}
|
|
|
|
// Dashboard
|
|
if (document.getElementById("contact-count")) {
|
|
loadDashboardStats();
|
|
}
|
|
|
|
// Email templates
|
|
const emailTemplatesPage = document.getElementById("emailTemplatesPage");
|
|
if (emailTemplatesPage) {
|
|
loadEmailTemplatesPage();
|
|
const form = document.getElementById("emailTemplateForm");
|
|
if (form) {
|
|
form.addEventListener("submit", (event) => {
|
|
event.preventDefault();
|
|
saveEmailTemplate(event);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Newsletter creation
|
|
if (document.getElementById("newsletterForm")) {
|
|
loadNewsletterStats();
|
|
}
|
|
|
|
// Newsletter subscribers
|
|
if (document.getElementById("subscribersTable")) {
|
|
loadSubscribers();
|
|
}
|
|
|
|
// Contact submissions
|
|
if (document.getElementById("submissionsTable")) {
|
|
loadSubmissions();
|
|
|
|
const filterForm = document.getElementById("filterForm");
|
|
if (filterForm) {
|
|
filterForm.addEventListener("submit", (e) => {
|
|
e.preventDefault();
|
|
submissionsCurrentPage = 1;
|
|
loadSubmissions();
|
|
});
|
|
}
|
|
|
|
// Table sorting
|
|
document.querySelectorAll("th[data-sort]").forEach((header) => {
|
|
header.addEventListener("click", function () {
|
|
const sortBy = this.dataset.sort;
|
|
if (submissionsSortBy === sortBy) {
|
|
submissionsSortOrder =
|
|
submissionsSortOrder === "asc" ? "desc" : "asc";
|
|
} else {
|
|
submissionsSortBy = sortBy;
|
|
submissionsSortOrder = "asc";
|
|
}
|
|
submissionsCurrentPage = 1;
|
|
loadSubmissions();
|
|
});
|
|
});
|
|
}
|
|
|
|
// Settings
|
|
if (document.getElementById("settingsList")) {
|
|
loadSettingsForList();
|
|
}
|
|
|
|
// Email settings
|
|
const emailSettingsForm = document.getElementById("emailSettingsForm");
|
|
if (emailSettingsForm) {
|
|
loadEmailSettings();
|
|
emailSettingsForm.addEventListener("submit", submitEmailSettings);
|
|
}
|
|
});
|