updateweb
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user