updateweb
This commit is contained in:
@@ -7,6 +7,50 @@ window.adminAuth = {
|
||||
isAuthenticated: false,
|
||||
};
|
||||
|
||||
// Load and apply theme on all admin pages
|
||||
function loadAdminTheme() {
|
||||
const savedTheme = localStorage.getItem("adminTheme") || "light";
|
||||
applyAdminTheme(savedTheme);
|
||||
|
||||
// Watch for system theme changes if in auto mode
|
||||
if (savedTheme === "auto") {
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", (e) => {
|
||||
if (localStorage.getItem("adminTheme") === "auto") {
|
||||
applyAdminTheme("auto");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function applyAdminTheme(theme) {
|
||||
const body = document.body;
|
||||
|
||||
if (theme === "dark") {
|
||||
body.classList.add("dark-mode");
|
||||
body.classList.remove("light-mode");
|
||||
} else if (theme === "light") {
|
||||
body.classList.add("light-mode");
|
||||
body.classList.remove("dark-mode");
|
||||
} else if (theme === "auto") {
|
||||
// Check system preference
|
||||
const prefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
if (prefersDark) {
|
||||
body.classList.add("dark-mode");
|
||||
body.classList.remove("light-mode");
|
||||
} else {
|
||||
body.classList.add("light-mode");
|
||||
body.classList.remove("dark-mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme immediately (before page loads)
|
||||
loadAdminTheme();
|
||||
|
||||
// Check authentication and redirect if needed - attach to window
|
||||
window.checkAuth = async function () {
|
||||
try {
|
||||
@@ -360,3 +404,22 @@ if (window.location.pathname !== "/admin/login.html") {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Fix Bootstrap modal aria-hidden focus warning for all modals - Universal Solution
|
||||
(function () {
|
||||
// Use event delegation on document level to catch all modal hide events
|
||||
document.addEventListener(
|
||||
"hide.bs.modal",
|
||||
function (event) {
|
||||
// Get the modal that's closing
|
||||
const modalElement = event.target;
|
||||
|
||||
// Blur any focused element inside the modal before it closes
|
||||
const focusedElement = document.activeElement;
|
||||
if (focusedElement && modalElement.contains(focusedElement)) {
|
||||
focusedElement.blur();
|
||||
}
|
||||
},
|
||||
true
|
||||
); // Use capture phase to run before Bootstrap's handlers
|
||||
})();
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
let postsData = [];
|
||||
let postModal;
|
||||
let quillEditor;
|
||||
let isModalExpanded = false;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
postModal = new bootstrap.Modal(document.getElementById("postModal"));
|
||||
initializeQuillEditor();
|
||||
checkAuth().then((authenticated) => {
|
||||
if (authenticated) {
|
||||
loadPosts();
|
||||
@@ -24,16 +27,224 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
});
|
||||
|
||||
function resetModalSize() {
|
||||
const modalDialog = document.querySelector("#postModal .modal-dialog");
|
||||
const expandIcon = document.getElementById("expandIcon");
|
||||
const expandText = document.querySelector("#btnExpandModal span");
|
||||
const editor = document.getElementById("postContentEditor");
|
||||
|
||||
if (modalDialog && expandIcon && expandText && editor) {
|
||||
modalDialog.classList.remove("modal-fullscreen");
|
||||
modalDialog.classList.add("modal-xl");
|
||||
expandIcon.className = "bi bi-arrows-fullscreen";
|
||||
expandText.textContent = "Expand";
|
||||
editor.style.height = "400px";
|
||||
const container = editor.querySelector(".ql-container");
|
||||
if (container) {
|
||||
container.style.height = "calc(400px - 42px)";
|
||||
}
|
||||
isModalExpanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleModalSize() {
|
||||
const modalDialog = document.querySelector("#postModal .modal-dialog");
|
||||
const expandIcon = document.getElementById("expandIcon");
|
||||
const expandText = document.querySelector("#btnExpandModal span");
|
||||
const editor = document.getElementById("postContentEditor");
|
||||
|
||||
if (!modalDialog || !expandIcon || !expandText || !editor) {
|
||||
console.error("Modal elements not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isModalExpanded) {
|
||||
// Collapse to normal size
|
||||
modalDialog.classList.remove("modal-fullscreen");
|
||||
modalDialog.classList.add("modal-xl");
|
||||
expandIcon.className = "bi bi-arrows-fullscreen";
|
||||
expandText.textContent = "Expand";
|
||||
editor.style.height = "400px";
|
||||
const container = editor.querySelector(".ql-container");
|
||||
if (container) {
|
||||
container.style.height = "calc(400px - 42px)";
|
||||
}
|
||||
isModalExpanded = false;
|
||||
} else {
|
||||
// Expand to fullscreen
|
||||
modalDialog.classList.remove("modal-xl");
|
||||
modalDialog.classList.add("modal-fullscreen");
|
||||
expandIcon.className = "bi bi-fullscreen-exit";
|
||||
expandText.textContent = "Collapse";
|
||||
editor.style.height = "60vh";
|
||||
const container = editor.querySelector(".ql-container");
|
||||
if (container) {
|
||||
container.style.height = "calc(60vh - 42px)";
|
||||
}
|
||||
isModalExpanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function initializeQuillEditor() {
|
||||
quillEditor = new Quill("#postContentEditor", {
|
||||
theme: "snow",
|
||||
placeholder: "Write your blog post content here...",
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
["bold", "italic", "underline", "strike"],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
[{ color: [] }, { background: [] }],
|
||||
["link", "image"],
|
||||
["blockquote", "code-block"],
|
||||
["clean"],
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
`;
|
||||
|
||||
// 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 closeMediaLibrary() {
|
||||
const backdrop = document.getElementById("mediaLibraryBackdrop");
|
||||
if (backdrop) {
|
||||
backdrop.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function updateFeaturedImagePreview(url) {
|
||||
const preview = document.getElementById("featuredImagePreview");
|
||||
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;" />
|
||||
<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 = "";
|
||||
}
|
||||
}
|
||||
|
||||
function removeFeaturedImage() {
|
||||
document.getElementById("postFeaturedImage").value = "";
|
||||
updateFeaturedImagePreview("");
|
||||
showToast("Featured image removed", "info");
|
||||
}
|
||||
|
||||
async function loadPosts() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/blog", { credentials: "include" });
|
||||
const data = await response.json();
|
||||
console.log("Blog API Response:", data);
|
||||
if (data.success) {
|
||||
postsData = data.posts;
|
||||
console.log("Loaded posts:", postsData);
|
||||
renderPosts(postsData);
|
||||
} else {
|
||||
console.error("API returned success=false:", data);
|
||||
const tbody = document.getElementById("postsTableBody");
|
||||
tbody.innerHTML = `
|
||||
<tr><td colspan="7" class="text-center p-4 text-danger">
|
||||
<i class="bi bi-exclamation-triangle" style="font-size: 3rem;"></i>
|
||||
<p class="mt-3">Failed to load posts: ${
|
||||
data.message || "Unknown error"
|
||||
}</p>
|
||||
</td></tr>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load posts:", error);
|
||||
const tbody = document.getElementById("postsTableBody");
|
||||
tbody.innerHTML = `
|
||||
<tr><td colspan="7" class="text-center p-4 text-danger">
|
||||
<i class="bi bi-exclamation-triangle" style="font-size: 3rem;"></i>
|
||||
<p class="mt-3">Error loading posts. Please refresh the page.</p>
|
||||
</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,22 +266,24 @@ function renderPosts(posts) {
|
||||
.map(
|
||||
(p) => `
|
||||
<tr>
|
||||
<td>${p.id}</td>
|
||||
<td>${escapeHtml(String(p.id))}</td>
|
||||
<td><strong>${escapeHtml(p.title)}</strong></td>
|
||||
<td><code>${escapeHtml(p.slug)}</code></td>
|
||||
<td>${escapeHtml((p.excerpt || "").substring(0, 40))}...</td>
|
||||
<td><span class="badge ${
|
||||
p.ispublished ? "badge-success" : "badge-warning"
|
||||
p.ispublished ? "bg-success text-white" : "bg-warning text-dark"
|
||||
}">
|
||||
${p.ispublished ? "Published" : "Draft"}</span></td>
|
||||
<td>${formatDate(p.createdat)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-info" onclick="editPost(${p.id})">
|
||||
<button class="btn btn-sm btn-info" onclick="editPost('${escapeHtml(
|
||||
String(p.id)
|
||||
)}')">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deletePost(${
|
||||
p.id
|
||||
}, '${escapeHtml(p.title)}')">
|
||||
<button class="btn btn-sm btn-danger" onclick="deletePost('${escapeHtml(
|
||||
String(p.id)
|
||||
)}', '${escapeHtml(p.title).replace(/'/g, "'")}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
@@ -94,6 +307,12 @@ function showCreatePost() {
|
||||
document.getElementById("postForm").reset();
|
||||
document.getElementById("postId").value = "";
|
||||
document.getElementById("postPublished").checked = false;
|
||||
document.getElementById("postFeaturedImage").value = "";
|
||||
updateFeaturedImagePreview("");
|
||||
if (quillEditor) {
|
||||
quillEditor.setContents([]);
|
||||
}
|
||||
resetModalSize();
|
||||
postModal.show();
|
||||
}
|
||||
|
||||
@@ -110,33 +329,49 @@ async function editPost(id) {
|
||||
document.getElementById("postTitle").value = post.title;
|
||||
document.getElementById("postSlug").value = post.slug;
|
||||
document.getElementById("postExcerpt").value = post.excerpt || "";
|
||||
document.getElementById("postContent").value = post.content || "";
|
||||
|
||||
// Set Quill content
|
||||
if (quillEditor) {
|
||||
quillEditor.root.innerHTML = post.content || "";
|
||||
}
|
||||
|
||||
// Set featured image
|
||||
const featuredImage = post.featuredimage || post.imageurl || "";
|
||||
document.getElementById("postFeaturedImage").value = featuredImage;
|
||||
updateFeaturedImagePreview(featuredImage);
|
||||
|
||||
document.getElementById("postMetaTitle").value = post.metatitle || "";
|
||||
document.getElementById("postMetaDescription").value =
|
||||
post.metadescription || "";
|
||||
document.getElementById("postPublished").checked = post.ispublished;
|
||||
resetModalSize();
|
||||
postModal.show();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load post:", error);
|
||||
showError("Failed to load post details");
|
||||
showToast("Failed to load post details", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function savePost() {
|
||||
const id = document.getElementById("postId").value;
|
||||
|
||||
// Get content from Quill editor
|
||||
const content = quillEditor ? quillEditor.root.innerHTML : "";
|
||||
|
||||
const formData = {
|
||||
title: document.getElementById("postTitle").value,
|
||||
slug: document.getElementById("postSlug").value,
|
||||
excerpt: document.getElementById("postExcerpt").value,
|
||||
content: document.getElementById("postContent").value,
|
||||
content: content,
|
||||
featuredimage: document.getElementById("postFeaturedImage").value,
|
||||
metatitle: document.getElementById("postMetaTitle").value,
|
||||
metadescription: document.getElementById("postMetaDescription").value,
|
||||
ispublished: document.getElementById("postPublished").checked,
|
||||
};
|
||||
|
||||
if (!formData.title || !formData.slug || !formData.content) {
|
||||
showError("Please fill in all required fields");
|
||||
showToast("Please fill in all required fields", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -152,17 +387,18 @@ async function savePost() {
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showSuccess(
|
||||
id ? "Post updated successfully" : "Post created successfully"
|
||||
showToast(
|
||||
id ? "Post updated successfully" : "Post created successfully",
|
||||
"success"
|
||||
);
|
||||
postModal.hide();
|
||||
loadPosts();
|
||||
} else {
|
||||
showError(data.message || "Failed to save post");
|
||||
showToast(data.message || "Failed to save post", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save post:", error);
|
||||
showError("Failed to save post");
|
||||
showToast("Failed to save post", "error");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,17 +411,53 @@ async function deletePost(id, title) {
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showSuccess("Post deleted successfully");
|
||||
showToast("Post deleted successfully", "success");
|
||||
loadPosts();
|
||||
} else {
|
||||
showError(data.message || "Failed to delete post");
|
||||
showToast(data.message || "Failed to delete post", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete post:", error);
|
||||
showError("Failed to delete post");
|
||||
showToast("Failed to delete post", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type = "info") {
|
||||
const toastContainer =
|
||||
document.getElementById("toastContainer") || createToastContainer();
|
||||
const toast = document.createElement("div");
|
||||
toast.className = `toast toast-${type}`;
|
||||
|
||||
const icons = {
|
||||
success: "check-circle-fill",
|
||||
error: "exclamation-triangle-fill",
|
||||
warning: "exclamation-circle-fill",
|
||||
info: "info-circle-fill",
|
||||
};
|
||||
|
||||
toast.innerHTML = `
|
||||
<i class="bi bi-${icons[type] || icons.info}"></i>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
setTimeout(() => toast.classList.add("show"), 10);
|
||||
setTimeout(() => {
|
||||
toast.classList.remove("show");
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function createToastContainer() {
|
||||
const container = document.createElement("div");
|
||||
container.id = "toastContainer";
|
||||
container.style.cssText =
|
||||
"position: fixed; top: 80px; right: 20px; z-index: 9999;";
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
function slugify(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
@@ -194,18 +466,6 @@ function slugify(text) {
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/logout", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (response.ok) window.location.href = "/admin/login.html";
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
"&": "&",
|
||||
@@ -224,10 +484,3 @@ function formatDate(dateString) {
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
alert(message);
|
||||
}
|
||||
function showError(message) {
|
||||
alert("Error: " + message);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,91 @@
|
||||
// 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", {
|
||||
@@ -18,14 +94,58 @@ async function loadHomepageSettings() {
|
||||
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 =
|
||||
@@ -34,12 +154,32 @@ function populateFields() {
|
||||
homepageData.hero.headline || "";
|
||||
document.getElementById("heroSubheading").value =
|
||||
homepageData.hero.subheading || "";
|
||||
document.getElementById("heroDescription").value =
|
||||
homepageData.hero.description || "";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -49,8 +189,46 @@ function populateFields() {
|
||||
homepageData.promotion.enabled !== false;
|
||||
document.getElementById("promotionTitle").value =
|
||||
homepageData.promotion.title || "";
|
||||
document.getElementById("promotionDescription").value =
|
||||
homepageData.promotion.description || "";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -60,12 +238,33 @@ function populateFields() {
|
||||
homepageData.portfolio.enabled !== false;
|
||||
document.getElementById("portfolioTitle").value =
|
||||
homepageData.portfolio.title || "";
|
||||
document.getElementById("portfolioDescription").value =
|
||||
homepageData.portfolio.description || "";
|
||||
|
||||
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) {
|
||||
@@ -75,80 +274,258 @@ function toggleSection(sectionName) {
|
||||
|
||||
if (enabled) {
|
||||
section.classList.remove("disabled");
|
||||
content
|
||||
.querySelectorAll("input, textarea, button, select")
|
||||
.forEach((el) => {
|
||||
el.disabled = false;
|
||||
});
|
||||
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, textarea, button, select")
|
||||
.forEach((el) => {
|
||||
el.disabled = true;
|
||||
});
|
||||
content.querySelectorAll("input, button, select").forEach((el) => {
|
||||
el.disabled = true;
|
||||
});
|
||||
// Disable Quill editor
|
||||
if (quillEditors[sectionName]) {
|
||||
quillEditors[sectionName].disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function previewImage(sectionName) {
|
||||
const fileInput =
|
||||
document.getElementById(`${sectionName}Background`) ||
|
||||
document.getElementById(`${sectionName}Image`);
|
||||
const preview = document.getElementById(`${sectionName}Preview`);
|
||||
// Open media library in a modal
|
||||
function openMediaLibrary(section, field) {
|
||||
currentMediaPicker = { section, field };
|
||||
|
||||
if (fileInput.files && fileInput.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
preview.classList.remove("empty");
|
||||
preview.innerHTML = `<img src="${e.target.result}" alt="Preview" />`;
|
||||
};
|
||||
reader.readAsDataURL(fileInput.files[0]);
|
||||
// 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 buttons = document.querySelectorAll(
|
||||
`#${sectionName}Section .alignment-btn`
|
||||
);
|
||||
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: document.getElementById("heroDescription").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: document.getElementById("promotionDescription").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: document.getElementById("portfolioDescription").value,
|
||||
description: quillEditors.portfolio.root.innerHTML,
|
||||
count: parseInt(document.getElementById("portfolioCount").value) || 6,
|
||||
},
|
||||
};
|
||||
@@ -164,8 +541,9 @@ async function saveHomepage() {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showSuccess(
|
||||
"Homepage settings saved successfully! Changes are now live."
|
||||
"Homepage settings saved successfully! Changes are now live on the frontend."
|
||||
);
|
||||
homepageData = settings;
|
||||
} else {
|
||||
showError(data.message || "Failed to save homepage settings");
|
||||
}
|
||||
@@ -175,22 +553,30 @@ async function saveHomepage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/logout", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (response.ok) window.location.href = "/admin/login.html";
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
alert(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) {
|
||||
alert("Error: " + 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);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,23 @@
|
||||
|
||||
let projectsData = [];
|
||||
let projectModal;
|
||||
let quillEditor;
|
||||
let portfolioImages = [];
|
||||
let currentMediaPicker = null;
|
||||
let isModalExpanded = false;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
projectModal = new bootstrap.Modal(document.getElementById("projectModal"));
|
||||
|
||||
// Fix aria-hidden accessibility issue
|
||||
const projectModalElement = document.getElementById("projectModal");
|
||||
projectModalElement.addEventListener("hide.bs.modal", function () {
|
||||
document.querySelector(".btn.btn-primary")?.focus();
|
||||
});
|
||||
|
||||
// Initialize Quill editor
|
||||
initializeQuillEditor();
|
||||
|
||||
checkAuth().then((authenticated) => {
|
||||
if (authenticated) {
|
||||
loadProjects();
|
||||
@@ -17,14 +31,100 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
});
|
||||
|
||||
function resetModalSize() {
|
||||
const modalDialog = document.querySelector("#projectModal .modal-dialog");
|
||||
const expandIcon = document.getElementById("expandIcon");
|
||||
const expandText = document.querySelector("#btnExpandModal span");
|
||||
const editor = document.getElementById("projectDescriptionEditor");
|
||||
|
||||
if (modalDialog && expandIcon && expandText && editor) {
|
||||
modalDialog.classList.remove("modal-fullscreen");
|
||||
modalDialog.classList.add("modal-xl");
|
||||
expandIcon.className = "bi bi-arrows-fullscreen";
|
||||
expandText.textContent = "Expand";
|
||||
editor.style.height = "300px";
|
||||
const container = editor.querySelector(".ql-container");
|
||||
if (container) {
|
||||
container.style.height = "calc(300px - 42px)";
|
||||
}
|
||||
isModalExpanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleModalSize() {
|
||||
const modalDialog = document.querySelector("#projectModal .modal-dialog");
|
||||
const expandIcon = document.getElementById("expandIcon");
|
||||
const expandText = document.querySelector("#btnExpandModal span");
|
||||
const editor = document.getElementById("projectDescriptionEditor");
|
||||
|
||||
if (!modalDialog || !expandIcon || !expandText || !editor) {
|
||||
console.error("Modal elements not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isModalExpanded) {
|
||||
// Collapse to normal size
|
||||
modalDialog.classList.remove("modal-fullscreen");
|
||||
modalDialog.classList.add("modal-xl");
|
||||
expandIcon.className = "bi bi-arrows-fullscreen";
|
||||
expandText.textContent = "Expand";
|
||||
editor.style.height = "300px";
|
||||
const container = editor.querySelector(".ql-container");
|
||||
if (container) {
|
||||
container.style.height = "calc(300px - 42px)";
|
||||
}
|
||||
isModalExpanded = false;
|
||||
} else {
|
||||
// Expand to fullscreen
|
||||
modalDialog.classList.remove("modal-xl");
|
||||
modalDialog.classList.add("modal-fullscreen");
|
||||
expandIcon.className = "bi bi-fullscreen-exit";
|
||||
expandText.textContent = "Collapse";
|
||||
editor.style.height = "60vh";
|
||||
const container = editor.querySelector(".ql-container");
|
||||
if (container) {
|
||||
container.style.height = "calc(60vh - 42px)";
|
||||
}
|
||||
isModalExpanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Quill Editor
|
||||
function initializeQuillEditor() {
|
||||
quillEditor = new Quill("#projectDescriptionEditor", {
|
||||
theme: "snow",
|
||||
placeholder: "Describe your portfolio project here...",
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
["bold", "italic", "underline", "strike"],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
[{ color: [] }, { background: [] }],
|
||||
["link", "image"],
|
||||
["clean"],
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/portfolio/projects", {
|
||||
credentials: "include",
|
||||
cache: "no-cache", // Force fresh data
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
projectsData = data.projects;
|
||||
console.log(
|
||||
"📊 Loaded projects:",
|
||||
projectsData.map((p) => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
isactive: p.isactive,
|
||||
isactiveType: typeof p.isactive,
|
||||
}))
|
||||
);
|
||||
renderProjects(projectsData);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -47,28 +147,45 @@ function renderProjects(projects) {
|
||||
}
|
||||
|
||||
tbody.innerHTML = projects
|
||||
.map(
|
||||
(p) => `
|
||||
.map((p) => {
|
||||
// Explicitly check and log the status
|
||||
console.log(
|
||||
`Project ${p.id}: isactive =`,
|
||||
p.isactive,
|
||||
`(type: ${typeof p.isactive})`
|
||||
);
|
||||
const isActive =
|
||||
p.isactive === true || p.isactive === "true" || p.isactive === 1;
|
||||
console.log(` -> Evaluated as: ${isActive ? "ACTIVE" : "INACTIVE"}`);
|
||||
const statusClass = isActive
|
||||
? "bg-success text-white"
|
||||
: "bg-danger text-white";
|
||||
const statusText = isActive ? "Active" : "Inactive";
|
||||
const statusIcon = isActive ? "✓" : "✗";
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${p.id}</td>
|
||||
<td>${escapeHtml(String(p.id))}</td>
|
||||
<td><strong>${escapeHtml(p.title)}</strong></td>
|
||||
<td>${escapeHtml((p.description || "").substring(0, 50))}...</td>
|
||||
<td>${p.category || "-"}</td>
|
||||
<td><span class="badge ${p.isactive ? "badge-success" : "badge-danger"}">
|
||||
${p.isactive ? "Active" : "Inactive"}</span></td>
|
||||
<td><span class="badge ${statusClass}">
|
||||
${statusIcon} ${statusText}</span></td>
|
||||
<td>${formatDate(p.createdat)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-info" onclick="editProject(${p.id})">
|
||||
<button class="btn btn-sm btn-info" onclick="editProject('${escapeHtml(
|
||||
String(p.id)
|
||||
)}')">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteProject(${
|
||||
p.id
|
||||
}, '${escapeHtml(p.title)}')">
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteProject('${escapeHtml(
|
||||
String(p.id)
|
||||
)}', '${escapeHtml(p.title).replace(/'/g, "'")}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>`
|
||||
)
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
@@ -85,6 +202,17 @@ function showCreateProject() {
|
||||
document.getElementById("projectForm").reset();
|
||||
document.getElementById("projectId").value = "";
|
||||
document.getElementById("projectActive").checked = true;
|
||||
|
||||
// Clear Quill editor
|
||||
if (quillEditor) {
|
||||
quillEditor.setContents([]);
|
||||
}
|
||||
|
||||
// Clear images
|
||||
portfolioImages = [];
|
||||
renderPortfolioImages();
|
||||
|
||||
resetModalSize();
|
||||
projectModal.show();
|
||||
}
|
||||
|
||||
@@ -100,10 +228,27 @@ async function editProject(id) {
|
||||
"Edit Portfolio Project";
|
||||
document.getElementById("projectId").value = project.id;
|
||||
document.getElementById("projectTitle").value = project.title;
|
||||
document.getElementById("projectDescription").value =
|
||||
project.description || "";
|
||||
|
||||
// Set Quill editor content
|
||||
if (quillEditor && project.description) {
|
||||
quillEditor.root.innerHTML = project.description;
|
||||
}
|
||||
|
||||
document.getElementById("projectCategory").value = project.category || "";
|
||||
document.getElementById("projectActive").checked = project.isactive;
|
||||
|
||||
// Load images if available (imageurl field or parse from description)
|
||||
portfolioImages = [];
|
||||
if (project.imageurl) {
|
||||
// If single image URL exists
|
||||
portfolioImages.push({
|
||||
url: project.imageurl,
|
||||
filename: project.imageurl.split("/").pop(),
|
||||
});
|
||||
}
|
||||
renderPortfolioImages();
|
||||
|
||||
resetModalSize();
|
||||
projectModal.show();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -114,15 +259,21 @@ async function editProject(id) {
|
||||
|
||||
async function saveProject() {
|
||||
const id = document.getElementById("projectId").value;
|
||||
|
||||
// Get description from Quill editor
|
||||
const description = quillEditor.root.innerHTML;
|
||||
|
||||
const formData = {
|
||||
title: document.getElementById("projectTitle").value,
|
||||
description: document.getElementById("projectDescription").value,
|
||||
description: description,
|
||||
category: document.getElementById("projectCategory").value,
|
||||
isactive: document.getElementById("projectActive").checked,
|
||||
imageurl: portfolioImages.length > 0 ? portfolioImages[0].url : null,
|
||||
images: portfolioImages.map((img) => img.url),
|
||||
};
|
||||
|
||||
if (!formData.title || !formData.description) {
|
||||
showError("Please fill in all required fields");
|
||||
showError("Please fill in all required fields (Title and Description)");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,7 +292,9 @@ async function saveProject() {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showSuccess(
|
||||
id ? "Project updated successfully" : "Project created successfully"
|
||||
id
|
||||
? "Project updated successfully! 🎉"
|
||||
: "Project created successfully! 🎉"
|
||||
);
|
||||
projectModal.hide();
|
||||
loadProjects();
|
||||
@@ -174,18 +327,6 @@ async function deleteProject(id, name) {
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/logout", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (response.ok) window.location.href = "/admin/login.html";
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
"&": "&",
|
||||
@@ -205,9 +346,213 @@ function formatDate(dateString) {
|
||||
});
|
||||
}
|
||||
|
||||
// Render portfolio images gallery
|
||||
function renderPortfolioImages() {
|
||||
const gallery = document.getElementById("portfolioImagesGallery");
|
||||
|
||||
if (!gallery) return;
|
||||
|
||||
if (portfolioImages.length === 0) {
|
||||
gallery.innerHTML = `
|
||||
<div class="text-muted small">
|
||||
No images added yet. Click above to add images.
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
gallery.innerHTML = portfolioImages
|
||||
.map(
|
||||
(img, index) => `
|
||||
<div class="position-relative" style="width: 100px; height: 100px;">
|
||||
<img
|
||||
src="${img.url}"
|
||||
alt="${img.filename}"
|
||||
class="img-thumbnail w-100 h-100 object-fit-cover"
|
||||
title="${img.filename}"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger position-absolute top-0 end-0 m-1 p-1"
|
||||
onclick="removePortfolioImage(${index})"
|
||||
style="line-height: 1; width: 24px; height: 24px; font-size: 12px;"
|
||||
>
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// Remove portfolio image
|
||||
function removePortfolioImage(index) {
|
||||
portfolioImages.splice(index, 1);
|
||||
renderPortfolioImages();
|
||||
}
|
||||
|
||||
// Media Library Integration
|
||||
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;
|
||||
`;
|
||||
|
||||
// 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();
|
||||
}
|
||||
currentMediaPicker = null;
|
||||
}
|
||||
|
||||
function handleMediaSelection(media) {
|
||||
if (!currentMediaPicker) return;
|
||||
|
||||
if (currentMediaPicker.purpose === "portfolioImages") {
|
||||
// Handle multiple images
|
||||
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)) {
|
||||
portfolioImages.push({
|
||||
url: item.url,
|
||||
filename: item.filename || item.url.split("/").pop(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
renderPortfolioImages();
|
||||
showSuccess(`${mediaArray.length} image(s) added to portfolio gallery`);
|
||||
}
|
||||
|
||||
closeMediaLibrary();
|
||||
}
|
||||
|
||||
// Toast Notification System
|
||||
function showSuccess(message) {
|
||||
alert(message);
|
||||
showToast(message, "success");
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
alert("Error: " + message);
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
function showToast(message, type = "info") {
|
||||
// Create toast container if it doesn't exist
|
||||
let container = document.getElementById("toastContainer");
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = "toastContainer";
|
||||
container.className = "toast-container";
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement("div");
|
||||
toast.className = `toast toast-${type} toast-show`;
|
||||
|
||||
// Set icon based on type
|
||||
let icon = "";
|
||||
if (type === "success") {
|
||||
icon = '<i class="bi bi-check-circle-fill"></i>';
|
||||
} else if (type === "error") {
|
||||
icon = '<i class="bi bi-exclamation-circle-fill"></i>';
|
||||
} else if (type === "info") {
|
||||
icon = '<i class="bi bi-info-circle-fill"></i>';
|
||||
} else if (type === "warning") {
|
||||
icon = '<i class="bi bi-exclamation-triangle-fill"></i>';
|
||||
}
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">${icon}</div>
|
||||
<div class="toast-message">${escapeHtml(message)}</div>
|
||||
<button class="toast-close" onclick="this.parentElement.remove()">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Auto remove after 4 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.remove("toast-show");
|
||||
toast.classList.add("toast-hide");
|
||||
setTimeout(() => {
|
||||
if (toast.parentElement) {
|
||||
toast.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,55 @@
|
||||
|
||||
let productsData = [];
|
||||
let productModal;
|
||||
let quillEditor;
|
||||
let imageVariants = [];
|
||||
let productImages = []; // Stores general product images
|
||||
let currentMediaPicker = null; // Tracks which field is selecting media
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Initialize Bootstrap modal
|
||||
productModal = new bootstrap.Modal(document.getElementById("productModal"));
|
||||
const productModalElement = document.getElementById("productModal");
|
||||
productModal = new bootstrap.Modal(productModalElement);
|
||||
|
||||
// Fix aria-hidden accessibility issue: move focus before modal hides
|
||||
productModalElement.addEventListener("hide.bs.modal", function () {
|
||||
// Move focus to a safe element outside the modal before it gets aria-hidden
|
||||
document.getElementById("btnAddProduct")?.focus();
|
||||
});
|
||||
|
||||
// Initialize Quill editor
|
||||
initializeQuillEditor();
|
||||
|
||||
// Add event listeners for buttons
|
||||
const btnAddProduct = document.getElementById("btnAddProduct");
|
||||
if (btnAddProduct) {
|
||||
btnAddProduct.addEventListener("click", showCreateProduct);
|
||||
}
|
||||
|
||||
// Add event listener for search input
|
||||
const searchInput = document.getElementById("searchInput");
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", filterProducts);
|
||||
}
|
||||
|
||||
// Add event listener for save product button
|
||||
const btnSaveProduct = document.getElementById("btnSaveProduct");
|
||||
if (btnSaveProduct) {
|
||||
btnSaveProduct.addEventListener("click", saveProduct);
|
||||
}
|
||||
|
||||
// Add event listener for logout button
|
||||
const btnLogout = document.getElementById("btnLogout");
|
||||
if (btnLogout) {
|
||||
btnLogout.addEventListener("click", logout);
|
||||
}
|
||||
|
||||
// Add event listener for add image variant button
|
||||
const btnAddImageVariant = document.getElementById("btnAddImageVariant");
|
||||
if (btnAddImageVariant) {
|
||||
btnAddImageVariant.addEventListener("click", addImageVariantField);
|
||||
}
|
||||
|
||||
// Check authentication (from auth.js)
|
||||
checkAuth().then((authenticated) => {
|
||||
@@ -22,6 +66,24 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Quill Editor
|
||||
function initializeQuillEditor() {
|
||||
quillEditor = new Quill("#productDescriptionEditor", {
|
||||
theme: "snow",
|
||||
placeholder: "Write your product description here...",
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
["bold", "italic", "underline", "strike"],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
[{ color: [] }, { background: [] }],
|
||||
["link", "image"],
|
||||
["clean"],
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Load all products
|
||||
async function loadProducts() {
|
||||
try {
|
||||
@@ -50,12 +112,19 @@ function renderProducts(products) {
|
||||
<td colspan="8" class="text-center p-4">
|
||||
<i class="bi bi-inbox" style="font-size: 3rem; color: #ccc;"></i>
|
||||
<p class="mt-3 text-muted">No products found</p>
|
||||
<button class="btn btn-primary" onclick="showCreateProduct()">
|
||||
<button class="btn btn-primary" id="btnAddFirstProduct">
|
||||
<i class="bi bi-plus-circle"></i> Add Your First Product
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
// Add event listener to the "Add First Product" button
|
||||
setTimeout(() => {
|
||||
const btn = document.getElementById("btnAddFirstProduct");
|
||||
if (btn) {
|
||||
btn.addEventListener("click", showCreateProduct);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,14 +152,14 @@ function renderProducts(products) {
|
||||
</td>
|
||||
<td>${formatDate(product.createdat)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-info" onclick="editProduct(${
|
||||
<button class="btn btn-sm btn-info" data-action="edit" data-id="${
|
||||
product.id
|
||||
})">
|
||||
}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteProduct(${
|
||||
<button class="btn btn-sm btn-danger" data-action="delete" data-id="${
|
||||
product.id
|
||||
}, '${escapeHtml(product.name)}')">
|
||||
}" data-name="${escapeHtml(product.name)}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
@@ -98,6 +167,17 @@ function renderProducts(products) {
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
// Add event listeners to edit and delete buttons
|
||||
tbody.querySelectorAll('button[data-action="edit"]').forEach((btn) => {
|
||||
btn.addEventListener("click", () => editProduct(btn.dataset.id));
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('button[data-action="delete"]').forEach((btn) => {
|
||||
btn.addEventListener("click", () =>
|
||||
deleteProduct(btn.dataset.id, btn.dataset.name)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter products
|
||||
@@ -115,9 +195,333 @@ function showCreateProduct() {
|
||||
document.getElementById("productForm").reset();
|
||||
document.getElementById("productId").value = "";
|
||||
document.getElementById("productActive").checked = true;
|
||||
|
||||
// Clear Quill editor
|
||||
if (quillEditor) {
|
||||
quillEditor.setContents([]);
|
||||
}
|
||||
|
||||
// Clear arrays
|
||||
productImages = [];
|
||||
imageVariants = [];
|
||||
renderProductImages();
|
||||
renderImageVariants();
|
||||
|
||||
productModal.show();
|
||||
}
|
||||
|
||||
// Add image variant field
|
||||
function addImageVariantField() {
|
||||
const variant = {
|
||||
id: Date.now().toString(),
|
||||
image_url: "",
|
||||
color_variant: "",
|
||||
color_code: "#000000",
|
||||
alt_text: "",
|
||||
variant_price: null,
|
||||
variant_stock: 0,
|
||||
is_primary: imageVariants.length === 0,
|
||||
};
|
||||
imageVariants.push(variant);
|
||||
renderImageVariants();
|
||||
}
|
||||
|
||||
// Render product images gallery
|
||||
function renderProductImages() {
|
||||
const gallery = document.getElementById("productImagesGallery");
|
||||
|
||||
if (!gallery) return;
|
||||
|
||||
if (productImages.length === 0) {
|
||||
gallery.innerHTML = `
|
||||
<div class="text-muted small">
|
||||
No images added yet. Click above to add images.
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
gallery.innerHTML = productImages
|
||||
.map(
|
||||
(img, index) => `
|
||||
<div class="position-relative" style="width: 100px; height: 100px;">
|
||||
<img
|
||||
src="${img.url}"
|
||||
alt="${img.filename}"
|
||||
class="img-thumbnail w-100 h-100 object-fit-cover"
|
||||
title="${img.filename}"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger position-absolute top-0 end-0 m-1 p-1"
|
||||
onclick="removeProductImage(${index})"
|
||||
style="line-height: 1; width: 24px; height: 24px; font-size: 12px;"
|
||||
>
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// Remove product image
|
||||
function removeProductImage(index) {
|
||||
productImages.splice(index, 1);
|
||||
renderProductImages();
|
||||
}
|
||||
|
||||
// Render image variant fields
|
||||
function renderImageVariants() {
|
||||
const container = document.getElementById("imageVariantsContainer");
|
||||
|
||||
if (imageVariants.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center text-muted p-3">
|
||||
<i class="bi bi-palette" style="font-size: 2rem;"></i>
|
||||
<p class="mb-0 mt-2">No color variants added yet. Add product images above first, then create color variants here.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = imageVariants
|
||||
.map((variant, index) => {
|
||||
// Generate image picker HTML with thumbnails
|
||||
const imagePickerHTML =
|
||||
productImages.length > 0
|
||||
? `
|
||||
<div class="image-picker-grid" data-variant-id="${variant.id}">
|
||||
${productImages
|
||||
.map((img, idx) => {
|
||||
const isSelected = img.url === variant.image_url;
|
||||
return `
|
||||
<div class="image-picker-item ${isSelected ? "selected" : ""}"
|
||||
data-image-url="${img.url}"
|
||||
data-variant-id="${variant.id}"
|
||||
title="${img.filename || "Image " + (idx + 1)}">
|
||||
<img src="${img.url}" alt="${
|
||||
img.filename || "Image " + (idx + 1)
|
||||
}">
|
||||
<div class="image-picker-overlay">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
</div>
|
||||
<small class="image-picker-label">${
|
||||
img.filename || "Image " + (idx + 1)
|
||||
}</small>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</div>
|
||||
`
|
||||
: '<small class="text-danger">Add product images first</small>';
|
||||
|
||||
return `
|
||||
<div class="image-variant-item mb-3 p-3 border rounded" data-variant-id="${
|
||||
variant.id
|
||||
}" style="background: #f8f9fa;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-palette"></i> Color Variant ${index + 1}
|
||||
${
|
||||
variant.is_primary
|
||||
? '<span class="badge bg-primary ms-2">Primary</span>'
|
||||
: ""
|
||||
}
|
||||
</h6>
|
||||
<button type="button" class="btn btn-sm btn-danger" data-action="remove" data-variant-id="${
|
||||
variant.id
|
||||
}">
|
||||
<i class="bi bi-trash"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Image Selector with Visual Preview -->
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label small fw-bold">Select Image *</label>
|
||||
${imagePickerHTML}
|
||||
</div>
|
||||
|
||||
<!-- Color Name -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label small fw-bold">Color Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="e.g., Ruby Red, Ocean Blue"
|
||||
value="${variant.color_variant || ""}"
|
||||
data-field="color_variant"
|
||||
data-variant-id="${variant.id}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Color Picker -->
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label small fw-bold">Color Code</label>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<input
|
||||
type="color"
|
||||
class="form-control form-control-color"
|
||||
value="${variant.color_code || "#000000"}"
|
||||
data-field="color_code"
|
||||
data-variant-id="${variant.id}"
|
||||
style="width: 60px; height: 38px;"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="#000000"
|
||||
value="${variant.color_code || ""}"
|
||||
data-field="color_code_text"
|
||||
data-variant-id="${variant.id}"
|
||||
maxlength="7"
|
||||
style="font-family: monospace;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variant Price -->
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label small fw-bold">Variant Price</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">$</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Optional"
|
||||
value="${variant.variant_price || ""}"
|
||||
data-field="variant_price"
|
||||
data-variant-id="${variant.id}"
|
||||
step="0.01"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
<small class="text-muted">Leave empty to use base price</small>
|
||||
</div>
|
||||
|
||||
<!-- Variant Stock -->
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label small fw-bold">Stock Quantity *</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="0"
|
||||
value="${variant.variant_stock || 0}"
|
||||
data-field="variant_stock"
|
||||
data-variant-id="${variant.id}"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Primary Checkbox -->
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label small fw-bold">Primary Image</label>
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="radio"
|
||||
class="form-check-input"
|
||||
name="primaryVariant"
|
||||
${variant.is_primary ? "checked" : ""}
|
||||
data-field="is_primary"
|
||||
data-variant-id="${variant.id}"
|
||||
/>
|
||||
<label class="form-check-label small">
|
||||
Set as primary
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alt Text -->
|
||||
<div class="row">
|
||||
<div class="col-12 mb-2">
|
||||
<label class="form-label small">Alt Text (for accessibility)</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Description of the image"
|
||||
value="${variant.alt_text || ""}"
|
||||
data-field="alt_text"
|
||||
data-variant-id="${variant.id}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
// Add event listeners for remove buttons
|
||||
container.querySelectorAll('[data-action="remove"]').forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
const id = e.currentTarget.dataset.variantId;
|
||||
imageVariants = imageVariants.filter((v) => v.id !== id);
|
||||
renderImageVariants();
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners for image picker items
|
||||
container.querySelectorAll(".image-picker-item").forEach((item) => {
|
||||
item.addEventListener("click", (e) => {
|
||||
const variantId = e.currentTarget.dataset.variantId;
|
||||
const imageUrl = e.currentTarget.dataset.imageUrl;
|
||||
const variant = imageVariants.find((v) => v.id === variantId);
|
||||
|
||||
if (variant) {
|
||||
variant.image_url = imageUrl;
|
||||
|
||||
// Update visual selection
|
||||
const pickerGrid = e.currentTarget.closest(".image-picker-grid");
|
||||
pickerGrid
|
||||
.querySelectorAll(".image-picker-item")
|
||||
.forEach((i) => i.classList.remove("selected"));
|
||||
e.currentTarget.classList.add("selected");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners for input changes
|
||||
container.querySelectorAll("[data-variant-id]").forEach((input) => {
|
||||
input.addEventListener("input", (e) => {
|
||||
const id = e.target.dataset.variantId;
|
||||
const field = e.target.dataset.field;
|
||||
const variant = imageVariants.find((v) => v.id === id);
|
||||
|
||||
if (variant) {
|
||||
if (field === "color_code_text") {
|
||||
// Update both color picker and text
|
||||
variant.color_code = e.target.value;
|
||||
const colorPicker = container.querySelector(
|
||||
`input[type="color"][data-variant-id="${id}"]`
|
||||
);
|
||||
if (colorPicker && /^#[0-9A-F]{6}$/i.test(e.target.value)) {
|
||||
colorPicker.value = e.target.value;
|
||||
}
|
||||
} else if (field === "color_code") {
|
||||
// Update both color picker and text
|
||||
variant.color_code = e.target.value;
|
||||
const colorText = container.querySelector(
|
||||
`input[data-field="color_code_text"][data-variant-id="${id}"]`
|
||||
);
|
||||
if (colorText) {
|
||||
colorText.value = e.target.value;
|
||||
}
|
||||
} else if (field === "is_primary") {
|
||||
// Set all to false, then this one to true
|
||||
imageVariants.forEach((v) => (v.is_primary = false));
|
||||
variant.is_primary = true;
|
||||
} else {
|
||||
variant[field] = e.target.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Edit product
|
||||
async function editProduct(id) {
|
||||
try {
|
||||
@@ -132,16 +536,48 @@ async function editProduct(id) {
|
||||
document.getElementById("modalTitle").textContent = "Edit Product";
|
||||
document.getElementById("productId").value = product.id;
|
||||
document.getElementById("productName").value = product.name;
|
||||
document.getElementById("productDescription").value =
|
||||
product.description || "";
|
||||
document.getElementById("productShortDescription").value =
|
||||
product.shortdescription || "";
|
||||
|
||||
// Set Quill editor content
|
||||
if (quillEditor && product.description) {
|
||||
quillEditor.root.innerHTML = product.description;
|
||||
}
|
||||
|
||||
document.getElementById("productPrice").value = product.price;
|
||||
document.getElementById("productStock").value =
|
||||
product.stockquantity || 0;
|
||||
document.getElementById("productSKU").value = product.sku || "";
|
||||
document.getElementById("productCategory").value = product.category || "";
|
||||
document.getElementById("productMaterial").value = product.material || "";
|
||||
document.getElementById("productDimensions").value =
|
||||
product.dimensions || "";
|
||||
document.getElementById("productWeight").value = product.weight || "";
|
||||
document.getElementById("productActive").checked = product.isactive;
|
||||
document.getElementById("productFeatured").checked =
|
||||
product.isfeatured || false;
|
||||
document.getElementById("productBestSeller").checked =
|
||||
product.isbestseller || false;
|
||||
|
||||
// Load image variants and extract unique product images
|
||||
imageVariants = product.images || [];
|
||||
|
||||
// Build productImages array from unique image URLs in variants
|
||||
const uniqueImages = {};
|
||||
imageVariants.forEach((variant) => {
|
||||
if (variant.image_url && !uniqueImages[variant.image_url]) {
|
||||
uniqueImages[variant.image_url] = {
|
||||
url: variant.image_url,
|
||||
filename: variant.image_url.split("/").pop(),
|
||||
alt_text: variant.alt_text || "",
|
||||
};
|
||||
}
|
||||
});
|
||||
productImages = Object.values(uniqueImages);
|
||||
|
||||
renderProductImages();
|
||||
renderImageVariants();
|
||||
|
||||
productModal.show();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -153,19 +589,52 @@ async function editProduct(id) {
|
||||
// Save product
|
||||
async function saveProduct() {
|
||||
const id = document.getElementById("productId").value;
|
||||
|
||||
// Get description from Quill editor
|
||||
const description = quillEditor.root.innerHTML;
|
||||
|
||||
// Prepare images array for backend with all new fields
|
||||
const images = imageVariants.map((variant, index) => ({
|
||||
image_url: variant.image_url,
|
||||
color_variant: variant.color_variant || null,
|
||||
color_code: variant.color_code || null,
|
||||
alt_text: variant.alt_text || document.getElementById("productName").value,
|
||||
display_order: index,
|
||||
is_primary: variant.is_primary || false,
|
||||
variant_price: variant.variant_price
|
||||
? parseFloat(variant.variant_price)
|
||||
: null,
|
||||
variant_stock: parseInt(variant.variant_stock) || 0,
|
||||
}));
|
||||
|
||||
const formData = {
|
||||
name: document.getElementById("productName").value,
|
||||
description: document.getElementById("productDescription").value,
|
||||
shortdescription: document.getElementById("productShortDescription").value,
|
||||
description: description,
|
||||
price: parseFloat(document.getElementById("productPrice").value),
|
||||
stockquantity: parseInt(document.getElementById("productStock").value) || 0,
|
||||
sku: document.getElementById("productSKU").value,
|
||||
category: document.getElementById("productCategory").value,
|
||||
material: document.getElementById("productMaterial").value,
|
||||
dimensions: document.getElementById("productDimensions").value,
|
||||
weight: parseFloat(document.getElementById("productWeight").value) || null,
|
||||
isactive: document.getElementById("productActive").checked,
|
||||
isfeatured: document.getElementById("productFeatured").checked,
|
||||
isbestseller: document.getElementById("productBestSeller").checked,
|
||||
images: images,
|
||||
};
|
||||
|
||||
// Validation
|
||||
if (!formData.name || !formData.price) {
|
||||
showError("Please fill in all required fields");
|
||||
showError("Please fill in all required fields (Name and Price)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
imageVariants.length > 0 &&
|
||||
imageVariants.some((v) => !v.image_url || !v.color_variant)
|
||||
) {
|
||||
showError("All color variants must have an image and color name selected");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -185,7 +654,9 @@ async function saveProduct() {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showSuccess(
|
||||
id ? "Product updated successfully" : "Product created successfully"
|
||||
id
|
||||
? "Product updated successfully! 🎉"
|
||||
: "Product created successfully! 🎉"
|
||||
);
|
||||
productModal.hide();
|
||||
loadProducts();
|
||||
@@ -223,22 +694,131 @@ async function deleteProduct(id, name) {
|
||||
}
|
||||
}
|
||||
|
||||
// Logout function
|
||||
async function logout() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/logout", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
// ===== 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);
|
||||
}
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
currentMediaPicker = null;
|
||||
}
|
||||
|
||||
function handleMediaSelection(media) {
|
||||
if (!currentMediaPicker) return;
|
||||
|
||||
if (currentMediaPicker.purpose === "productImage") {
|
||||
// Handle multiple images
|
||||
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)) {
|
||||
productImages.push({
|
||||
url: item.url,
|
||||
alt_text: item.filename || "",
|
||||
filename: item.filename,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.href = "/admin/login.html";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
renderProductImages();
|
||||
showSuccess(`${mediaArray.length} image(s) added to product gallery`);
|
||||
}
|
||||
|
||||
closeMediaLibrary();
|
||||
}
|
||||
|
||||
// ===== UTILITY FUNCTIONS =====
|
||||
|
||||
// Utility functions
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
@@ -261,10 +841,57 @@ function formatDate(dateString) {
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
// Simple alert for now - can be replaced with toast notification
|
||||
alert(message);
|
||||
showToast(message, "success");
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
alert("Error: " + message);
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
function showToast(message, type = "info") {
|
||||
// Create toast container if it doesn't exist
|
||||
let container = document.getElementById("toastContainer");
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = "toastContainer";
|
||||
container.className = "toast-container";
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement("div");
|
||||
toast.className = `toast toast-${type} toast-show`;
|
||||
|
||||
// Set icon based on type
|
||||
let icon = "";
|
||||
if (type === "success") {
|
||||
icon = '<i class="bi bi-check-circle-fill"></i>';
|
||||
} else if (type === "error") {
|
||||
icon = '<i class="bi bi-exclamation-circle-fill"></i>';
|
||||
} else if (type === "info") {
|
||||
icon = '<i class="bi bi-info-circle-fill"></i>';
|
||||
} else if (type === "warning") {
|
||||
icon = '<i class="bi bi-exclamation-triangle-fill"></i>';
|
||||
}
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">${icon}</div>
|
||||
<div class="toast-message">${escapeHtml(message)}</div>
|
||||
<button class="toast-close" onclick="this.parentElement.remove()">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Auto remove after 4 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.remove("toast-show");
|
||||
toast.classList.add("toast-hide");
|
||||
setTimeout(() => {
|
||||
if (toast.parentElement) {
|
||||
toast.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
// Settings Management JavaScript
|
||||
|
||||
let currentSettings = {};
|
||||
let mediaLibraryModal;
|
||||
let currentMediaTarget = null;
|
||||
let selectedMediaUrl = null;
|
||||
let allMedia = [];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Load saved theme
|
||||
loadTheme();
|
||||
|
||||
checkAuth().then((authenticated) => {
|
||||
if (authenticated) {
|
||||
loadSettings();
|
||||
@@ -10,6 +34,128 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
});
|
||||
|
||||
// Toast Notification System - Make global for onclick handlers
|
||||
window.showToast = function (message, type = "success") {
|
||||
const container = document.getElementById("toastContainer");
|
||||
if (!container) {
|
||||
console.error("Toast container not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
const icons = {
|
||||
success: "bi-check-circle-fill",
|
||||
error: "bi-x-circle-fill",
|
||||
warning: "bi-exclamation-triangle-fill",
|
||||
info: "bi-info-circle-fill",
|
||||
};
|
||||
|
||||
const toast = document.createElement("div");
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">
|
||||
<i class="bi ${icons[type] || icons.info}"></i>
|
||||
</div>
|
||||
<div class="toast-message">${message}</div>
|
||||
<button class="toast-close" onclick="window.closeToast(this)">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Trigger animation
|
||||
setTimeout(() => toast.classList.add("toast-show"), 10);
|
||||
|
||||
// Add visual feedback for success saves
|
||||
if (type === "success" && message.includes("saved")) {
|
||||
const saveBtn = document.querySelector('button[onclick*="saveSettings"]');
|
||||
if (saveBtn) {
|
||||
const originalBg = saveBtn.style.background;
|
||||
const originalTransform = saveBtn.style.transform;
|
||||
saveBtn.style.background =
|
||||
"linear-gradient(135deg, #10b981 0%, #059669 100%)";
|
||||
saveBtn.style.transform = "scale(1.05)";
|
||||
saveBtn.innerHTML = '<i class="bi bi-check-lg"></i> Saved!';
|
||||
setTimeout(() => {
|
||||
saveBtn.style.background = originalBg;
|
||||
saveBtn.style.transform = originalTransform;
|
||||
saveBtn.innerHTML = '<i class="bi bi-save"></i> Save All Settings';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add("toast-hide");
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
window.closeToast = function (button) {
|
||||
const toast = button.closest(".toast");
|
||||
toast.classList.add("toast-hide");
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
};
|
||||
|
||||
// Theme Management - Make global for onclick handlers
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem("adminTheme") || "light";
|
||||
applyTheme(savedTheme);
|
||||
}
|
||||
|
||||
window.selectTheme = function (theme) {
|
||||
console.log("selectTheme called with:", theme);
|
||||
|
||||
// Update UI
|
||||
document.querySelectorAll(".theme-selector .theme-option").forEach((el) => {
|
||||
el.classList.remove("active");
|
||||
});
|
||||
event.target.closest(".theme-option").classList.add("active");
|
||||
|
||||
// Save and apply theme
|
||||
localStorage.setItem("adminTheme", theme);
|
||||
applyTheme(theme);
|
||||
window.showToast(`Theme changed to ${theme} mode`, "success");
|
||||
};
|
||||
|
||||
function applyTheme(theme) {
|
||||
console.log("applyTheme called with:", theme);
|
||||
const body = document.body;
|
||||
|
||||
if (theme === "dark") {
|
||||
body.classList.add("dark-mode");
|
||||
body.classList.remove("light-mode");
|
||||
} else if (theme === "light") {
|
||||
body.classList.add("light-mode");
|
||||
body.classList.remove("dark-mode");
|
||||
} else if (theme === "auto") {
|
||||
// Check system preference
|
||||
const prefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
if (prefersDark) {
|
||||
body.classList.add("dark-mode");
|
||||
body.classList.remove("light-mode");
|
||||
} else {
|
||||
body.classList.add("light-mode");
|
||||
body.classList.remove("dark-mode");
|
||||
}
|
||||
}
|
||||
|
||||
// Update active state in UI
|
||||
const themeOptions = document.querySelectorAll(
|
||||
".theme-selector .theme-option"
|
||||
);
|
||||
themeOptions.forEach((option, index) => {
|
||||
const themes = ["light", "dark", "auto"];
|
||||
if (themes[index] === theme) {
|
||||
option.classList.add("active");
|
||||
} else {
|
||||
option.classList.remove("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/settings", {
|
||||
@@ -38,6 +184,22 @@ function populateSettings() {
|
||||
currentSettings.general.sitePhone || "";
|
||||
document.getElementById("timezone").value =
|
||||
currentSettings.general.timezone || "UTC";
|
||||
|
||||
// Logo and Favicon
|
||||
if (currentSettings.general.siteLogo) {
|
||||
document.getElementById("siteLogo").value =
|
||||
currentSettings.general.siteLogo;
|
||||
document.getElementById(
|
||||
"logoPreview"
|
||||
).innerHTML = `<img src="${currentSettings.general.siteLogo}" alt="Logo" />`;
|
||||
}
|
||||
if (currentSettings.general.siteFavicon) {
|
||||
document.getElementById("siteFavicon").value =
|
||||
currentSettings.general.siteFavicon;
|
||||
document.getElementById(
|
||||
"faviconPreview"
|
||||
).innerHTML = `<img src="${currentSettings.general.siteFavicon}" alt="Favicon" />`;
|
||||
}
|
||||
}
|
||||
|
||||
// Homepage Settings
|
||||
@@ -88,45 +250,184 @@ function populateSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
function previewLogo() {
|
||||
const fileInput = document.getElementById("siteLogo");
|
||||
const preview = document.getElementById("logoPreview");
|
||||
// Media Library Functions - Make global for onclick handlers
|
||||
window.openMediaLibrary = async function (targetField) {
|
||||
console.log("openMediaLibrary called for:", targetField);
|
||||
currentMediaTarget = targetField;
|
||||
selectedMediaUrl = null;
|
||||
|
||||
if (fileInput.files && fileInput.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
preview.innerHTML = `<img src="${e.target.result}" alt="Logo" />`;
|
||||
};
|
||||
reader.readAsDataURL(fileInput.files[0]);
|
||||
// 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");
|
||||
}
|
||||
};
|
||||
|
||||
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");
|
||||
};
|
||||
|
||||
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;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
||||
}
|
||||
|
||||
function previewLogo() {
|
||||
const url = document.getElementById("siteLogo").value;
|
||||
const preview = document.getElementById("logoPreview");
|
||||
if (url) {
|
||||
preview.innerHTML = `<img src="${url}" alt="Logo" />`;
|
||||
}
|
||||
}
|
||||
|
||||
function previewFavicon() {
|
||||
const fileInput = document.getElementById("siteFavicon");
|
||||
const url = document.getElementById("siteFavicon").value;
|
||||
const preview = document.getElementById("faviconPreview");
|
||||
|
||||
if (fileInput.files && fileInput.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
preview.innerHTML = `<img src="${e.target.result}" alt="Favicon" />`;
|
||||
};
|
||||
reader.readAsDataURL(fileInput.files[0]);
|
||||
if (url) {
|
||||
preview.innerHTML = `<img src="${url}" alt="Favicon" />`;
|
||||
}
|
||||
}
|
||||
|
||||
function selectLayout(layout) {
|
||||
window.selectLayout = function (layout) {
|
||||
document.querySelectorAll(".theme-selector .theme-option").forEach((el) => {
|
||||
el.classList.remove("active");
|
||||
});
|
||||
event.target.closest(".theme-option").classList.add("active");
|
||||
}
|
||||
|
||||
function selectTheme(theme) {
|
||||
document.querySelectorAll(".theme-selector .theme-option").forEach((el) => {
|
||||
el.classList.remove("active");
|
||||
});
|
||||
event.target.closest(".theme-option").classList.add("active");
|
||||
}
|
||||
};
|
||||
|
||||
function updateColorPreview() {
|
||||
const color = document.getElementById("accentColor").value;
|
||||
@@ -134,7 +435,9 @@ function updateColorPreview() {
|
||||
document.getElementById("colorValue").textContent = color;
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
window.saveSettings = async function () {
|
||||
console.log("saveSettings called");
|
||||
|
||||
const settings = {
|
||||
general: {
|
||||
siteName: document.getElementById("siteName").value,
|
||||
@@ -142,6 +445,8 @@ async function saveSettings() {
|
||||
siteEmail: document.getElementById("siteEmail").value,
|
||||
sitePhone: document.getElementById("sitePhone").value,
|
||||
timezone: document.getElementById("timezone").value,
|
||||
siteLogo: document.getElementById("siteLogo").value,
|
||||
siteFavicon: document.getElementById("siteFavicon").value,
|
||||
},
|
||||
homepage: {
|
||||
showHero: document.getElementById("showHero").checked,
|
||||
@@ -171,6 +476,8 @@ async function saveSettings() {
|
||||
},
|
||||
};
|
||||
|
||||
console.log("Settings to save:", settings);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/admin/settings", {
|
||||
method: "POST",
|
||||
@@ -180,34 +487,16 @@ async function saveSettings() {
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Save response:", data);
|
||||
|
||||
if (data.success) {
|
||||
showSuccess("Settings saved successfully!");
|
||||
window.showToast("Settings saved successfully!", "success");
|
||||
currentSettings = settings;
|
||||
} else {
|
||||
showError(data.message || "Failed to save settings");
|
||||
window.showToast(data.message || "Failed to save settings", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save settings:", error);
|
||||
showError("Failed to save settings");
|
||||
window.showToast("Failed to save settings. Please try again.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/logout", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (response.ok) window.location.href = "/admin/login.html";
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
alert(message);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
alert("Error: " + message);
|
||||
}
|
||||
};
|
||||
|
||||
266
website/admin/js/team-members.js
Normal file
266
website/admin/js/team-members.js
Normal file
@@ -0,0 +1,266 @@
|
||||
let teamMemberModal, notificationModal, confirmationModal;
|
||||
let currentMemberId = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
teamMemberModal = new bootstrap.Modal(
|
||||
document.getElementById("teamMemberModal")
|
||||
);
|
||||
notificationModal = new bootstrap.Modal(
|
||||
document.getElementById("notificationModal")
|
||||
);
|
||||
confirmationModal = new bootstrap.Modal(
|
||||
document.getElementById("confirmationModal")
|
||||
);
|
||||
|
||||
checkAuth().then((authenticated) => {
|
||||
if (authenticated) {
|
||||
loadTeamMembers();
|
||||
}
|
||||
});
|
||||
|
||||
// Image preview on URL change
|
||||
document.getElementById("memberImage").addEventListener("input", function () {
|
||||
updateImagePreview(this.value);
|
||||
});
|
||||
});
|
||||
|
||||
// Load all team members
|
||||
async function loadTeamMembers() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/team-members");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.teamMembers) {
|
||||
displayTeamMembers(data.teamMembers);
|
||||
} else {
|
||||
showNotification("Failed to load team members", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading team members:", error);
|
||||
showNotification("Error loading team members", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Display team members in grid
|
||||
function displayTeamMembers(members) {
|
||||
const container = document.getElementById("teamMembersContainer");
|
||||
|
||||
if (members.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="bi bi-people" style="font-size: 4rem; color: #cbd5e0;"></i>
|
||||
<p class="mt-3 text-muted">No team members yet. Add your first team member!</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = members
|
||||
.map(
|
||||
(member) => `
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="team-preview-card">
|
||||
<div class="team-preview-image">
|
||||
${
|
||||
member.image_url
|
||||
? `<img src="${member.image_url}" alt="${member.name}" />`
|
||||
: `<i class="bi bi-person-circle"></i>`
|
||||
}
|
||||
</div>
|
||||
<div class="team-preview-name">${escapeHtml(member.name)}</div>
|
||||
<div class="team-preview-position">${escapeHtml(member.position)}</div>
|
||||
<div class="team-preview-bio">${
|
||||
member.bio ? escapeHtml(member.bio) : ""
|
||||
}</div>
|
||||
<div class="mt-3 d-flex justify-content-center gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="editTeamMember('${
|
||||
member.id
|
||||
}')">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="confirmDelete('${
|
||||
member.id
|
||||
}', '${escapeHtml(member.name)}')">
|
||||
<i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Order: ${member.display_order}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// Show add modal
|
||||
function showAddModal() {
|
||||
currentMemberId = null;
|
||||
document.getElementById("modalTitle").textContent = "Add Team Member";
|
||||
document.getElementById("teamMemberForm").reset();
|
||||
document.getElementById("memberId").value = "";
|
||||
document.getElementById("imagePreview").innerHTML = "";
|
||||
teamMemberModal.show();
|
||||
}
|
||||
|
||||
// Edit team member
|
||||
async function editTeamMember(id) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/team-members/${id}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.teamMember) {
|
||||
currentMemberId = id;
|
||||
const member = data.teamMember;
|
||||
|
||||
document.getElementById("modalTitle").textContent = "Edit Team Member";
|
||||
document.getElementById("memberId").value = member.id;
|
||||
document.getElementById("memberName").value = member.name;
|
||||
document.getElementById("memberPosition").value = member.position;
|
||||
document.getElementById("memberBio").value = member.bio || "";
|
||||
document.getElementById("memberImage").value = member.image_url || "";
|
||||
document.getElementById("displayOrder").value = member.display_order || 0;
|
||||
|
||||
updateImagePreview(member.image_url);
|
||||
teamMemberModal.show();
|
||||
} else {
|
||||
showNotification("Failed to load team member details", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading team member:", error);
|
||||
showNotification("Error loading team member", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Save team member (create or update)
|
||||
async function saveTeamMember() {
|
||||
const id = document.getElementById("memberId").value;
|
||||
const name = document.getElementById("memberName").value.trim();
|
||||
const position = document.getElementById("memberPosition").value.trim();
|
||||
const bio = document.getElementById("memberBio").value.trim();
|
||||
const image_url = document.getElementById("memberImage").value.trim();
|
||||
const display_order =
|
||||
parseInt(document.getElementById("displayOrder").value) || 0;
|
||||
|
||||
if (!name || !position) {
|
||||
showNotification("Name and position are required", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
position,
|
||||
bio,
|
||||
image_url,
|
||||
display_order,
|
||||
};
|
||||
|
||||
try {
|
||||
const url = id
|
||||
? `/api/admin/team-members/${id}`
|
||||
: "/api/admin/team-members";
|
||||
const method = id ? "PUT" : "POST";
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification(
|
||||
data.message || "Team member saved successfully",
|
||||
"success"
|
||||
);
|
||||
teamMemberModal.hide();
|
||||
loadTeamMembers();
|
||||
} else {
|
||||
showNotification(data.message || "Failed to save team member", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving team member:", error);
|
||||
showNotification("Error saving team member", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm delete
|
||||
function confirmDelete(id, name) {
|
||||
currentMemberId = id;
|
||||
document.getElementById(
|
||||
"confirmationMessage"
|
||||
).textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
|
||||
|
||||
const confirmBtn = document.getElementById("confirmButton");
|
||||
confirmBtn.onclick = () => deleteTeamMember(id);
|
||||
|
||||
confirmationModal.show();
|
||||
}
|
||||
|
||||
// Delete team member
|
||||
async function deleteTeamMember(id) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/team-members/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification("Team member deleted successfully", "success");
|
||||
confirmationModal.hide();
|
||||
loadTeamMembers();
|
||||
} else {
|
||||
showNotification("Failed to delete team member", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting team member:", error);
|
||||
showNotification("Error deleting team member", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Update image preview
|
||||
function updateImagePreview(url) {
|
||||
const preview = document.getElementById("imagePreview");
|
||||
if (url) {
|
||||
preview.innerHTML = `
|
||||
<img src="${url}" alt="Preview" style="max-width: 150px; max-height: 150px; border-radius: 50%; border: 3px solid #667eea;" />
|
||||
`;
|
||||
} else {
|
||||
preview.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Open media library (placeholder for future integration)
|
||||
function openMediaLibrary() {
|
||||
// For now, redirect to media library in a new window
|
||||
window.open("/admin/media-library.html", "_blank");
|
||||
showNotification(
|
||||
"Select an image from the media library and copy its URL back here",
|
||||
"success"
|
||||
);
|
||||
}
|
||||
|
||||
// Show notification
|
||||
function showNotification(message, type = "success") {
|
||||
const modal = document.getElementById("notificationModal");
|
||||
const header = modal.querySelector(".modal-header");
|
||||
const messageEl = document.getElementById("notificationMessage");
|
||||
const title = document.getElementById("notificationTitle");
|
||||
|
||||
header.className = "modal-header " + type;
|
||||
title.textContent = type === "success" ? "Success" : "Error";
|
||||
messageEl.textContent = message;
|
||||
|
||||
notificationModal.show();
|
||||
}
|
||||
|
||||
// Escape HTML
|
||||
function escapeHtml(text) {
|
||||
if (!text) return "";
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
@@ -313,18 +313,6 @@ function updatePermissionsPreview() {
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
const response = await fetch("/api/admin/logout", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (response.ok) window.location.href = "/admin/login.html";
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
"&": "&",
|
||||
|
||||
Reference in New Issue
Block a user