webupdate
This commit is contained in:
329
website/admin/js/admin-utils.js
Normal file
329
website/admin/js/admin-utils.js
Normal file
@@ -0,0 +1,329 @@
|
||||
// =====================================================
|
||||
// Admin Utilities - Shared Functions for Admin Panel
|
||||
// =====================================================
|
||||
|
||||
/**
|
||||
* Show a custom confirmation dialog instead of browser confirm()
|
||||
* @param {string} message - The confirmation message
|
||||
* @param {Function} onConfirm - Callback when confirmed
|
||||
* @param {Object} options - Optional configuration
|
||||
*/
|
||||
function showDeleteConfirm(message, onConfirm, options = {}) {
|
||||
const {
|
||||
title = "Confirm Delete",
|
||||
confirmText = "Delete",
|
||||
cancelText = "Cancel",
|
||||
type = "danger",
|
||||
} = options;
|
||||
|
||||
// Check if modal already exists, if not create it
|
||||
let modal = document.getElementById("adminConfirmModal");
|
||||
if (!modal) {
|
||||
modal = document.createElement("div");
|
||||
modal.id = "adminConfirmModal";
|
||||
modal.className = "admin-confirm-modal";
|
||||
modal.innerHTML = `
|
||||
<div class="admin-confirm-overlay"></div>
|
||||
<div class="admin-confirm-dialog">
|
||||
<div class="admin-confirm-header">
|
||||
<div class="admin-confirm-icon ${type}">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
</div>
|
||||
<h3 class="admin-confirm-title">${title}</h3>
|
||||
</div>
|
||||
<div class="admin-confirm-body">
|
||||
<p class="admin-confirm-message">${message}</p>
|
||||
</div>
|
||||
<div class="admin-confirm-footer">
|
||||
<button type="button" class="admin-confirm-btn cancel">${cancelText}</button>
|
||||
<button type="button" class="admin-confirm-btn confirm ${type}">${confirmText}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Add styles if not already present
|
||||
if (!document.getElementById("adminConfirmStyles")) {
|
||||
const styles = document.createElement("style");
|
||||
styles.id = "adminConfirmStyles";
|
||||
styles.textContent = `
|
||||
.admin-confirm-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.admin-confirm-modal.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
.admin-confirm-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.admin-confirm-dialog {
|
||||
position: relative;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
transition: transform 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.admin-confirm-modal.show .admin-confirm-dialog {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
.admin-confirm-header {
|
||||
padding: 24px 24px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.admin-confirm-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 16px;
|
||||
font-size: 28px;
|
||||
}
|
||||
.admin-confirm-icon.danger {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
.admin-confirm-icon.warning {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
.admin-confirm-icon.info {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
}
|
||||
.admin-confirm-title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
.admin-confirm-body {
|
||||
padding: 0 24px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.admin-confirm-message {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.admin-confirm-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px 24px 24px;
|
||||
justify-content: center;
|
||||
}
|
||||
.admin-confirm-btn {
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
.admin-confirm-btn.cancel {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
.admin-confirm-btn.cancel:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
.admin-confirm-btn.confirm {
|
||||
color: white;
|
||||
}
|
||||
.admin-confirm-btn.confirm.danger {
|
||||
background: #dc2626;
|
||||
}
|
||||
.admin-confirm-btn.confirm.danger:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
.admin-confirm-btn.confirm.warning {
|
||||
background: #d97706;
|
||||
}
|
||||
.admin-confirm-btn.confirm.warning:hover {
|
||||
background: #b45309;
|
||||
}
|
||||
.admin-confirm-btn.confirm.info {
|
||||
background: #2563eb;
|
||||
}
|
||||
.admin-confirm-btn.confirm.info:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styles);
|
||||
}
|
||||
} else {
|
||||
// Update existing modal content
|
||||
modal.querySelector(
|
||||
".admin-confirm-icon"
|
||||
).className = `admin-confirm-icon ${type}`;
|
||||
modal.querySelector(".admin-confirm-title").textContent = title;
|
||||
modal.querySelector(".admin-confirm-message").textContent = message;
|
||||
modal.querySelector(".admin-confirm-btn.confirm").textContent = confirmText;
|
||||
modal.querySelector(
|
||||
".admin-confirm-btn.confirm"
|
||||
).className = `admin-confirm-btn confirm ${type}`;
|
||||
modal.querySelector(".admin-confirm-btn.cancel").textContent = cancelText;
|
||||
}
|
||||
|
||||
// Show modal
|
||||
requestAnimationFrame(() => {
|
||||
modal.classList.add("show");
|
||||
});
|
||||
|
||||
// Get buttons
|
||||
const confirmBtn = modal.querySelector(".admin-confirm-btn.confirm");
|
||||
const cancelBtn = modal.querySelector(".admin-confirm-btn.cancel");
|
||||
const overlay = modal.querySelector(".admin-confirm-overlay");
|
||||
|
||||
// Close function
|
||||
const closeModal = () => {
|
||||
modal.classList.remove("show");
|
||||
};
|
||||
|
||||
// Remove old listeners by cloning
|
||||
const newConfirmBtn = confirmBtn.cloneNode(true);
|
||||
const newCancelBtn = cancelBtn.cloneNode(true);
|
||||
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
|
||||
cancelBtn.parentNode.replaceChild(newCancelBtn, cancelBtn);
|
||||
|
||||
// Add new listeners
|
||||
newConfirmBtn.addEventListener("click", () => {
|
||||
closeModal();
|
||||
onConfirm();
|
||||
});
|
||||
|
||||
newCancelBtn.addEventListener("click", closeModal);
|
||||
overlay.addEventListener("click", closeModal);
|
||||
|
||||
// Escape key to close
|
||||
const escHandler = (e) => {
|
||||
if (e.key === "Escape") {
|
||||
closeModal();
|
||||
document.removeEventListener("keydown", escHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", escHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a success notification toast
|
||||
* @param {string} message - The success message
|
||||
*/
|
||||
function showAdminToast(message, type = "success") {
|
||||
// Create toast container if it doesn't exist
|
||||
let container = document.getElementById("adminToastContainer");
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = "adminToastContainer";
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10001;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
`;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const toast = document.createElement("div");
|
||||
toast.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
transform: translateX(120%);
|
||||
transition: transform 0.3s ease;
|
||||
min-width: 280px;
|
||||
border-left: 4px solid ${
|
||||
type === "success" ? "#10b981" : type === "error" ? "#ef4444" : "#3b82f6"
|
||||
};
|
||||
`;
|
||||
|
||||
const icons = {
|
||||
success:
|
||||
'<i class="bi bi-check-circle-fill" style="color: #10b981; font-size: 1.25rem;"></i>',
|
||||
error:
|
||||
'<i class="bi bi-x-circle-fill" style="color: #ef4444; font-size: 1.25rem;"></i>',
|
||||
info: '<i class="bi bi-info-circle-fill" style="color: #3b82f6; font-size: 1.25rem;"></i>',
|
||||
};
|
||||
|
||||
toast.innerHTML = `
|
||||
${icons[type] || icons.info}
|
||||
<span style="flex: 1; color: #374151; font-size: 0.9rem;">${message}</span>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Animate in
|
||||
requestAnimationFrame(() => {
|
||||
toast.style.transform = "translateX(0)";
|
||||
});
|
||||
|
||||
// Auto remove after 4 seconds
|
||||
setTimeout(() => {
|
||||
toast.style.transform = "translateX(120%)";
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify frontend that data has changed
|
||||
* This updates a timestamp in localStorage that frontend pages check
|
||||
* @param {string} dataType - Type of data changed (products, pages, settings, etc.)
|
||||
*/
|
||||
function notifyFrontendChange(dataType = "all") {
|
||||
const timestamp = Date.now();
|
||||
const changeKey = `skyartshop_change_${dataType}`;
|
||||
|
||||
// Store in localStorage for cross-tab communication
|
||||
localStorage.setItem(changeKey, timestamp.toString());
|
||||
localStorage.setItem("skyartshop_last_change", timestamp.toString());
|
||||
|
||||
// Also broadcast to any open frontend tabs via BroadcastChannel
|
||||
try {
|
||||
const channel = new BroadcastChannel("skyartshop_updates");
|
||||
channel.postMessage({ type: dataType, timestamp });
|
||||
channel.close();
|
||||
} catch (e) {
|
||||
// BroadcastChannel not supported in some browsers
|
||||
}
|
||||
|
||||
console.log(`[Admin] Notified frontend of ${dataType} change`);
|
||||
}
|
||||
|
||||
// Export for use in other files
|
||||
window.showDeleteConfirm = showDeleteConfirm;
|
||||
window.showAdminToast = showAdminToast;
|
||||
window.notifyFrontendChange = notifyFrontendChange;
|
||||
@@ -103,98 +103,70 @@ function initializeQuillEditor() {
|
||||
});
|
||||
}
|
||||
|
||||
function openMediaLibraryForFeaturedImage() {
|
||||
// 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 media library
|
||||
let blogMediaLibrary = null;
|
||||
let galleryImages = [];
|
||||
|
||||
// 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 = closeMediaLibrary;
|
||||
|
||||
// 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) {
|
||||
closeMediaLibrary();
|
||||
}
|
||||
};
|
||||
|
||||
// Setup media selection handler
|
||||
window.handleMediaSelection = function (media) {
|
||||
const mediaItem = Array.isArray(media) ? media[0] : media;
|
||||
if (mediaItem && mediaItem.url) {
|
||||
document.getElementById("postFeaturedImage").value = mediaItem.url;
|
||||
updateFeaturedImagePreview(mediaItem.url);
|
||||
showToast("Featured image selected", "success");
|
||||
}
|
||||
closeMediaLibrary();
|
||||
};
|
||||
function initBlogMediaLibrary() {
|
||||
blogMediaLibrary = new MediaLibrary({
|
||||
selectMode: true,
|
||||
multiple: false,
|
||||
onSelect: function (media) {
|
||||
if (media && media.path) {
|
||||
document.getElementById("postFeaturedImage").value = media.path;
|
||||
updateFeaturedImagePreview(media.path);
|
||||
showToast("Featured image selected", "success");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function closeMediaLibrary() {
|
||||
const backdrop = document.getElementById("mediaLibraryBackdrop");
|
||||
if (backdrop) {
|
||||
backdrop.remove();
|
||||
function openMediaLibraryForFeaturedImage() {
|
||||
if (!blogMediaLibrary) {
|
||||
initBlogMediaLibrary();
|
||||
}
|
||||
blogMediaLibrary.options.multiple = false;
|
||||
blogMediaLibrary.options.onSelect = function (media) {
|
||||
if (media && media.path) {
|
||||
document.getElementById("postFeaturedImage").value = media.path;
|
||||
updateFeaturedImagePreview(media.path);
|
||||
showToast("Featured image selected", "success");
|
||||
}
|
||||
};
|
||||
blogMediaLibrary.open();
|
||||
}
|
||||
|
||||
function openMediaLibraryForGallery() {
|
||||
if (!blogMediaLibrary) {
|
||||
initBlogMediaLibrary();
|
||||
}
|
||||
blogMediaLibrary.options.multiple = true;
|
||||
blogMediaLibrary.options.onSelect = function (mediaList) {
|
||||
const items = Array.isArray(mediaList) ? mediaList : [mediaList];
|
||||
items.forEach((media) => {
|
||||
if (media && media.path && !galleryImages.includes(media.path)) {
|
||||
galleryImages.push(media.path);
|
||||
}
|
||||
});
|
||||
updateGalleryPreview();
|
||||
showToast(`${items.length} image(s) added to gallery`, "success");
|
||||
};
|
||||
blogMediaLibrary.open();
|
||||
}
|
||||
|
||||
function openMediaLibraryForVideo() {
|
||||
if (!blogMediaLibrary) {
|
||||
initBlogMediaLibrary();
|
||||
}
|
||||
blogMediaLibrary.options.multiple = false;
|
||||
blogMediaLibrary.options.onSelect = function (media) {
|
||||
if (media && media.path) {
|
||||
document.getElementById("postVideoUrl").value = media.path;
|
||||
updateVideoPreview(media.path);
|
||||
showToast("Video selected", "success");
|
||||
}
|
||||
};
|
||||
blogMediaLibrary.open();
|
||||
}
|
||||
|
||||
function updateFeaturedImagePreview(url) {
|
||||
@@ -202,21 +174,157 @@ function updateFeaturedImagePreview(url) {
|
||||
if (url) {
|
||||
preview.innerHTML = `
|
||||
<div style="position: relative; display: inline-block;">
|
||||
<img src="${url}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 2px solid #e0e0e0;" />
|
||||
<img src="${url}" style="max-width: 100%; max-height: 150px; border-radius: 8px;" />
|
||||
<button type="button" onclick="removeFeaturedImage()" style="position: absolute; top: -8px; right: -8px; background: #dc3545; color: white; border: none; border-radius: 50%; width: 24px; height: 24px; cursor: pointer; font-size: 14px;">×</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
preview.innerHTML = "";
|
||||
preview.innerHTML =
|
||||
'<div class="text-muted text-center p-3"><i class="bi bi-image" style="font-size: 2rem;"></i><br><small>No image selected</small></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateGalleryPreview() {
|
||||
const preview = document.getElementById("galleryImagesPreview");
|
||||
if (galleryImages.length === 0) {
|
||||
preview.innerHTML =
|
||||
'<div class="text-muted text-center p-3 w-100"><i class="bi bi-images" style="font-size: 2rem;"></i><br><small>No gallery images</small></div>';
|
||||
return;
|
||||
}
|
||||
preview.innerHTML = galleryImages
|
||||
.map(
|
||||
(img, idx) => `
|
||||
<div class="gallery-thumb">
|
||||
<img src="${img}" alt="Gallery ${idx + 1}" />
|
||||
<button type="button" class="remove-btn" onclick="removeGalleryImage(${idx})">×</button>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function removeGalleryImage(index) {
|
||||
galleryImages.splice(index, 1);
|
||||
updateGalleryPreview();
|
||||
showToast("Image removed from gallery", "info");
|
||||
}
|
||||
|
||||
function updateVideoPreview(url) {
|
||||
const preview = document.getElementById("videoPreview");
|
||||
if (url) {
|
||||
const isVideo = url.match(/\.(mp4|webm|mov|avi|mkv)$/i);
|
||||
if (isVideo) {
|
||||
preview.innerHTML = `
|
||||
<div style="position: relative; width: 100%;">
|
||||
<video controls style="max-width: 100%; max-height: 200px;">
|
||||
<source src="${url}" type="video/mp4">
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
<button type="button" onclick="removeVideo()" style="position: absolute; top: 5px; right: 5px; background: #dc3545; color: white; border: none; border-radius: 50%; width: 28px; height: 28px; cursor: pointer;">×</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
preview.innerHTML = `<div class="video-placeholder"><i class="bi bi-link-45deg"></i>${url}</div>`;
|
||||
}
|
||||
} else {
|
||||
preview.innerHTML =
|
||||
'<div class="video-placeholder"><i class="bi bi-camera-video"></i>No video selected</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function removeVideo() {
|
||||
document.getElementById("postVideoUrl").value = "";
|
||||
document.getElementById("postExternalVideo").value = "";
|
||||
updateVideoPreview("");
|
||||
showToast("Video removed", "info");
|
||||
}
|
||||
|
||||
function removeFeaturedImage() {
|
||||
document.getElementById("postFeaturedImage").value = "";
|
||||
updateFeaturedImagePreview("");
|
||||
showToast("Featured image removed", "info");
|
||||
}
|
||||
|
||||
// Poll functions
|
||||
function togglePollSection() {
|
||||
const pollSection = document.getElementById("pollSection");
|
||||
const enabled = document.getElementById("enablePoll").checked;
|
||||
pollSection.style.display = enabled ? "block" : "none";
|
||||
}
|
||||
|
||||
function addPollOption() {
|
||||
const container = document.getElementById("pollOptionsContainer");
|
||||
const count = container.querySelectorAll(".poll-option-row").length + 1;
|
||||
const row = document.createElement("div");
|
||||
row.className = "input-group mb-2 poll-option-row";
|
||||
row.innerHTML = `
|
||||
<span class="input-group-text">${count}</span>
|
||||
<input type="text" class="form-control poll-option-input" placeholder="Option ${count}" />
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removePollOption(this)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function removePollOption(btn) {
|
||||
const row = btn.closest(".poll-option-row");
|
||||
row.remove();
|
||||
// Re-number options
|
||||
const container = document.getElementById("pollOptionsContainer");
|
||||
container.querySelectorAll(".poll-option-row").forEach((row, idx) => {
|
||||
row.querySelector(".input-group-text").textContent = idx + 1;
|
||||
});
|
||||
}
|
||||
|
||||
function getPollData() {
|
||||
if (!document.getElementById("enablePoll").checked) {
|
||||
return null;
|
||||
}
|
||||
const question = document.getElementById("pollQuestion").value.trim();
|
||||
const options = Array.from(document.querySelectorAll(".poll-option-input"))
|
||||
.map((input) => input.value.trim())
|
||||
.filter((v) => v);
|
||||
if (!question || options.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return { question, options, votes: options.map(() => 0) };
|
||||
}
|
||||
|
||||
function loadPollData(poll) {
|
||||
if (poll && typeof poll === "object") {
|
||||
document.getElementById("enablePoll").checked = true;
|
||||
document.getElementById("pollSection").style.display = "block";
|
||||
document.getElementById("pollQuestion").value = poll.question || "";
|
||||
const container = document.getElementById("pollOptionsContainer");
|
||||
container.innerHTML = "";
|
||||
(poll.options || []).forEach((opt, idx) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "input-group mb-2 poll-option-row";
|
||||
row.innerHTML = `
|
||||
<span class="input-group-text">${idx + 1}</span>
|
||||
<input type="text" class="form-control poll-option-input" value="${opt}" />
|
||||
${idx >= 2 ? '<button type="button" class="btn btn-outline-danger" onclick="removePollOption(this)"><i class="bi bi-trash"></i></button>' : ""}
|
||||
`;
|
||||
container.appendChild(row);
|
||||
});
|
||||
} else {
|
||||
document.getElementById("enablePoll").checked = false;
|
||||
document.getElementById("pollSection").style.display = "none";
|
||||
document.getElementById("pollQuestion").value = "";
|
||||
document.getElementById("pollOptionsContainer").innerHTML = `
|
||||
<div class="input-group mb-2 poll-option-row">
|
||||
<span class="input-group-text">1</span>
|
||||
<input type="text" class="form-control poll-option-input" placeholder="Option 1" />
|
||||
</div>
|
||||
<div class="input-group mb-2 poll-option-row">
|
||||
<span class="input-group-text">2</span>
|
||||
<input type="text" class="form-control poll-option-input" placeholder="Option 2" />
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPosts() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/blog", { credentials: "include" });
|
||||
@@ -277,17 +385,17 @@ function renderPosts(posts) {
|
||||
<td>${formatDate(p.createdat)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-info" onclick="editPost('${escapeHtml(
|
||||
String(p.id)
|
||||
String(p.id),
|
||||
)}')">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deletePost('${escapeHtml(
|
||||
String(p.id)
|
||||
String(p.id),
|
||||
)}', '${escapeHtml(p.title).replace(/'/g, "'")}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>`
|
||||
</tr>`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
@@ -297,7 +405,7 @@ function filterPosts() {
|
||||
const filtered = postsData.filter(
|
||||
(p) =>
|
||||
p.title.toLowerCase().includes(searchTerm) ||
|
||||
p.slug.toLowerCase().includes(searchTerm)
|
||||
p.slug.toLowerCase().includes(searchTerm),
|
||||
);
|
||||
renderPosts(filtered);
|
||||
}
|
||||
@@ -306,9 +414,15 @@ function showCreatePost() {
|
||||
document.getElementById("modalTitle").textContent = "Create Blog Post";
|
||||
document.getElementById("postForm").reset();
|
||||
document.getElementById("postId").value = "";
|
||||
document.getElementById("postPublished").checked = false;
|
||||
document.getElementById("postPublished").checked = true; // Default to published
|
||||
document.getElementById("postFeaturedImage").value = "";
|
||||
document.getElementById("postVideoUrl").value = "";
|
||||
document.getElementById("postExternalVideo").value = "";
|
||||
galleryImages = [];
|
||||
updateFeaturedImagePreview("");
|
||||
updateGalleryPreview();
|
||||
updateVideoPreview("");
|
||||
loadPollData(null);
|
||||
if (quillEditor) {
|
||||
quillEditor.setContents([]);
|
||||
}
|
||||
@@ -340,6 +454,28 @@ async function editPost(id) {
|
||||
document.getElementById("postFeaturedImage").value = featuredImage;
|
||||
updateFeaturedImagePreview(featuredImage);
|
||||
|
||||
// Set gallery images
|
||||
try {
|
||||
galleryImages = post.images ? JSON.parse(post.images) : [];
|
||||
} catch (e) {
|
||||
galleryImages = [];
|
||||
}
|
||||
updateGalleryPreview();
|
||||
|
||||
// Set video
|
||||
const videoUrl = post.videourl || "";
|
||||
document.getElementById("postVideoUrl").value = videoUrl;
|
||||
document.getElementById("postExternalVideo").value = "";
|
||||
updateVideoPreview(videoUrl);
|
||||
|
||||
// Set poll
|
||||
try {
|
||||
const poll = post.poll ? JSON.parse(post.poll) : null;
|
||||
loadPollData(poll);
|
||||
} catch (e) {
|
||||
loadPollData(null);
|
||||
}
|
||||
|
||||
document.getElementById("postMetaTitle").value = post.metatitle || "";
|
||||
document.getElementById("postMetaDescription").value =
|
||||
post.metadescription || "";
|
||||
@@ -359,12 +495,24 @@ async function savePost() {
|
||||
// Get content from Quill editor
|
||||
const content = quillEditor ? quillEditor.root.innerHTML : "";
|
||||
|
||||
// Get video URL (prefer uploaded, then external)
|
||||
let videoUrl = document.getElementById("postVideoUrl").value;
|
||||
if (!videoUrl) {
|
||||
videoUrl = document.getElementById("postExternalVideo").value;
|
||||
}
|
||||
|
||||
// Get poll data
|
||||
const poll = getPollData();
|
||||
|
||||
const formData = {
|
||||
title: document.getElementById("postTitle").value,
|
||||
slug: document.getElementById("postSlug").value,
|
||||
excerpt: document.getElementById("postExcerpt").value,
|
||||
content: content,
|
||||
featuredimage: document.getElementById("postFeaturedImage").value,
|
||||
images: JSON.stringify(galleryImages),
|
||||
videourl: videoUrl,
|
||||
poll: poll ? JSON.stringify(poll) : null,
|
||||
metatitle: document.getElementById("postMetaTitle").value,
|
||||
metadescription: document.getElementById("postMetaDescription").value,
|
||||
ispublished: document.getElementById("postPublished").checked,
|
||||
@@ -389,7 +537,7 @@ async function savePost() {
|
||||
if (data.success) {
|
||||
showToast(
|
||||
id ? "Post updated successfully" : "Post created successfully",
|
||||
"success"
|
||||
"success",
|
||||
);
|
||||
postModal.hide();
|
||||
loadPosts();
|
||||
@@ -403,23 +551,30 @@ async function savePost() {
|
||||
}
|
||||
|
||||
async function deletePost(id, title) {
|
||||
if (!confirm(`Are you sure you want to delete "${title}"?`)) return;
|
||||
try {
|
||||
const response = await fetch(`/api/admin/blog/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showToast("Post deleted successfully", "success");
|
||||
loadPosts();
|
||||
} else {
|
||||
showToast(data.message || "Failed to delete post", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete post:", error);
|
||||
showToast("Failed to delete post", "error");
|
||||
}
|
||||
showDeleteConfirm(
|
||||
`Are you sure you want to delete "${title}"? This action cannot be undone.`,
|
||||
async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/blog/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// Immediately remove from local array and re-render
|
||||
postsData = postsData.filter((p) => String(p.id) !== String(id));
|
||||
renderPosts(postsData);
|
||||
showToast("Post deleted successfully", "success");
|
||||
} else {
|
||||
showToast(data.message || "Failed to delete post", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete post:", error);
|
||||
showToast("Failed to delete post", "error");
|
||||
}
|
||||
},
|
||||
{ title: "Delete Blog Post", confirmText: "Delete Post" },
|
||||
);
|
||||
}
|
||||
|
||||
function showToast(message, type = "info") {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
582
website/admin/js/homepage.js.old
Normal file
582
website/admin/js/homepage.js.old
Normal file
@@ -0,0 +1,582 @@
|
||||
// Homepage Editor JavaScript
|
||||
|
||||
let homepageData = {};
|
||||
let quillEditors = {};
|
||||
let currentMediaPicker = null;
|
||||
|
||||
// Initialize Quill editors
|
||||
function initializeQuillEditors() {
|
||||
// Check if Quill is loaded
|
||||
if (typeof Quill === "undefined") {
|
||||
console.error("Quill.js is not loaded!");
|
||||
alert("Text editor failed to load. Please refresh the page.");
|
||||
return;
|
||||
}
|
||||
|
||||
const toolbarOptions = [
|
||||
["bold", "italic", "underline", "strike"],
|
||||
["blockquote", "code-block"],
|
||||
[{ header: 1 }, { header: 2 }],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
[{ script: "sub" }, { script: "super" }],
|
||||
[{ indent: "-1" }, { indent: "+1" }],
|
||||
[{ direction: "rtl" }],
|
||||
[{ size: ["small", false, "large", "huge"] }],
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
[{ color: [] }, { background: [] }],
|
||||
[{ font: [] }],
|
||||
[{ align: [] }],
|
||||
["link"],
|
||||
["clean"],
|
||||
];
|
||||
|
||||
try {
|
||||
// Initialize Quill for each description field
|
||||
quillEditors.hero = new Quill("#heroDescription", {
|
||||
theme: "snow",
|
||||
modules: { toolbar: toolbarOptions },
|
||||
placeholder: "Enter hero section description...",
|
||||
});
|
||||
|
||||
quillEditors.promotion = new Quill("#promotionDescription", {
|
||||
theme: "snow",
|
||||
modules: { toolbar: toolbarOptions },
|
||||
placeholder: "Enter promotion description...",
|
||||
});
|
||||
|
||||
quillEditors.portfolio = new Quill("#portfolioDescription", {
|
||||
theme: "snow",
|
||||
modules: { toolbar: toolbarOptions },
|
||||
placeholder: "Enter portfolio description...",
|
||||
});
|
||||
|
||||
console.log("Quill editors initialized successfully");
|
||||
} catch (error) {
|
||||
console.error("Error initializing Quill editors:", error);
|
||||
alert(
|
||||
"Failed to initialize text editors. Please check the console for errors."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
initializeQuillEditors();
|
||||
|
||||
checkAuth().then((authenticated) => {
|
||||
if (authenticated) {
|
||||
loadHomepageSettings();
|
||||
setupMediaLibraryListener();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Setup media library selection listener
|
||||
function setupMediaLibraryListener() {
|
||||
window.addEventListener("message", function (event) {
|
||||
// Security: verify origin if needed
|
||||
if (
|
||||
event.data &&
|
||||
event.data.type === "mediaSelected" &&
|
||||
currentMediaPicker
|
||||
) {
|
||||
const { section, field } = currentMediaPicker;
|
||||
handleMediaSelection(section, field, event.data.media);
|
||||
currentMediaPicker = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadHomepageSettings() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/homepage/settings", {
|
||||
credentials: "include",
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
homepageData = data.settings || {};
|
||||
|
||||
// If no data exists, load defaults from the frontend
|
||||
if (Object.keys(homepageData).length === 0) {
|
||||
console.log("No homepage data found, loading defaults from frontend");
|
||||
await loadDefaultsFromFrontend();
|
||||
}
|
||||
|
||||
populateFields();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load homepage settings:", error);
|
||||
// Load defaults if API fails
|
||||
await loadDefaultsFromFrontend();
|
||||
populateFields();
|
||||
}
|
||||
}
|
||||
|
||||
// Load default content from the current homepage
|
||||
async function loadDefaultsFromFrontend() {
|
||||
homepageData = {
|
||||
hero: {
|
||||
enabled: true,
|
||||
headline: "Welcome to Sky Art Shop",
|
||||
subheading: "Your destination for creative stationery and supplies",
|
||||
description:
|
||||
"<p>Discover our curated collection of scrapbooking, journaling, cardmaking, and collaging supplies. Express your creativity and bring your artistic vision to life.</p>",
|
||||
ctaText: "Shop Now",
|
||||
ctaLink: "/shop.html",
|
||||
backgroundUrl: "",
|
||||
layout: "text-left",
|
||||
},
|
||||
promotion: {
|
||||
enabled: true,
|
||||
title: "Get Inspired",
|
||||
description:
|
||||
"<p>At Sky Art Shop, we believe in the power of creativity to transform and inspire. Whether you're an experienced crafter or just beginning your creative journey, we have everything you need to bring your ideas to life.</p><p>Explore our collection of washi tapes, stickers, stamps, and more. Each item is carefully selected to help you create something beautiful and meaningful.</p>",
|
||||
imageUrl: "",
|
||||
imagePosition: "left",
|
||||
textAlignment: "left",
|
||||
},
|
||||
portfolio: {
|
||||
enabled: true,
|
||||
title: "Featured Products",
|
||||
description: "<p>Discover our most popular items</p>",
|
||||
count: 6,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function populateFields() {
|
||||
console.log("Populating fields with data:", homepageData);
|
||||
|
||||
// Hero Section
|
||||
if (homepageData.hero) {
|
||||
document.getElementById("heroEnabled").checked =
|
||||
homepageData.hero.enabled !== false;
|
||||
document.getElementById("heroHeadline").value =
|
||||
homepageData.hero.headline || "";
|
||||
document.getElementById("heroSubheading").value =
|
||||
homepageData.hero.subheading || "";
|
||||
|
||||
if (homepageData.hero.description) {
|
||||
quillEditors.hero.root.innerHTML = homepageData.hero.description;
|
||||
}
|
||||
|
||||
document.getElementById("heroCtaText").value =
|
||||
homepageData.hero.ctaText || "";
|
||||
document.getElementById("heroCtaLink").value =
|
||||
homepageData.hero.ctaLink || "";
|
||||
|
||||
if (homepageData.hero.backgroundUrl) {
|
||||
document.getElementById("heroBackgroundUrl").value =
|
||||
homepageData.hero.backgroundUrl;
|
||||
displayMediaPreview(
|
||||
"hero",
|
||||
"background",
|
||||
homepageData.hero.backgroundUrl
|
||||
);
|
||||
}
|
||||
|
||||
if (homepageData.hero.layout) {
|
||||
const heroSection = document.getElementById("heroSection");
|
||||
heroSection.setAttribute("data-layout", homepageData.hero.layout);
|
||||
setActiveButton(`heroSection`, `layout-${homepageData.hero.layout}`);
|
||||
}
|
||||
|
||||
toggleSection("hero");
|
||||
}
|
||||
|
||||
// Promotion Section
|
||||
if (homepageData.promotion) {
|
||||
document.getElementById("promotionEnabled").checked =
|
||||
homepageData.promotion.enabled !== false;
|
||||
document.getElementById("promotionTitle").value =
|
||||
homepageData.promotion.title || "";
|
||||
|
||||
if (homepageData.promotion.description) {
|
||||
quillEditors.promotion.root.innerHTML =
|
||||
homepageData.promotion.description;
|
||||
}
|
||||
|
||||
if (homepageData.promotion.imageUrl) {
|
||||
document.getElementById("promotionImageUrl").value =
|
||||
homepageData.promotion.imageUrl;
|
||||
displayMediaPreview(
|
||||
"promotion",
|
||||
"image",
|
||||
homepageData.promotion.imageUrl
|
||||
);
|
||||
}
|
||||
|
||||
if (homepageData.promotion.imagePosition) {
|
||||
const promotionSection = document.getElementById("promotionSection");
|
||||
promotionSection.setAttribute(
|
||||
"data-image-position",
|
||||
homepageData.promotion.imagePosition
|
||||
);
|
||||
setActiveButton(
|
||||
`promotionSection`,
|
||||
`position-${homepageData.promotion.imagePosition}`
|
||||
);
|
||||
}
|
||||
|
||||
if (homepageData.promotion.textAlignment) {
|
||||
const promotionSection = document.getElementById("promotionSection");
|
||||
promotionSection.setAttribute(
|
||||
"data-text-alignment",
|
||||
homepageData.promotion.textAlignment
|
||||
);
|
||||
setActiveButton(
|
||||
`promotionSection`,
|
||||
`align-${homepageData.promotion.textAlignment}`
|
||||
);
|
||||
}
|
||||
|
||||
toggleSection("promotion");
|
||||
}
|
||||
|
||||
// Portfolio Section
|
||||
if (homepageData.portfolio) {
|
||||
document.getElementById("portfolioEnabled").checked =
|
||||
homepageData.portfolio.enabled !== false;
|
||||
document.getElementById("portfolioTitle").value =
|
||||
homepageData.portfolio.title || "";
|
||||
|
||||
if (homepageData.portfolio.description) {
|
||||
quillEditors.portfolio.root.innerHTML =
|
||||
homepageData.portfolio.description;
|
||||
}
|
||||
|
||||
document.getElementById("portfolioCount").value =
|
||||
homepageData.portfolio.count || 6;
|
||||
toggleSection("portfolio");
|
||||
}
|
||||
|
||||
// Show success message
|
||||
showSuccess(
|
||||
"Homepage content loaded! You can now edit and preview your changes."
|
||||
);
|
||||
}
|
||||
|
||||
function setActiveButton(sectionId, className) {
|
||||
const section = document.getElementById(sectionId);
|
||||
if (section) {
|
||||
const buttons = section.querySelectorAll(".alignment-btn");
|
||||
buttons.forEach((btn) => {
|
||||
if (btn.classList.contains(className)) {
|
||||
btn.classList.add("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSection(sectionName) {
|
||||
const enabled = document.getElementById(`${sectionName}Enabled`).checked;
|
||||
const section = document.getElementById(`${sectionName}Section`);
|
||||
const content = section.querySelector(".section-content");
|
||||
|
||||
if (enabled) {
|
||||
section.classList.remove("disabled");
|
||||
content.querySelectorAll("input, button, select").forEach((el) => {
|
||||
el.disabled = false;
|
||||
});
|
||||
// Enable Quill editor
|
||||
if (quillEditors[sectionName]) {
|
||||
quillEditors[sectionName].enable();
|
||||
}
|
||||
} else {
|
||||
section.classList.add("disabled");
|
||||
content.querySelectorAll("input, button, select").forEach((el) => {
|
||||
el.disabled = true;
|
||||
});
|
||||
// Disable Quill editor
|
||||
if (quillEditors[sectionName]) {
|
||||
quillEditors[sectionName].disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open media library in a modal
|
||||
function openMediaLibrary(section, field) {
|
||||
currentMediaPicker = { section, field };
|
||||
|
||||
// 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;
|
||||
`;
|
||||
|
||||
// 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 = closeMediaLibrary;
|
||||
|
||||
// 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;
|
||||
`;
|
||||
|
||||
// Setup iframe message listener
|
||||
iframe.onload = function () {
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: "initSelectMode",
|
||||
section: section,
|
||||
field: field,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
};
|
||||
|
||||
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) {
|
||||
closeMediaLibrary();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function closeMediaLibrary() {
|
||||
const backdrop = document.getElementById("mediaLibraryBackdrop");
|
||||
if (backdrop) {
|
||||
backdrop.remove();
|
||||
}
|
||||
currentMediaPicker = null;
|
||||
}
|
||||
|
||||
function handleMediaSelection(section, field, media) {
|
||||
closeMediaLibrary();
|
||||
|
||||
const urlField = document.getElementById(
|
||||
`${section}${field === "background" ? "Background" : "Image"}Url`
|
||||
);
|
||||
if (urlField) {
|
||||
urlField.value = media.url;
|
||||
}
|
||||
|
||||
displayMediaPreview(section, field, media.url);
|
||||
|
||||
showSuccess(`Media selected successfully!`);
|
||||
}
|
||||
|
||||
function displayMediaPreview(section, field, url) {
|
||||
const previewId = `${section}Preview`;
|
||||
const preview = document.getElementById(previewId);
|
||||
const clearBtnId = `${section}${
|
||||
field === "background" ? "Background" : "Image"
|
||||
}Clear`;
|
||||
const clearBtn = document.getElementById(clearBtnId);
|
||||
|
||||
if (preview) {
|
||||
preview.classList.remove("empty");
|
||||
|
||||
// Check if it's a video
|
||||
const isVideo = url.match(/\.(mp4|webm|ogg)$/i);
|
||||
|
||||
if (isVideo) {
|
||||
preview.innerHTML = `<video src="${url}" style="max-width: 100%; max-height: 100%;" controls></video>`;
|
||||
} else {
|
||||
preview.innerHTML = `<img src="${url}" alt="Preview" />`;
|
||||
}
|
||||
}
|
||||
|
||||
if (clearBtn) {
|
||||
clearBtn.style.display = "inline-block";
|
||||
}
|
||||
}
|
||||
|
||||
function clearMedia(section, field) {
|
||||
const urlField = document.getElementById(
|
||||
`${section}${field === "background" ? "Background" : "Image"}Url`
|
||||
);
|
||||
if (urlField) {
|
||||
urlField.value = "";
|
||||
}
|
||||
|
||||
const previewId = `${section}Preview`;
|
||||
const preview = document.getElementById(previewId);
|
||||
if (preview) {
|
||||
preview.classList.add("empty");
|
||||
preview.innerHTML = '<i class="bi bi-image" style="font-size: 3rem"></i>';
|
||||
}
|
||||
|
||||
const clearBtnId = `${section}${
|
||||
field === "background" ? "Background" : "Image"
|
||||
}Clear`;
|
||||
const clearBtn = document.getElementById(clearBtnId);
|
||||
if (clearBtn) {
|
||||
clearBtn.style.display = "none";
|
||||
}
|
||||
|
||||
showSuccess("Media cleared");
|
||||
}
|
||||
|
||||
function setLayout(sectionName, layout) {
|
||||
const section = document.getElementById(`${sectionName}Section`);
|
||||
const buttons = section.querySelectorAll(".alignment-btn");
|
||||
buttons.forEach((btn) => btn.classList.remove("active"));
|
||||
event.target.closest(".alignment-btn").classList.add("active");
|
||||
|
||||
// Store in a data attribute
|
||||
section.setAttribute(`data-layout`, layout);
|
||||
}
|
||||
|
||||
function setImagePosition(sectionName, position) {
|
||||
const section = document.getElementById(`${sectionName}Section`);
|
||||
const buttons = event.target
|
||||
.closest(".alignment-selector")
|
||||
.querySelectorAll(".alignment-btn");
|
||||
buttons.forEach((btn) => btn.classList.remove("active"));
|
||||
event.target.closest(".alignment-btn").classList.add("active");
|
||||
|
||||
section.setAttribute(`data-image-position`, position);
|
||||
}
|
||||
|
||||
function setTextAlignment(sectionName, alignment) {
|
||||
const section = document.getElementById(`${sectionName}Section`);
|
||||
const buttons = event.target
|
||||
.closest(".alignment-selector")
|
||||
.querySelectorAll(".alignment-btn");
|
||||
buttons.forEach((btn) => btn.classList.remove("active"));
|
||||
event.target.closest(".alignment-btn").classList.add("active");
|
||||
|
||||
section.setAttribute(`data-text-alignment`, alignment);
|
||||
}
|
||||
|
||||
async function saveHomepage() {
|
||||
// Get hero layout
|
||||
const heroSection = document.getElementById("heroSection");
|
||||
const heroLayout = heroSection.getAttribute("data-layout") || "text-left";
|
||||
|
||||
// Get promotion layout settings
|
||||
const promotionSection = document.getElementById("promotionSection");
|
||||
const promotionImagePosition =
|
||||
promotionSection.getAttribute("data-image-position") || "left";
|
||||
const promotionTextAlignment =
|
||||
promotionSection.getAttribute("data-text-alignment") || "left";
|
||||
|
||||
const settings = {
|
||||
hero: {
|
||||
enabled: document.getElementById("heroEnabled").checked,
|
||||
headline: document.getElementById("heroHeadline").value,
|
||||
subheading: document.getElementById("heroSubheading").value,
|
||||
description: quillEditors.hero.root.innerHTML,
|
||||
ctaText: document.getElementById("heroCtaText").value,
|
||||
ctaLink: document.getElementById("heroCtaLink").value,
|
||||
backgroundUrl: document.getElementById("heroBackgroundUrl")?.value || "",
|
||||
layout: heroLayout,
|
||||
},
|
||||
promotion: {
|
||||
enabled: document.getElementById("promotionEnabled").checked,
|
||||
title: document.getElementById("promotionTitle").value,
|
||||
description: quillEditors.promotion.root.innerHTML,
|
||||
imageUrl: document.getElementById("promotionImageUrl")?.value || "",
|
||||
imagePosition: promotionImagePosition,
|
||||
textAlignment: promotionTextAlignment,
|
||||
},
|
||||
portfolio: {
|
||||
enabled: document.getElementById("portfolioEnabled").checked,
|
||||
title: document.getElementById("portfolioTitle").value,
|
||||
description: quillEditors.portfolio.root.innerHTML,
|
||||
count: parseInt(document.getElementById("portfolioCount").value) || 6,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/admin/homepage/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showSuccess(
|
||||
"Homepage settings saved successfully! Changes are now live on the frontend."
|
||||
);
|
||||
homepageData = settings;
|
||||
} else {
|
||||
showError(data.message || "Failed to save homepage settings");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save homepage:", error);
|
||||
showError("Failed to save homepage settings");
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
const alert = document.createElement("div");
|
||||
alert.className =
|
||||
"alert alert-success alert-dismissible fade show position-fixed";
|
||||
alert.style.cssText =
|
||||
"top: 20px; right: 20px; z-index: 9999; min-width: 300px;";
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
setTimeout(() => alert.remove(), 5000);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const alert = document.createElement("div");
|
||||
alert.className =
|
||||
"alert alert-danger alert-dismissible fade show position-fixed";
|
||||
alert.style.cssText =
|
||||
"top: 20px; right: 20px; z-index: 9999; min-width: 300px;";
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
setTimeout(() => alert.remove(), 5000);
|
||||
}
|
||||
1131
website/admin/js/media-library.js
Normal file
1131
website/admin/js/media-library.js
Normal file
File diff suppressed because it is too large
Load Diff
1683
website/admin/js/pages-new.js
Normal file
1683
website/admin/js/pages-new.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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";
|
||||
|
||||
@@ -6,6 +6,20 @@ let quillEditor;
|
||||
let portfolioImages = [];
|
||||
let currentMediaPicker = null;
|
||||
let isModalExpanded = false;
|
||||
let portfolioMediaLibrary = null;
|
||||
|
||||
// Initialize portfolio media library
|
||||
function initPortfolioMediaLibrary() {
|
||||
if (typeof MediaLibrary !== "undefined" && !portfolioMediaLibrary) {
|
||||
portfolioMediaLibrary = new MediaLibrary({
|
||||
selectMode: true,
|
||||
multiple: true, // Allow multiple image selection for portfolio gallery
|
||||
onSelect: function (media) {
|
||||
handleMediaSelection(media);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
projectModal = new bootstrap.Modal(document.getElementById("projectModal"));
|
||||
@@ -19,6 +33,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
// Initialize Quill editor
|
||||
initializeQuillEditor();
|
||||
|
||||
// Initialize media library
|
||||
initPortfolioMediaLibrary();
|
||||
|
||||
checkAuth().then((authenticated) => {
|
||||
if (authenticated) {
|
||||
loadProjects();
|
||||
@@ -123,7 +140,7 @@ async function loadProjects() {
|
||||
title: p.title,
|
||||
isactive: p.isactive,
|
||||
isactiveType: typeof p.isactive,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
renderProjects(projectsData);
|
||||
}
|
||||
@@ -152,7 +169,7 @@ function renderProjects(projects) {
|
||||
console.log(
|
||||
`Project ${p.id}: isactive =`,
|
||||
p.isactive,
|
||||
`(type: ${typeof p.isactive})`
|
||||
`(type: ${typeof p.isactive})`,
|
||||
);
|
||||
const isActive =
|
||||
p.isactive === true || p.isactive === "true" || p.isactive === 1;
|
||||
@@ -174,12 +191,12 @@ function renderProjects(projects) {
|
||||
<td>${formatDate(p.createdat)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-info" onclick="editProject('${escapeHtml(
|
||||
String(p.id)
|
||||
String(p.id),
|
||||
)}')">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteProject('${escapeHtml(
|
||||
String(p.id)
|
||||
String(p.id),
|
||||
)}', '${escapeHtml(p.title).replace(/'/g, "'")}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
@@ -192,7 +209,7 @@ function renderProjects(projects) {
|
||||
function filterProjects() {
|
||||
const searchTerm = document.getElementById("searchInput").value.toLowerCase();
|
||||
const filtered = projectsData.filter((p) =>
|
||||
p.title.toLowerCase().includes(searchTerm)
|
||||
p.title.toLowerCase().includes(searchTerm),
|
||||
);
|
||||
renderProjects(filtered);
|
||||
}
|
||||
@@ -237,10 +254,31 @@ async function editProject(id) {
|
||||
document.getElementById("projectCategory").value = project.category || "";
|
||||
document.getElementById("projectActive").checked = project.isactive;
|
||||
|
||||
// Load images if available (imageurl field or parse from description)
|
||||
// Load images - check images array first, then fall back to imageurl
|
||||
portfolioImages = [];
|
||||
if (project.imageurl) {
|
||||
// If single image URL exists
|
||||
|
||||
// Try to parse images array
|
||||
if (project.images) {
|
||||
try {
|
||||
const imagesArr =
|
||||
typeof project.images === "string"
|
||||
? JSON.parse(project.images)
|
||||
: project.images;
|
||||
if (Array.isArray(imagesArr) && imagesArr.length > 0) {
|
||||
imagesArr.forEach((url) => {
|
||||
portfolioImages.push({
|
||||
url: url,
|
||||
filename: url.split("/").pop(),
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse images:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to imageurl if no images array
|
||||
if (portfolioImages.length === 0 && project.imageurl) {
|
||||
portfolioImages.push({
|
||||
url: project.imageurl,
|
||||
filename: project.imageurl.split("/").pop(),
|
||||
@@ -286,6 +324,7 @@ async function saveProject() {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
cache: "no-cache",
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
@@ -294,9 +333,15 @@ async function saveProject() {
|
||||
showSuccess(
|
||||
id
|
||||
? "Project updated successfully! 🎉"
|
||||
: "Project created successfully! 🎉"
|
||||
: "Project created successfully! 🎉",
|
||||
);
|
||||
projectModal.hide();
|
||||
// Immediately add to local data and re-render for instant feedback
|
||||
if (!id && data.project) {
|
||||
projectsData.unshift(data.project);
|
||||
renderProjects(projectsData);
|
||||
}
|
||||
// Also reload from server to ensure full sync
|
||||
loadProjects();
|
||||
} else {
|
||||
showError(data.message || "Failed to save project");
|
||||
@@ -308,23 +353,33 @@ async function saveProject() {
|
||||
}
|
||||
|
||||
async function deleteProject(id, name) {
|
||||
if (!confirm(`Are you sure you want to delete "${name}"?`)) return;
|
||||
try {
|
||||
const response = await fetch(`/api/admin/portfolio/projects/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showSuccess("Project deleted successfully");
|
||||
loadProjects();
|
||||
} else {
|
||||
showError(data.message || "Failed to delete project");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete project:", error);
|
||||
showError("Failed to delete project");
|
||||
}
|
||||
showDeleteConfirm(
|
||||
`Are you sure you want to delete "${name}"? This action cannot be undone.`,
|
||||
async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/portfolio/projects/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
cache: "no-cache",
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showSuccess("Project deleted successfully");
|
||||
// Remove immediately from local data and re-render
|
||||
// Compare as strings to handle type mismatches
|
||||
const deletedId = String(id);
|
||||
projectsData = projectsData.filter((p) => String(p.id) !== deletedId);
|
||||
renderProjects(projectsData);
|
||||
} else {
|
||||
showError(data.message || "Failed to delete project");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete project:", error);
|
||||
showError("Failed to delete project");
|
||||
}
|
||||
},
|
||||
{ title: "Delete Project", confirmText: "Delete Project" },
|
||||
);
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
@@ -380,7 +435,7 @@ function renderPortfolioImages() {
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
@@ -395,100 +450,30 @@ function removePortfolioImage(index) {
|
||||
function openMediaLibrary(purpose) {
|
||||
currentMediaPicker = { purpose };
|
||||
|
||||
// Create 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: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
// Initialize if not already
|
||||
initPortfolioMediaLibrary();
|
||||
|
||||
// Create modal
|
||||
const modal = document.createElement("div");
|
||||
modal.style.cssText = `
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
// Create close button
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.innerHTML = "×";
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
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 = closeMediaLibrary;
|
||||
|
||||
// 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) {
|
||||
closeMediaLibrary();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function closeMediaLibrary() {
|
||||
const backdrop = document.getElementById("mediaLibraryBackdrop");
|
||||
if (backdrop) {
|
||||
backdrop.remove();
|
||||
if (portfolioMediaLibrary) {
|
||||
portfolioMediaLibrary.open();
|
||||
}
|
||||
currentMediaPicker = null;
|
||||
}
|
||||
|
||||
function handleMediaSelection(media) {
|
||||
if (!currentMediaPicker) return;
|
||||
|
||||
if (currentMediaPicker.purpose === "portfolioImages") {
|
||||
// Handle multiple images
|
||||
// Handle multiple images - media can be array or single object
|
||||
const mediaArray = Array.isArray(media) ? media : [media];
|
||||
|
||||
// Add all selected images to portfolio images array
|
||||
mediaArray.forEach((item) => {
|
||||
// Check if image already exists
|
||||
if (!portfolioImages.find((img) => img.url === item.url)) {
|
||||
const itemUrl = item.path || item.url;
|
||||
if (!portfolioImages.find((img) => img.url === itemUrl)) {
|
||||
portfolioImages.push({
|
||||
url: item.url,
|
||||
filename: item.filename || item.url.split("/").pop(),
|
||||
url: itemUrl,
|
||||
filename:
|
||||
item.filename || item.originalName || itemUrl.split("/").pop(),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -497,7 +482,7 @@ function handleMediaSelection(media) {
|
||||
showSuccess(`${mediaArray.length} image(s) added to portfolio gallery`);
|
||||
}
|
||||
|
||||
closeMediaLibrary();
|
||||
currentMediaPicker = null;
|
||||
}
|
||||
|
||||
// Toast Notification System
|
||||
|
||||
@@ -87,8 +87,10 @@ function initializeQuillEditor() {
|
||||
// Load all products
|
||||
async function loadProducts() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/products", {
|
||||
// Add cache-busting to ensure fresh data
|
||||
const response = await fetch(`/api/admin/products?_t=${Date.now()}`, {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
@@ -459,7 +461,7 @@ function renderImageVariants() {
|
||||
container.querySelectorAll('[data-action="remove"]').forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
const id = e.currentTarget.dataset.variantId;
|
||||
imageVariants = imageVariants.filter((v) => v.id !== id);
|
||||
imageVariants = imageVariants.filter((v) => String(v.id) !== String(id));
|
||||
renderImageVariants();
|
||||
});
|
||||
});
|
||||
@@ -469,7 +471,11 @@ function renderImageVariants() {
|
||||
item.addEventListener("click", (e) => {
|
||||
const variantId = e.currentTarget.dataset.variantId;
|
||||
const imageUrl = e.currentTarget.dataset.imageUrl;
|
||||
const variant = imageVariants.find((v) => v.id === variantId);
|
||||
const variant = imageVariants.find(
|
||||
(v) => String(v.id) === String(variantId)
|
||||
);
|
||||
|
||||
console.log("Image picker clicked:", { variantId, imageUrl, variant });
|
||||
|
||||
if (variant) {
|
||||
variant.image_url = imageUrl;
|
||||
@@ -480,16 +486,28 @@ function renderImageVariants() {
|
||||
.querySelectorAll(".image-picker-item")
|
||||
.forEach((i) => i.classList.remove("selected"));
|
||||
e.currentTarget.classList.add("selected");
|
||||
|
||||
console.log("Updated variant with new image:", variant);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners for input changes
|
||||
container.querySelectorAll("[data-variant-id]").forEach((input) => {
|
||||
input.addEventListener("input", (e) => {
|
||||
// Use 'input' for text/number fields, 'change' for radio/checkbox
|
||||
const eventType =
|
||||
input.type === "radio" || input.type === "checkbox" ? "change" : "input";
|
||||
input.addEventListener(eventType, (e) => {
|
||||
const id = e.target.dataset.variantId;
|
||||
const field = e.target.dataset.field;
|
||||
const variant = imageVariants.find((v) => v.id === id);
|
||||
const variant = imageVariants.find((v) => String(v.id) === String(id));
|
||||
|
||||
console.log("Input change:", {
|
||||
id,
|
||||
field,
|
||||
value: e.target.value,
|
||||
variant,
|
||||
});
|
||||
|
||||
if (variant) {
|
||||
if (field === "color_code_text") {
|
||||
@@ -517,6 +535,7 @@ function renderImageVariants() {
|
||||
} else {
|
||||
variant[field] = e.target.value;
|
||||
}
|
||||
console.log("Updated variant:", variant);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -564,7 +583,11 @@ async function editProduct(id) {
|
||||
product.isbestseller || false;
|
||||
|
||||
// Load image variants and extract unique product images
|
||||
imageVariants = product.images || [];
|
||||
// Convert numeric IDs to strings for consistency with newly created variants
|
||||
imageVariants = (product.images || []).map((img) => ({
|
||||
...img,
|
||||
id: String(img.id),
|
||||
}));
|
||||
console.log("Loaded image variants:", imageVariants);
|
||||
|
||||
// Build productImages array from unique image URLs in variants
|
||||
@@ -749,6 +772,9 @@ async function saveProduct() {
|
||||
: "✅ Product Created Successfully! Now visible on your shop page."
|
||||
);
|
||||
|
||||
// Notify frontend of product changes
|
||||
notifyFrontendChange("products");
|
||||
|
||||
// Wait a moment then close modal
|
||||
setTimeout(async () => {
|
||||
productModal.hide();
|
||||
@@ -783,141 +809,91 @@ async function saveProduct() {
|
||||
|
||||
// Delete product
|
||||
async function deleteProduct(id, name) {
|
||||
if (!confirm(`Are you sure you want to delete "${name}"?`)) {
|
||||
return;
|
||||
}
|
||||
showDeleteConfirm(
|
||||
`Are you sure you want to delete "${name}"? This action cannot be undone.`,
|
||||
async () => {
|
||||
try {
|
||||
// Immediately remove from UI for instant feedback
|
||||
const row = document
|
||||
.querySelector(`tr button[data-id="${id}"]`)
|
||||
?.closest("tr");
|
||||
if (row) {
|
||||
row.style.opacity = "0.5";
|
||||
row.style.pointerEvents = "none";
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/products/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
const response = await fetch(`/api/admin/products/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showSuccess("Product deleted successfully");
|
||||
loadProducts();
|
||||
} else {
|
||||
showError(data.message || "Failed to delete product");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete product:", error);
|
||||
showError("Failed to delete product");
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Remove from local array immediately
|
||||
productsData = productsData.filter((p) => p.id !== id);
|
||||
// Re-render without waiting for server
|
||||
renderProducts(productsData);
|
||||
showSuccess("Product deleted successfully");
|
||||
// Also trigger frontend cache invalidation
|
||||
notifyFrontendChange("products");
|
||||
} else {
|
||||
// Restore row if delete failed
|
||||
if (row) {
|
||||
row.style.opacity = "1";
|
||||
row.style.pointerEvents = "auto";
|
||||
}
|
||||
showError(data.message || "Failed to delete product");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Delete error:", error);
|
||||
showError("Failed to delete product");
|
||||
// Reload to restore state
|
||||
loadProducts();
|
||||
}
|
||||
},
|
||||
{ title: "Delete Product", confirmText: "Delete Product" }
|
||||
);
|
||||
}
|
||||
|
||||
// ===== MEDIA LIBRARY INTEGRATION =====
|
||||
|
||||
// Listen for media selections from media library
|
||||
window.addEventListener("message", function (event) {
|
||||
// Security: verify origin if needed
|
||||
if (event.data.type === "mediaSelected" && currentMediaPicker) {
|
||||
handleMediaSelection(event.data.media);
|
||||
}
|
||||
});
|
||||
let productMediaLibrary = null;
|
||||
|
||||
function initProductMediaLibrary() {
|
||||
productMediaLibrary = new MediaLibrary({
|
||||
selectMode: true,
|
||||
multiple: true,
|
||||
onSelect: handleMediaSelection,
|
||||
});
|
||||
}
|
||||
|
||||
// Open media library modal
|
||||
function openMediaLibrary(purpose) {
|
||||
currentMediaPicker = { purpose }; // purpose: 'productImage' or 'variantImage'
|
||||
|
||||
// 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;
|
||||
`;
|
||||
|
||||
// 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 = closeMediaLibrary;
|
||||
|
||||
// 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) {
|
||||
closeMediaLibrary();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function closeMediaLibrary() {
|
||||
const backdrop = document.getElementById("mediaLibraryBackdrop");
|
||||
if (backdrop) {
|
||||
backdrop.remove();
|
||||
if (!productMediaLibrary) {
|
||||
initProductMediaLibrary();
|
||||
}
|
||||
currentMediaPicker = null;
|
||||
|
||||
productMediaLibrary.open();
|
||||
}
|
||||
|
||||
function handleMediaSelection(media) {
|
||||
if (!currentMediaPicker) return;
|
||||
|
||||
if (currentMediaPicker.purpose === "productImage") {
|
||||
// Handle multiple images
|
||||
// Handle multiple images - media is array in multi-select mode
|
||||
const mediaArray = Array.isArray(media) ? media : [media];
|
||||
|
||||
// Add all selected images to product images array
|
||||
mediaArray.forEach((item) => {
|
||||
// Check if image already exists
|
||||
if (!productImages.find((img) => img.url === item.url)) {
|
||||
// Check if image already exists - use path instead of url
|
||||
if (!productImages.find((img) => img.url === item.path)) {
|
||||
productImages.push({
|
||||
url: item.url,
|
||||
alt_text: item.filename || "",
|
||||
filename: item.filename,
|
||||
url: item.path,
|
||||
alt_text: item.name || "",
|
||||
filename: item.name,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -926,7 +902,7 @@ function handleMediaSelection(media) {
|
||||
showSuccess(`${mediaArray.length} image(s) added to product gallery`);
|
||||
}
|
||||
|
||||
closeMediaLibrary();
|
||||
currentMediaPicker = null;
|
||||
}
|
||||
|
||||
// ===== UTILITY FUNCTIONS =====
|
||||
|
||||
@@ -1,28 +1,41 @@
|
||||
// Settings Management JavaScript
|
||||
|
||||
let currentSettings = {};
|
||||
let mediaLibraryModal;
|
||||
let settingsMediaLibrary = null;
|
||||
let currentMediaTarget = null;
|
||||
let selectedMediaUrl = null;
|
||||
let allMedia = [];
|
||||
|
||||
// Initialize settings media library
|
||||
function initSettingsMediaLibrary() {
|
||||
if (typeof MediaLibrary !== "undefined" && !settingsMediaLibrary) {
|
||||
settingsMediaLibrary = new MediaLibrary({
|
||||
selectMode: true,
|
||||
multiple: false,
|
||||
onSelect: function (media) {
|
||||
if (!currentMediaTarget) return;
|
||||
|
||||
// Set the selected URL to the target field
|
||||
document.getElementById(currentMediaTarget).value = media.path;
|
||||
|
||||
// Update preview
|
||||
if (currentMediaTarget === "siteLogo") {
|
||||
document.getElementById(
|
||||
"logoPreview"
|
||||
).innerHTML = `<img src="${media.path}" alt="Logo" />`;
|
||||
} else if (currentMediaTarget === "siteFavicon") {
|
||||
document.getElementById(
|
||||
"faviconPreview"
|
||||
).innerHTML = `<img src="${media.path}" alt="Favicon" />`;
|
||||
}
|
||||
|
||||
showToast("Image selected successfully", "success");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Initialize modal
|
||||
const modalElement = document.getElementById("mediaLibraryModal");
|
||||
if (modalElement) {
|
||||
mediaLibraryModal = new bootstrap.Modal(modalElement);
|
||||
}
|
||||
|
||||
// Setup media search
|
||||
const searchInput = document.getElementById("mediaSearch");
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", filterMedia);
|
||||
}
|
||||
|
||||
const typeFilter = document.getElementById("mediaTypeFilter");
|
||||
if (typeFilter) {
|
||||
typeFilter.addEventListener("change", filterMedia);
|
||||
}
|
||||
// Initialize media library
|
||||
initSettingsMediaLibrary();
|
||||
|
||||
// Load saved theme
|
||||
loadTheme();
|
||||
@@ -251,153 +264,25 @@ function populateSettings() {
|
||||
}
|
||||
|
||||
// Media Library Functions - Make global for onclick handlers
|
||||
window.openMediaLibrary = async function (targetField) {
|
||||
window.openMediaLibrary = function (targetField) {
|
||||
console.log("openMediaLibrary called for:", targetField);
|
||||
currentMediaTarget = targetField;
|
||||
selectedMediaUrl = null;
|
||||
|
||||
// Load media files
|
||||
try {
|
||||
const response = await fetch("/api/admin/uploads", {
|
||||
credentials: "include",
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
allMedia = data.files || [];
|
||||
renderMediaGrid(allMedia);
|
||||
mediaLibraryModal.show();
|
||||
} else {
|
||||
showToast(data.message || "Failed to load media library", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load media library:", error);
|
||||
showToast("Failed to load media library. Please try again.", "error");
|
||||
// Initialize if not already
|
||||
initSettingsMediaLibrary();
|
||||
|
||||
if (settingsMediaLibrary) {
|
||||
settingsMediaLibrary.open();
|
||||
} else {
|
||||
showToast("Media library not available", "error");
|
||||
}
|
||||
};
|
||||
|
||||
function renderMediaGrid(media) {
|
||||
const grid = document.getElementById("mediaGrid");
|
||||
if (media.length === 0) {
|
||||
grid.innerHTML = `
|
||||
<div class="text-center py-5" style="grid-column: 1/-1;">
|
||||
<i class="bi bi-inbox fs-1 text-muted"></i>
|
||||
<p class="text-muted">No media files found</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = media
|
||||
.map(
|
||||
(file) => `
|
||||
<div class="media-item" data-url="${
|
||||
file.path
|
||||
}" style="cursor: pointer; border: 3px solid transparent; border-radius: 8px; overflow: hidden; transition: all 0.3s;">
|
||||
${
|
||||
file.mimetype?.startsWith("image/")
|
||||
? `<img src="${file.path}" alt="${
|
||||
file.originalName || file.filename
|
||||
}" style="width: 100%; height: 150px; object-fit: cover;" />`
|
||||
: `<div style="width: 100%; height: 150px; background: #f8f9fa; display: flex; align-items: center; justify-content: center;">
|
||||
<i class="bi bi-file-earmark fs-1 text-muted"></i>
|
||||
</div>`
|
||||
}
|
||||
<div style="padding: 8px; font-size: 12px; text-align: center; background: white;">
|
||||
<div style="font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${
|
||||
file.originalName || file.filename
|
||||
}</div>
|
||||
<div style="color: #6c757d; font-size: 11px;">${formatFileSize(
|
||||
file.size
|
||||
)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
// Add click listeners to all media items
|
||||
document.querySelectorAll(".media-item").forEach((item) => {
|
||||
item.addEventListener("click", function () {
|
||||
selectMedia(this.dataset.url);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function selectMedia(url) {
|
||||
// Remove previous selection
|
||||
document.querySelectorAll(".media-item").forEach((el) => {
|
||||
el.style.border = "3px solid transparent";
|
||||
});
|
||||
|
||||
// Mark current selection - find the clicked item
|
||||
document.querySelectorAll(".media-item").forEach((el) => {
|
||||
if (el.dataset.url === url) {
|
||||
el.style.border = "3px solid #667eea";
|
||||
el.style.background = "#f8f9fa";
|
||||
}
|
||||
});
|
||||
|
||||
selectedMediaUrl = url;
|
||||
}
|
||||
|
||||
window.selectMediaFile = function () {
|
||||
if (!selectedMediaUrl) {
|
||||
window.showToast("Please select a media file", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the selected URL to the target field
|
||||
document.getElementById(currentMediaTarget).value = selectedMediaUrl;
|
||||
|
||||
// Update preview
|
||||
if (currentMediaTarget === "siteLogo") {
|
||||
document.getElementById(
|
||||
"logoPreview"
|
||||
).innerHTML = `<img src="${selectedMediaUrl}" alt="Logo" />`;
|
||||
} else if (currentMediaTarget === "siteFavicon") {
|
||||
document.getElementById(
|
||||
"faviconPreview"
|
||||
).innerHTML = `<img src="${selectedMediaUrl}" alt="Favicon" />`;
|
||||
}
|
||||
|
||||
// Close modal
|
||||
mediaLibraryModal.hide();
|
||||
window.showToast("Image selected successfully", "success");
|
||||
// This is now handled by the MediaLibrary component's onSelect callback
|
||||
showToast("Please click on an image to select it", "info");
|
||||
};
|
||||
|
||||
function filterMedia() {
|
||||
const searchTerm = document.getElementById("mediaSearch").value.toLowerCase();
|
||||
const typeFilter = document.getElementById("mediaTypeFilter").value;
|
||||
|
||||
let filtered = allMedia;
|
||||
|
||||
// Filter by search term
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(
|
||||
(file) =>
|
||||
file.filename.toLowerCase().includes(searchTerm) ||
|
||||
file.originalName?.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by type
|
||||
if (typeFilter !== "all") {
|
||||
filtered = filtered.filter((file) => {
|
||||
if (typeFilter === "image") return file.mimetype?.startsWith("image/");
|
||||
if (typeFilter === "video") return file.mimetype?.startsWith("video/");
|
||||
if (typeFilter === "document")
|
||||
return (
|
||||
file.mimetype?.includes("pdf") ||
|
||||
file.mimetype?.includes("document") ||
|
||||
file.mimetype?.includes("text")
|
||||
);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
renderMediaGrid(filtered);
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (!bytes) return "0 B";
|
||||
const k = 1024;
|
||||
|
||||
@@ -12,6 +12,7 @@ const rolePermissions = {
|
||||
"View Reports",
|
||||
"View Financial Data",
|
||||
],
|
||||
Sales: ["Manage Products", "Manage Orders", "View Reports"],
|
||||
Admin: [
|
||||
"Manage Products",
|
||||
"Manage Portfolio",
|
||||
@@ -19,14 +20,8 @@ const rolePermissions = {
|
||||
"Manage Pages",
|
||||
"Manage Users",
|
||||
"View Reports",
|
||||
],
|
||||
MasterAdmin: [
|
||||
"Full System Access",
|
||||
"Manage Settings",
|
||||
"Manage Users",
|
||||
"Manage All Content",
|
||||
"View Logs",
|
||||
"System Configuration",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -85,22 +80,22 @@ function renderUsers(users) {
|
||||
<td>${formatDate(u.createdat)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-info" onclick="editUser('${escapeHtml(
|
||||
u.id
|
||||
u.id,
|
||||
)}')" title="Edit User">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-warning" onclick="showChangePassword('${escapeHtml(
|
||||
u.id
|
||||
u.id,
|
||||
)}', '${escapeHtml(u.name)}')" title="Change Password">
|
||||
<i class="bi bi-key"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser('${escapeHtml(
|
||||
u.id
|
||||
u.id,
|
||||
)}', '${escapeHtml(u.name)}')" title="Delete User">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>`
|
||||
</tr>`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
@@ -111,7 +106,7 @@ function filterUsers() {
|
||||
(u) =>
|
||||
u.name.toLowerCase().includes(searchTerm) ||
|
||||
u.email.toLowerCase().includes(searchTerm) ||
|
||||
u.username.toLowerCase().includes(searchTerm)
|
||||
u.username.toLowerCase().includes(searchTerm),
|
||||
);
|
||||
renderUsers(filtered);
|
||||
}
|
||||
@@ -174,6 +169,18 @@ async function saveUser() {
|
||||
showError("Password must be at least 8 characters long");
|
||||
return;
|
||||
}
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
showError("Password must contain at least one uppercase letter");
|
||||
return;
|
||||
}
|
||||
if (!/[a-z]/.test(password)) {
|
||||
showError("Password must contain at least one lowercase letter");
|
||||
return;
|
||||
}
|
||||
if (!/[0-9]/.test(password)) {
|
||||
showError("Password must contain at least one number");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const formData = {
|
||||
@@ -212,7 +219,7 @@ async function saveUser() {
|
||||
|
||||
if (data.success) {
|
||||
showSuccess(
|
||||
id ? "User updated successfully" : "User created successfully"
|
||||
id ? "User updated successfully" : "User created successfully",
|
||||
);
|
||||
userModal.hide();
|
||||
loadUsers();
|
||||
@@ -254,6 +261,21 @@ async function changePassword() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(newPassword)) {
|
||||
showError("Password must contain at least one uppercase letter");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/[a-z]/.test(newPassword)) {
|
||||
showError("Password must contain at least one lowercase letter");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/[0-9]/.test(newPassword)) {
|
||||
showError("Password must contain at least one number");
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading("Changing password...");
|
||||
|
||||
try {
|
||||
@@ -281,34 +303,33 @@ async function changePassword() {
|
||||
}
|
||||
|
||||
async function deleteUser(id, name) {
|
||||
if (
|
||||
!confirm(
|
||||
`Are you sure you want to delete user "${name}"? This action cannot be undone.`
|
||||
)
|
||||
)
|
||||
return;
|
||||
showDeleteConfirm(
|
||||
`Are you sure you want to delete user "${name}"? This action cannot be undone.`,
|
||||
async () => {
|
||||
showLoading("Deleting user...");
|
||||
|
||||
showLoading("Deleting user...");
|
||||
try {
|
||||
const response = await fetch(`/api/admin/users/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
const data = await response.json();
|
||||
hideLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/users/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
const data = await response.json();
|
||||
hideLoading();
|
||||
|
||||
if (data.success) {
|
||||
showSuccess("User deleted successfully");
|
||||
loadUsers();
|
||||
} else {
|
||||
showError(data.message || "Failed to delete user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete user:", error);
|
||||
hideLoading();
|
||||
showError("Failed to delete user");
|
||||
}
|
||||
if (data.success) {
|
||||
showSuccess("User deleted successfully");
|
||||
loadUsers();
|
||||
} else {
|
||||
showError(data.message || "Failed to delete user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete user:", error);
|
||||
hideLoading();
|
||||
showError("Failed to delete user");
|
||||
}
|
||||
},
|
||||
{ title: "Delete User", confirmText: "Delete User" },
|
||||
);
|
||||
}
|
||||
|
||||
function updatePermissionsPreview() {
|
||||
@@ -323,7 +344,7 @@ function updatePermissionsPreview() {
|
||||
<i class="bi bi-check-circle-fill" style="color: #10b981; margin-right: 8px;"></i>
|
||||
<span>${perm}</span>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user