544 lines
16 KiB
JavaScript
544 lines
16 KiB
JavaScript
// Portfolio Management JavaScript
|
|
|
|
let projectsData = [];
|
|
let projectModal;
|
|
let quillEditor;
|
|
let portfolioImages = [];
|
|
let currentMediaPicker = null;
|
|
let isModalExpanded = false;
|
|
let portfolioMediaLibrary = null;
|
|
|
|
// Initialize portfolio media library
|
|
function initPortfolioMediaLibrary() {
|
|
if (typeof MediaLibrary !== "undefined" && !portfolioMediaLibrary) {
|
|
portfolioMediaLibrary = new MediaLibrary({
|
|
selectMode: true,
|
|
multiple: true, // Allow multiple image selection for portfolio gallery
|
|
onSelect: function (media) {
|
|
handleMediaSelection(media);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
projectModal = new bootstrap.Modal(document.getElementById("projectModal"));
|
|
|
|
// 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();
|
|
|
|
// Initialize media library
|
|
initPortfolioMediaLibrary();
|
|
|
|
checkAuth().then((authenticated) => {
|
|
if (authenticated) {
|
|
loadProjects();
|
|
}
|
|
});
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.get("action") === "create") {
|
|
showCreateProject();
|
|
}
|
|
});
|
|
|
|
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) {
|
|
console.error("Failed to load projects:", error);
|
|
}
|
|
}
|
|
|
|
function renderProjects(projects) {
|
|
const tbody = document.getElementById("projectsTableBody");
|
|
if (projects.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr><td colspan="7" class="text-center p-4">
|
|
<i class="bi bi-inbox" style="font-size: 3rem; color: #ccc;"></i>
|
|
<p class="mt-3 text-muted">No projects found</p>
|
|
<button class="btn btn-primary" onclick="showCreateProject()">
|
|
<i class="bi bi-plus-circle"></i> Add Your First Project
|
|
</button>
|
|
</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = projects
|
|
.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>${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 ${statusClass}">
|
|
${statusIcon} ${statusText}</span></td>
|
|
<td>${formatDate(p.createdat)}</td>
|
|
<td>
|
|
<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('${escapeHtml(
|
|
String(p.id),
|
|
)}', '${escapeHtml(p.title).replace(/'/g, "'")}')">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
function filterProjects() {
|
|
const searchTerm = document.getElementById("searchInput").value.toLowerCase();
|
|
const filtered = projectsData.filter((p) =>
|
|
p.title.toLowerCase().includes(searchTerm),
|
|
);
|
|
renderProjects(filtered);
|
|
}
|
|
|
|
function showCreateProject() {
|
|
document.getElementById("modalTitle").textContent = "Add Portfolio Project";
|
|
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();
|
|
}
|
|
|
|
async function editProject(id) {
|
|
try {
|
|
const response = await fetch(`/api/admin/portfolio/projects/${id}`, {
|
|
credentials: "include",
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
const project = data.project;
|
|
document.getElementById("modalTitle").textContent =
|
|
"Edit Portfolio Project";
|
|
document.getElementById("projectId").value = project.id;
|
|
document.getElementById("projectTitle").value = project.title;
|
|
|
|
// 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 - check images array first, then fall back to imageurl
|
|
portfolioImages = [];
|
|
|
|
// Try to parse images array
|
|
if (project.images) {
|
|
try {
|
|
const imagesArr =
|
|
typeof project.images === "string"
|
|
? JSON.parse(project.images)
|
|
: project.images;
|
|
if (Array.isArray(imagesArr) && imagesArr.length > 0) {
|
|
imagesArr.forEach((url) => {
|
|
portfolioImages.push({
|
|
url: url,
|
|
filename: url.split("/").pop(),
|
|
});
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.warn("Failed to parse images:", e);
|
|
}
|
|
}
|
|
|
|
// Fall back to imageurl if no images array
|
|
if (portfolioImages.length === 0 && project.imageurl) {
|
|
portfolioImages.push({
|
|
url: project.imageurl,
|
|
filename: project.imageurl.split("/").pop(),
|
|
});
|
|
}
|
|
renderPortfolioImages();
|
|
|
|
resetModalSize();
|
|
projectModal.show();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load project:", error);
|
|
showError("Failed to load project details");
|
|
}
|
|
}
|
|
|
|
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: 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 (Title and Description)");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const url = id
|
|
? `/api/admin/portfolio/projects/${id}`
|
|
: "/api/admin/portfolio/projects";
|
|
const method = id ? "PUT" : "POST";
|
|
const response = await fetch(url, {
|
|
method: method,
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
cache: "no-cache",
|
|
body: JSON.stringify(formData),
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
showSuccess(
|
|
id
|
|
? "Project updated successfully! 🎉"
|
|
: "Project created successfully! 🎉",
|
|
);
|
|
projectModal.hide();
|
|
// Immediately add to local data and re-render for instant feedback
|
|
if (!id && data.project) {
|
|
projectsData.unshift(data.project);
|
|
renderProjects(projectsData);
|
|
}
|
|
// Also reload from server to ensure full sync
|
|
loadProjects();
|
|
} else {
|
|
showError(data.message || "Failed to save project");
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to save project:", error);
|
|
showError("Failed to save project");
|
|
}
|
|
}
|
|
|
|
async function deleteProject(id, name) {
|
|
showDeleteConfirm(
|
|
`Are you sure you want to delete "${name}"? This action cannot be undone.`,
|
|
async () => {
|
|
try {
|
|
const response = await fetch(`/api/admin/portfolio/projects/${id}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
cache: "no-cache",
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
showSuccess("Project deleted successfully");
|
|
// Remove immediately from local data and re-render
|
|
// Compare as strings to handle type mismatches
|
|
const deletedId = String(id);
|
|
projectsData = projectsData.filter((p) => String(p.id) !== deletedId);
|
|
renderProjects(projectsData);
|
|
} else {
|
|
showError(data.message || "Failed to delete project");
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to delete project:", error);
|
|
showError("Failed to delete project");
|
|
}
|
|
},
|
|
{ title: "Delete Project", confirmText: "Delete Project" },
|
|
);
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const map = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
};
|
|
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
return new Date(dateString).toLocaleDateString("en-US", {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric",
|
|
});
|
|
}
|
|
|
|
// 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 };
|
|
|
|
// Initialize if not already
|
|
initPortfolioMediaLibrary();
|
|
|
|
if (portfolioMediaLibrary) {
|
|
portfolioMediaLibrary.open();
|
|
}
|
|
}
|
|
|
|
function handleMediaSelection(media) {
|
|
if (!currentMediaPicker) return;
|
|
|
|
if (currentMediaPicker.purpose === "portfolioImages") {
|
|
// Handle multiple images - media can be array or single object
|
|
const mediaArray = Array.isArray(media) ? media : [media];
|
|
|
|
// Add all selected images to portfolio images array
|
|
mediaArray.forEach((item) => {
|
|
// Check if image already exists
|
|
const itemUrl = item.path || item.url;
|
|
if (!portfolioImages.find((img) => img.url === itemUrl)) {
|
|
portfolioImages.push({
|
|
url: itemUrl,
|
|
filename:
|
|
item.filename || item.originalName || itemUrl.split("/").pop(),
|
|
});
|
|
}
|
|
});
|
|
|
|
renderPortfolioImages();
|
|
showSuccess(`${mediaArray.length} image(s) added to portfolio gallery`);
|
|
}
|
|
|
|
currentMediaPicker = null;
|
|
}
|
|
|
|
// Toast Notification System
|
|
function showSuccess(message) {
|
|
showToast(message, "success");
|
|
}
|
|
|
|
function showError(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);
|
|
}
|