Files
cms.uldp.edu.vn/views/admin/blog/edit.ejs
2026-04-10 15:55:15 +07:00

1765 lines
101 KiB
Plaintext

<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
<%= title %>
</h1>
<p class="text-muted mb-0">Edit blog post</p>
</div>
<div>
<a href="/admin/blog" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Blog List
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form action="/admin/blog/<%= blog._id %>/edit" method="POST" id="blogForm" class="content-with-fixed-buttons">
<!-- Navigation Tabs -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#blogInfo" role="tab">
<i class="fas fa-info-circle me-2"></i>Blog Information
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#categorization" role="tab">
<i class="fas fa-tags me-2"></i>Categorization
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#settings" role="tab">
<i class="fas fa-cog me-2"></i>Settings
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#comments" role="tab">
<i class="fas fa-comments me-2"></i>Comments
<% if (commentsCount > 0) { %>
<span class="badge bg-primary ms-1"><%= commentsCount %></span>
<% } %>
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Blog Information Tab -->
<div class="tab-pane fade show active" id="blogInfo" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<!-- Featured Image - Vị trí trên đầu -->
<div class="row g-3 mb-4">
<div class="col-md-12">
<label for="featuredImageUrl" class="form-label fw-medium">Featured
Image <span class="text-danger">*</span></label>
<div class="input-group">
<input type="text" class="form-control" id="featuredImageUrl"
name="featuredImageUrl" value="<%= blog.featuredImage || '' %>"
required placeholder="Enter image URL or upload">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="featuredImageUrl" data-image-type="blog">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<div class="form-text">Upload a new featured image or enter image URL.
<br><strong>Recommended size:</strong> 852 x 400 px
</div>
<div id="featuredImagePreview" class="mt-2">
<% if (blog.featuredImage) { %>
<img src="<%= getFullImageUrl(blog.featuredImage, backendUrl) %>"
class="img-thumbnail"
style="max-width: 300px; max-height: 200px; object-fit: cover;"
alt="Featured image preview">
<% } %>
</div>
</div>
</div>
<!-- Title - Vị trí dưới Featured Image -->
<div class="row g-3 mb-4">
<div class="col-md-12">
<label for="title" class="form-label fw-medium">Title <span
class="text-danger">*</span></label>
<input type="text" class="form-control" id="title" name="title" required
value="<%= blog.title %>" placeholder="Enter blog post title">
<div class="form-text">The title will be used to generate the URL slug
automatically.</div>
</div>
</div>
<!-- Content (EditorJS) - Nằm dưới Title -->
<div class="row g-3 mb-4">
<div class="col-md-12">
<label for="content" class="form-label fw-medium">Content <span
class="text-danger">*</span></label>
<div id="editorjs-content" class="border rounded p-3"
style="min-height: 400px;"></div>
<input type="hidden" id="content" name="content" required>
<div class="form-text">Write the main content of the blog post using the
editor.</div>
</div>
</div>
<!-- Gallery Images - Bắt buộc 2 ảnh -->
<div class="row g-3 mb-4">
<div class="col-md-12">
<label class="form-label fw-medium">Gallery Images <span
class="text-danger">*</span></label>
<div class="form-text mb-2">Exactly 2 images required (row, 2 columns)
<br><strong>Recommended size:</strong> 410 x 264 px each
</div>
<div id="galleryContainer" class="row g-3">
<% const galleryImages=blog.galleryImages || []; const
image1=galleryImages[0] || '' ; const image2=galleryImages[1]
|| '' ; %>
<div class="col-md-6">
<div class="input-group">
<input type="text"
class="form-control gallery-image-input"
id="galleryImages_0" name="galleryImages[]"
value="<%= image1 %>" required
placeholder="Enter image URL or upload">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="galleryImages_0"
data-image-type="blog">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<div id="galleryPreview_0" class="mt-2">
<% if (image1) { %>
<img src="<%= getFullImageUrl(image1, backendUrl) %>"
class="img-thumbnail"
style="max-width: 200px; max-height: 150px; object-fit: cover;"
alt="Gallery image 1 preview">
<% } %>
</div>
</div>
<div class="col-md-6">
<div class="input-group">
<input type="text"
class="form-control gallery-image-input"
id="galleryImages_1" name="galleryImages[]"
value="<%= image2 %>" required
placeholder="Enter image URL or upload">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="galleryImages_1"
data-image-type="blog">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<div id="galleryPreview_1" class="mt-2">
<% if (image2) { %>
<img src="<%= getFullImageUrl(image2, backendUrl) %>"
class="img-thumbnail"
style="max-width: 200px; max-height: 150px; object-fit: cover;"
alt="Gallery image 2 preview">
<% } %>
</div>
</div>
</div>
</div>
</div>
<!-- Quote/Sidebar - Nằm ngay dưới Gallery Images -->
<div class="row g-3 mb-4">
<div class="col-md-12">
<label for="quote" class="form-label fw-medium">Quote/Sidebar</label>
<textarea class="form-control" id="quote" name="quote" rows="3"
placeholder="Enter a quote or sidebar text (optional)"><%= blog.quote || '' %></textarea>
<div class="form-text">This will be displayed as a highlighted quote in
the blog post.</div>
</div>
</div>
<!-- Content After Quote (EditorJS) -->
<div class="row g-3 mb-4">
<div class="col-md-12">
<label for="contentAfterQuote" class="form-label fw-medium">Content
After Quote</label>
<div id="editorjs-contentAfterQuote" class="border rounded p-3"
style="min-height: 400px;"></div>
<input type="hidden" id="contentAfterQuote" name="contentAfterQuote">
<div class="form-text">Content that appears after the quote section.
</div>
</div>
</div>
<!-- Additional Fields -->
<div class="row g-3">
<div class="col-md-12">
<label for="excerpt" class="form-label fw-medium">Excerpt <span
class="text-danger">*</span></label>
<textarea class="form-control" id="excerpt" name="excerpt" rows="3"
required maxlength="500"
placeholder="Enter a brief summary of the blog post (max 500 characters)"><%= blog.excerpt || '' %></textarea>
<div class="form-text">Maximum 500 characters.</div>
</div>
</div>
</div>
</div>
</div>
<!-- Categorization Tab -->
<div class="tab-pane fade" id="categorization" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="row g-3">
<!-- Categories Column -->
<div class="col-md-6">
<label class="form-label fw-medium">Categories</label>
<div class="d-flex gap-2 mb-2">
<input type="text" class="form-control form-control-sm"
id="newCategoryInput" placeholder="Enter new category name">
<button type="button" class="btn btn-sm btn-outline-primary"
id="addCategoryBtn">
<i class="fas fa-plus me-1"></i>Add
</button>
</div>
<div class="form-check-group" id="categoriesContainer"
style="max-height: 300px; overflow-y: auto;">
<% categories.forEach(category=> { %>
<div class="form-check d-flex align-items-center justify-content-between mb-2"
data-category-id="<%= category._id %>">
<div class="d-flex align-items-center">
<input class="form-check-input" type="checkbox"
name="category" value="<%= category.name %>"
id="category_<%= category._id %>" <%=blog.category
&& blog.category.includes(category.name) ? 'checked'
: '' %>>
<label class="form-check-label ms-2"
for="category_<%= category._id %>">
<%= category.name %>
</label>
</div>
<button type="button"
class="btn btn-sm btn-outline-danger delete-category-btn"
data-category-id="<%= category._id %>"
data-category-name="<%= category.name %>"
title="Delete category">
<i class="fas fa-trash"></i>
</button>
</div>
<% }); %>
</div>
<div class="form-text">Select one or more categories for this blog post.
</div>
</div>
<!-- Tags Column -->
<div class="col-md-6">
<label class="form-label fw-medium">Tags</label>
<div class="d-flex gap-2 mb-2">
<input type="text" class="form-control form-control-sm"
id="newTagInput" placeholder="Enter new tag name">
<button type="button" class="btn btn-sm btn-outline-primary"
id="addTagBtn">
<i class="fas fa-plus me-1"></i>Add
</button>
</div>
<div class="form-check-group" id="tagsContainer"
style="max-height: 300px; overflow-y: auto;">
<% tags.forEach(tag=> { %>
<div class="form-check d-flex align-items-center justify-content-between mb-2"
data-tag-id="<%= tag._id %>">
<div class="d-flex align-items-center">
<input class="form-check-input" type="checkbox"
name="tags" value="<%= tag.name %>"
id="tag_<%= tag._id %>" <%=blog.tags &&
blog.tags.includes(tag.name) ? 'checked' : '' %>>
<label class="form-check-label ms-2"
for="tag_<%= tag._id %>">
<%= tag.name %>
</label>
</div>
<button type="button"
class="btn btn-sm btn-outline-danger delete-tag-btn"
data-tag-id="<%= tag._id %>"
data-tag-name="<%= tag.name %>" title="Delete tag">
<i class="fas fa-trash"></i>
</button>
</div>
<% }); %>
</div>
<div class="form-text">Select one or more tags for this blog post.</div>
</div>
</div>
</div>
</div>
</div>
<!-- Settings Tab -->
<div class="tab-pane fade" id="settings" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label for="author" class="form-label fw-medium">Author</label>
<input type="text" class="form-control" id="author" name="author"
value="<%= blog.author || 'Admin' %>"
placeholder="Enter author name">
</div>
<div class="col-md-6">
<label for="status" class="form-label fw-medium">Status</label>
<select class="form-select" id="status" name="status">
<option value="published" <%=blog.status==='published' ? 'selected'
: '' %>>Published</option>
<option value="draft" <%=blog.status==='draft' ? 'selected' : '' %>
>Draft</option>
</select>
</div>
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="isFeatured"
id="isFeatured" <%=blog.isFeatured ? 'checked' : '' %>>
<label class="form-check-label" for="isFeatured">
Mark as Featured Post
</label>
</div>
<div class="form-text">Featured posts can be highlighted on the blog
page.</div>
</div>
</div>
</div>
</div>
</div>
<!-- Comments Tab -->
<div class="tab-pane fade" id="comments" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0">
<i class="fas fa-comments me-2"></i>Comments Management
</h5>
<div>
<span class="badge bg-primary me-2">Total: <%= commentsCount || 0 %></span>
<span class="badge bg-success me-2">Approved: <%= comments ? comments.reduce((sum, c) => sum + (c.status === 'approved' ? 1 : 0) + (c.replies ? c.replies.filter(r => r.status === 'approved').length : 0), 0) : 0 %></span>
<span class="badge bg-warning me-2">Pending: <%= comments ? comments.reduce((sum, c) => sum + (c.status === 'pending' ? 1 : 0) + (c.replies ? c.replies.filter(r => r.status === 'pending').length : 0), 0) : 0 %></span>
<span class="badge bg-danger">Rejected: <%= comments ? comments.reduce((sum, c) => sum + (c.status === 'rejected' ? 1 : 0) + (c.replies ? c.replies.filter(r => r.status === 'rejected').length : 0), 0) : 0 %></span>
</div>
</div>
<!-- Filter and Sort Controls -->
<div class="card mb-4 bg-light">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-3">
<label for="filterStatus" class="form-label small fw-bold mb-1">
<i class="fas fa-filter me-1"></i>Filter by Status
</label>
<select class="form-select form-select-sm" id="filterStatus">
<option value="all">All Status</option>
<option value="approved">Approved</option>
<option value="pending">Pending</option>
<option value="rejected">Rejected</option>
</select>
</div>
<div class="col-md-3">
<label for="sortComments" class="form-label small fw-bold mb-1">
<i class="fas fa-sort me-1"></i>Sort by
</label>
<select class="form-select form-select-sm" id="sortComments">
<option value="newest">Newest First</option>
<option value="oldest">Oldest First</option>
<option value="name-asc">Name (A-Z)</option>
<option value="name-desc">Name (Z-A)</option>
</select>
</div>
<div class="col-md-4">
<label for="searchComments" class="form-label small fw-bold mb-1">
<i class="fas fa-search me-1"></i>Search
</label>
<input type="text" class="form-control form-control-sm" id="searchComments"
placeholder="Search by name, email, phone, or content...">
</div>
<div class="col-md-2">
<button type="button" class="btn btn-outline-secondary btn-sm w-100" id="resetFilters">
<i class="fas fa-redo me-1"></i>Reset
</button>
</div>
</div>
</div>
</div>
<% if (comments && comments.length > 0) { %>
<div class="comments-list" id="commentsList">
<% comments.forEach((comment, index) => { %>
<div class="comment-item card mb-3 border-<%= comment.status === 'approved' ? 'success' : comment.status === 'rejected' ? 'danger' : 'warning' %> shadow-sm"
data-status="<%= comment.status %>"
data-author-name="<%= comment.authorName.toLowerCase() %>"
data-author-email="<%= (comment.authorEmail || '').toLowerCase() %>"
data-author-phone="<%= (comment.authorPhone || '').toLowerCase() %>"
data-content="<%= comment.content.toLowerCase().replace(/"/g, '&quot;') %>"
data-created-at="<%= new Date(comment.createdAt).getTime() %>">
<div class="card-body">
<div class="row">
<div class="col-md-9">
<!-- Author Info -->
<div class="d-flex align-items-center gap-2 mb-3">
<div class="avatar-circle bg-primary text-white d-flex align-items-center justify-content-center" style="width: 40px; height: 40px; border-radius: 50%; font-weight: bold;">
<%= comment.authorName.charAt(0).toUpperCase() %>
</div>
<div class="flex-grow-1">
<h6 class="mb-0 fw-bold"><%= comment.authorName %></h6>
<small class="text-muted">
<i class="fas fa-clock me-1"></i><%= new Date(comment.createdAt).toLocaleString() %>
</small>
</div>
<span class="badge bg-<%= comment.status === 'approved' ? 'success' : comment.status === 'rejected' ? 'danger' : 'warning' %> px-3 py-2">
<i class="fas fa-<%= comment.status === 'approved' ? 'check-circle' : comment.status === 'rejected' ? 'times-circle' : 'clock' %> me-1"></i>
<%= comment.status.charAt(0).toUpperCase() + comment.status.slice(1) %>
</span>
</div>
<!-- Contact Information -->
<% if (comment.authorEmail || comment.authorPhone || comment.authorAddress || comment.authorDate) { %>
<div class="contact-info mb-3 p-3 bg-light rounded">
<div class="row g-2">
<% if (comment.authorEmail) { %>
<div class="col-md-6">
<div class="d-flex align-items-center">
<i class="fas fa-envelope text-primary me-2"></i>
<div>
<small class="text-muted d-block">Email</small>
<strong class="small"><%= comment.authorEmail %></strong>
</div>
</div>
</div>
<% } %>
<% if (comment.authorPhone) { %>
<div class="col-md-6">
<div class="d-flex align-items-center">
<i class="fas fa-phone text-success me-2"></i>
<div>
<small class="text-muted d-block">Phone</small>
<strong class="small"><%= comment.authorPhone %></strong>
</div>
</div>
</div>
<% } %>
<% if (comment.authorAddress) { %>
<div class="col-md-6">
<div class="d-flex align-items-center">
<i class="fas fa-map-marker-alt text-danger me-2"></i>
<div>
<small class="text-muted d-block">Address</small>
<strong class="small"><%= comment.authorAddress %></strong>
</div>
</div>
</div>
<% } %>
<% if (comment.authorDate) { %>
<div class="col-md-6">
<div class="d-flex align-items-center">
<i class="fas fa-calendar text-info me-2"></i>
<div>
<small class="text-muted d-block">Date</small>
<strong class="small"><%= comment.authorDate %></strong>
</div>
</div>
</div>
<% } %>
</div>
</div>
<% } %>
<!-- Comment Content -->
<div class="comment-content p-3 bg-white border rounded">
<p class="mb-0"><%= comment.content %></p>
</div>
</div>
<!-- Action Buttons -->
<div class="col-md-3">
<div class="d-flex flex-column gap-2">
<% if (comment.status !== 'approved') { %>
<button type="button" class="btn btn-success approve-comment-btn w-100"
data-comment-id="<%= comment._id %>" title="Approve Comment">
<i class="fas fa-check-circle me-2"></i>Approve
</button>
<% } %>
<% if (comment.status !== 'rejected') { %>
<button type="button" class="btn btn-warning reject-comment-btn w-100"
data-comment-id="<%= comment._id %>" title="Reject Comment">
<i class="fas fa-times-circle me-2"></i>Reject
</button>
<% } %>
<button type="button" class="btn btn-outline-danger delete-comment-btn w-100"
data-comment-id="<%= comment._id %>" title="Delete Comment">
<i class="fas fa-trash-alt me-2"></i>Delete
</button>
</div>
</div>
</div>
<!-- Replies -->
<% if (comment.replies && comment.replies.length > 0) { %>
<div class="mt-4">
<h6 class="text-muted mb-3">
<i class="fas fa-reply me-2"></i>Replies (<%= comment.replies.length %>)
</h6>
<div class="replies-container ms-4 ps-3 border-start border-3">
<% comment.replies.forEach((reply) => { %>
<div class="card mb-3 border-<%= reply.status === 'approved' ? 'success' : reply.status === 'rejected' ? 'danger' : 'warning' %> shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-md-9">
<!-- Reply Author Info -->
<div class="d-flex align-items-center gap-2 mb-2">
<div class="avatar-circle bg-secondary text-white d-flex align-items-center justify-content-center" style="width: 32px; height: 32px; border-radius: 50%; font-weight: bold; font-size: 0.85rem;">
<%= reply.authorName.charAt(0).toUpperCase() %>
</div>
<div class="flex-grow-1">
<strong class="small"><%= reply.authorName %></strong>
<small class="text-muted d-block">
<i class="fas fa-clock me-1"></i><%= new Date(reply.createdAt).toLocaleString() %>
</small>
</div>
<span class="badge bg-<%= reply.status === 'approved' ? 'success' : reply.status === 'rejected' ? 'danger' : 'warning' %> px-2 py-1">
<i class="fas fa-<%= reply.status === 'approved' ? 'check-circle' : reply.status === 'rejected' ? 'times-circle' : 'clock' %> me-1"></i>
<%= reply.status.charAt(0).toUpperCase() + reply.status.slice(1) %>
</span>
</div>
<!-- Reply Contact Info -->
<% if (reply.authorEmail || reply.authorPhone || reply.authorAddress || reply.authorDate) { %>
<div class="contact-info mb-2 p-2 bg-light rounded">
<div class="row g-2">
<% if (reply.authorEmail) { %>
<div class="col-md-6">
<small class="text-muted"><i class="fas fa-envelope text-primary me-1"></i><%= reply.authorEmail %></small>
</div>
<% } %>
<% if (reply.authorPhone) { %>
<div class="col-md-6">
<small class="text-muted"><i class="fas fa-phone text-success me-1"></i><%= reply.authorPhone %></small>
</div>
<% } %>
<% if (reply.authorAddress) { %>
<div class="col-md-6">
<small class="text-muted"><i class="fas fa-map-marker-alt text-danger me-1"></i><%= reply.authorAddress %></small>
</div>
<% } %>
<% if (reply.authorDate) { %>
<div class="col-md-6">
<small class="text-muted"><i class="fas fa-calendar text-info me-1"></i><%= reply.authorDate %></small>
</div>
<% } %>
</div>
</div>
<% } %>
<!-- Reply Content -->
<div class="reply-content p-2 bg-white border rounded">
<p class="mb-0 small"><%= reply.content %></p>
</div>
</div>
<!-- Reply Action Buttons -->
<div class="col-md-3">
<div class="d-flex flex-column gap-2">
<% if (reply.status !== 'approved') { %>
<button type="button" class="btn btn-sm btn-success approve-comment-btn w-100"
data-comment-id="<%= reply._id %>" title="Approve">
<i class="fas fa-check-circle me-1"></i>Approve
</button>
<% } %>
<% if (reply.status !== 'rejected') { %>
<button type="button" class="btn btn-sm btn-warning reject-comment-btn w-100"
data-comment-id="<%= reply._id %>" title="Reject">
<i class="fas fa-times-circle me-1"></i>Reject
</button>
<% } %>
<button type="button" class="btn btn-sm btn-outline-danger delete-comment-btn w-100"
data-comment-id="<%= reply._id %>" title="Delete">
<i class="fas fa-trash-alt me-1"></i>Delete
</button>
</div>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
<% } %>
</div>
</div>
<% }); %>
</div>
<% } else { %>
<div class="text-center py-5">
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
<p class="text-muted">No comments yet for this blog post.</p>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<a href="/admin/blog" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Update Blog Post
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Editor.js Dependencies -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.7.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@1.8.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.8.1"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@2.5.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@1.3.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@2.5.3"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@2.8.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@1.4.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/table@2.2.2"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/checklist@1.5.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@1.4.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/underline@1.1.0"></script>
<script>
document.addEventListener('DOMContentLoaded', async function () {
// Wait a bit for all scripts to load
await new Promise(resolve => setTimeout(resolve, 100));
// Check if EditorJS is loaded
if (typeof EditorJS === 'undefined') {
console.error('EditorJS is not loaded');
alert('EditorJS failed to load. Please refresh the page.');
return;
}
// Parse existing content
let initialContentData = { blocks: [] };
let initialContentAfterQuoteData = { blocks: [] };
try {
<% if (blog.content) { %>
const blogContentRaw = <%- JSON.stringify(blog.content) %>;
if (blogContentRaw) {
if (typeof blogContentRaw === 'string' && blogContentRaw.trim()) {
// Try to parse as JSON string
try {
initialContentData = JSON.parse(blogContentRaw);
// Validate it has blocks property
if (!initialContentData.blocks || !Array.isArray(initialContentData.blocks)) {
throw new Error('Invalid EditorJS format');
}
} catch (e) {
// If not JSON, convert to EditorJS format
initialContentData = {
blocks: [{
type: 'paragraph',
data: {
text: blogContentRaw
}
}]
};
}
} else if (typeof blogContentRaw === 'object' && blogContentRaw !== null) {
// Already an object, check if it's valid EditorJS format
if (blogContentRaw.blocks && Array.isArray(blogContentRaw.blocks)) {
initialContentData = blogContentRaw;
} else {
// Convert object to EditorJS format
initialContentData = {
blocks: [{
type: 'paragraph',
data: {
text: JSON.stringify(blogContentRaw)
}
}]
};
}
}
}
<% } %>
} catch (error) {
console.error('Error parsing content:', error);
}
try {
<% if (blog.contentAfterQuote) { %>
const blogContentAfterQuoteRaw = <%- JSON.stringify(blog.contentAfterQuote) %>;
if (blogContentAfterQuoteRaw) {
if (typeof blogContentAfterQuoteRaw === 'string' && blogContentAfterQuoteRaw.trim()) {
// Try to parse as JSON string
try {
initialContentAfterQuoteData = JSON.parse(blogContentAfterQuoteRaw);
// Validate it has blocks property
if (!initialContentAfterQuoteData.blocks || !Array.isArray(initialContentAfterQuoteData.blocks)) {
throw new Error('Invalid EditorJS format');
}
} catch (e) {
// If not JSON, convert to EditorJS format
initialContentAfterQuoteData = {
blocks: [{
type: 'paragraph',
data: {
text: blogContentAfterQuoteRaw
}
}]
};
}
} else if (typeof blogContentAfterQuoteRaw === 'object' && blogContentAfterQuoteRaw !== null) {
// Already an object, check if it's valid EditorJS format
if (blogContentAfterQuoteRaw.blocks && Array.isArray(blogContentAfterQuoteRaw.blocks)) {
initialContentAfterQuoteData = blogContentAfterQuoteRaw;
} else {
// Convert object to EditorJS format
initialContentAfterQuoteData = {
blocks: [{
type: 'paragraph',
data: {
text: JSON.stringify(blogContentAfterQuoteRaw)
}
}]
};
}
}
}
<% } %>
} catch (error) {
console.error('Error parsing contentAfterQuote:', error);
}
// Build tools object, only including plugins that are loaded
const tools = {
header: {
class: Header,
inlineToolbar: true
},
paragraph: {
class: Paragraph,
inlineToolbar: true
},
list: {
class: List,
inlineToolbar: true,
config: {
defaultStyle: 'unordered'
}
},
image: {
class: ImageTool,
config: {
endpoints: {
byFile: '/admin/upload/image?imageType=blog'
},
// Map backend response (success:true, path/url) to EditorJS expected shape
uploader: {
async uploadByFile(file) {
const formData = new FormData();
formData.append('image', file);
const response = await fetch('/admin/upload/image?imageType=blog', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!response.ok || !result.success || !(result.url || result.path)) {
throw new Error(result.error || 'Upload failed');
}
const url = result.url || result.path;
return {
success: 1,
file: { url }
};
}
}
}
},
quote: {
class: Quote,
inlineToolbar: true,
shortcut: 'CMD+SHIFT+O',
config: {
quotePlaceholder: 'Enter a quote',
captionPlaceholder: 'Quote\'s author'
}
},
marker: {
class: Marker,
shortcut: 'CMD+SHIFT+M'
},
embed: {
class: Embed,
config: {
services: {
youtube: true,
vimeo: true
}
}
}
};
// Add optional plugins if they're loaded
if (typeof Code !== 'undefined') {
tools.code = {
class: Code,
config: {
placeholder: 'Enter code'
}
};
}
if (typeof Delimiter !== 'undefined') {
tools.delimiter = Delimiter;
}
if (typeof Table !== 'undefined') {
tools.table = {
class: Table,
inlineToolbar: true,
config: {
rows: 2,
cols: 2
}
};
}
if (typeof Checklist !== 'undefined') {
tools.checklist = {
class: Checklist,
inlineToolbar: true
};
}
if (typeof InlineCode !== 'undefined') {
tools.inlineCode = {
class: InlineCode,
shortcut: 'CMD+SHIFT+I'
};
}
if (typeof Underline !== 'undefined') {
tools.underline = Underline;
}
// Initialize EditorJS for Content
let contentEditor = null;
try {
contentEditor = new EditorJS({
holder: 'editorjs-content',
placeholder: 'Write your blog content here...',
data: initialContentData,
tools: tools
});
window.contentEditor = contentEditor;
} catch (error) {
console.error('Error initializing content editor:', error);
}
// Initialize EditorJS for Content After Quote
let contentAfterQuoteEditor = null;
try {
contentAfterQuoteEditor = new EditorJS({
holder: 'editorjs-contentAfterQuote',
placeholder: 'Write content after quote here...',
data: initialContentAfterQuoteData,
tools: tools
});
window.contentAfterQuoteEditor = contentAfterQuoteEditor;
} catch (error) {
console.error('Error initializing contentAfterQuote editor:', error);
}
// Image upload handler
document.querySelectorAll('.btn-upload-image').forEach(button => {
button.addEventListener('click', function () {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
});
function openImageUploader(targetInput, imageType) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
try {
const formData = new FormData();
formData.append('image', file);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
if (uploadBtn) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
}
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Upload failed');
}
// Update input value
let input = document.getElementById(targetInput);
if (!input) {
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
if (uploadBtn) {
const inputGroup = uploadBtn.closest('.input-group');
if (inputGroup) {
input = inputGroup.querySelector('input[type="text"]');
}
}
}
if (input && input.tagName === 'INPUT') {
input.value = result.path;
// Show preview
const backendUrl = '<%= typeof backendUrl !== "undefined" ? backendUrl : "http://localhost:3001" %>';
const getFullImageUrl = function(imagePath, baseUrl) {
if (!imagePath) return "";
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
return imagePath;
}
const base = (baseUrl || "http://localhost:3001").replace(/\/$/, "");
let imgSrc = imagePath;
if (!imgSrc.startsWith("/")) {
imgSrc = "/" + imgSrc;
}
return base + imgSrc;
};
if (targetInput === 'featuredImageUrl') {
const preview = document.getElementById('featuredImagePreview');
const previewUrl = getFullImageUrl(result.path, backendUrl);
preview.innerHTML = `
<img src="${previewUrl}" class="img-thumbnail"
style="max-width: 300px; max-height: 200px; object-fit: cover;"
alt="Featured image preview">
`;
} else if (targetInput.startsWith('galleryImages_')) {
const index = targetInput.split('_')[1];
const preview = document.getElementById(`galleryPreview_${index}`);
if (preview) {
const previewUrl = getFullImageUrl(result.path, backendUrl);
preview.innerHTML = `
<img src="${previewUrl}" class="img-thumbnail"
style="max-width: 200px; max-height: 150px; object-fit: cover;"
alt="Gallery image preview">
`;
}
}
} else {
console.error('Could not find input for:', targetInput);
}
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
} catch (error) {
console.error('Upload error:', error);
alert('Failed to upload image: ' + error.message);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = uploadBtn.innerHTML.replace('Uploading...', 'Upload');
}
} finally {
document.body.removeChild(fileInput);
}
};
fileInput.click();
}
// Form submission handler
const form = document.getElementById('blogForm');
form.addEventListener('submit', async function (e) {
e.preventDefault();
const submitBtn = form.querySelector('button[type="submit"]');
const originalBtnHtml = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Updating...';
try {
// Save content from EditorJS
if (contentEditor) {
const contentData = await contentEditor.save();
document.getElementById('content').value = JSON.stringify(contentData);
}
// Save contentAfterQuote from EditorJS
if (contentAfterQuoteEditor) {
const contentAfterQuoteData = await contentAfterQuoteEditor.save();
document.getElementById('contentAfterQuote').value = JSON.stringify(contentAfterQuoteData);
}
// Validate gallery images (must have exactly 2)
const galleryInputs = document.querySelectorAll('input[name="galleryImages[]"]');
let filledCount = 0;
galleryInputs.forEach(input => {
if (input.value.trim()) {
filledCount++;
}
});
if (filledCount < 2) {
alert('Please upload exactly 2 gallery images.');
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnHtml;
return;
}
form.submit();
} catch (error) {
console.error('Save error:', error);
alert('Error saving content: ' + error.message);
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnHtml;
}
});
// Add Category functionality
const addCategoryBtn = document.getElementById('addCategoryBtn');
const newCategoryInput = document.getElementById('newCategoryInput');
const categoriesContainer = document.getElementById('categoriesContainer');
addCategoryBtn.addEventListener('click', async function () {
const categoryName = newCategoryInput.value.trim();
if (!categoryName) {
alert('Please enter a category name');
return;
}
const btnHtml = addCategoryBtn.innerHTML;
addCategoryBtn.disabled = true;
addCategoryBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Adding...';
try {
const response = await fetch('/admin/blog/categories/quick-create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: categoryName })
});
const result = await response.json();
if (result.success) {
// Check if checkbox already exists
const existingCheckbox = document.querySelector(`input[name="category"][value="${result.data.name}"]`);
if (!existingCheckbox) {
// Add new checkbox with delete button
const newItem = document.createElement('div');
newItem.className = 'form-check d-flex align-items-center justify-content-between mb-2';
newItem.setAttribute('data-category-id', result.data._id);
newItem.innerHTML = `
<div class="d-flex align-items-center">
<input class="form-check-input" type="checkbox"
name="category" value="${result.data.name}"
id="category_${result.data._id}" checked>
<label class="form-check-label ms-2" for="category_${result.data._id}">
${result.data.name}
</label>
</div>
<button type="button" class="btn btn-sm btn-outline-danger delete-category-btn"
data-category-id="${result.data._id}"
data-category-name="${result.data.name}"
title="Delete category">
<i class="fas fa-trash"></i>
</button>
`;
categoriesContainer.appendChild(newItem);
} else {
// Just check it if it exists
existingCheckbox.checked = true;
}
newCategoryInput.value = '';
} else {
alert(result.message || 'Error creating category');
}
} catch (error) {
console.error('Error creating category:', error);
alert('Error creating category: ' + error.message);
} finally {
addCategoryBtn.disabled = false;
addCategoryBtn.innerHTML = btnHtml;
}
});
// Allow Enter key to add category
newCategoryInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
addCategoryBtn.click();
}
});
// Add Tag functionality
const addTagBtn = document.getElementById('addTagBtn');
const newTagInput = document.getElementById('newTagInput');
const tagsContainer = document.getElementById('tagsContainer');
addTagBtn.addEventListener('click', async function () {
const tagName = newTagInput.value.trim();
if (!tagName) {
alert('Please enter a tag name');
return;
}
const btnHtml = addTagBtn.innerHTML;
addTagBtn.disabled = true;
addTagBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Adding...';
try {
const response = await fetch('/admin/blog/tags/quick-create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: tagName })
});
const result = await response.json();
if (result.success) {
// Check if checkbox already exists
const existingCheckbox = document.querySelector(`input[name="tags"][value="${result.data.name}"]`);
if (!existingCheckbox) {
// Add new checkbox with delete button
const newItem = document.createElement('div');
newItem.className = 'form-check d-flex align-items-center justify-content-between mb-2';
newItem.setAttribute('data-tag-id', result.data._id);
newItem.innerHTML = `
<div class="d-flex align-items-center">
<input class="form-check-input" type="checkbox" name="tags"
value="${result.data.name}" id="tag_${result.data._id}" checked>
<label class="form-check-label ms-2" for="tag_${result.data._id}">
${result.data.name}
</label>
</div>
<button type="button" class="btn btn-sm btn-outline-danger delete-tag-btn"
data-tag-id="${result.data._id}"
data-tag-name="${result.data.name}"
title="Delete tag">
<i class="fas fa-trash"></i>
</button>
`;
tagsContainer.appendChild(newItem);
} else {
// Just check it if it exists
existingCheckbox.checked = true;
}
newTagInput.value = '';
} else {
alert(result.message || 'Error creating tag');
}
} catch (error) {
console.error('Error creating tag:', error);
alert('Error creating tag: ' + error.message);
} finally {
addTagBtn.disabled = false;
addTagBtn.innerHTML = btnHtml;
}
});
// Allow Enter key to add tag
newTagInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
addTagBtn.click();
}
});
});
</script>
<!-- Delete Category Confirmation Modal -->
<div class="modal fade" id="deleteCategoryModal" tabindex="-1" aria-labelledby="deleteCategoryModalLabel"
aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteCategoryModalLabel">
<i class="fas fa-trash me-2"></i>Confirm Delete Category
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the category "<span id="deleteCategoryName" class="fw-bold"></span>"?
</p>
<p class="text-danger mb-0">
<small>
<i class="fas fa-exclamation-triangle me-1"></i>This action cannot be undone.</small>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteCategoryBtn">
<i class="fas fa-trash me-1"></i>Delete
</button>
</div>
</div>
</div>
</div>
<!-- Delete Tag Confirmation Modal -->
<div class="modal fade" id="deleteTagModal" tabindex="-1" aria-labelledby="deleteTagModalLabel" aria-hidden="true"
data-bs-backdrop="false" data-bs-keyboard="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteTagModalLabel">
<i class="fas fa-trash me-2"></i>Confirm Delete Tag
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the tag "<span id="deleteTagName" class="fw-bold"></span>"?</p>
<p class="text-danger mb-0">
<small>
<i class="fas fa-exclamation-triangle me-1"></i>This action cannot be undone.</small>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteTagBtn">
<i class="fas fa-trash me-1"></i>Delete
</button>
</div>
</div>
</div>
</div>
<style>
/* Fix modal z-index */
#deleteCategoryModal,
#deleteTagModal {
z-index: 2050 !important;
}
#deleteCategoryModal .modal-content,
#deleteTagModal .modal-content {
z-index: 2070 !important;
position: relative;
}
#deleteCategoryModal.show,
#deleteTagModal.show {
display: block !important;
}
/* EditorJS delimiter -> render as HR line */
.codex-editor .ce-delimiter {
line-height: 0;
margin: 16px 0;
}
.codex-editor .ce-delimiter::before {
content: "";
display: block;
width: 100%;
border-top: 2px solid rgba(0,0,0,0.75);
margin: 0;
}
.codex-editor .ce-delimiter::after {
content: none !important; /* hide the *** */
}
/* Comment Management Styles */
.avatar-circle {
flex-shrink: 0;
font-size: 1rem;
}
.contact-info {
border-left: 3px solid #dee2e6;
}
.contact-info .fas {
width: 20px;
text-align: center;
}
.comment-content {
border-left: 3px solid #0d6efd;
background-color: #f8f9fa;
}
.reply-content {
border-left: 3px solid #6c757d;
background-color: #f8f9fa;
}
.replies-container {
border-color: #dee2e6 !important;
}
.approve-comment-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(25, 135, 84, 0.3);
}
.reject-comment-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(255, 193, 7, 0.3);
}
.delete-comment-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(220, 53, 69, 0.3);
}
.card.border-success {
border-width: 2px !important;
}
.card.border-danger {
border-width: 2px !important;
}
.card.border-warning {
border-width: 2px !important;
}
.comment-item {
transition: opacity 0.3s ease;
}
.comment-item[style*="display: none"] {
opacity: 0;
}
.no-results-message {
display: none;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Initialize Category Delete Modal
const deleteCategoryModalElement = document.getElementById('deleteCategoryModal');
const deleteCategoryModal = new bootstrap.Modal(deleteCategoryModalElement, {
backdrop: false,
keyboard: true,
focus: true
});
let currentCategoryId = null;
let currentCategoryBtn = null;
// Handle category delete buttons
document.addEventListener('click', function (e) {
if (e.target.closest('.delete-category-btn')) {
e.preventDefault();
const btn = e.target.closest('.delete-category-btn');
currentCategoryId = btn.dataset.categoryId;
const categoryName = btn.dataset.categoryName;
currentCategoryBtn = btn;
// Set category name in modal
document.getElementById('deleteCategoryName').textContent = categoryName;
// Show modal
deleteCategoryModal.show();
}
});
// Handle confirm delete category
document.getElementById('confirmDeleteCategoryBtn').addEventListener('click', async function () {
if (!currentCategoryId || !currentCategoryBtn) return;
const btn = currentCategoryBtn;
const btnHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
const response = await fetch(`/admin/blog/categories/${currentCategoryId}/delete`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
}
});
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const result = await response.json();
if (result.success) {
// Close modal
deleteCategoryModal.hide();
// Remove the category from the list
const categoryItem = btn.closest('[data-category-id]');
if (categoryItem) {
categoryItem.remove();
}
} else {
alert(result.message || 'Error deleting category');
btn.disabled = false;
btn.innerHTML = btnHtml;
}
} else {
// If response is not JSON, assume success (redirect response)
deleteCategoryModal.hide();
const categoryItem = btn.closest('[data-category-id]');
if (categoryItem) {
categoryItem.remove();
}
}
} catch (error) {
console.error('Error deleting category:', error);
alert('Error deleting category: ' + error.message);
btn.disabled = false;
btn.innerHTML = btnHtml;
} finally {
currentCategoryId = null;
currentCategoryBtn = null;
}
});
// Initialize Tag Delete Modal
const deleteTagModalElement = document.getElementById('deleteTagModal');
const deleteTagModal = new bootstrap.Modal(deleteTagModalElement, {
backdrop: false,
keyboard: true,
focus: true
});
let currentTagId = null;
let currentTagBtn = null;
// Handle tag delete buttons
document.addEventListener('click', function (e) {
if (e.target.closest('.delete-tag-btn')) {
e.preventDefault();
const btn = e.target.closest('.delete-tag-btn');
currentTagId = btn.dataset.tagId;
const tagName = btn.dataset.tagName;
currentTagBtn = btn;
// Set tag name in modal
document.getElementById('deleteTagName').textContent = tagName;
// Show modal
deleteTagModal.show();
}
});
// Handle confirm delete tag
document.getElementById('confirmDeleteTagBtn').addEventListener('click', async function () {
if (!currentTagId || !currentTagBtn) return;
const btn = currentTagBtn;
const btnHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
const response = await fetch(`/admin/blog/tags/${currentTagId}/delete`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
}
});
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const result = await response.json();
if (result.success) {
// Close modal
deleteTagModal.hide();
// Remove the tag from the list
const tagItem = btn.closest('[data-tag-id]');
if (tagItem) {
tagItem.remove();
}
} else {
alert(result.message || 'Error deleting tag');
btn.disabled = false;
btn.innerHTML = btnHtml;
}
} else {
// If response is not JSON, assume success (redirect response)
deleteTagModal.hide();
const tagItem = btn.closest('[data-tag-id]');
if (tagItem) {
tagItem.remove();
}
}
} catch (error) {
console.error('Error deleting tag:', error);
alert('Error deleting tag: ' + error.message);
btn.disabled = false;
btn.innerHTML = btnHtml;
} finally {
currentTagId = null;
currentTagBtn = null;
}
});
// Comment Management
const blogId = '<%= blog._id %>';
// Filter and Sort Comments
const filterStatus = document.getElementById('filterStatus');
const sortComments = document.getElementById('sortComments');
const searchComments = document.getElementById('searchComments');
const resetFilters = document.getElementById('resetFilters');
const commentsList = document.getElementById('commentsList');
function filterAndSortComments() {
if (!commentsList) return;
const statusFilter = filterStatus ? filterStatus.value : 'all';
const sortBy = sortComments ? sortComments.value : 'newest';
const searchTerm = searchComments ? searchComments.value.toLowerCase().trim() : '';
const commentItems = Array.from(commentsList.querySelectorAll('.comment-item'));
// Filter comments
let filtered = commentItems.filter(item => {
// Status filter
if (statusFilter !== 'all' && item.dataset.status !== statusFilter) {
return false;
}
// Search filter
if (searchTerm) {
const name = item.dataset.authorName || '';
const email = item.dataset.authorEmail || '';
const phone = item.dataset.authorPhone || '';
const content = item.dataset.content || '';
if (!name.includes(searchTerm) &&
!email.includes(searchTerm) &&
!phone.includes(searchTerm) &&
!content.includes(searchTerm)) {
return false;
}
}
return true;
});
// Sort comments
filtered.sort((a, b) => {
switch (sortBy) {
case 'newest':
return parseInt(b.dataset.createdAt) - parseInt(a.dataset.createdAt);
case 'oldest':
return parseInt(a.dataset.createdAt) - parseInt(b.dataset.createdAt);
case 'name-asc':
return (a.dataset.authorName || '').localeCompare(b.dataset.authorName || '');
case 'name-desc':
return (b.dataset.authorName || '').localeCompare(a.dataset.authorName || '');
default:
return 0;
}
});
// Remove existing no-results message
const existingNoResults = commentsList.querySelector('.no-results-message');
if (existingNoResults) {
existingNoResults.remove();
}
// Hide all comments first
commentItems.forEach(item => {
item.style.display = 'none';
});
// Reorder and show filtered and sorted comments
filtered.forEach(item => {
item.style.display = 'block';
// Move to correct position in DOM to reflect sort order
commentsList.appendChild(item);
});
// Show message if no results
if (filtered.length === 0 && commentItems.length > 0) {
const noResultsMsg = document.createElement('div');
noResultsMsg.className = 'no-results-message text-center py-5';
noResultsMsg.innerHTML = `
<i class="fas fa-search fa-3x text-muted mb-3"></i>
<p class="text-muted">No comments match your filters.</p>
`;
commentsList.appendChild(noResultsMsg);
}
}
// Event listeners for filter and sort
if (filterStatus) {
filterStatus.addEventListener('change', filterAndSortComments);
}
if (sortComments) {
sortComments.addEventListener('change', filterAndSortComments);
}
if (searchComments) {
searchComments.addEventListener('input', filterAndSortComments);
}
if (resetFilters) {
resetFilters.addEventListener('click', function() {
if (filterStatus) filterStatus.value = 'all';
if (sortComments) sortComments.value = 'newest';
if (searchComments) searchComments.value = '';
filterAndSortComments();
});
}
// Initialize filter and sort on page load
if (commentsList && commentsList.querySelectorAll('.comment-item').length > 0) {
filterAndSortComments();
}
// Approve comment
document.addEventListener('click', async function(e) {
if (e.target.closest('.approve-comment-btn')) {
const btn = e.target.closest('.approve-comment-btn');
const commentId = btn.dataset.commentId;
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
const response = await fetch(`/admin/blog/${blogId}/comments/${commentId}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (result.success) {
location.reload();
} else {
alert(result.message || 'Error approving comment');
btn.disabled = false;
btn.innerHTML = originalHtml;
}
} catch (error) {
console.error('Error approving comment:', error);
alert('Error approving comment: ' + error.message);
btn.disabled = false;
btn.innerHTML = originalHtml;
}
}
});
// Reject comment
document.addEventListener('click', async function(e) {
if (e.target.closest('.reject-comment-btn')) {
const btn = e.target.closest('.reject-comment-btn');
const commentId = btn.dataset.commentId;
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
const response = await fetch(`/admin/blog/${blogId}/comments/${commentId}/reject`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (result.success) {
location.reload();
} else {
alert(result.message || 'Error rejecting comment');
btn.disabled = false;
btn.innerHTML = originalHtml;
}
} catch (error) {
console.error('Error rejecting comment:', error);
alert('Error rejecting comment: ' + error.message);
btn.disabled = false;
btn.innerHTML = originalHtml;
}
}
});
// Delete comment
document.addEventListener('click', async function(e) {
if (e.target.closest('.delete-comment-btn')) {
if (!confirm('Are you sure you want to delete this comment? This action cannot be undone.')) {
return;
}
const btn = e.target.closest('.delete-comment-btn');
const commentId = btn.dataset.commentId;
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
const response = await fetch(`/admin/blog/${blogId}/comments/${commentId}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (result.success) {
location.reload();
} else {
alert(result.message || 'Error deleting comment');
btn.disabled = false;
btn.innerHTML = originalHtml;
}
} catch (error) {
console.error('Error deleting comment:', error);
alert('Error deleting comment: ' + error.message);
btn.disabled = false;
btn.innerHTML = originalHtml;
}
}
});
});
</script>