forked from UKSOURCE/cms.hailearning.edu.vn
1765 lines
101 KiB
Plaintext
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, '"') %>"
|
|
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>
|