Files
SkyArtShop/website/admin/js/portfolio.js
Local Server 017c6376fc updateweb
2025-12-24 00:13:23 -06:00

559 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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, "&#39;")}')">
<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 = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
};
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);
}