webupdate

This commit is contained in:
Local Server
2026-01-18 02:22:05 -06:00
parent 6fc159051a
commit 2a2a3d99e5
135 changed files with 54897 additions and 9825 deletions

View File

@@ -103,98 +103,70 @@ function initializeQuillEditor() {
});
}
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;
`;
// Initialize media library
let blogMediaLibrary = null;
let galleryImages = [];
// 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 initBlogMediaLibrary() {
blogMediaLibrary = new MediaLibrary({
selectMode: true,
multiple: false,
onSelect: function (media) {
if (media && media.path) {
document.getElementById("postFeaturedImage").value = media.path;
updateFeaturedImagePreview(media.path);
showToast("Featured image selected", "success");
}
},
});
}
function closeMediaLibrary() {
const backdrop = document.getElementById("mediaLibraryBackdrop");
if (backdrop) {
backdrop.remove();
function openMediaLibraryForFeaturedImage() {
if (!blogMediaLibrary) {
initBlogMediaLibrary();
}
blogMediaLibrary.options.multiple = false;
blogMediaLibrary.options.onSelect = function (media) {
if (media && media.path) {
document.getElementById("postFeaturedImage").value = media.path;
updateFeaturedImagePreview(media.path);
showToast("Featured image selected", "success");
}
};
blogMediaLibrary.open();
}
function openMediaLibraryForGallery() {
if (!blogMediaLibrary) {
initBlogMediaLibrary();
}
blogMediaLibrary.options.multiple = true;
blogMediaLibrary.options.onSelect = function (mediaList) {
const items = Array.isArray(mediaList) ? mediaList : [mediaList];
items.forEach((media) => {
if (media && media.path && !galleryImages.includes(media.path)) {
galleryImages.push(media.path);
}
});
updateGalleryPreview();
showToast(`${items.length} image(s) added to gallery`, "success");
};
blogMediaLibrary.open();
}
function openMediaLibraryForVideo() {
if (!blogMediaLibrary) {
initBlogMediaLibrary();
}
blogMediaLibrary.options.multiple = false;
blogMediaLibrary.options.onSelect = function (media) {
if (media && media.path) {
document.getElementById("postVideoUrl").value = media.path;
updateVideoPreview(media.path);
showToast("Video selected", "success");
}
};
blogMediaLibrary.open();
}
function updateFeaturedImagePreview(url) {
@@ -202,21 +174,157 @@ function updateFeaturedImagePreview(url) {
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;" />
<img src="${url}" style="max-width: 100%; max-height: 150px; border-radius: 8px;" />
<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 = "";
preview.innerHTML =
'<div class="text-muted text-center p-3"><i class="bi bi-image" style="font-size: 2rem;"></i><br><small>No image selected</small></div>';
}
}
function updateGalleryPreview() {
const preview = document.getElementById("galleryImagesPreview");
if (galleryImages.length === 0) {
preview.innerHTML =
'<div class="text-muted text-center p-3 w-100"><i class="bi bi-images" style="font-size: 2rem;"></i><br><small>No gallery images</small></div>';
return;
}
preview.innerHTML = galleryImages
.map(
(img, idx) => `
<div class="gallery-thumb">
<img src="${img}" alt="Gallery ${idx + 1}" />
<button type="button" class="remove-btn" onclick="removeGalleryImage(${idx})">&times;</button>
</div>
`,
)
.join("");
}
function removeGalleryImage(index) {
galleryImages.splice(index, 1);
updateGalleryPreview();
showToast("Image removed from gallery", "info");
}
function updateVideoPreview(url) {
const preview = document.getElementById("videoPreview");
if (url) {
const isVideo = url.match(/\.(mp4|webm|mov|avi|mkv)$/i);
if (isVideo) {
preview.innerHTML = `
<div style="position: relative; width: 100%;">
<video controls style="max-width: 100%; max-height: 200px;">
<source src="${url}" type="video/mp4">
Your browser does not support video.
</video>
<button type="button" onclick="removeVideo()" style="position: absolute; top: 5px; right: 5px; background: #dc3545; color: white; border: none; border-radius: 50%; width: 28px; height: 28px; cursor: pointer;">&times;</button>
</div>
`;
} else {
preview.innerHTML = `<div class="video-placeholder"><i class="bi bi-link-45deg"></i>${url}</div>`;
}
} else {
preview.innerHTML =
'<div class="video-placeholder"><i class="bi bi-camera-video"></i>No video selected</div>';
}
}
function removeVideo() {
document.getElementById("postVideoUrl").value = "";
document.getElementById("postExternalVideo").value = "";
updateVideoPreview("");
showToast("Video removed", "info");
}
function removeFeaturedImage() {
document.getElementById("postFeaturedImage").value = "";
updateFeaturedImagePreview("");
showToast("Featured image removed", "info");
}
// Poll functions
function togglePollSection() {
const pollSection = document.getElementById("pollSection");
const enabled = document.getElementById("enablePoll").checked;
pollSection.style.display = enabled ? "block" : "none";
}
function addPollOption() {
const container = document.getElementById("pollOptionsContainer");
const count = container.querySelectorAll(".poll-option-row").length + 1;
const row = document.createElement("div");
row.className = "input-group mb-2 poll-option-row";
row.innerHTML = `
<span class="input-group-text">${count}</span>
<input type="text" class="form-control poll-option-input" placeholder="Option ${count}" />
<button type="button" class="btn btn-outline-danger" onclick="removePollOption(this)">
<i class="bi bi-trash"></i>
</button>
`;
container.appendChild(row);
}
function removePollOption(btn) {
const row = btn.closest(".poll-option-row");
row.remove();
// Re-number options
const container = document.getElementById("pollOptionsContainer");
container.querySelectorAll(".poll-option-row").forEach((row, idx) => {
row.querySelector(".input-group-text").textContent = idx + 1;
});
}
function getPollData() {
if (!document.getElementById("enablePoll").checked) {
return null;
}
const question = document.getElementById("pollQuestion").value.trim();
const options = Array.from(document.querySelectorAll(".poll-option-input"))
.map((input) => input.value.trim())
.filter((v) => v);
if (!question || options.length < 2) {
return null;
}
return { question, options, votes: options.map(() => 0) };
}
function loadPollData(poll) {
if (poll && typeof poll === "object") {
document.getElementById("enablePoll").checked = true;
document.getElementById("pollSection").style.display = "block";
document.getElementById("pollQuestion").value = poll.question || "";
const container = document.getElementById("pollOptionsContainer");
container.innerHTML = "";
(poll.options || []).forEach((opt, idx) => {
const row = document.createElement("div");
row.className = "input-group mb-2 poll-option-row";
row.innerHTML = `
<span class="input-group-text">${idx + 1}</span>
<input type="text" class="form-control poll-option-input" value="${opt}" />
${idx >= 2 ? '<button type="button" class="btn btn-outline-danger" onclick="removePollOption(this)"><i class="bi bi-trash"></i></button>' : ""}
`;
container.appendChild(row);
});
} else {
document.getElementById("enablePoll").checked = false;
document.getElementById("pollSection").style.display = "none";
document.getElementById("pollQuestion").value = "";
document.getElementById("pollOptionsContainer").innerHTML = `
<div class="input-group mb-2 poll-option-row">
<span class="input-group-text">1</span>
<input type="text" class="form-control poll-option-input" placeholder="Option 1" />
</div>
<div class="input-group mb-2 poll-option-row">
<span class="input-group-text">2</span>
<input type="text" class="form-control poll-option-input" placeholder="Option 2" />
</div>
`;
}
}
async function loadPosts() {
try {
const response = await fetch("/api/admin/blog", { credentials: "include" });
@@ -277,17 +385,17 @@ function renderPosts(posts) {
<td>${formatDate(p.createdat)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editPost('${escapeHtml(
String(p.id)
String(p.id),
)}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deletePost('${escapeHtml(
String(p.id)
String(p.id),
)}', '${escapeHtml(p.title).replace(/'/g, "&#39;")}')">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>`
</tr>`,
)
.join("");
}
@@ -297,7 +405,7 @@ function filterPosts() {
const filtered = postsData.filter(
(p) =>
p.title.toLowerCase().includes(searchTerm) ||
p.slug.toLowerCase().includes(searchTerm)
p.slug.toLowerCase().includes(searchTerm),
);
renderPosts(filtered);
}
@@ -306,9 +414,15 @@ function showCreatePost() {
document.getElementById("modalTitle").textContent = "Create Blog Post";
document.getElementById("postForm").reset();
document.getElementById("postId").value = "";
document.getElementById("postPublished").checked = false;
document.getElementById("postPublished").checked = true; // Default to published
document.getElementById("postFeaturedImage").value = "";
document.getElementById("postVideoUrl").value = "";
document.getElementById("postExternalVideo").value = "";
galleryImages = [];
updateFeaturedImagePreview("");
updateGalleryPreview();
updateVideoPreview("");
loadPollData(null);
if (quillEditor) {
quillEditor.setContents([]);
}
@@ -340,6 +454,28 @@ async function editPost(id) {
document.getElementById("postFeaturedImage").value = featuredImage;
updateFeaturedImagePreview(featuredImage);
// Set gallery images
try {
galleryImages = post.images ? JSON.parse(post.images) : [];
} catch (e) {
galleryImages = [];
}
updateGalleryPreview();
// Set video
const videoUrl = post.videourl || "";
document.getElementById("postVideoUrl").value = videoUrl;
document.getElementById("postExternalVideo").value = "";
updateVideoPreview(videoUrl);
// Set poll
try {
const poll = post.poll ? JSON.parse(post.poll) : null;
loadPollData(poll);
} catch (e) {
loadPollData(null);
}
document.getElementById("postMetaTitle").value = post.metatitle || "";
document.getElementById("postMetaDescription").value =
post.metadescription || "";
@@ -359,12 +495,24 @@ async function savePost() {
// Get content from Quill editor
const content = quillEditor ? quillEditor.root.innerHTML : "";
// Get video URL (prefer uploaded, then external)
let videoUrl = document.getElementById("postVideoUrl").value;
if (!videoUrl) {
videoUrl = document.getElementById("postExternalVideo").value;
}
// Get poll data
const poll = getPollData();
const formData = {
title: document.getElementById("postTitle").value,
slug: document.getElementById("postSlug").value,
excerpt: document.getElementById("postExcerpt").value,
content: content,
featuredimage: document.getElementById("postFeaturedImage").value,
images: JSON.stringify(galleryImages),
videourl: videoUrl,
poll: poll ? JSON.stringify(poll) : null,
metatitle: document.getElementById("postMetaTitle").value,
metadescription: document.getElementById("postMetaDescription").value,
ispublished: document.getElementById("postPublished").checked,
@@ -389,7 +537,7 @@ async function savePost() {
if (data.success) {
showToast(
id ? "Post updated successfully" : "Post created successfully",
"success"
"success",
);
postModal.hide();
loadPosts();
@@ -403,23 +551,30 @@ async function savePost() {
}
async function deletePost(id, title) {
if (!confirm(`Are you sure you want to delete "${title}"?`)) return;
try {
const response = await fetch(`/api/admin/blog/${id}`, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
if (data.success) {
showToast("Post deleted successfully", "success");
loadPosts();
} else {
showToast(data.message || "Failed to delete post", "error");
}
} catch (error) {
console.error("Failed to delete post:", error);
showToast("Failed to delete post", "error");
}
showDeleteConfirm(
`Are you sure you want to delete "${title}"? This action cannot be undone.`,
async () => {
try {
const response = await fetch(`/api/admin/blog/${id}`, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
if (data.success) {
// Immediately remove from local array and re-render
postsData = postsData.filter((p) => String(p.id) !== String(id));
renderPosts(postsData);
showToast("Post deleted successfully", "success");
} else {
showToast(data.message || "Failed to delete post", "error");
}
} catch (error) {
console.error("Failed to delete post:", error);
showToast("Failed to delete post", "error");
}
},
{ title: "Delete Blog Post", confirmText: "Delete Post" },
);
}
function showToast(message, type = "info") {