/** * 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} 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} 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"); } } // ==================== 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 = ``; } const newsletterTextarea = document.getElementById("iframeNewsletterCode"); if (newsletterTextarea) { newsletterTextarea.value = ``; } const contactPreview = document.getElementById("contactFormPreview"); if (contactPreview) { contactPreview.innerHTML = ``; } const newsletterPreview = document.getElementById("newsletterFormPreview"); if (newsletterPreview) { newsletterPreview.innerHTML = ``; } } /** * 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 ==================== /** * Loads email template from settings */ function loadEmailTemplate() { const textarea = document.getElementById("newsletterTemplate"); if (!textarea) return; 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)); } /** * Saves email template to settings */ function saveEmailTemplate() { const textarea = document.getElementById("newsletterTemplate"); const message = document.getElementById("message"); if (!textarea || !message) return; const value = textarea.value || ""; 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"; }); } // ==================== 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 = `

${escapeHtml(subject)}

${ senderName ? `

From: ${escapeHtml(senderName)}

` : "" }
${content.replace(/\n/g, "
")}
`; } 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 = 'No subscribers found'; } else { subscribers.forEach((subscriber) => { const row = document.createElement("tr"); row.innerHTML = ` ${escapeHtml(subscriber.email)} ${new Date(subscriber.subscribed_at).toLocaleDateString()} `; 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 = 'No submissions found'; return; } tbody.innerHTML = submissions .map( (submission) => ` ${submission.id} ${escapeHtml(submission.name)} ${escapeHtml(submission.email)} ${escapeHtml(submission.company || "")} ${escapeHtml(submission.message)} ${new Date(submission.created_at).toLocaleString()} ` ) .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( `` ); // 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( `` ); } // Next button buttons.push( `` ); 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, loadEmailTemplate, saveEmailTemplate, 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 if (document.getElementById("newsletterTemplate")) { loadEmailTemplate(); const form = document.getElementById("templateForm"); if (form) { form.addEventListener("submit", (e) => { e.preventDefault(); saveEmailTemplate(); }); } } // 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(); } });