2025-12-14 01:54:40 -06:00
|
|
|
// Blog Management JavaScript
|
|
|
|
|
|
|
|
|
|
let postsData = [];
|
|
|
|
|
let postModal;
|
2025-12-24 00:13:23 -06:00
|
|
|
let quillEditor;
|
|
|
|
|
let isModalExpanded = false;
|
2025-12-14 01:54:40 -06:00
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
|
postModal = new bootstrap.Modal(document.getElementById("postModal"));
|
2025-12-24 00:13:23 -06:00
|
|
|
initializeQuillEditor();
|
2025-12-14 01:54:40 -06:00
|
|
|
checkAuth().then((authenticated) => {
|
|
|
|
|
if (authenticated) {
|
|
|
|
|
loadPosts();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
if (urlParams.get("action") === "create") {
|
|
|
|
|
showCreatePost();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Auto-generate slug from title
|
|
|
|
|
document.getElementById("postTitle")?.addEventListener("input", function () {
|
|
|
|
|
if (!document.getElementById("postId").value) {
|
|
|
|
|
document.getElementById("postSlug").value = slugify(this.value);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 00:13:23 -06:00
|
|
|
function resetModalSize() {
|
|
|
|
|
const modalDialog = document.querySelector("#postModal .modal-dialog");
|
|
|
|
|
const expandIcon = document.getElementById("expandIcon");
|
|
|
|
|
const expandText = document.querySelector("#btnExpandModal span");
|
|
|
|
|
const editor = document.getElementById("postContentEditor");
|
|
|
|
|
|
|
|
|
|
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 = "400px";
|
|
|
|
|
const container = editor.querySelector(".ql-container");
|
|
|
|
|
if (container) {
|
|
|
|
|
container.style.height = "calc(400px - 42px)";
|
|
|
|
|
}
|
|
|
|
|
isModalExpanded = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleModalSize() {
|
|
|
|
|
const modalDialog = document.querySelector("#postModal .modal-dialog");
|
|
|
|
|
const expandIcon = document.getElementById("expandIcon");
|
|
|
|
|
const expandText = document.querySelector("#btnExpandModal span");
|
|
|
|
|
const editor = document.getElementById("postContentEditor");
|
|
|
|
|
|
|
|
|
|
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 = "400px";
|
|
|
|
|
const container = editor.querySelector(".ql-container");
|
|
|
|
|
if (container) {
|
|
|
|
|
container.style.height = "calc(400px - 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initializeQuillEditor() {
|
|
|
|
|
quillEditor = new Quill("#postContentEditor", {
|
|
|
|
|
theme: "snow",
|
|
|
|
|
placeholder: "Write your blog post content here...",
|
|
|
|
|
modules: {
|
|
|
|
|
toolbar: [
|
|
|
|
|
[{ header: [1, 2, 3, false] }],
|
|
|
|
|
["bold", "italic", "underline", "strike"],
|
|
|
|
|
[{ list: "ordered" }, { list: "bullet" }],
|
|
|
|
|
[{ color: [] }, { background: [] }],
|
|
|
|
|
["link", "image"],
|
|
|
|
|
["blockquote", "code-block"],
|
|
|
|
|
["clean"],
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Initialize media library
|
|
|
|
|
let blogMediaLibrary = null;
|
|
|
|
|
let galleryImages = [];
|
|
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
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");
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
};
|
2026-01-18 02:22:05 -06:00
|
|
|
blogMediaLibrary.open();
|
|
|
|
|
}
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
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");
|
2025-12-24 00:13:23 -06:00
|
|
|
};
|
2026-01-18 02:22:05 -06:00
|
|
|
blogMediaLibrary.open();
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function openMediaLibraryForVideo() {
|
|
|
|
|
if (!blogMediaLibrary) {
|
|
|
|
|
initBlogMediaLibrary();
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
2026-01-18 02:22:05 -06:00
|
|
|
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();
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateFeaturedImagePreview(url) {
|
|
|
|
|
const preview = document.getElementById("featuredImagePreview");
|
|
|
|
|
if (url) {
|
|
|
|
|
preview.innerHTML = `
|
|
|
|
|
<div style="position: relative; display: inline-block;">
|
2026-01-18 02:22:05 -06:00
|
|
|
<img src="${url}" style="max-width: 100%; max-height: 150px; border-radius: 8px;" />
|
2025-12-24 00:13:23 -06:00
|
|
|
<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 {
|
2026-01-18 02:22:05 -06:00
|
|
|
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>';
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function removeVideo() {
|
|
|
|
|
document.getElementById("postVideoUrl").value = "";
|
|
|
|
|
document.getElementById("postExternalVideo").value = "";
|
|
|
|
|
updateVideoPreview("");
|
|
|
|
|
showToast("Video removed", "info");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 00:13:23 -06:00
|
|
|
function removeFeaturedImage() {
|
|
|
|
|
document.getElementById("postFeaturedImage").value = "";
|
|
|
|
|
updateFeaturedImagePreview("");
|
|
|
|
|
showToast("Featured image removed", "info");
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// 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>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-14 01:54:40 -06:00
|
|
|
async function loadPosts() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch("/api/admin/blog", { credentials: "include" });
|
|
|
|
|
const data = await response.json();
|
2025-12-24 00:13:23 -06:00
|
|
|
console.log("Blog API Response:", data);
|
2025-12-14 01:54:40 -06:00
|
|
|
if (data.success) {
|
|
|
|
|
postsData = data.posts;
|
2025-12-24 00:13:23 -06:00
|
|
|
console.log("Loaded posts:", postsData);
|
2025-12-14 01:54:40 -06:00
|
|
|
renderPosts(postsData);
|
2025-12-24 00:13:23 -06:00
|
|
|
} else {
|
|
|
|
|
console.error("API returned success=false:", data);
|
|
|
|
|
const tbody = document.getElementById("postsTableBody");
|
|
|
|
|
tbody.innerHTML = `
|
|
|
|
|
<tr><td colspan="7" class="text-center p-4 text-danger">
|
|
|
|
|
<i class="bi bi-exclamation-triangle" style="font-size: 3rem;"></i>
|
|
|
|
|
<p class="mt-3">Failed to load posts: ${
|
|
|
|
|
data.message || "Unknown error"
|
|
|
|
|
}</p>
|
|
|
|
|
</td></tr>`;
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to load posts:", error);
|
2025-12-24 00:13:23 -06:00
|
|
|
const tbody = document.getElementById("postsTableBody");
|
|
|
|
|
tbody.innerHTML = `
|
|
|
|
|
<tr><td colspan="7" class="text-center p-4 text-danger">
|
|
|
|
|
<i class="bi bi-exclamation-triangle" style="font-size: 3rem;"></i>
|
|
|
|
|
<p class="mt-3">Error loading posts. Please refresh the page.</p>
|
|
|
|
|
</td></tr>`;
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPosts(posts) {
|
|
|
|
|
const tbody = document.getElementById("postsTableBody");
|
|
|
|
|
if (posts.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 blog posts found</p>
|
|
|
|
|
<button class="btn btn-primary" onclick="showCreatePost()">
|
|
|
|
|
<i class="bi bi-plus-circle"></i> Create Your First Post
|
|
|
|
|
</button>
|
|
|
|
|
</td></tr>`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tbody.innerHTML = posts
|
|
|
|
|
.map(
|
|
|
|
|
(p) => `
|
|
|
|
|
<tr>
|
2025-12-24 00:13:23 -06:00
|
|
|
<td>${escapeHtml(String(p.id))}</td>
|
2025-12-14 01:54:40 -06:00
|
|
|
<td><strong>${escapeHtml(p.title)}</strong></td>
|
|
|
|
|
<td><code>${escapeHtml(p.slug)}</code></td>
|
|
|
|
|
<td>${escapeHtml((p.excerpt || "").substring(0, 40))}...</td>
|
|
|
|
|
<td><span class="badge ${
|
2025-12-24 00:13:23 -06:00
|
|
|
p.ispublished ? "bg-success text-white" : "bg-warning text-dark"
|
2025-12-14 01:54:40 -06:00
|
|
|
}">
|
|
|
|
|
${p.ispublished ? "Published" : "Draft"}</span></td>
|
|
|
|
|
<td>${formatDate(p.createdat)}</td>
|
|
|
|
|
<td>
|
2025-12-24 00:13:23 -06:00
|
|
|
<button class="btn btn-sm btn-info" onclick="editPost('${escapeHtml(
|
2026-01-18 02:22:05 -06:00
|
|
|
String(p.id),
|
2025-12-24 00:13:23 -06:00
|
|
|
)}')">
|
2025-12-14 01:54:40 -06:00
|
|
|
<i class="bi bi-pencil"></i>
|
|
|
|
|
</button>
|
2025-12-24 00:13:23 -06:00
|
|
|
<button class="btn btn-sm btn-danger" onclick="deletePost('${escapeHtml(
|
2026-01-18 02:22:05 -06:00
|
|
|
String(p.id),
|
2025-12-24 00:13:23 -06:00
|
|
|
)}', '${escapeHtml(p.title).replace(/'/g, "'")}')">
|
2025-12-14 01:54:40 -06:00
|
|
|
<i class="bi bi-trash"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</td>
|
2026-01-18 02:22:05 -06:00
|
|
|
</tr>`,
|
2025-12-14 01:54:40 -06:00
|
|
|
)
|
|
|
|
|
.join("");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterPosts() {
|
|
|
|
|
const searchTerm = document.getElementById("searchInput").value.toLowerCase();
|
|
|
|
|
const filtered = postsData.filter(
|
|
|
|
|
(p) =>
|
|
|
|
|
p.title.toLowerCase().includes(searchTerm) ||
|
2026-01-18 02:22:05 -06:00
|
|
|
p.slug.toLowerCase().includes(searchTerm),
|
2025-12-14 01:54:40 -06:00
|
|
|
);
|
|
|
|
|
renderPosts(filtered);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showCreatePost() {
|
|
|
|
|
document.getElementById("modalTitle").textContent = "Create Blog Post";
|
|
|
|
|
document.getElementById("postForm").reset();
|
|
|
|
|
document.getElementById("postId").value = "";
|
2026-01-18 02:22:05 -06:00
|
|
|
document.getElementById("postPublished").checked = true; // Default to published
|
2025-12-24 00:13:23 -06:00
|
|
|
document.getElementById("postFeaturedImage").value = "";
|
2026-01-18 02:22:05 -06:00
|
|
|
document.getElementById("postVideoUrl").value = "";
|
|
|
|
|
document.getElementById("postExternalVideo").value = "";
|
|
|
|
|
galleryImages = [];
|
2025-12-24 00:13:23 -06:00
|
|
|
updateFeaturedImagePreview("");
|
2026-01-18 02:22:05 -06:00
|
|
|
updateGalleryPreview();
|
|
|
|
|
updateVideoPreview("");
|
|
|
|
|
loadPollData(null);
|
2025-12-24 00:13:23 -06:00
|
|
|
if (quillEditor) {
|
|
|
|
|
quillEditor.setContents([]);
|
|
|
|
|
}
|
|
|
|
|
resetModalSize();
|
2025-12-14 01:54:40 -06:00
|
|
|
postModal.show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function editPost(id) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/admin/blog/${id}`, {
|
|
|
|
|
credentials: "include",
|
|
|
|
|
});
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (data.success) {
|
|
|
|
|
const post = data.post;
|
|
|
|
|
document.getElementById("modalTitle").textContent = "Edit Blog Post";
|
|
|
|
|
document.getElementById("postId").value = post.id;
|
|
|
|
|
document.getElementById("postTitle").value = post.title;
|
|
|
|
|
document.getElementById("postSlug").value = post.slug;
|
|
|
|
|
document.getElementById("postExcerpt").value = post.excerpt || "";
|
2025-12-24 00:13:23 -06:00
|
|
|
|
|
|
|
|
// Set Quill content
|
|
|
|
|
if (quillEditor) {
|
|
|
|
|
quillEditor.root.innerHTML = post.content || "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set featured image
|
|
|
|
|
const featuredImage = post.featuredimage || post.imageurl || "";
|
|
|
|
|
document.getElementById("postFeaturedImage").value = featuredImage;
|
|
|
|
|
updateFeaturedImagePreview(featuredImage);
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-14 01:54:40 -06:00
|
|
|
document.getElementById("postMetaTitle").value = post.metatitle || "";
|
|
|
|
|
document.getElementById("postMetaDescription").value =
|
|
|
|
|
post.metadescription || "";
|
|
|
|
|
document.getElementById("postPublished").checked = post.ispublished;
|
2025-12-24 00:13:23 -06:00
|
|
|
resetModalSize();
|
2025-12-14 01:54:40 -06:00
|
|
|
postModal.show();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to load post:", error);
|
2025-12-24 00:13:23 -06:00
|
|
|
showToast("Failed to load post details", "error");
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function savePost() {
|
|
|
|
|
const id = document.getElementById("postId").value;
|
2025-12-24 00:13:23 -06:00
|
|
|
|
|
|
|
|
// Get content from Quill editor
|
|
|
|
|
const content = quillEditor ? quillEditor.root.innerHTML : "";
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// 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();
|
|
|
|
|
|
2025-12-14 01:54:40 -06:00
|
|
|
const formData = {
|
|
|
|
|
title: document.getElementById("postTitle").value,
|
|
|
|
|
slug: document.getElementById("postSlug").value,
|
|
|
|
|
excerpt: document.getElementById("postExcerpt").value,
|
2025-12-24 00:13:23 -06:00
|
|
|
content: content,
|
|
|
|
|
featuredimage: document.getElementById("postFeaturedImage").value,
|
2026-01-18 02:22:05 -06:00
|
|
|
images: JSON.stringify(galleryImages),
|
|
|
|
|
videourl: videoUrl,
|
|
|
|
|
poll: poll ? JSON.stringify(poll) : null,
|
2025-12-14 01:54:40 -06:00
|
|
|
metatitle: document.getElementById("postMetaTitle").value,
|
|
|
|
|
metadescription: document.getElementById("postMetaDescription").value,
|
|
|
|
|
ispublished: document.getElementById("postPublished").checked,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!formData.title || !formData.slug || !formData.content) {
|
2025-12-24 00:13:23 -06:00
|
|
|
showToast("Please fill in all required fields", "error");
|
2025-12-14 01:54:40 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const url = id ? `/api/admin/blog/${id}` : "/api/admin/blog";
|
|
|
|
|
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) {
|
2025-12-24 00:13:23 -06:00
|
|
|
showToast(
|
|
|
|
|
id ? "Post updated successfully" : "Post created successfully",
|
2026-01-18 02:22:05 -06:00
|
|
|
"success",
|
2025-12-14 01:54:40 -06:00
|
|
|
);
|
|
|
|
|
postModal.hide();
|
|
|
|
|
loadPosts();
|
|
|
|
|
} else {
|
2025-12-24 00:13:23 -06:00
|
|
|
showToast(data.message || "Failed to save post", "error");
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to save post:", error);
|
2025-12-24 00:13:23 -06:00
|
|
|
showToast("Failed to save post", "error");
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deletePost(id, title) {
|
2026-01-18 02:22:05 -06:00
|
|
|
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" },
|
|
|
|
|
);
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 00:13:23 -06:00
|
|
|
function showToast(message, type = "info") {
|
|
|
|
|
const toastContainer =
|
|
|
|
|
document.getElementById("toastContainer") || createToastContainer();
|
|
|
|
|
const toast = document.createElement("div");
|
|
|
|
|
toast.className = `toast toast-${type}`;
|
|
|
|
|
|
|
|
|
|
const icons = {
|
|
|
|
|
success: "check-circle-fill",
|
|
|
|
|
error: "exclamation-triangle-fill",
|
|
|
|
|
warning: "exclamation-circle-fill",
|
|
|
|
|
info: "info-circle-fill",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
toast.innerHTML = `
|
|
|
|
|
<i class="bi bi-${icons[type] || icons.info}"></i>
|
|
|
|
|
<span>${message}</span>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
toastContainer.appendChild(toast);
|
|
|
|
|
|
|
|
|
|
setTimeout(() => toast.classList.add("show"), 10);
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
toast.classList.remove("show");
|
|
|
|
|
setTimeout(() => toast.remove(), 300);
|
|
|
|
|
}, 3000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createToastContainer() {
|
|
|
|
|
const container = document.createElement("div");
|
|
|
|
|
container.id = "toastContainer";
|
|
|
|
|
container.style.cssText =
|
|
|
|
|
"position: fixed; top: 80px; right: 20px; z-index: 9999;";
|
|
|
|
|
document.body.appendChild(container);
|
|
|
|
|
return container;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-14 01:54:40 -06:00
|
|
|
function slugify(text) {
|
|
|
|
|
return text
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/[^\w\s-]/g, "")
|
|
|
|
|
.replace(/[\s_-]+/g, "-")
|
|
|
|
|
.replace(/^-+|-+$/g, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
});
|
|
|
|
|
}
|