1684 lines
49 KiB
JavaScript
1684 lines
49 KiB
JavaScript
// Custom Pages Admin - Redesigned
|
|
// ================================
|
|
|
|
console.log("=== pages-new.js loading ===");
|
|
|
|
// State variables
|
|
let pages = [];
|
|
let pageModal = null;
|
|
let pageContentEditor = null;
|
|
let aboutContentEditor = null;
|
|
let privacyMainEditor = null;
|
|
let currentPageSlug = null;
|
|
let teamMembers = [];
|
|
let deletedTeamMemberIds = [];
|
|
let faqItems = [];
|
|
let returnsSections = [];
|
|
let shippingSections = [];
|
|
let privacySections = [];
|
|
let pagesMediaLibrary = null;
|
|
let currentMediaCallback = null;
|
|
|
|
// Quill editor instances for sections
|
|
let privacyEditors = [];
|
|
let shippingEditors = [];
|
|
let returnsEditors = [];
|
|
|
|
// Page icons mapping
|
|
const pageIcons = {
|
|
about: "bi-info-circle",
|
|
contact: "bi-envelope",
|
|
faq: "bi-question-circle",
|
|
returns: "bi-arrow-return-left",
|
|
"shipping-info": "bi-truck",
|
|
privacy: "bi-shield-lock",
|
|
terms: "bi-file-earmark-text",
|
|
default: "bi-file-text",
|
|
};
|
|
|
|
// ========================================
|
|
// Custom Modal Functions
|
|
// ========================================
|
|
|
|
// Show confirmation modal (returns Promise)
|
|
function showConfirmModal(
|
|
title,
|
|
message,
|
|
confirmText = "Delete",
|
|
type = "danger",
|
|
) {
|
|
return new Promise((resolve) => {
|
|
const overlay = document.getElementById("confirmModalOverlay");
|
|
const icon = document.getElementById("confirmModalIcon");
|
|
const titleEl = document.getElementById("confirmModalTitle");
|
|
const messageEl = document.getElementById("confirmModalMessage");
|
|
const confirmBtn = document.getElementById("confirmDeleteBtn");
|
|
const cancelBtn = document.getElementById("confirmCancelBtn");
|
|
|
|
// Set content
|
|
titleEl.textContent = title;
|
|
messageEl.textContent = message;
|
|
confirmBtn.textContent = confirmText;
|
|
|
|
// Set icon style based on type
|
|
if (type === "danger") {
|
|
icon.innerHTML = '<i class="bi bi-exclamation-triangle"></i>';
|
|
icon.className = "confirm-modal-icon";
|
|
confirmBtn.className = "btn btn-confirm-delete";
|
|
} else if (type === "warning") {
|
|
icon.innerHTML = '<i class="bi bi-exclamation-circle"></i>';
|
|
icon.className = "confirm-modal-icon warning";
|
|
confirmBtn.className = "btn btn-confirm-delete";
|
|
}
|
|
|
|
// Show modal
|
|
overlay.classList.add("active");
|
|
|
|
// Handle confirm
|
|
const handleConfirm = () => {
|
|
overlay.classList.remove("active");
|
|
cleanup();
|
|
resolve(true);
|
|
};
|
|
|
|
// Handle cancel
|
|
const handleCancel = () => {
|
|
overlay.classList.remove("active");
|
|
cleanup();
|
|
resolve(false);
|
|
};
|
|
|
|
// Handle overlay click
|
|
const handleOverlayClick = (e) => {
|
|
if (e.target === overlay) {
|
|
handleCancel();
|
|
}
|
|
};
|
|
|
|
// Cleanup listeners
|
|
const cleanup = () => {
|
|
confirmBtn.removeEventListener("click", handleConfirm);
|
|
cancelBtn.removeEventListener("click", handleCancel);
|
|
overlay.removeEventListener("click", handleOverlayClick);
|
|
};
|
|
|
|
// Add listeners
|
|
confirmBtn.addEventListener("click", handleConfirm);
|
|
cancelBtn.addEventListener("click", handleCancel);
|
|
overlay.addEventListener("click", handleOverlayClick);
|
|
});
|
|
}
|
|
|
|
// Show result modal (success/error popup)
|
|
function showResultModal(type, title, message) {
|
|
return new Promise((resolve) => {
|
|
const overlay = document.getElementById("resultModalOverlay");
|
|
const icon = document.getElementById("resultModalIcon");
|
|
const titleEl = document.getElementById("resultModalTitle");
|
|
const messageEl = document.getElementById("resultModalMessage");
|
|
const okBtn = document.getElementById("resultOkBtn");
|
|
|
|
// Set content
|
|
titleEl.textContent = title;
|
|
messageEl.textContent = message;
|
|
|
|
// Set icon based on type
|
|
if (type === "success") {
|
|
icon.innerHTML = '<i class="bi bi-check-circle"></i>';
|
|
icon.className = "result-modal-icon success";
|
|
} else if (type === "error") {
|
|
icon.innerHTML = '<i class="bi bi-x-circle"></i>';
|
|
icon.className = "result-modal-icon error";
|
|
}
|
|
|
|
// Show modal
|
|
overlay.classList.add("active");
|
|
|
|
// Handle OK
|
|
const handleOk = () => {
|
|
overlay.classList.remove("active");
|
|
okBtn.removeEventListener("click", handleOk);
|
|
overlay.removeEventListener("click", handleOverlayClick);
|
|
resolve();
|
|
};
|
|
|
|
// Handle overlay click
|
|
const handleOverlayClick = (e) => {
|
|
if (e.target === overlay) {
|
|
handleOk();
|
|
}
|
|
};
|
|
|
|
// Add listeners
|
|
okBtn.addEventListener("click", handleOk);
|
|
overlay.addEventListener("click", handleOverlayClick);
|
|
});
|
|
}
|
|
|
|
// Initialize on page load
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
console.log("=== DOMContentLoaded fired ===");
|
|
// Initialize page modal first
|
|
pageModal = new bootstrap.Modal(document.getElementById("pageModal"));
|
|
|
|
// Try to initialize editors (will fail if modal hidden, that's ok)
|
|
try {
|
|
initializeEditors();
|
|
} catch (error) {
|
|
console.warn("Editor initialization delayed:", error);
|
|
}
|
|
|
|
// Try to initialize media library
|
|
try {
|
|
initializeMediaLibrary();
|
|
} catch (error) {
|
|
console.warn("Media library initialization delayed:", error);
|
|
}
|
|
|
|
// ALWAYS load pages - this is critical
|
|
console.log("=== About to call loadPages() ===");
|
|
loadPages();
|
|
console.log("=== loadPages() call completed ===");
|
|
});
|
|
|
|
// Initialize Media Library
|
|
function initializeMediaLibrary() {
|
|
pagesMediaLibrary = new MediaLibrary({
|
|
selectMode: true,
|
|
multiple: false,
|
|
onSelect: handleMediaSelection,
|
|
});
|
|
}
|
|
|
|
// Handle media selection
|
|
function handleMediaSelection(media) {
|
|
if (!currentMediaCallback) return;
|
|
|
|
// Media object has url or path property
|
|
const imageUrl = media.url || media.path || "";
|
|
if (imageUrl && currentMediaCallback) {
|
|
currentMediaCallback(imageUrl);
|
|
}
|
|
currentMediaCallback = null;
|
|
}
|
|
|
|
// Initialize Quill editors
|
|
function initializeEditors() {
|
|
// Check if editors already exist
|
|
if (pageContentEditor && aboutContentEditor) {
|
|
return;
|
|
}
|
|
|
|
// Check if elements exist and are visible
|
|
const pageEditorEl = document.getElementById("pageContentEditor");
|
|
const aboutEditorEl = document.getElementById("aboutContentEditor");
|
|
|
|
if (!pageEditorEl || !aboutEditorEl) {
|
|
console.warn("Editor elements not found yet");
|
|
return;
|
|
}
|
|
|
|
const toolbarOptions = [
|
|
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
|
["bold", "italic", "underline", "strike"],
|
|
[{ color: [] }, { background: [] }],
|
|
[{ list: "ordered" }, { list: "bullet" }],
|
|
[{ align: [] }],
|
|
["blockquote", "code-block"],
|
|
["link", "image"],
|
|
["clean"],
|
|
];
|
|
|
|
try {
|
|
// Main content editor
|
|
pageContentEditor = new Quill("#pageContentEditor", {
|
|
theme: "snow",
|
|
modules: {
|
|
toolbar: toolbarOptions,
|
|
},
|
|
placeholder: "Write your page content here...",
|
|
});
|
|
|
|
// About page editor
|
|
aboutContentEditor = new Quill("#aboutContentEditor", {
|
|
theme: "snow",
|
|
modules: {
|
|
toolbar: toolbarOptions,
|
|
},
|
|
placeholder: "Write about your business...",
|
|
});
|
|
|
|
// Add image handlers
|
|
setupImageHandler(pageContentEditor);
|
|
setupImageHandler(aboutContentEditor);
|
|
|
|
console.log("Editors initialized successfully");
|
|
} catch (error) {
|
|
console.error("Error initializing editors:", error);
|
|
pageContentEditor = null;
|
|
aboutContentEditor = null;
|
|
}
|
|
}
|
|
|
|
// Setup image handler for Quill editor
|
|
function setupImageHandler(editor) {
|
|
const toolbar = editor.getModule("toolbar");
|
|
toolbar.addHandler("image", () => {
|
|
openMediaLibrary((imageUrl) => {
|
|
const range = editor.getSelection(true);
|
|
editor.insertEmbed(range.index, "image", imageUrl);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Open media library
|
|
function openMediaLibrary(callback) {
|
|
if (pagesMediaLibrary) {
|
|
currentMediaCallback = callback;
|
|
pagesMediaLibrary.open();
|
|
} else if (typeof MediaLibrary !== "undefined") {
|
|
// Fallback: create new instance
|
|
currentMediaCallback = callback;
|
|
pagesMediaLibrary = new MediaLibrary({
|
|
selectMode: true,
|
|
multiple: false,
|
|
onSelect: handleMediaSelection,
|
|
});
|
|
pagesMediaLibrary.open();
|
|
} else {
|
|
// Fallback to simple URL input
|
|
const url = prompt("Enter image URL:");
|
|
if (url) callback(url);
|
|
}
|
|
}
|
|
|
|
// Load pages from API
|
|
async function loadPages() {
|
|
console.log("loadPages() called");
|
|
try {
|
|
console.log("Fetching pages from API...");
|
|
const response = await fetch("/api/admin/pages", {
|
|
credentials: "include",
|
|
});
|
|
|
|
console.log("Response status:", response.status);
|
|
if (!response.ok) throw new Error("Failed to load pages");
|
|
|
|
const data = await response.json();
|
|
console.log("Pages data received:", data);
|
|
pages = data.pages || [];
|
|
console.log("Total pages:", pages.length);
|
|
renderPages();
|
|
} catch (error) {
|
|
console.error("Error loading pages:", error);
|
|
showToast("Failed to load pages", "error");
|
|
document.getElementById("pagesContainer").innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="bi bi-exclamation-triangle"></i>
|
|
<h5>Error Loading Pages</h5>
|
|
<p>Unable to fetch pages from server</p>
|
|
<button class="btn btn-primary" onclick="loadPages()">
|
|
<i class="bi bi-arrow-clockwise me-2"></i>Retry
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Render pages grid
|
|
function renderPages() {
|
|
console.log("renderPages() called with", pages.length, "pages");
|
|
const container = document.getElementById("pagesContainer");
|
|
if (!container) {
|
|
console.error("pagesContainer element not found!");
|
|
return;
|
|
}
|
|
|
|
const searchTerm =
|
|
document.getElementById("searchInput")?.value?.toLowerCase() || "";
|
|
|
|
const filteredPages = pages.filter(
|
|
(page) =>
|
|
page.title.toLowerCase().includes(searchTerm) ||
|
|
page.slug.toLowerCase().includes(searchTerm),
|
|
);
|
|
|
|
console.log("Filtered pages:", filteredPages.length);
|
|
|
|
if (filteredPages.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="bi bi-file-earmark-x"></i>
|
|
<h5>${searchTerm ? "No Matching Pages" : "No Pages Yet"}</h5>
|
|
<p>${searchTerm ? "Try a different search term" : "Create your first custom page"}</p>
|
|
${
|
|
!searchTerm
|
|
? `
|
|
<button class="btn btn-primary" onclick="showCreatePage()">
|
|
<i class="bi bi-plus-circle me-2"></i>Create Page
|
|
</button>
|
|
`
|
|
: ""
|
|
}
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = filteredPages
|
|
.map((page) => {
|
|
const icon = pageIcons[page.slug] || pageIcons.default;
|
|
const isPublished = page.ispublished !== false;
|
|
|
|
return `
|
|
<div class="page-card" data-page-id="${page.id}">
|
|
<div class="page-card-header">
|
|
<h4>
|
|
<span class="page-icon"><i class="bi ${icon}"></i></span>
|
|
${escapeHtml(page.title)}
|
|
</h4>
|
|
</div>
|
|
<div class="page-card-body">
|
|
<div class="page-slug">
|
|
<i class="bi bi-link-45deg"></i>
|
|
/${page.slug}
|
|
</div>
|
|
<div>
|
|
<span class="page-status ${isPublished ? "published" : "draft"}">
|
|
<i class="bi ${isPublished ? "bi-check-circle" : "bi-clock"}"></i>
|
|
${isPublished ? "Published" : "Draft"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="page-card-actions">
|
|
<button class="btn btn-edit" onclick="editPage('${page.id}')">
|
|
<i class="bi bi-pencil"></i> Edit
|
|
</button>
|
|
<button class="btn btn-delete" onclick="deletePage('${page.id}', '${escapeHtml(page.title)}')">
|
|
<i class="bi bi-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
// Filter pages based on search
|
|
function filterPages() {
|
|
renderPages();
|
|
}
|
|
|
|
// Refresh pages
|
|
function refreshPages() {
|
|
document.getElementById("pagesContainer").innerHTML = `
|
|
<div class="text-center p-5">
|
|
<div class="saving-spinner mx-auto mb-3"></div>
|
|
<p class="text-muted">Loading pages...</p>
|
|
</div>
|
|
`;
|
|
loadPages();
|
|
}
|
|
|
|
// Delete page
|
|
async function deletePage(pageId, pageTitle) {
|
|
const confirmed = await showConfirmModal(
|
|
"Delete Page",
|
|
`Are you sure you want to delete "${pageTitle}"? This action cannot be undone.`,
|
|
"Delete",
|
|
"danger",
|
|
);
|
|
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
|
|
showSaving(true);
|
|
|
|
try {
|
|
const response = await fetch(`/api/admin/pages/${pageId}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || "Failed to delete page");
|
|
}
|
|
|
|
showSaving(false);
|
|
|
|
// Show success modal
|
|
await showResultModal(
|
|
"success",
|
|
"Page Deleted",
|
|
`"${pageTitle}" has been deleted successfully.`,
|
|
);
|
|
|
|
// Reload pages list
|
|
loadPages();
|
|
|
|
// Invalidate cache
|
|
try {
|
|
await fetch("/api/admin/cache/invalidate", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify({ patterns: ["pages", "page:"] }),
|
|
});
|
|
} catch (e) {
|
|
console.log("Cache invalidation skipped");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error deleting page:", error);
|
|
showSaving(false);
|
|
await showResultModal(
|
|
"error",
|
|
"Delete Failed",
|
|
error.message || "Failed to delete page. Please try again.",
|
|
);
|
|
}
|
|
}
|
|
|
|
// Show create page modal
|
|
function showCreatePage() {
|
|
// Initialize editors if not already done
|
|
if (!pageContentEditor || !aboutContentEditor) {
|
|
initializeEditors();
|
|
}
|
|
|
|
currentPageSlug = null;
|
|
deletedTeamMemberIds = [];
|
|
teamMembers = [];
|
|
faqItems = [];
|
|
returnsSections = [];
|
|
shippingSections = [];
|
|
privacySections = [];
|
|
|
|
// Reset form
|
|
document.getElementById("pageForm").reset();
|
|
document.getElementById("pageId").value = "";
|
|
document.getElementById("pageSlugOriginal").value = "";
|
|
document.getElementById("pageIdBadge").style.display = "none";
|
|
|
|
// Reset editors
|
|
if (pageContentEditor) pageContentEditor.setContents([]);
|
|
if (aboutContentEditor) aboutContentEditor.setContents([]);
|
|
|
|
// Show regular content section
|
|
showContentSection("regular");
|
|
|
|
// Update modal title
|
|
document.getElementById("modalTitleText").textContent = "Create New Page";
|
|
document.getElementById("saveButtonText").textContent = "Create Page";
|
|
|
|
pageModal.show();
|
|
}
|
|
|
|
// Edit existing page
|
|
async function editPage(pageId) {
|
|
// Initialize editors if not already done
|
|
if (!pageContentEditor || !aboutContentEditor) {
|
|
initializeEditors();
|
|
}
|
|
|
|
// First, fetch the full page data
|
|
let page;
|
|
try {
|
|
const response = await fetch(`/api/admin/pages/${pageId}`, {
|
|
credentials: "include",
|
|
});
|
|
|
|
if (!response.ok) throw new Error("Failed to load page");
|
|
|
|
const data = await response.json();
|
|
page = data.page;
|
|
} catch (error) {
|
|
console.error("Error loading page:", error);
|
|
showToast("Failed to load page details", "error");
|
|
return;
|
|
}
|
|
|
|
if (!page) return;
|
|
|
|
currentPageSlug = page.slug;
|
|
deletedTeamMemberIds = [];
|
|
teamMembers = [];
|
|
faqItems = [];
|
|
returnsSections = [];
|
|
shippingSections = [];
|
|
privacySections = [];
|
|
|
|
// Populate form fields
|
|
document.getElementById("pageId").value = page.id;
|
|
document.getElementById("pageSlugOriginal").value = page.slug;
|
|
document.getElementById("pageTitle").value = page.title || "";
|
|
document.getElementById("pageSlug").value = page.slug || "";
|
|
document.getElementById("pageMetaTitle").value = page.metatitle || "";
|
|
document.getElementById("pageMetaDescription").value =
|
|
page.metadescription || "";
|
|
document.getElementById("pagePublished").checked = page.ispublished !== false;
|
|
|
|
// Show page ID
|
|
document.getElementById("pageIdBadge").style.display = "flex";
|
|
document.getElementById("pageIdDisplay").textContent = page.id;
|
|
|
|
// Update modal title
|
|
document.getElementById("modalTitleText").textContent = `Edit: ${page.title}`;
|
|
document.getElementById("saveButtonText").textContent = "Save Changes";
|
|
|
|
// Handle different page types
|
|
if (page.slug === "about") {
|
|
showContentSection("about");
|
|
await loadAboutPageContent(page);
|
|
} else if (page.slug === "contact") {
|
|
showContentSection("contact");
|
|
loadContactPageContent(page);
|
|
} else if (page.slug === "faq") {
|
|
showContentSection("faq");
|
|
loadFaqPageContent(page);
|
|
} else if (page.slug === "returns") {
|
|
showContentSection("returns");
|
|
loadReturnsPageContent(page);
|
|
} else if (page.slug === "shipping-info") {
|
|
showContentSection("shipping");
|
|
loadShippingPageContent(page);
|
|
} else if (page.slug === "privacy") {
|
|
showContentSection("privacy");
|
|
loadPrivacyPageContent(page);
|
|
} else {
|
|
showContentSection("regular");
|
|
loadRegularPageContent(page);
|
|
}
|
|
|
|
pageModal.show();
|
|
}
|
|
|
|
// Show appropriate content section
|
|
function showContentSection(type) {
|
|
document.getElementById("regularContentSection").style.display =
|
|
type === "regular" ? "block" : "none";
|
|
document.getElementById("aboutContentSection").style.display =
|
|
type === "about" ? "block" : "none";
|
|
document.getElementById("contactContentSection").style.display =
|
|
type === "contact" ? "block" : "none";
|
|
document.getElementById("faqContentSection").style.display =
|
|
type === "faq" ? "block" : "none";
|
|
document.getElementById("returnsContentSection").style.display =
|
|
type === "returns" ? "block" : "none";
|
|
document.getElementById("shippingContentSection").style.display =
|
|
type === "shipping" ? "block" : "none";
|
|
document.getElementById("privacyContentSection").style.display =
|
|
type === "privacy" ? "block" : "none";
|
|
}
|
|
|
|
// Load regular page content
|
|
function loadRegularPageContent(page) {
|
|
try {
|
|
const content = page.pagecontent
|
|
? JSON.parse(page.pagecontent)
|
|
: { ops: [] };
|
|
pageContentEditor.setContents(content);
|
|
} catch {
|
|
pageContentEditor.setText(page.pagecontent || "");
|
|
}
|
|
}
|
|
|
|
// Load about page content
|
|
async function loadAboutPageContent(page) {
|
|
try {
|
|
const content = page.pagecontent
|
|
? JSON.parse(page.pagecontent)
|
|
: { ops: [] };
|
|
aboutContentEditor.setContents(content);
|
|
} catch {
|
|
aboutContentEditor.setText(page.pagecontent || "");
|
|
}
|
|
|
|
// Load team members
|
|
await loadTeamMembers();
|
|
}
|
|
|
|
// Load team members
|
|
async function loadTeamMembers() {
|
|
try {
|
|
const response = await fetch("/api/admin/team-members", {
|
|
credentials: "include",
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
teamMembers = data.teamMembers || data || [];
|
|
} else {
|
|
teamMembers = [];
|
|
}
|
|
|
|
renderTeamMembers();
|
|
} catch (error) {
|
|
console.error("Error loading team members:", error);
|
|
teamMembers = [];
|
|
renderTeamMembers();
|
|
}
|
|
}
|
|
|
|
// Render team members grid
|
|
function renderTeamMembers() {
|
|
const grid = document.getElementById("teamMembersGrid");
|
|
|
|
const membersHtml = teamMembers
|
|
.map(
|
|
(member, index) => `
|
|
<div class="team-card" data-member-id="${member.id || "new-" + index}" data-index="${index}">
|
|
<button type="button" class="team-card-delete" onclick="removeTeamMember(${index})" title="Remove team member">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
<div class="team-card-avatar" onclick="selectTeamMemberImage(${index})" title="Click to change image">
|
|
${
|
|
member.image_url
|
|
? `<img src="${member.image_url}" alt="${escapeHtml(member.name || "")}" />`
|
|
: '<i class="bi bi-person"></i>'
|
|
}
|
|
</div>
|
|
<input type="text" placeholder="Name" value="${escapeHtml(member.name || "")}"
|
|
onchange="updateTeamMember(${index}, 'name', this.value)" />
|
|
<input type="text" placeholder="Position" value="${escapeHtml(member.position || "")}"
|
|
onchange="updateTeamMember(${index}, 'position', this.value)" />
|
|
<textarea placeholder="Bio/Description"
|
|
onchange="updateTeamMember(${index}, 'bio', this.value)">${escapeHtml(member.bio || "")}</textarea>
|
|
<input type="hidden" class="team-member-image" value="${member.image_url || ""}" />
|
|
</div>
|
|
`,
|
|
)
|
|
.join("");
|
|
|
|
grid.innerHTML =
|
|
membersHtml +
|
|
`
|
|
<button type="button" class="add-team-btn" onclick="addTeamMember()">
|
|
<i class="bi bi-person-plus"></i>
|
|
<span>Add Team Member</span>
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
// Add new team member
|
|
function addTeamMember() {
|
|
teamMembers.push({
|
|
name: "",
|
|
position: "",
|
|
bio: "",
|
|
image_url: "",
|
|
display_order: teamMembers.length,
|
|
});
|
|
renderTeamMembers();
|
|
}
|
|
|
|
// Remove team member
|
|
async function removeTeamMember(index) {
|
|
const member = teamMembers[index];
|
|
|
|
const confirmed = await showConfirmModal(
|
|
"Remove Team Member",
|
|
"Are you sure you want to remove this team member?",
|
|
"Remove",
|
|
"warning",
|
|
);
|
|
|
|
if (confirmed) {
|
|
// Track ID for deletion from database
|
|
if (member.id && typeof member.id === "number") {
|
|
deletedTeamMemberIds.push(member.id);
|
|
console.log("Marked for deletion:", member.id);
|
|
}
|
|
|
|
teamMembers.splice(index, 1);
|
|
renderTeamMembers();
|
|
showToast("Team member removed. Save to apply changes.", "info");
|
|
}
|
|
}
|
|
|
|
// Update team member field
|
|
function updateTeamMember(index, field, value) {
|
|
if (teamMembers[index]) {
|
|
teamMembers[index][field] = value;
|
|
}
|
|
}
|
|
|
|
// Select team member image
|
|
function selectTeamMemberImage(index) {
|
|
openMediaLibrary((imageUrl) => {
|
|
if (teamMembers[index]) {
|
|
teamMembers[index].image_url = imageUrl;
|
|
renderTeamMembers();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Load contact page content
|
|
function loadContactPageContent(page) {
|
|
let pageData = {};
|
|
|
|
try {
|
|
if (typeof page.pagedata === "string") {
|
|
pageData = JSON.parse(page.pagedata);
|
|
} else {
|
|
pageData = page.pagedata || {};
|
|
}
|
|
} catch {
|
|
pageData = {};
|
|
}
|
|
|
|
// Populate header fields
|
|
document.getElementById("contactHeaderTitle").value =
|
|
pageData.header?.title || "";
|
|
document.getElementById("contactHeaderSubtitle").value =
|
|
pageData.header?.subtitle || "";
|
|
|
|
// Populate contact details
|
|
document.getElementById("contactPhone").value = pageData.phone || "";
|
|
document.getElementById("contactEmail").value = pageData.email || "";
|
|
document.getElementById("contactAddress").value = pageData.address || "";
|
|
|
|
// Populate business hours
|
|
const hoursContainer = document.getElementById("businessHoursContainer");
|
|
hoursContainer.innerHTML = "";
|
|
|
|
const hours = pageData.businessHours || [];
|
|
hours.forEach((hour, index) => {
|
|
addBusinessHourRow(hour.day, hour.hours, index);
|
|
});
|
|
|
|
if (hours.length === 0) {
|
|
addBusinessHour();
|
|
}
|
|
}
|
|
|
|
// Load FAQ page content
|
|
function loadFaqPageContent(page) {
|
|
let pageData = {};
|
|
|
|
try {
|
|
if (typeof page.pagedata === "string") {
|
|
pageData = JSON.parse(page.pagedata);
|
|
} else {
|
|
pageData = page.pagedata || {};
|
|
}
|
|
} catch {
|
|
pageData = {};
|
|
}
|
|
|
|
// Populate header fields
|
|
document.getElementById("faqHeaderTitle").value =
|
|
pageData.header?.title || "Frequently Asked Questions";
|
|
document.getElementById("faqHeaderSubtitle").value =
|
|
pageData.header?.subtitle || "Quick answers to common questions";
|
|
|
|
// Load FAQ items
|
|
faqItems = pageData.items || [];
|
|
renderFaqItems();
|
|
}
|
|
|
|
// Render FAQ items
|
|
function renderFaqItems() {
|
|
const container = document.getElementById("faqItemsContainer");
|
|
|
|
if (faqItems.length === 0) {
|
|
container.innerHTML =
|
|
'<p class="text-muted text-center py-4">No FAQ items yet. Click "Add Question" to create one.</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = faqItems
|
|
.map(
|
|
(item, index) => `
|
|
<div class="faq-item-card" data-index="${index}">
|
|
<span class="item-number">${index + 1}</span>
|
|
<button type="button" class="btn-remove-item" onclick="removeFaqItem(${index})" title="Remove">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
<div class="mb-3">
|
|
<label class="form-label">Question</label>
|
|
<input type="text" class="form-control faq-question" value="${escapeHtml(item.question || "")}"
|
|
placeholder="Enter the question" onchange="updateFaqItem(${index}, 'question', this.value)" />
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Answer</label>
|
|
<textarea class="form-control faq-answer" rows="3" placeholder="Enter the answer"
|
|
onchange="updateFaqItem(${index}, 'answer', this.value)">${escapeHtml(item.answer || "")}</textarea>
|
|
</div>
|
|
</div>
|
|
`,
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
// Add new FAQ item
|
|
function addFaqItem() {
|
|
faqItems.push({
|
|
question: "",
|
|
answer: "",
|
|
});
|
|
renderFaqItems();
|
|
}
|
|
|
|
// Remove FAQ item
|
|
async function removeFaqItem(index) {
|
|
const confirmed = await showConfirmModal(
|
|
"Remove FAQ Item",
|
|
"Are you sure you want to remove this FAQ item?",
|
|
"Remove",
|
|
"warning",
|
|
);
|
|
|
|
if (confirmed) {
|
|
faqItems.splice(index, 1);
|
|
renderFaqItems();
|
|
showToast("FAQ item removed.", "info");
|
|
}
|
|
}
|
|
|
|
// Update FAQ item
|
|
function updateFaqItem(index, field, value) {
|
|
if (faqItems[index]) {
|
|
faqItems[index][field] = value;
|
|
}
|
|
}
|
|
|
|
// Load Returns page content
|
|
function loadReturnsPageContent(page) {
|
|
let pageData = {};
|
|
|
|
try {
|
|
if (typeof page.pagedata === "string") {
|
|
pageData = JSON.parse(page.pagedata);
|
|
} else {
|
|
pageData = page.pagedata || {};
|
|
}
|
|
} catch {
|
|
pageData = {};
|
|
}
|
|
|
|
// Populate header fields
|
|
document.getElementById("returnsHeaderTitle").value =
|
|
pageData.header?.title || "Returns & Refunds";
|
|
document.getElementById("returnsHeaderSubtitle").value =
|
|
pageData.header?.subtitle || "Our hassle-free return policy";
|
|
document.getElementById("returnsHighlight").value = pageData.highlight || "";
|
|
|
|
// Load sections
|
|
returnsSections = pageData.sections || [];
|
|
renderReturnsSections();
|
|
}
|
|
|
|
// Render Returns sections
|
|
function renderReturnsSections() {
|
|
const container = document.getElementById("returnsSectionsContainer");
|
|
|
|
if (returnsSections.length === 0) {
|
|
container.innerHTML =
|
|
'<p class="text-muted text-center py-4">No sections yet. Click "Add Section" to create one.</p>';
|
|
returnsEditors = [];
|
|
return;
|
|
}
|
|
|
|
// Clear existing editors
|
|
returnsEditors = [];
|
|
|
|
container.innerHTML = returnsSections
|
|
.map(
|
|
(section, index) => `
|
|
<div class="section-item-card" data-index="${index}">
|
|
<div class="section-header">
|
|
<span class="section-number">${index + 1}</span>
|
|
<input type="text" class="form-control section-title" value="${escapeHtml(section.title || "")}"
|
|
placeholder="Section Title (e.g., Return Eligibility) - displays as H2 heading"
|
|
onchange="updateReturnsSection(${index}, 'title', this.value)" />
|
|
<button type="button" class="btn-remove-section" onclick="removeReturnsSection(${index})" title="Remove">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Content <small class="text-muted">(displays as formatted text below the title)</small></label>
|
|
<div class="editor-wrapper">
|
|
<div id="returnsContentEditor_${index}" style="min-height: 150px;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="list-items-container">
|
|
<label class="form-label"><i class="bi bi-list-ul me-1"></i> List Items <small class="text-muted">(optional - displays as bullet points)</small></label>
|
|
<div class="list-items-list" id="returnsList-${index}">
|
|
${(section.listItems || [])
|
|
.map(
|
|
(item, i) => `
|
|
<div class="list-item-row">
|
|
<input type="text" class="form-control" value="${escapeHtml(item)}"
|
|
placeholder="List item" onchange="updateReturnsSectionListItem(${index}, ${i}, this.value)" />
|
|
<button type="button" class="btn-remove-list-item" onclick="removeReturnsSectionListItem(${index}, ${i})">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
</div>
|
|
`,
|
|
)
|
|
.join("")}
|
|
</div>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm mt-2" onclick="addReturnsSectionListItem(${index})">
|
|
<i class="bi bi-plus me-1"></i>Add List Item
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
)
|
|
.join("");
|
|
|
|
// Initialize Quill editors for each section after DOM is ready
|
|
setTimeout(() => {
|
|
returnsSections.forEach((section, index) => {
|
|
const editorId = `returnsContentEditor_${index}`;
|
|
const editorElement = document.getElementById(editorId);
|
|
|
|
if (editorElement) {
|
|
const quill = new Quill(`#${editorId}`, {
|
|
theme: "snow",
|
|
modules: {
|
|
toolbar: [
|
|
[{ header: [2, 3, false] }],
|
|
["bold", "italic", "underline"],
|
|
[{ list: "ordered" }, { list: "bullet" }],
|
|
["link"],
|
|
["clean"],
|
|
],
|
|
},
|
|
});
|
|
|
|
if (section.content) {
|
|
quill.clipboard.dangerouslyPasteHTML(section.content);
|
|
}
|
|
|
|
quill.on("text-change", () => {
|
|
returnsSections[index].content = quill.root.innerHTML;
|
|
});
|
|
|
|
returnsEditors[index] = quill;
|
|
}
|
|
});
|
|
}, 100);
|
|
}
|
|
|
|
// Returns section functions
|
|
function addReturnsSection() {
|
|
returnsSections.push({ title: "", content: "", listItems: [] });
|
|
renderReturnsSections();
|
|
}
|
|
|
|
async function removeReturnsSection(index) {
|
|
const confirmed = await showConfirmModal(
|
|
"Remove Section",
|
|
"Are you sure you want to remove this returns section?",
|
|
"Remove",
|
|
"warning",
|
|
);
|
|
|
|
if (confirmed) {
|
|
returnsSections.splice(index, 1);
|
|
renderReturnsSections();
|
|
}
|
|
}
|
|
|
|
function updateReturnsSection(index, field, value) {
|
|
if (returnsSections[index]) {
|
|
returnsSections[index][field] = value;
|
|
}
|
|
}
|
|
|
|
function addReturnsSectionListItem(sectionIndex) {
|
|
if (!returnsSections[sectionIndex].listItems) {
|
|
returnsSections[sectionIndex].listItems = [];
|
|
}
|
|
returnsSections[sectionIndex].listItems.push("");
|
|
renderReturnsSections();
|
|
}
|
|
|
|
function removeReturnsSectionListItem(sectionIndex, itemIndex) {
|
|
returnsSections[sectionIndex].listItems.splice(itemIndex, 1);
|
|
renderReturnsSections();
|
|
}
|
|
|
|
function updateReturnsSectionListItem(sectionIndex, itemIndex, value) {
|
|
returnsSections[sectionIndex].listItems[itemIndex] = value;
|
|
}
|
|
|
|
// Load Shipping page content
|
|
function loadShippingPageContent(page) {
|
|
let pageData = {};
|
|
|
|
try {
|
|
if (typeof page.pagedata === "string") {
|
|
pageData = JSON.parse(page.pagedata);
|
|
} else {
|
|
pageData = page.pagedata || {};
|
|
}
|
|
} catch {
|
|
pageData = {};
|
|
}
|
|
|
|
// Populate header fields
|
|
document.getElementById("shippingHeaderTitle").value =
|
|
pageData.header?.title || "Shipping Information";
|
|
document.getElementById("shippingHeaderSubtitle").value =
|
|
pageData.header?.subtitle || "Fast, reliable delivery to your door";
|
|
|
|
// Load sections
|
|
shippingSections = pageData.sections || [];
|
|
renderShippingSections();
|
|
}
|
|
|
|
// Render Shipping sections
|
|
function renderShippingSections() {
|
|
const container = document.getElementById("shippingSectionsContainer");
|
|
|
|
if (shippingSections.length === 0) {
|
|
container.innerHTML =
|
|
'<p class="text-muted text-center py-4">No sections yet. Click "Add Section" to create one.</p>';
|
|
shippingEditors = [];
|
|
return;
|
|
}
|
|
|
|
// Clear existing editors
|
|
shippingEditors = [];
|
|
|
|
container.innerHTML = shippingSections
|
|
.map(
|
|
(section, index) => `
|
|
<div class="section-item-card" data-index="${index}">
|
|
<div class="section-header">
|
|
<span class="section-number">${index + 1}</span>
|
|
<input type="text" class="form-control section-title" value="${escapeHtml(section.title || "")}"
|
|
placeholder="Section Title (e.g., Shipping Methods) - displays as H2 heading"
|
|
onchange="updateShippingSection(${index}, 'title', this.value)" />
|
|
<button type="button" class="btn-remove-section" onclick="removeShippingSection(${index})" title="Remove">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Content <small class="text-muted">(displays as formatted text below the title)</small></label>
|
|
<div class="editor-wrapper">
|
|
<div id="shippingContentEditor_${index}" style="min-height: 150px;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="list-items-container">
|
|
<label class="form-label"><i class="bi bi-list-ul me-1"></i> List Items <small class="text-muted">(optional - displays as bullet points)</small></label>
|
|
<div class="list-items-list" id="shippingList-${index}">
|
|
${(section.listItems || [])
|
|
.map(
|
|
(item, i) => `
|
|
<div class="list-item-row">
|
|
<input type="text" class="form-control" value="${escapeHtml(item)}"
|
|
placeholder="List item" onchange="updateShippingSectionListItem(${index}, ${i}, this.value)" />
|
|
<button type="button" class="btn-remove-list-item" onclick="removeShippingSectionListItem(${index}, ${i})">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
</div>
|
|
`,
|
|
)
|
|
.join("")}
|
|
</div>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm mt-2" onclick="addShippingSectionListItem(${index})">
|
|
<i class="bi bi-plus-circle me-1"></i>Add List Item
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
)
|
|
.join("");
|
|
|
|
// Initialize Quill editors for each section after DOM is ready
|
|
setTimeout(() => {
|
|
shippingSections.forEach((section, index) => {
|
|
const editorId = `shippingContentEditor_${index}`;
|
|
const editorElement = document.getElementById(editorId);
|
|
|
|
if (editorElement) {
|
|
const quill = new Quill(`#${editorId}`, {
|
|
theme: "snow",
|
|
modules: {
|
|
toolbar: [
|
|
[{ header: [2, 3, false] }],
|
|
["bold", "italic", "underline"],
|
|
[{ list: "ordered" }, { list: "bullet" }],
|
|
["link"],
|
|
["clean"],
|
|
],
|
|
},
|
|
});
|
|
|
|
if (section.content) {
|
|
quill.clipboard.dangerouslyPasteHTML(section.content);
|
|
}
|
|
|
|
quill.on("text-change", () => {
|
|
shippingSections[index].content = quill.root.innerHTML;
|
|
});
|
|
|
|
shippingEditors[index] = quill;
|
|
}
|
|
});
|
|
}, 100);
|
|
}
|
|
|
|
// Shipping section functions
|
|
function addShippingSection() {
|
|
shippingSections.push({ title: "", content: "", listItems: [] });
|
|
renderShippingSections();
|
|
}
|
|
|
|
async function removeShippingSection(index) {
|
|
const confirmed = await showConfirmModal(
|
|
"Remove Section",
|
|
"Are you sure you want to remove this shipping section?",
|
|
"Remove",
|
|
"warning",
|
|
);
|
|
|
|
if (confirmed) {
|
|
shippingSections.splice(index, 1);
|
|
renderShippingSections();
|
|
}
|
|
}
|
|
|
|
function updateShippingSection(index, field, value) {
|
|
if (shippingSections[index]) {
|
|
shippingSections[index][field] = value;
|
|
}
|
|
}
|
|
|
|
function addShippingSectionListItem(sectionIndex) {
|
|
if (!shippingSections[sectionIndex].listItems) {
|
|
shippingSections[sectionIndex].listItems = [];
|
|
}
|
|
shippingSections[sectionIndex].listItems.push("");
|
|
renderShippingSections();
|
|
}
|
|
|
|
function removeShippingSectionListItem(sectionIndex, itemIndex) {
|
|
shippingSections[sectionIndex].listItems.splice(itemIndex, 1);
|
|
renderShippingSections();
|
|
}
|
|
|
|
function updateShippingSectionListItem(sectionIndex, itemIndex, value) {
|
|
shippingSections[sectionIndex].listItems[itemIndex] = value;
|
|
}
|
|
|
|
// Load Privacy page content
|
|
function loadPrivacyPageContent(page) {
|
|
let pageData = {};
|
|
|
|
try {
|
|
if (typeof page.pagedata === "string") {
|
|
pageData = JSON.parse(page.pagedata);
|
|
} else {
|
|
pageData = page.pagedata || {};
|
|
}
|
|
} catch {
|
|
pageData = {};
|
|
}
|
|
|
|
// Populate header fields
|
|
document.getElementById("privacyHeaderTitle").value =
|
|
pageData.header?.title || "Privacy Policy";
|
|
document.getElementById("privacyHeaderSubtitle").value =
|
|
pageData.header?.subtitle || "How we protect and use your information";
|
|
document.getElementById("privacyLastUpdated").value =
|
|
pageData.lastUpdated || "";
|
|
|
|
// Populate contact box fields
|
|
document.getElementById("privacyContactTitle").value =
|
|
pageData.contactBox?.title || "Privacy Questions?";
|
|
document.getElementById("privacyContactMessage").value =
|
|
pageData.contactBox?.message ||
|
|
"If you have any questions about this Privacy Policy, please contact us:";
|
|
document.getElementById("privacyContactEmail").value =
|
|
pageData.contactBox?.email || "privacy@skyartshop.com";
|
|
document.getElementById("privacyContactPhone").value =
|
|
pageData.contactBox?.phone || "(555) 123-4567";
|
|
document.getElementById("privacyContactAddress").value =
|
|
pageData.contactBox?.address || "123 Creative Lane, City, ST 12345";
|
|
|
|
// Convert old sections format to new mainContent format if needed
|
|
let mainContent = pageData.mainContent || "";
|
|
|
|
// If no mainContent but has sections (old format), convert them
|
|
if (!mainContent && pageData.sections && pageData.sections.length > 0) {
|
|
console.log("Converting old sections format to new mainContent format");
|
|
mainContent = pageData.sections
|
|
.map((section) => {
|
|
let html = "";
|
|
if (section.title) {
|
|
html += `<h2>${section.title}</h2>`;
|
|
}
|
|
if (section.content) {
|
|
html += section.content;
|
|
}
|
|
return html;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
console.log(
|
|
"Loading privacy content into editor, length:",
|
|
mainContent.length,
|
|
);
|
|
|
|
// Initialize main content editor
|
|
initializePrivacyMainEditor(mainContent);
|
|
}
|
|
|
|
// Initialize Privacy main content editor
|
|
function initializePrivacyMainEditor(content) {
|
|
const editorElement = document.getElementById("privacyMainContentEditor");
|
|
|
|
if (!editorElement) {
|
|
console.error("privacyMainContentEditor element not found!");
|
|
return;
|
|
}
|
|
|
|
// Check if editor already exists
|
|
if (privacyMainEditor) {
|
|
console.log("Privacy editor already exists, updating content");
|
|
if (content) {
|
|
privacyMainEditor.clipboard.dangerouslyPasteHTML(content);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Clear any existing editor
|
|
editorElement.innerHTML = "";
|
|
|
|
// Create new Quill editor using dedicated variable
|
|
privacyMainEditor = new Quill("#privacyMainContentEditor", {
|
|
theme: "snow",
|
|
modules: {
|
|
toolbar: [
|
|
[{ header: [1, 2, 3, false] }],
|
|
["bold", "italic", "underline", "strike"],
|
|
[{ list: "ordered" }, { list: "bullet" }],
|
|
[{ indent: "-1" }, { indent: "+1" }],
|
|
["link"],
|
|
["clean"],
|
|
],
|
|
},
|
|
});
|
|
|
|
// Set content if exists
|
|
if (content) {
|
|
privacyMainEditor.clipboard.dangerouslyPasteHTML(content);
|
|
}
|
|
|
|
console.log(
|
|
"Privacy editor initialized with content length:",
|
|
content ? content.length : 0,
|
|
);
|
|
}
|
|
|
|
// Add business hour row
|
|
function addBusinessHour() {
|
|
const container = document.getElementById("businessHoursContainer");
|
|
const index = container.children.length;
|
|
addBusinessHourRow("", "", index);
|
|
}
|
|
|
|
function addBusinessHourRow(day, hours, index) {
|
|
const container = document.getElementById("businessHoursContainer");
|
|
const row = document.createElement("div");
|
|
row.className = "hours-row";
|
|
row.innerHTML = `
|
|
<input type="text" class="form-control hour-day" placeholder="Day (e.g., Monday - Friday)" value="${escapeHtml(day)}" />
|
|
<input type="text" class="form-control hour-time" placeholder="Hours (e.g., 9:00 AM - 5:00 PM)" value="${escapeHtml(hours)}" />
|
|
<button type="button" class="btn-remove-hour" onclick="removeBusinessHour(this)" title="Remove">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
`;
|
|
container.appendChild(row);
|
|
}
|
|
|
|
// Remove business hour row
|
|
function removeBusinessHour(btn) {
|
|
btn.closest(".hours-row").remove();
|
|
}
|
|
|
|
// Save page
|
|
async function savePage() {
|
|
const pageId = document.getElementById("pageId").value;
|
|
const slug = document
|
|
.getElementById("pageSlug")
|
|
.value.toLowerCase()
|
|
.replace(/\s+/g, "-");
|
|
|
|
showSaving(true);
|
|
|
|
try {
|
|
// First, handle team member deletions if on about page
|
|
if (currentPageSlug === "about" && deletedTeamMemberIds.length > 0) {
|
|
console.log("Deleting team members:", deletedTeamMemberIds);
|
|
|
|
for (const memberId of deletedTeamMemberIds) {
|
|
try {
|
|
const deleteResponse = await fetch(
|
|
`/api/admin/team-members/${memberId}`,
|
|
{
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
},
|
|
);
|
|
|
|
if (deleteResponse.ok) {
|
|
console.log(`Deleted team member ${memberId}`);
|
|
} else {
|
|
console.error(`Failed to delete team member ${memberId}`);
|
|
}
|
|
} catch (err) {
|
|
console.error(`Error deleting team member ${memberId}:`, err);
|
|
}
|
|
}
|
|
|
|
deletedTeamMemberIds = [];
|
|
}
|
|
|
|
// Build page data based on page type
|
|
let pagecontent = "";
|
|
let pagedata = null;
|
|
|
|
if (slug === "about" || currentPageSlug === "about") {
|
|
pagecontent = JSON.stringify(aboutContentEditor.getContents());
|
|
|
|
// Save team members
|
|
await saveTeamMembers();
|
|
} else if (slug === "contact" || currentPageSlug === "contact") {
|
|
pagedata = buildContactPageData();
|
|
pagecontent = JSON.stringify({ ops: [] });
|
|
} else if (slug === "faq" || currentPageSlug === "faq") {
|
|
pagedata = buildFaqPageData();
|
|
pagecontent = JSON.stringify({ ops: [] });
|
|
} else if (slug === "returns" || currentPageSlug === "returns") {
|
|
pagedata = buildReturnsPageData();
|
|
pagecontent = JSON.stringify({ ops: [] });
|
|
} else if (
|
|
slug === "shipping-info" ||
|
|
currentPageSlug === "shipping-info"
|
|
) {
|
|
pagedata = buildShippingPageData();
|
|
pagecontent = JSON.stringify({ ops: [] });
|
|
} else if (slug === "privacy" || currentPageSlug === "privacy") {
|
|
pagedata = buildPrivacyPageData();
|
|
pagecontent = JSON.stringify({ ops: [] });
|
|
} else {
|
|
pagecontent = JSON.stringify(pageContentEditor.getContents());
|
|
}
|
|
|
|
// Build request body
|
|
const body = {
|
|
title: document.getElementById("pageTitle").value,
|
|
slug: slug,
|
|
content: pagecontent,
|
|
contenthtml: pagecontent,
|
|
metatitle: document.getElementById("pageMetaTitle").value,
|
|
metadescription: document.getElementById("pageMetaDescription").value,
|
|
ispublished: document.getElementById("pagePublished").checked,
|
|
};
|
|
|
|
if (pagedata) {
|
|
body.pagedata = pagedata;
|
|
}
|
|
|
|
// Make API request
|
|
const url = pageId ? `/api/admin/pages/${pageId}` : "/api/admin/pages";
|
|
const method = pageId ? "PUT" : "POST";
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || "Failed to save page");
|
|
}
|
|
|
|
const savedPage = await response.json();
|
|
|
|
showSaving(false);
|
|
pageModal.hide();
|
|
|
|
// Show success modal
|
|
await showResultModal(
|
|
"success",
|
|
"Page Saved",
|
|
`Page has been ${pageId ? "updated" : "created"} successfully!`,
|
|
);
|
|
|
|
loadPages();
|
|
|
|
// Clear cache to reflect changes on frontend immediately
|
|
try {
|
|
await fetch("/api/admin/cache/invalidate", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify({ patterns: ["pages", "page:", slug] }),
|
|
});
|
|
} catch (e) {
|
|
console.log("Cache invalidation skipped");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error saving page:", error);
|
|
showSaving(false);
|
|
await showResultModal(
|
|
"error",
|
|
"Save Failed",
|
|
error.message || "Failed to save page. Please try again.",
|
|
);
|
|
}
|
|
}
|
|
|
|
// Save team members
|
|
async function saveTeamMembers() {
|
|
for (let i = 0; i < teamMembers.length; i++) {
|
|
const member = teamMembers[i];
|
|
member.display_order = i;
|
|
|
|
const url = member.id
|
|
? `/api/admin/team-members/${member.id}`
|
|
: "/api/admin/team-members";
|
|
const method = member.id ? "PUT" : "POST";
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify(member),
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
teamMembers[i] = data.teamMember || data;
|
|
}
|
|
} catch (err) {
|
|
console.error("Error saving team member:", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build contact page data
|
|
function buildContactPageData() {
|
|
const hours = [];
|
|
const hourRows = document.querySelectorAll(".hours-row");
|
|
|
|
hourRows.forEach((row) => {
|
|
const day = row.querySelector(".hour-day").value.trim();
|
|
const time = row.querySelector(".hour-time").value.trim();
|
|
if (day || time) {
|
|
hours.push({ day, hours: time });
|
|
}
|
|
});
|
|
|
|
return {
|
|
header: {
|
|
title: document.getElementById("contactHeaderTitle").value,
|
|
subtitle: document.getElementById("contactHeaderSubtitle").value,
|
|
},
|
|
phone: document.getElementById("contactPhone").value,
|
|
email: document.getElementById("contactEmail").value,
|
|
address: document.getElementById("contactAddress").value,
|
|
businessHours: hours,
|
|
};
|
|
}
|
|
|
|
// Build FAQ page data
|
|
function buildFaqPageData() {
|
|
return {
|
|
header: {
|
|
title:
|
|
document.getElementById("faqHeaderTitle").value ||
|
|
"Frequently Asked Questions",
|
|
subtitle:
|
|
document.getElementById("faqHeaderSubtitle").value ||
|
|
"Quick answers to common questions",
|
|
},
|
|
items: faqItems.filter(
|
|
(item) => item.question.trim() || item.answer.trim(),
|
|
),
|
|
};
|
|
}
|
|
|
|
// Build Returns page data
|
|
function buildReturnsPageData() {
|
|
return {
|
|
header: {
|
|
title:
|
|
document.getElementById("returnsHeaderTitle").value ||
|
|
"Returns & Refunds",
|
|
subtitle:
|
|
document.getElementById("returnsHeaderSubtitle").value ||
|
|
"Our hassle-free return policy",
|
|
},
|
|
highlight: document.getElementById("returnsHighlight").value,
|
|
sections: returnsSections.filter((s) => s.title.trim() || s.content.trim()),
|
|
};
|
|
}
|
|
|
|
// Build Shipping page data
|
|
function buildShippingPageData() {
|
|
return {
|
|
header: {
|
|
title:
|
|
document.getElementById("shippingHeaderTitle").value ||
|
|
"Shipping Information",
|
|
subtitle:
|
|
document.getElementById("shippingHeaderSubtitle").value ||
|
|
"Fast, reliable delivery to your door",
|
|
},
|
|
sections: shippingSections.filter(
|
|
(s) => s.title.trim() || s.content.trim(),
|
|
),
|
|
};
|
|
}
|
|
|
|
// Build Privacy page data
|
|
function buildPrivacyPageData() {
|
|
const data = {
|
|
header: {
|
|
title:
|
|
document.getElementById("privacyHeaderTitle").value || "Privacy Policy",
|
|
subtitle:
|
|
document.getElementById("privacyHeaderSubtitle").value ||
|
|
"How we protect and use your information",
|
|
},
|
|
lastUpdated: document.getElementById("privacyLastUpdated").value || "",
|
|
mainContent: privacyMainEditor ? privacyMainEditor.root.innerHTML : "",
|
|
contactBox: {
|
|
title:
|
|
document.getElementById("privacyContactTitle").value ||
|
|
"Privacy Questions?",
|
|
message:
|
|
document.getElementById("privacyContactMessage").value ||
|
|
"If you have any questions about this Privacy Policy, please contact us:",
|
|
email:
|
|
document.getElementById("privacyContactEmail").value ||
|
|
"privacy@skyartshop.com",
|
|
phone:
|
|
document.getElementById("privacyContactPhone").value ||
|
|
"(555) 123-4567",
|
|
address:
|
|
document.getElementById("privacyContactAddress").value ||
|
|
"123 Creative Lane, City, ST 12345",
|
|
},
|
|
};
|
|
console.log("Privacy page data being saved:", data);
|
|
console.log("Main content length:", data.mainContent.length);
|
|
return data;
|
|
}
|
|
|
|
// Show/hide saving overlay
|
|
function showSaving(show) {
|
|
const overlay = document.getElementById("savingOverlay");
|
|
if (show) {
|
|
overlay.classList.add("active");
|
|
document.getElementById("savePageBtn").disabled = true;
|
|
} else {
|
|
overlay.classList.remove("active");
|
|
document.getElementById("savePageBtn").disabled = false;
|
|
}
|
|
}
|
|
|
|
// Show toast notification
|
|
function showToast(message, type = "info") {
|
|
const container = document.getElementById("toastContainer");
|
|
|
|
const bgClass =
|
|
{
|
|
success: "bg-success",
|
|
error: "bg-danger",
|
|
warning: "bg-warning",
|
|
info: "bg-info",
|
|
}[type] || "bg-info";
|
|
|
|
const icon =
|
|
{
|
|
success: "bi-check-circle",
|
|
error: "bi-x-circle",
|
|
warning: "bi-exclamation-triangle",
|
|
info: "bi-info-circle",
|
|
}[type] || "bi-info-circle";
|
|
|
|
const toast = document.createElement("div");
|
|
toast.className = `toast show ${bgClass} text-white`;
|
|
toast.setAttribute("role", "alert");
|
|
toast.innerHTML = `
|
|
<div class="d-flex align-items-center p-3">
|
|
<i class="bi ${icon} me-2"></i>
|
|
<div class="flex-grow-1">${escapeHtml(message)}</div>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
`;
|
|
|
|
container.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.remove();
|
|
}, 5000);
|
|
}
|
|
|
|
// Escape HTML
|
|
function escapeHtml(text) {
|
|
if (!text) return "";
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Logout function
|
|
function logout() {
|
|
window.location.href = "/admin/logout";
|
|
}
|