Files
cms.uldp.edu.vn/views/admin/blog/edit.ejs

1152 lines
59 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">
<!-- 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>
</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.
</div>
<div id="featuredImagePreview" class="mt-2">
<% if (blog.featuredImage) { %>
<img src="<%= blog.featuredImage.startsWith('http') ? blog.featuredImage : (typeof frontendUrl !== 'undefined' ? frontendUrl : '') + blog.featuredImage %>"
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)
</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="<%= image1.startsWith('http') ? image1 : (typeof frontendUrl !== 'undefined' ? frontendUrl : '') + image1 %>"
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="<%= image2.startsWith('http') ? image2 : (typeof frontendUrl !== 'undefined' ? frontendUrl : '') + image2 %>"
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">
<span id="excerptCount">
<%= (blog.excerpt || '' ).length %>
</span>/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>
</div>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end mb-4">
<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,
config: {
levels: [2, 3, 4],
defaultLevel: 2
}
},
paragraph: {
class: Paragraph,
inlineToolbar: true
},
list: {
class: List,
inlineToolbar: true,
config: {
defaultStyle: 'unordered'
}
},
image: {
class: Image,
config: {
endpoints: {
byFile: '/admin/upload/image?imageType=blog'
}
}
},
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);
}
// Excerpt character counter
const excerptInput = document.getElementById('excerpt');
const excerptCount = document.getElementById('excerptCount');
excerptInput.addEventListener('input', function () {
excerptCount.textContent = this.value.length;
});
// 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
if (targetInput === 'featuredImageUrl') {
const preview = document.getElementById('featuredImagePreview');
const previewUrl = (result.path && (result.path.startsWith('http://') || result.path.startsWith('https://')))
? result.path
: (window.location.origin + result.path);
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 = (result.path && (result.path.startsWith('http://') || result.path.startsWith('https://')))
? result.path
: (window.location.origin + result.path);
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;
}
</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;
}
});
});
</script>