Remove unused templates for newsletter creation, settings, and submissions; update unsubscribe confirmation link; add tests for email templates API.
This commit is contained in:
231
static/css/admin.css
Normal file
231
static/css/admin.css
Normal file
@@ -0,0 +1,231 @@
|
||||
/* Navigation links */
|
||||
nav a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
margin-right: 20px;
|
||||
}
|
||||
nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.nav {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.nav a {
|
||||
margin-right: 15px;
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Admin newsletter */
|
||||
.filters {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filters form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: end;
|
||||
}
|
||||
.filters input,
|
||||
.filters select {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
min-width: 150px;
|
||||
}
|
||||
.filters button {
|
||||
padding: 8px 16px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.filters button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
/* Subscribers table */
|
||||
.subscribers-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.subscribers-table th,
|
||||
.subscribers-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.subscribers-table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
.subscribers-table tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* Newsletter creation form */
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.form-section h2 {
|
||||
margin-top: 0;
|
||||
color: #555;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.form-group textarea {
|
||||
min-height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.form-row .form-group {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.newsletter-preview {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.newsletter-preview h3 {
|
||||
margin-top: 0;
|
||||
color: #555;
|
||||
}
|
||||
.newsletter-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Admin settings */
|
||||
.setting strong {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
.iframe-code {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
font-family: monospace;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Admin settings cards grid */
|
||||
.settings-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin: 8px 0 16px 0;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
flex: 1 1 300px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Keep individual dynamic setting layout readable */
|
||||
.setting {
|
||||
font-size: 0.95rem;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.settings-card {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Spinner styles used by embed pages */
|
||||
#spinnerIcon {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
#submitSpinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
/* Base styles */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
}
|
||||
@@ -76,21 +78,32 @@ button:hover,
|
||||
.btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #545b62;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.btn-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
.btn-success:hover {
|
||||
background-color: #1e7e34;
|
||||
}
|
||||
|
||||
/* Message styles */
|
||||
@@ -164,13 +177,21 @@ button:hover,
|
||||
min-width: 150px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
margin: 0;
|
||||
color: #495057;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.stat-card h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
.stat-card .number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.stat-card p {
|
||||
margin: 5px 0 0 0;
|
||||
color: #6c757d;
|
||||
@@ -260,17 +281,6 @@ button:hover,
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Navigation links */
|
||||
nav a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Table styles */
|
||||
table {
|
||||
width: 100%;
|
||||
@@ -298,6 +308,7 @@ tr:hover {
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
@@ -321,6 +332,26 @@ tr:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination button {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.pagination button:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.pagination button:disabled {
|
||||
background-color: #e9ecef;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.pagination .current-page {
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
|
||||
986
static/js/admin.js
Normal file
986
static/js/admin.js
Normal file
@@ -0,0 +1,986 @@
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 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 ====================
|
||||
|
||||
/**
|
||||
* 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 = `
|
||||
<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,
|
||||
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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user