updateweb

This commit is contained in:
Local Server
2025-12-24 00:13:23 -06:00
parent e4b3de4a46
commit 017c6376fc
88 changed files with 17866 additions and 1191 deletions

View File

@@ -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
})();

View File

@@ -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;">&times;</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, "&#39;")}')">
<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 = {
"&": "&amp;",
@@ -224,10 +484,3 @@ function formatDate(dateString) {
day: "numeric",
});
}
function showSuccess(message) {
alert(message);
}
function showError(message) {
alert("Error: " + message);
}

View File

@@ -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

View File

@@ -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, "&#39;")}')">
<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 = {
"&": "&amp;",
@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
};

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

View File

@@ -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 = {
"&": "&amp;",