forked from UKSOURCE/cms.hailearning.edu.vn
1056 lines
53 KiB
Plaintext
1056 lines
53 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">Create a new 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/create" 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>
|
|
</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" 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 featured image for this blog post or
|
|
enter image URL.
|
|
<br><strong>Recommended size:</strong> 852 x 400 px
|
|
</div>
|
|
<div id="featuredImagePreview" class="mt-2"></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
|
|
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">
|
|
<div class="col-md-6">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control gallery-image-input"
|
|
id="galleryImages_0" name="galleryImages[]" 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"></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[]" 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"></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)"></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)"></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 %>">
|
|
<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 %>">
|
|
<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="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" selected>Published</option>
|
|
<option value="draft">Draft</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-12">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="isFeatured"
|
|
id="isFeatured">
|
|
<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>
|
|
|
|
<!-- 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>Create 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;
|
|
}
|
|
|
|
// 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: { blocks: [] },
|
|
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: { blocks: [] },
|
|
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') {
|
|
// Lưu đúng relative path vào input (ví dụ: /uploads/blog/xxx.png)
|
|
input.value = result.path;
|
|
|
|
// Helper function để tạo full URL
|
|
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;
|
|
};
|
|
|
|
// Show preview sử dụng helper function
|
|
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>Creating...';
|
|
|
|
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 *** */
|
|
}
|
|
</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>
|