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

1439 lines
46 KiB
JavaScript

// Pages Management JavaScript
let pagesData = [];
let pageModal;
let quillEditor;
let aboutContentEditor;
let aboutTeamMembers = [];
let deletedTeamMemberIds = []; // Track deleted team members for database deletion
let currentMediaPicker = null;
let pagesMediaLibrary = null;
// Initialize pages media library
function initPagesMediaLibrary() {
if (typeof MediaLibrary !== "undefined" && !pagesMediaLibrary) {
pagesMediaLibrary = new MediaLibrary({
selectMode: true,
multiple: false,
onSelect: function (media) {
handleMediaSelection(media);
},
});
}
}
document.addEventListener("DOMContentLoaded", function () {
pageModal = new bootstrap.Modal(document.getElementById("pageModal"));
initializeQuillEditor();
initializeAboutEditor();
initPagesMediaLibrary();
checkAuth().then((authenticated) => {
if (authenticated) {
loadPages();
}
});
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("action") === "create") {
showCreatePage();
}
// Auto-generate slug from title
document.getElementById("pageTitle")?.addEventListener("input", function () {
if (!document.getElementById("pageId").value) {
document.getElementById("pageSlug").value = slugify(this.value);
}
});
});
function initializeQuillEditor() {
quillEditor = new Quill("#pageContentEditor", {
theme: "snow",
modules: {
toolbar: [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ font: [] }],
[{ size: ["small", false, "large", "huge"] }],
["bold", "italic", "underline", "strike"],
[{ color: [] }, { background: [] }],
[{ script: "sub" }, { script: "super" }],
[{ list: "ordered" }, { list: "bullet" }],
[{ indent: "-1" }, { indent: "+1" }],
[{ align: [] }],
["blockquote", "code-block"],
["link", "image", "video"],
["clean"],
],
},
});
// Custom image handler to use media library
const toolbar = quillEditor.getModule("toolbar");
toolbar.addHandler("image", function () {
openMediaLibraryForPageEditor();
});
}
// Open media library for main page editor image insertion
function openMediaLibraryForPageEditor() {
if (!pagesMediaLibrary) {
initPagesMediaLibrary();
}
currentMediaPicker = "pageEditorImage";
pagesMediaLibrary.show();
}
// Handle media selection for main page editor
function handlePageEditorImageSelection(media) {
if (quillEditor && media && media.path) {
const range = quillEditor.getSelection(true);
quillEditor.insertEmbed(range.index, "image", media.path);
quillEditor.setSelection(range.index + 1);
}
}
function initializeAboutEditor() {
aboutContentEditor = new Quill("#aboutContentEditor", {
theme: "snow",
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
["bold", "italic", "underline"],
[{ color: [] }, { background: [] }],
[{ list: "ordered" }, { list: "bullet" }],
[{ align: [] }],
["link", "image"],
["clean"],
],
},
placeholder: "Write your page content here...",
});
// Custom image handler to use media library
const toolbar = aboutContentEditor.getModule("toolbar");
toolbar.addHandler("image", function () {
openMediaLibraryForAboutEditor();
});
}
// Open media library for About editor image insertion
function openMediaLibraryForAboutEditor() {
if (!pagesMediaLibrary) {
initPagesMediaLibrary();
}
currentMediaPicker = "aboutEditorImage";
pagesMediaLibrary.show();
}
// Handle media selection for About editor
function handleAboutEditorImageSelection(media) {
if (aboutContentEditor && media && media.path) {
const range = aboutContentEditor.getSelection(true);
aboutContentEditor.insertEmbed(range.index, "image", media.path);
aboutContentEditor.setSelection(range.index + 1);
}
}
async function loadPages() {
try {
const response = await fetch("/api/admin/pages", {
credentials: "include",
});
const data = await response.json();
if (data.success) {
pagesData = data.pages;
renderPages(pagesData);
}
} catch (error) {
console.error("Failed to load pages:", error);
}
}
function renderPages(pages) {
const tbody = document.getElementById("pagesTableBody");
if (pages.length === 0) {
tbody.innerHTML = `
<tr><td colspan="6" class="text-center p-4">
<i class="bi bi-inbox" style="font-size: 3rem; color: #ccc;"></i>
<p class="mt-3 text-muted">No custom pages found</p>
<button class="btn btn-primary" onclick="showCreatePage()">
<i class="bi bi-plus-circle"></i> Create Your First Page
</button>
</td></tr>`;
return;
}
tbody.innerHTML = pages
.map(
(p) => `
<tr>
<td>${escapeHtml(p.id)}</td>
<td><strong>${escapeHtml(p.title)}</strong></td>
<td><code>${escapeHtml(p.slug)}</code></td>
<td><span class="badge ${
p.ispublished ? "badge-success" : "badge-warning"
}">
${p.ispublished ? "Published" : "Draft"}</span></td>
<td>${formatDate(p.createdat)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editPage('${escapeHtml(
p.id,
)}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deletePage('${escapeHtml(
p.id,
)}', '${escapeHtml(p.title).replace(/'/g, "\\'")}')">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>`,
)
.join("");
}
function filterPages() {
const searchTerm = document.getElementById("searchInput").value.toLowerCase();
const filtered = pagesData.filter(
(p) =>
p.title.toLowerCase().includes(searchTerm) ||
p.slug.toLowerCase().includes(searchTerm),
);
renderPages(filtered);
}
function showCreatePage() {
document.getElementById("modalTitle").textContent = "Create Custom Page";
document.getElementById("pageForm").reset();
document.getElementById("pageId").value = "";
document.getElementById("pagePublished").checked = true;
quillEditor.setContents([]);
// Show regular editor by default
document.getElementById("contactStructuredFields").style.display = "none";
document.getElementById("aboutWithTeamFields").style.display = "none";
document.getElementById("privacyContentSection").style.display = "none";
document.getElementById("regularContentEditor").style.display = "block";
pageModal.show();
}
async function editPage(id) {
try {
const response = await fetch(`/api/admin/pages/${id}`, {
credentials: "include",
});
const data = await response.json();
if (data.success) {
const page = data.page;
document.getElementById("modalTitle").textContent = "Edit Custom Page";
document.getElementById("pageId").value = page.id;
document.getElementById("pageTitle").value = page.title;
document.getElementById("pageSlug").value = page.slug;
// Check if this is the contact page - use structured fields
if (page.slug === "contact" || page.slug === "page-contact") {
if (page.pagedata) {
showContactStructuredFields(page.pagedata);
} else {
// Contact page without pagedata, use regular editor
document.getElementById("contactStructuredFields").style.display =
"none";
document.getElementById("aboutWithTeamFields").style.display = "none";
document.getElementById("regularContentEditor").style.display =
"block";
if (page.content) {
try {
const delta = JSON.parse(page.content);
quillEditor.setContents(delta);
} catch {
quillEditor.clipboard.dangerouslyPasteHTML(page.content);
}
} else {
quillEditor.setContents([]);
}
}
} else if (
page.slug === "about" ||
page.slug === "page-about" ||
page.slug.includes("about")
) {
// Show About page with team members
await showAboutWithTeamFields(page);
} else if (
page.slug === "privacy" ||
page.slug === "page-privacy" ||
page.slug.includes("privacy") ||
page.slug === "shipping" ||
page.slug === "shipping-info" ||
page.slug.includes("shipping") ||
page.slug === "returns" ||
page.slug.includes("return")
) {
// Show Privacy/Shipping/Returns page with structured fields
if (page.pagedata) {
showPrivacyStructuredFields(page.pagedata);
} else {
// No pagedata, use regular editor
document.getElementById("contactStructuredFields").style.display =
"none";
document.getElementById("aboutWithTeamFields").style.display = "none";
document.getElementById("privacyContentSection").style.display =
"none";
document.getElementById("regularContentEditor").style.display =
"block";
if (page.content) {
try {
const delta = JSON.parse(page.content);
quillEditor.setContents(delta);
} catch {
quillEditor.clipboard.dangerouslyPasteHTML(page.content);
}
} else {
quillEditor.setContents([]);
}
}
} else {
// Use regular Quill editor for all other pages (privacy, etc)
document.getElementById("contactStructuredFields").style.display =
"none";
document.getElementById("aboutWithTeamFields").style.display = "none";
document.getElementById("regularContentEditor").style.display = "block";
// Set Quill editor content
if (page.content) {
try {
const delta = JSON.parse(page.content);
quillEditor.setContents(delta);
} catch {
// If content is plain HTML/text, set it directly
quillEditor.clipboard.dangerouslyPasteHTML(page.content);
}
} else {
quillEditor.setContents([]);
}
}
document.getElementById("pageMetaTitle").value = page.metatitle || "";
document.getElementById("pageMetaDescription").value =
page.metadescription || "";
document.getElementById("pagePublished").checked = page.ispublished;
pageModal.show();
}
} catch (error) {
console.error("Failed to load page:", error);
showError("Failed to load page details");
}
}
function showContactStructuredFields(pagedata) {
// Hide regular editor, show structured fields
document.getElementById("regularContentEditor").style.display = "none";
document.getElementById("aboutWithTeamFields").style.display = "none";
document.getElementById("contactStructuredFields").style.display = "block";
// Populate header fields
if (pagedata.header) {
document.getElementById("contactHeaderTitle").value =
pagedata.header.title || "";
document.getElementById("contactHeaderSubtitle").value =
pagedata.header.subtitle || "";
}
// Populate contact info
if (pagedata.contactInfo) {
document.getElementById("contactPhone").value =
pagedata.contactInfo.phone || "";
document.getElementById("contactEmail").value =
pagedata.contactInfo.email || "";
document.getElementById("contactAddress").value =
pagedata.contactInfo.address || "";
}
// Populate business hours
if (pagedata.businessHours) {
renderBusinessHours(pagedata.businessHours);
}
}
function renderBusinessHours(hours) {
const container = document.getElementById("businessHoursList");
container.innerHTML = hours
.map(
(hour, index) => `
<div class="row mb-2" data-hour-index="${index}">
<div class="col-md-5">
<input type="text" class="form-control" placeholder="Days (e.g., Monday - Friday)"
value="${escapeHtml(hour.days)}" data-field="days">
</div>
<div class="col-md-5">
<input type="text" class="form-control" placeholder="Hours (e.g., 9:00 AM - 6:00 PM)"
value="${escapeHtml(hour.hours)}" data-field="hours">
</div>
<div class="col-md-2">
<button type="button" class="btn btn-sm btn-danger" onclick="removeBusinessHour(${index})">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`,
)
.join("");
}
function addBusinessHour() {
const container = document.getElementById("businessHoursList");
const index = container.children.length;
const newHour = document.createElement("div");
newHour.className = "row mb-2";
newHour.setAttribute("data-hour-index", index);
newHour.innerHTML = `
<div class="col-md-5">
<input type="text" class="form-control" placeholder="Days (e.g., Monday - Friday)" data-field="days">
</div>
<div class="col-md-5">
<input type="text" class="form-control" placeholder="Hours (e.g., 9:00 AM - 6:00 PM)" data-field="hours">
</div>
<div class="col-md-2">
<button type="button" class="btn btn-sm btn-danger" onclick="removeBusinessHour(${index})">
<i class="bi bi-trash"></i>
</button>
</div>
`;
container.appendChild(newHour);
}
function removeBusinessHour(index) {
const container = document.getElementById("businessHoursList");
const hourRow = container.querySelector(`[data-hour-index="${index}"]`);
if (hourRow) {
hourRow.remove();
}
}
// Privacy Policy Page Functions
let privacySectionEditors = []; // Array to store Quill editors for each section
function showPrivacyStructuredFields(pagedata) {
// Hide regular editor, show privacy fields
document.getElementById("regularContentEditor").style.display = "none";
document.getElementById("aboutWithTeamFields").style.display = "none";
document.getElementById("contactStructuredFields").style.display = "none";
document.getElementById("privacyContentSection").style.display = "block";
// Populate header fields
if (pagedata.header) {
document.getElementById("privacyHeaderTitle").value =
pagedata.header.title || "";
}
// Populate last updated
document.getElementById("privacyLastUpdated").value =
pagedata.lastUpdated || "";
// Populate sections
if (pagedata.sections && pagedata.sections.length > 0) {
renderPrivacySections(pagedata.sections);
} else {
// Start with one empty section
privacySectionEditors = [];
document.getElementById("privacySectionsContainer").innerHTML = "";
}
}
function renderPrivacySections(sections) {
const container = document.getElementById("privacySectionsContainer");
privacySectionEditors = []; // Reset editors array
container.innerHTML = "";
sections.forEach((section, index) => {
addPrivacySectionWithData(section, index);
});
}
function addPrivacySection() {
addPrivacySectionWithData(
{ title: "", content: "" },
privacySectionEditors.length,
);
}
function addPrivacySectionWithData(section, index) {
const container = document.getElementById("privacySectionsContainer");
const sectionDiv = document.createElement("div");
sectionDiv.className = "contact-field-group mb-4";
sectionDiv.setAttribute("data-section-index", index);
// Create unique IDs for this section's editor
const editorId = `privacySectionContent_${index}`;
sectionDiv.innerHTML = `
<div class="d-flex justify-content-between align-items-center mb-3">
<h6><i class="bi bi-file-text"></i> Section ${index + 1}</h6>
<button type="button" class="btn btn-sm btn-danger" onclick="removePrivacySection(${index})">
<i class="bi bi-trash"></i> Remove
</button>
</div>
<div class="mb-3">
<label class="form-label">Section Title</label>
<input type="text" class="form-control"
value="${escapeHtml(section.title || "")}"
data-field="title"
placeholder="e.g., Information We Collect">
</div>
<div class="mb-3">
<label class="form-label">Content</label>
<div class="editor-wrapper">
<div id="${editorId}" style="min-height: 200px;"></div>
</div>
</div>
`;
container.appendChild(sectionDiv);
// Initialize Quill editor for this section's content
const editor = new Quill(`#${editorId}`, {
theme: "snow",
modules: {
toolbar: [
[{ header: [2, 3, false] }],
["bold", "italic", "underline"],
[{ list: "ordered" }, { list: "bullet" }],
["link"],
["clean"],
],
},
});
// Set the content if provided
if (section.content) {
try {
const delta = JSON.parse(section.content);
editor.setContents(delta);
} catch {
// If it's plain text/HTML, set it directly
editor.clipboard.dangerouslyPasteHTML(section.content);
}
}
privacySectionEditors[index] = editor;
}
function removePrivacySection(index) {
const container = document.getElementById("privacySectionsContainer");
const sectionDiv = container.querySelector(`[data-section-index="${index}"]`);
if (sectionDiv) {
sectionDiv.remove();
// Remove editor from array (set to null to keep indices)
privacySectionEditors[index] = null;
}
}
// About Page with Team Members Functions
async function showAboutWithTeamFields(page) {
// Hide other editors
document.getElementById("contactStructuredFields").style.display = "none";
document.getElementById("regularContentEditor").style.display = "none";
document.getElementById("aboutWithTeamFields").style.display = "block";
// Set About content
if (page.content) {
try {
const delta = JSON.parse(page.content);
aboutContentEditor.setContents(delta);
} catch {
aboutContentEditor.clipboard.dangerouslyPasteHTML(page.content);
}
} else {
aboutContentEditor.setContents([]);
}
// Load team members from database
await loadTeamMembersForAbout();
}
async function loadTeamMembersForAbout() {
try {
// Reset the deleted IDs when loading fresh data
deletedTeamMemberIds = [];
const response = await fetch("/api/admin/team-members", {
credentials: "include",
});
const data = await response.json();
if (data.success && data.teamMembers) {
aboutTeamMembers = data.teamMembers;
displayTeamMembersInEditor();
}
} catch (error) {
console.error("Error loading team members:", error);
}
}
function displayTeamMembersInEditor() {
const container = document.getElementById("teamMembersList");
if (aboutTeamMembers.length === 0) {
container.innerHTML = `
<div class="col-12 text-center text-muted py-3">
<i class="bi bi-people" style="font-size: 3rem;"></i>
<p class="mt-2">No team members yet. Click "Add Member" to get started.</p>
</div>
`;
return;
}
container.innerHTML = aboutTeamMembers
.map(
(member, index) => `
<div class="col-md-6 col-lg-4">
<div class="team-member-card">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="team-member-handle">
<i class="bi bi-grip-vertical"></i>
</span>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeTeamMemberFromAbout(${index})">
<i class="bi bi-trash"></i>
</button>
</div>
<div class="team-member-preview">
${
member.image_url
? `<img src="${member.image_url}" alt="${escapeHtml(
member.name,
)}" />`
: `<i class="bi bi-person-circle"></i>`
}
</div>
<div class="mb-2">
<input type="text" class="form-control form-control-sm mb-2"
value="${escapeHtml(member.name)}"
placeholder="Name"
onchange="updateTeamMember(${index}, 'name', this.value)">
</div>
<div class="mb-2">
<input type="text" class="form-control form-control-sm mb-2"
value="${escapeHtml(member.position)}"
placeholder="Position"
onchange="updateTeamMember(${index}, 'position', this.value)">
</div>
<div class="mb-2">
<textarea class="form-control form-control-sm mb-2"
rows="2"
placeholder="Bio"
onchange="updateTeamMember(${index}, 'bio', this.value)">${escapeHtml(
member.bio || "",
)}</textarea>
</div>
<div class="mb-2">
<div class="input-group input-group-sm">
<input type="text" class="form-control"
value="${member.image_url || ""}"
placeholder="Image URL"
onchange="updateTeamMember(${index}, 'image_url', this.value)">
<button type="button" class="btn btn-outline-secondary" onclick="selectImageForMember(${index})">
<i class="bi bi-image"></i>
</button>
</div>
</div>
<div class="mb-2">
<input type="number" class="form-control form-control-sm"
value="${member.display_order || 0}"
placeholder="Order"
onchange="updateTeamMember(${index}, 'display_order', this.value)">
</div>
</div>
</div>
`,
)
.join("");
}
function addTeamMember() {
const newMember = {
id: null,
name: "",
position: "",
bio: "",
image_url: "",
display_order: aboutTeamMembers.length,
};
aboutTeamMembers.push(newMember);
displayTeamMembersInEditor();
}
function updateTeamMember(index, field, value) {
if (aboutTeamMembers[index]) {
aboutTeamMembers[index][field] = value;
}
}
function removeTeamMemberFromAbout(index) {
const member = aboutTeamMembers[index];
// If member has an ID, track it for deletion from database
if (member && member.id) {
deletedTeamMemberIds.push(member.id);
}
aboutTeamMembers.splice(index, 1);
displayTeamMembersInEditor();
}
function selectImageForMember(index) {
currentMediaPicker = { purpose: "teamMember", index };
// Initialize if not already
initPagesMediaLibrary();
if (pagesMediaLibrary) {
pagesMediaLibrary.open();
}
}
function handleMediaSelection(media) {
if (!currentMediaPicker) return;
// Handle About editor image insertion
if (currentMediaPicker === "aboutEditorImage") {
handleAboutEditorImageSelection(media);
currentMediaPicker = null;
return;
}
// Handle main page editor image insertion
if (currentMediaPicker === "pageEditorImage") {
handlePageEditorImageSelection(media);
currentMediaPicker = null;
return;
}
if (currentMediaPicker.purpose === "teamMember") {
const index = currentMediaPicker.index;
if (aboutTeamMembers[index]) {
aboutTeamMembers[index].image_url = media.path;
displayTeamMembersInEditor();
}
}
currentMediaPicker = null;
}
async function saveTeamMembers() {
try {
// First, delete any removed team members from the database
for (const memberId of deletedTeamMemberIds) {
try {
await fetch(`/api/admin/team-members/${memberId}`, {
method: "DELETE",
credentials: "include",
});
console.log(`Deleted team member ${memberId}`);
} catch (err) {
console.error(`Failed to delete team member ${memberId}:`, err);
}
}
// Clear the deleted IDs array after processing
deletedTeamMemberIds = [];
// Save or update each team member
for (const member of aboutTeamMembers) {
if (!member.name || !member.position) continue; // Skip incomplete members
const payload = {
name: member.name,
position: member.position,
bio: member.bio || "",
image_url: member.image_url || "",
display_order: parseInt(member.display_order) || 0,
};
if (member.id) {
// Update existing
await fetch(`/api/admin/team-members/${member.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(payload),
});
} else {
// Create new
const response = await fetch("/api/admin/team-members", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(payload),
});
const data = await response.json();
if (data.success && data.teamMember) {
member.id = data.teamMember.id; // Update with new ID
}
}
}
console.log("Team members saved successfully");
} catch (error) {
console.error("Error saving team members:", error);
}
}
async function savePage() {
const id = document.getElementById("pageId").value;
const slug = document.getElementById("pageSlug").value;
let formData = {
title: document.getElementById("pageTitle").value,
slug: slug,
metatitle: document.getElementById("pageMetaTitle").value,
metadescription: document.getElementById("pageMetaDescription").value,
ispublished: document.getElementById("pagePublished").checked,
};
// Check if this is About page with team members
if (
(slug === "about" || slug === "page-about") &&
document.getElementById("aboutWithTeamFields").style.display !== "none"
) {
// Get About content from editor
const contentDelta = aboutContentEditor.getContents();
formData.content = JSON.stringify(contentDelta);
// Save team members separately
await saveTeamMembers();
}
// Check if this is contact page with structured fields visible
else if (
slug === "contact" &&
document.getElementById("contactStructuredFields").style.display !== "none"
) {
// Collect structured data
const pagedata = {
header: {
title: document.getElementById("contactHeaderTitle").value,
subtitle: document.getElementById("contactHeaderSubtitle").value,
},
contactInfo: {
phone: document.getElementById("contactPhone").value,
email: document.getElementById("contactEmail").value,
address: document.getElementById("contactAddress").value,
},
businessHours: [],
};
// Collect business hours
const hourRows = document.getElementById("businessHoursList").children;
for (let row of hourRows) {
const days = row.querySelector('[data-field="days"]').value;
const hours = row.querySelector('[data-field="hours"]').value;
if (days && hours) {
pagedata.businessHours.push({ days, hours });
}
}
// Generate HTML from structured data
const generatedHTML = generateContactHTML(pagedata);
formData.pagedata = pagedata;
formData.content = generatedHTML; // Store HTML in content field
formData.contenthtml = generatedHTML; // Also in contenthtml
}
// Check if this is privacy/shipping/returns page with structured fields
else if (
(slug.includes("privacy") ||
slug.includes("shipping") ||
slug.includes("return")) &&
document.getElementById("privacyContentSection").style.display !== "none"
) {
// Collect structured privacy data
const pagedata = {
header: {
title: document.getElementById("privacyHeaderTitle").value,
},
lastUpdated: document.getElementById("privacyLastUpdated").value,
sections: [],
};
// Collect sections
const sectionDivs = document.getElementById(
"privacySectionsContainer",
).children;
for (let i = 0; i < sectionDivs.length; i++) {
const sectionDiv = sectionDivs[i];
const title = sectionDiv.querySelector('[data-field="title"]').value;
const editor = privacySectionEditors[i];
if (editor && title) {
// Get content as Delta JSON
const contentDelta = editor.getContents();
const contentHTML = editor.root.innerHTML;
pagedata.sections.push({
title: title,
content: JSON.stringify(contentDelta), // Store as Delta
contentHTML: contentHTML, // Also store rendered HTML
});
}
}
// Store the structured data
formData.pagedata = pagedata;
// Also generate and store as content for backwards compatibility
const generatedHTML = generatePrivacyHTML(pagedata);
formData.content = generatedHTML;
formData.contenthtml = generatedHTML;
} else {
// Use Quill editor content for other pages
const contentDelta = quillEditor.getContents();
const contentHTML = quillEditor.root.innerHTML;
formData.content = JSON.stringify(contentDelta); // Store as Delta for editing
formData.contenthtml = contentHTML; // Store rendered HTML for display
if (contentDelta.ops.length === 0) {
showError("Please fill in all required fields");
return;
}
}
if (!formData.title || !formData.slug) {
showError("Please fill in all required fields");
return;
}
try {
const url = id ? `/api/admin/pages/${id}` : "/api/admin/pages";
const method = id ? "PUT" : "POST";
const response = await fetch(url, {
method: method,
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(formData),
});
const data = await response.json();
if (data.success) {
showSuccess(
id ? "Page updated successfully" : "Page created successfully",
);
pageModal.hide();
loadPages();
} else {
showError(data.message || "Failed to save page");
}
} catch (error) {
console.error("Failed to save page:", error);
showError("Failed to save page");
}
}
function generateContactHTML(pagedata) {
const { header, contactInfo, businessHours } = pagedata;
// Generate business hours HTML
const businessHoursHTML = businessHours
.map(
(hour) => `
<div>
<p style="font-weight: 600; margin-bottom: 8px;">${escapeHtml(
hour.days,
)}</p>
<p style="opacity: 0.95; margin: 0;">${escapeHtml(hour.hours)}</p>
</div>
`,
)
.join("");
return `
<div style="text-align: center; margin-bottom: 48px;">
<h2 style="font-size: 2rem; font-weight: 700; color: #2d3436; margin-bottom: 12px;">
${escapeHtml(header.title)}
</h2>
<p style="font-size: 1rem; color: #636e72">
${escapeHtml(header.subtitle)}
</p>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px; margin-bottom: 48px;">
<!-- Phone Card -->
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 32px; border-radius: 16px; text-align: center; color: white; box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);">
<div style="font-size: 48px; margin-bottom: 16px;">
<i class="bi bi-telephone-fill"></i>
</div>
<h3 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Phone</h3>
<p style="font-size: 1rem; opacity: 0.9; margin: 0;">${escapeHtml(
contactInfo.phone,
)}</p>
</div>
<!-- Email Card -->
<div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); padding: 32px; border-radius: 16px; text-align: center; color: white; box-shadow: 0 8px 24px rgba(240, 147, 251, 0.3);">
<div style="font-size: 48px; margin-bottom: 16px;">
<i class="bi bi-envelope-fill"></i>
</div>
<h3 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Email</h3>
<p style="font-size: 1rem; opacity: 0.9; margin: 0;">${escapeHtml(
contactInfo.email,
)}</p>
</div>
<!-- Location Card -->
<div style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); padding: 32px; border-radius: 16px; text-align: center; color: white; box-shadow: 0 8px 24px rgba(79, 172, 254, 0.3);">
<div style="font-size: 48px; margin-bottom: 16px;">
<i class="bi bi-geo-alt-fill"></i>
</div>
<h3 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Location</h3>
<p style="font-size: 1rem; opacity: 0.9; margin: 0;">${escapeHtml(
contactInfo.address,
)}</p>
</div>
</div>
<!-- Business Hours -->
<div style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); padding: 40px; border-radius: 16px; text-align: center; color: white; box-shadow: 0 8px 24px rgba(250, 112, 154, 0.3);">
<h3 style="font-size: 1.5rem; font-weight: 700; margin-bottom: 24px;">Business Hours</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; max-width: 800px; margin: 0 auto;">
${businessHoursHTML}
</div>
</div>
`;
}
function generatePrivacyHTML(pagedata) {
const { header, lastUpdated, sections } = pagedata;
// Generate sections HTML
const sectionsHTML = sections
.map((section) => {
// Parse the content Delta if it's stored as JSON
let contentHTML = section.contentHTML;
if (!contentHTML && section.content) {
try {
// If we don't have contentHTML, try to get it from the content field
contentHTML = section.content;
} catch {
contentHTML = section.content;
}
}
return `
<h2>${escapeHtml(section.title)}</h2>
<div class="section-content">${contentHTML}</div>
`;
})
.join("");
return `
<div style="max-width: 900px; margin: 0 auto;">
${lastUpdated ? `<p class="policy-meta" style="color: #636e72; font-style: italic; margin-bottom: 24px;">Last updated: ${escapeHtml(lastUpdated)}</p>` : ""}
${sectionsHTML}
</div>
`;
}
async function deletePage(id, title) {
// Show custom confirmation modal instead of browser confirm
showConfirmation(
`Are you sure you want to delete "<strong>${escapeHtml(
title,
)}</strong>"?<br><br>` +
`<small class="text-muted">This action cannot be undone.</small>`,
async () => {
try {
const response = await fetch(`/api/admin/pages/${id}`, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
if (data.success) {
showSuccess("Page deleted successfully");
loadPages();
} else {
showError(data.message || "Failed to delete page");
}
} catch (error) {
console.error("Failed to delete page:", error);
showError("Failed to delete page");
}
},
);
}
function slugify(text) {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/[\s_-]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function escapeHtml(text) {
const map = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
function formatDate(dateString) {
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
}
function showSuccess(message) {
showNotification(message, "success");
}
function showError(message) {
showNotification(message, "error");
}
function showNotification(message, type) {
const modal = new bootstrap.Modal(
document.getElementById("notificationModal"),
);
const modalContent = document.getElementById("notificationModalContent");
const modalHeader = document.getElementById("notificationModalHeader");
const modalIcon = document.getElementById("notificationModalIcon");
const modalTitleText = document.getElementById("notificationModalTitleText");
const modalBody = document.getElementById("notificationModalBody");
// Configure based on type
if (type === "success") {
modalContent.classList.remove("border-danger");
modalContent.classList.add("border-success");
modalContent.style.borderWidth = "3px";
modalHeader.classList.remove("bg-danger", "text-white");
modalHeader.classList.add("bg-success", "text-white");
modalIcon.className = "bi bi-check-circle-fill me-2";
modalTitleText.textContent = "Success";
modalBody.innerHTML = `<p class="mb-0"><i class="bi bi-check-circle text-success me-2"></i>${escapeHtml(
message,
)}</p>`;
} else {
modalContent.classList.remove("border-success");
modalContent.classList.add("border-danger");
modalContent.style.borderWidth = "3px";
modalHeader.classList.remove("bg-success", "text-white");
modalHeader.classList.add("bg-danger", "text-white");
modalIcon.className = "bi bi-exclamation-triangle-fill me-2";
modalTitleText.textContent = "Error";
modalBody.innerHTML = `<p class="mb-0"><i class="bi bi-x-circle text-danger me-2"></i>${escapeHtml(
message,
)}</p>`;
}
modal.show();
}
function showConfirmation(message, onConfirm) {
const modal = new bootstrap.Modal(document.getElementById("confirmModal"));
const modalBody = document.getElementById("confirmModalBody");
const confirmButton = document.getElementById("confirmModalButton");
// Set message
modalBody.innerHTML = `<p class="mb-0">${message}</p>`;
// Remove old event listeners by cloning button
const newConfirmButton = confirmButton.cloneNode(true);
confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
// Add new click handler
newConfirmButton.addEventListener("click", () => {
modal.hide();
onConfirm();
});
modal.show();
}
// Modal Drag and Resize Functionality - DISABLED
// function initializeModalDragResize() {
// const modal = document.getElementById("pageModal");
// const modalDialog = modal.querySelector(".modal-dialog");
// const modalContent = modal.querySelector(".modal-content");
// const modalHeader = modal.querySelector(".modal-header");
// const resizeHandle = modal.querySelector(".modal-resize-handle");
//
// let isDragging = false;
// let isResizing = false;
// let startX, startY, startWidth, startHeight, startLeft, startTop;
//
// // Dragging functionality
// modalHeader.addEventListener("mousedown", function (e) {
// // Don't drag if clicking on buttons
// if (
// e.target.classList.contains("btn-close") ||
// e.target.classList.contains("btn-fullscreen") ||
// e.target.closest(".btn-close") ||
// e.target.closest(".btn-fullscreen")
// ) {
// return;
// }
//
// isDragging = true;
// const rect = modalDialog.getBoundingClientRect();
// startX = e.clientX - rect.left;
// startY = e.clientY - rect.top;
// modalDialog.style.margin = "0";
// modalDialog.style.position = "fixed";
// e.preventDefault();
// });
//
// document.addEventListener("mousemove", function (e) {
// if (isDragging) {
// const newLeft = e.clientX - startX;
// const newTop = e.clientY - startY;
//
// // Keep within viewport bounds
// const maxLeft = window.innerWidth - modalDialog.offsetWidth;
// const maxTop = window.innerHeight - modalDialog.offsetHeight;
//
// modalDialog.style.left = Math.max(0, Math.min(newLeft, maxLeft)) + "px";
// modalDialog.style.top = Math.max(0, Math.min(newTop, maxTop)) + "px";
// }
//
// if (isResizing) {
// const newWidth = startWidth + (e.clientX - startX);
// const newHeight = startHeight + (e.clientY - startY);
//
// // Set minimum and maximum sizes
// const minWidth = 400;
// const maxWidth = window.innerWidth - 40;
// const minHeight = 300;
// const maxHeight = window.innerHeight - 40;
//
// modalDialog.style.maxWidth =
// Math.max(minWidth, Math.min(newWidth, maxWidth)) + "px";
// modalContent.style.maxHeight =
// Math.max(minHeight, Math.min(newHeight, maxHeight)) + "px";
// modalContent.style.height = modalContent.style.maxHeight;
//
// // Adjust body height
// const bodyMaxHeight = parseInt(modalContent.style.maxHeight) - 140 + "px";
// modal.querySelector(".modal-body").style.maxHeight = bodyMaxHeight;
// }
// });
//
// document.addEventListener("mouseup", function () {
// isDragging = false;
// isResizing = false;
// });
//
// // Resizing functionality
// if (resizeHandle) {
// resizeHandle.addEventListener("mousedown", function (e) {
// isResizing = true;
// const rect = modalDialog.getBoundingClientRect();
// startX = e.clientX;
// startY = e.clientY;
// startWidth = rect.width;
// startHeight = modalContent.offsetHeight;
// e.preventDefault();
// e.stopPropagation();
// });
// }
//
// // Reset position when modal is closed
// modal.addEventListener("hidden.bs.modal", function () {
// modalDialog.style.position = "";
// modalDialog.style.left = "";
// modalDialog.style.top = "";
// modalDialog.style.margin = "1.75rem auto";
// });
// }
// Toggle Fullscreen
function toggleFullscreen() {
const modal = document.getElementById("pageModal");
const icon = document.getElementById("fullscreenIcon");
if (modal.classList.contains("modal-fullscreen")) {
modal.classList.remove("modal-fullscreen");
icon.classList.remove("bi-fullscreen-exit");
icon.classList.add("bi-arrows-fullscreen");
} else {
modal.classList.add("modal-fullscreen");
icon.classList.remove("bi-arrows-fullscreen");
icon.classList.add("bi-fullscreen-exit");
}
}
// Toggle Content Expand/Collapse
function toggleContentExpand(editorType) {
if (editorType === "quillEditor") {
const container = document.querySelector(".ql-container");
const icon = document.getElementById("quillExpandIcon");
if (container.classList.contains("expanded")) {
container.classList.remove("expanded");
icon.classList.remove("bi-arrows-collapse");
icon.classList.add("bi-arrows-expand");
} else {
container.classList.add("expanded");
icon.classList.remove("bi-arrows-expand");
icon.classList.add("bi-arrows-collapse");
}
} else if (editorType === "contactStructuredFields") {
const container = document.getElementById("contactStructuredFields");
const icon = document.getElementById("contactExpandIcon");
if (container.classList.contains("expanded")) {
container.classList.remove("expanded");
icon.classList.remove("bi-arrows-collapse");
icon.classList.add("bi-arrows-expand");
} else {
container.classList.add("expanded");
icon.classList.remove("bi-arrows-expand");
icon.classList.add("bi-arrows-collapse");
}
}
}
// Simple Drag-to-Resize for Editor Content
(function () {
let resizeState = null;
document.addEventListener("mousedown", function (e) {
if (e.target.classList.contains("editor-resize-handle")) {
e.preventDefault();
e.stopPropagation();
const targetId = e.target.getAttribute("data-target");
const targetElement = document.getElementById(targetId);
console.log(
"Resize handle clicked! Target:",
targetId,
"Element found:",
!!targetElement,
);
if (!targetElement) {
console.error("Target element not found:", targetId);
return;
}
resizeState = {
target: targetElement,
handle: e.target,
startY: e.clientY,
startHeight: targetElement.offsetHeight,
};
console.log("Starting resize from height:", resizeState.startHeight);
document.body.style.cursor = "nwse-resize";
document.body.style.userSelect = "none";
e.target.style.pointerEvents = "none";
}
});
document.addEventListener("mousemove", function (e) {
if (resizeState) {
e.preventDefault();
e.stopPropagation();
const deltaY = e.clientY - resizeState.startY;
const newHeight = Math.max(
200,
Math.min(1200, resizeState.startHeight + deltaY),
);
// Update target element height
resizeState.target.style.height = newHeight + "px";
// For Quill editor (pageContentEditor), update the internal structure
if (resizeState.target.id === "pageContentEditor") {
// The target div BECOMES .ql-container when Quill initializes
const editor = resizeState.target.querySelector(".ql-editor");
// Toolbar is a SIBLING, not a child - look in parent
const parent = resizeState.target.parentElement;
const toolbar = parent ? parent.querySelector(".ql-toolbar") : null;
console.log(
"pageContentEditor resize - editor:",
!!editor,
"toolbar:",
!!toolbar,
);
if (editor && toolbar) {
const toolbarHeight = toolbar.offsetHeight;
const editorHeight = newHeight - toolbarHeight;
console.log(
"Setting heights - toolbar:",
toolbarHeight,
"editor:",
editorHeight,
"total:",
newHeight,
);
resizeState.target.style.height = editorHeight + "px";
editor.style.height = editorHeight + "px";
} else if (editor) {
// Fallback: just resize the editor if no toolbar found
console.log("Toolbar not found, resizing editor directly");
editor.style.height = newHeight + "px";
}
}
// For About editor (aboutContentEditor)
if (resizeState.target.id === "aboutContentEditor") {
// The target div BECOMES .ql-container when Quill initializes
const editor = resizeState.target.querySelector(".ql-editor");
// Toolbar is a SIBLING, not a child - look in parent
const parent = resizeState.target.parentElement;
const toolbar = parent ? parent.querySelector(".ql-toolbar") : null;
console.log(
"aboutContentEditor resize - editor:",
!!editor,
"toolbar:",
!!toolbar,
);
if (editor && toolbar) {
const toolbarHeight = toolbar.offsetHeight;
const editorHeight = newHeight - toolbarHeight;
console.log(
"Setting heights - toolbar:",
toolbarHeight,
"editor:",
editorHeight,
"total:",
newHeight,
);
resizeState.target.style.height = editorHeight + "px";
editor.style.height = editorHeight + "px";
} else if (editor) {
// Fallback: just resize the editor if no toolbar found
console.log("Toolbar not found, resizing editor directly");
editor.style.height = newHeight + "px";
}
}
}
});
document.addEventListener("mouseup", function () {
if (resizeState) {
resizeState.handle.style.pointerEvents = "";
resizeState = null;
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
});
})();