webupdate

This commit is contained in:
Local Server
2026-01-18 02:22:05 -06:00
parent 6fc159051a
commit 2a2a3d99e5
135 changed files with 54897 additions and 9825 deletions

View File

@@ -5,12 +5,29 @@ 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();
@@ -30,13 +47,6 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
// Media Library Selection Handler
window.addEventListener("message", function (event) {
if (event.data.type === "mediaSelected" && currentMediaPicker) {
handleMediaSelection(event.data.media);
}
});
function initializeQuillEditor() {
quillEditor = new Quill("#pageContentEditor", {
theme: "snow",
@@ -57,6 +67,31 @@ function initializeQuillEditor() {
],
},
});
// 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() {
@@ -75,6 +110,31 @@ function initializeAboutEditor() {
},
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() {
@@ -120,17 +180,17 @@ function renderPages(pages) {
<td>${formatDate(p.createdat)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editPage('${escapeHtml(
p.id
p.id,
)}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deletePage('${escapeHtml(
p.id
p.id,
)}', '${escapeHtml(p.title).replace(/'/g, "\\'")}')">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>`
</tr>`,
)
.join("");
}
@@ -140,7 +200,7 @@ function filterPages() {
const filtered = pagesData.filter(
(p) =>
p.title.toLowerCase().includes(searchTerm) ||
p.slug.toLowerCase().includes(searchTerm)
p.slug.toLowerCase().includes(searchTerm),
);
renderPages(filtered);
}
@@ -155,6 +215,7 @@ function showCreatePage() {
// 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();
@@ -203,6 +264,40 @@ async function editPage(id) {
) {
// 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 =
@@ -286,7 +381,7 @@ function renderBusinessHours(hours) {
</button>
</div>
</div>
`
`,
)
.join("");
}
@@ -321,6 +416,124 @@ function removeBusinessHour(index) {
}
}
// 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
@@ -346,7 +559,12 @@ async function showAboutWithTeamFields(page) {
async function loadTeamMembersForAbout() {
try {
const response = await fetch("/api/admin/team-members");
// 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;
@@ -387,7 +605,7 @@ function displayTeamMembersInEditor() {
${
member.image_url
? `<img src="${member.image_url}" alt="${escapeHtml(
member.name
member.name,
)}" />`
: `<i class="bi bi-person-circle"></i>`
}
@@ -409,8 +627,8 @@ function displayTeamMembersInEditor() {
rows="2"
placeholder="Bio"
onchange="updateTeamMember(${index}, 'bio', this.value)">${escapeHtml(
member.bio || ""
)}</textarea>
member.bio || "",
)}</textarea>
</div>
<div class="mb-2">
<div class="input-group input-group-sm">
@@ -431,7 +649,7 @@ function displayTeamMembersInEditor() {
</div>
</div>
</div>
`
`,
)
.join("");
}
@@ -456,117 +674,71 @@ function updateTeamMember(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 };
openMediaLibraryModal();
}
function openMediaLibraryModal() {
// Create modal backdrop
const backdrop = document.createElement("div");
backdrop.id = "mediaLibraryBackdrop";
backdrop.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
`;
// Initialize if not already
initPagesMediaLibrary();
// Create modal container
const modal = document.createElement("div");
modal.id = "mediaLibraryModal";
modal.style.cssText = `
position: relative;
width: 90%;
max-width: 1200px;
height: 85vh;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
`;
// Create close button
const closeBtn = document.createElement("button");
closeBtn.innerHTML = '<i class="bi bi-x-lg"></i>';
closeBtn.style.cssText = `
position: absolute;
top: 15px;
right: 15px;
z-index: 10000;
background: #dc3545;
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
`;
closeBtn.onclick = closeMediaLibraryModal;
// Create iframe
const iframe = document.createElement("iframe");
iframe.id = "mediaLibraryFrame";
iframe.src = "/admin/media-library.html?selectMode=true";
iframe.style.cssText = `
width: 100%;
height: 100%;
border: none;
`;
modal.appendChild(closeBtn);
modal.appendChild(iframe);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
// Close on backdrop click
backdrop.onclick = function (e) {
if (e.target === backdrop) {
closeMediaLibraryModal();
}
};
}
function closeMediaLibraryModal() {
const backdrop = document.getElementById("mediaLibraryBackdrop");
if (backdrop) {
backdrop.remove();
if (pagesMediaLibrary) {
pagesMediaLibrary.open();
}
currentMediaPicker = null;
}
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]) {
// Media is an array, get the first item's URL
const selectedMedia = Array.isArray(media) ? media[0] : media;
aboutTeamMembers[index].image_url = selectedMedia.url;
aboutTeamMembers[index].image_url = media.path;
displayTeamMembersInEditor();
}
}
closeMediaLibraryModal();
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
@@ -584,6 +756,7 @@ async function saveTeamMembers() {
await fetch(`/api/admin/team-members/${member.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(payload),
});
} else {
@@ -591,6 +764,7 @@ async function saveTeamMembers() {
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();
@@ -599,6 +773,7 @@ async function saveTeamMembers() {
}
}
}
console.log("Team members saved successfully");
} catch (error) {
console.error("Error saving team members:", error);
}
@@ -663,6 +838,52 @@ async function savePage() {
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();
@@ -695,7 +916,7 @@ async function savePage() {
const data = await response.json();
if (data.success) {
showSuccess(
id ? "Page updated successfully" : "Page created successfully"
id ? "Page updated successfully" : "Page created successfully",
);
pageModal.hide();
loadPages();
@@ -717,11 +938,11 @@ function generateContactHTML(pagedata) {
(hour) => `
<div>
<p style="font-weight: 600; margin-bottom: 8px;">${escapeHtml(
hour.days
hour.days,
)}</p>
<p style="opacity: 0.95; margin: 0;">${escapeHtml(hour.hours)}</p>
</div>
`
`,
)
.join("");
@@ -743,7 +964,7 @@ function generateContactHTML(pagedata) {
</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
contactInfo.phone,
)}</p>
</div>
@@ -754,7 +975,7 @@ function generateContactHTML(pagedata) {
</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
contactInfo.email,
)}</p>
</div>
@@ -765,7 +986,7 @@ function generateContactHTML(pagedata) {
</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
contactInfo.address,
)}</p>
</div>
</div>
@@ -780,11 +1001,43 @@ function generateContactHTML(pagedata) {
`;
}
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
title,
)}</strong>"?<br><br>` +
`<small class="text-muted">This action cannot be undone.</small>`,
async () => {
@@ -804,7 +1057,7 @@ async function deletePage(id, title) {
console.error("Failed to delete page:", error);
showError("Failed to delete page");
}
}
},
);
}
@@ -845,7 +1098,7 @@ function showError(message) {
function showNotification(message, type) {
const modal = new bootstrap.Modal(
document.getElementById("notificationModal")
document.getElementById("notificationModal"),
);
const modalContent = document.getElementById("notificationModalContent");
const modalHeader = document.getElementById("notificationModalHeader");
@@ -863,7 +1116,7 @@ function showNotification(message, type) {
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
message,
)}</p>`;
} else {
modalContent.classList.remove("border-success");
@@ -874,7 +1127,7 @@ function showNotification(message, type) {
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
message,
)}</p>`;
}
@@ -1061,7 +1314,7 @@ function toggleContentExpand(editorType) {
"Resize handle clicked! Target:",
targetId,
"Element found:",
!!targetElement
!!targetElement,
);
if (!targetElement) {
@@ -1092,7 +1345,7 @@ function toggleContentExpand(editorType) {
const deltaY = e.clientY - resizeState.startY;
const newHeight = Math.max(
200,
Math.min(1200, resizeState.startHeight + deltaY)
Math.min(1200, resizeState.startHeight + deltaY),
);
// Update target element height
@@ -1110,7 +1363,7 @@ function toggleContentExpand(editorType) {
"pageContentEditor resize - editor:",
!!editor,
"toolbar:",
!!toolbar
!!toolbar,
);
if (editor && toolbar) {
@@ -1123,7 +1376,7 @@ function toggleContentExpand(editorType) {
"editor:",
editorHeight,
"total:",
newHeight
newHeight,
);
resizeState.target.style.height = editorHeight + "px";
@@ -1147,7 +1400,7 @@ function toggleContentExpand(editorType) {
"aboutContentEditor resize - editor:",
!!editor,
"toolbar:",
!!toolbar
!!toolbar,
);
if (editor && toolbar) {
@@ -1160,7 +1413,7 @@ function toggleContentExpand(editorType) {
"editor:",
editorHeight,
"total:",
newHeight
newHeight,
);
resizeState.target.style.height = editorHeight + "px";