webupdate
This commit is contained in:
@@ -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;">×</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})">×</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;">×</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, "'")}')">
|
||||
<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") {
|
||||
|
||||
Reference in New Issue
Block a user