559 lines
16 KiB
JavaScript
559 lines
16 KiB
JavaScript
// Portfolio Management JavaScript
|
||
|
||
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();
|
||
}
|
||
});
|
||
|
||
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 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) {
|
||
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",
|
||
body: JSON.stringify(formData),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showSuccess(
|
||
id
|
||
? "Project updated successfully! 🎉"
|
||
: "Project created successfully! 🎉"
|
||
);
|
||
projectModal.hide();
|
||
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) {
|
||
if (!confirm(`Are you sure you want to delete "${name}"?`)) return;
|
||
try {
|
||
const response = await fetch(`/api/admin/portfolio/projects/${id}`, {
|
||
method: "DELETE",
|
||
credentials: "include",
|
||
});
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showSuccess("Project deleted successfully");
|
||
loadProjects();
|
||
} else {
|
||
showError(data.message || "Failed to delete project");
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to delete project:", error);
|
||
showError("Failed to delete project");
|
||
}
|
||
}
|
||
|
||
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 };
|
||
|
||
// 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) {
|
||
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);
|
||
}
|