Files
SkyArtShop/website/admin/js/pages-new.js
Local Server 2a2a3d99e5 webupdate
2026-01-18 02:22:05 -06:00

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";
}