Files
shopify-ai-backup/chat/public/js/blog-admin.js
southseact-3d 8e8129d71c Fix Editor.js plugin loading errors in blog admin
- Add multiple fallback global variable names for Editor.js plugins
- Add debug logging to track plugin availability
- Add error handling for editor initialization
- Ensure at least paragraph tool is available before initializing
- Fix ReferenceError: List is not defined when opening post modal
2026-02-10 18:13:13 +00:00

627 lines
19 KiB
JavaScript

/**
* Blog Admin JavaScript
* Handles blog post management with Editor.js integration
*/
let editor = null;
let categories = [];
let posts = [];
let currentEditingPost = null;
// Initialize
async function init() {
await loadCategories();
await loadPosts();
setupEventListeners();
setupTabs();
}
// Setup tab switching
function setupTabs() {
const tabButtons = document.querySelectorAll('.tab-button');
tabButtons.forEach(btn => {
btn.addEventListener('click', () => {
const tabName = btn.dataset.tab;
// Update buttons
tabButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Update content
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.getElementById(`${tabName}-tab`).classList.add('active');
});
});
}
// Setup event listeners
function setupEventListeners() {
// Refresh button
document.getElementById('refresh-blogs')?.addEventListener('click', async () => {
await loadPosts();
showStatus('Posts refreshed', 'success');
});
// New post button
document.getElementById('new-post')?.addEventListener('click', () => {
openModal();
});
// Modal close
document.getElementById('modal-close')?.addEventListener('click', closeModal);
document.getElementById('cancel-post')?.addEventListener('click', closeModal);
// Click outside modal to close
document.getElementById('post-modal')?.addEventListener('click', (e) => {
if (e.target.id === 'post-modal') closeModal();
});
// Post form submit
document.getElementById('post-form')?.addEventListener('submit', handlePostSubmit);
// Category form submit
document.getElementById('category-form')?.addEventListener('submit', handleCategorySubmit);
// Generate slug from title
document.getElementById('post-title')?.addEventListener('blur', () => {
const title = document.getElementById('post-title').value;
const slugInput = document.getElementById('post-slug');
if (title && !slugInput.value) {
slugInput.value = generateSlug(title);
}
});
// Filters
['filter-type', 'filter-status', 'filter-source'].forEach(id => {
document.getElementById(id)?.addEventListener('change', () => {
renderPostsList();
});
});
document.getElementById('filter-search')?.addEventListener('input', () => {
renderPostsList();
});
// Generate category slug
document.getElementById('category-name')?.addEventListener('blur', () => {
const name = document.getElementById('category-name').value;
const slugInput = document.getElementById('category-slug');
if (name && !slugInput.value) {
slugInput.value = generateSlug(name);
}
});
}
// Generate slug
function generateSlug(text) {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.substring(0, 100);
}
// Load categories from API
async function loadCategories() {
try {
const response = await fetch('/api/blogs/categories');
const data = await response.json();
categories = data.categories || [];
populateCategorySelect();
renderCategoriesList();
} catch (error) {
console.error('Failed to load categories:', error);
}
}
// Load posts from API
async function loadPosts() {
try {
const response = await fetch('/api/admin/blogs');
const data = await response.json();
posts = data.posts || [];
renderPostsList();
} catch (error) {
console.error('Failed to load posts:', error);
document.getElementById('blog-list').innerHTML = '<p class="error">Failed to load posts</p>';
}
}
// Populate category select dropdown
function populateCategorySelect() {
const select = document.getElementById('post-category');
select.innerHTML = '<option value="">No Category</option>';
categories.forEach(cat => {
const option = document.createElement('option');
option.value = cat.slug;
option.textContent = cat.name;
select.appendChild(option);
});
}
// Render posts list
function renderPostsList() {
const container = document.getElementById('blog-list');
const typeFilter = document.getElementById('filter-type')?.value;
const statusFilter = document.getElementById('filter-status')?.value;
const sourceFilter = document.getElementById('filter-source')?.value;
const searchFilter = document.getElementById('filter-search')?.value.toLowerCase();
let filtered = posts;
if (typeFilter) filtered = filtered.filter(p => p.type === typeFilter);
if (statusFilter) filtered = filtered.filter(p => p.status === statusFilter);
if (sourceFilter) filtered = filtered.filter(p => p.source === sourceFilter);
if (searchFilter) {
filtered = filtered.filter(p =>
p.title?.toLowerCase().includes(searchFilter) ||
p.excerpt?.toLowerCase().includes(searchFilter) ||
p.author?.toLowerCase().includes(searchFilter)
);
}
if (filtered.length === 0) {
container.innerHTML = '<p>No posts found</p>';
return;
}
container.innerHTML = filtered.map(post => `
<div class="blog-item ${post.source}">
<div>
<div style="display: flex; gap: 8px; align-items: center; margin-bottom: 4px;">
<strong>${escapeHtml(post.title)}</strong>
<span class="type-badge type-${post.type}">${post.type}</span>
<span class="status-badge status-${post.status}">${post.status}</span>
<span class="source-badge source-${post.source}">${post.source}</span>
</div>
<div class="blog-meta">
<span>By ${escapeHtml(post.author || 'Unknown')}</span>
<span>${formatDate(post.published_at || post.created_at)}</span>
${post.category ? `<span>Category: ${escapeHtml(post.category)}</span>` : ''}
${post.tags?.length ? `<span>Tags: ${post.tags.join(', ')}</span>` : ''}
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="ghost" onclick="editPost('${post.id}')" ${post.source === 'repo' ? 'disabled title="Repo posts are read-only"' : ''}>
${post.source === 'repo' ? 'View' : 'Edit'}
</button>
${post.source === 'database' ? `
<button class="ghost danger" onclick="deletePost('${post.id}')">Delete</button>
` : ''}
</div>
</div>
`).join('');
}
// Render categories list
function renderCategoriesList() {
const container = document.getElementById('category-list');
if (categories.length === 0) {
container.innerHTML = '<p>No categories found</p>';
return;
}
container.innerHTML = categories.map(cat => `
<div class="category-item">
<div>
<strong>${escapeHtml(cat.name)}</strong>
<span style="color: var(--muted); font-size: 0.85em; margin-left: 8px;">/${escapeHtml(cat.slug)}</span>
${cat.description ? `<p style="margin: 4px 0 0; font-size: 0.85em; color: var(--muted);">${escapeHtml(cat.description)}</p>` : ''}
</div>
<button class="ghost danger" onclick="deleteCategory('${cat.id}')">Delete</button>
</div>
`).join('');
}
// Open modal for new post
function openModal(post = null) {
currentEditingPost = post;
const modal = document.getElementById('post-modal');
const titleEl = document.getElementById('modal-title');
const sourceEl = document.getElementById('post-source');
if (post) {
titleEl.textContent = post.source === 'repo' ? 'View Post (Read-Only)' : 'Edit Post';
document.getElementById('post-id').value = post.id;
document.getElementById('post-title').value = post.title || '';
document.getElementById('post-slug').value = post.slug || '';
document.getElementById('post-type').value = post.type || 'news';
document.getElementById('post-status').value = post.status || 'draft';
document.getElementById('post-category').value = post.category || '';
document.getElementById('post-author').value = post.author || '';
document.getElementById('post-excerpt').value = post.excerpt || '';
document.getElementById('post-meta-title').value = post.meta_title || '';
document.getElementById('post-meta-description').value = post.meta_description || '';
document.getElementById('post-featured-image').value = post.featured_image || '';
document.getElementById('post-tags').value = (post.tags || []).join(', ');
sourceEl.textContent = post.source === 'repo' ? 'Read-Only (Repo)' : 'Editable (Database)';
sourceEl.className = `source-badge source-${post.source}`;
// Disable form for repo posts
const isRepo = post.source === 'repo';
document.querySelectorAll('#post-form input, #post-form select, #post-form textarea').forEach(el => {
el.disabled = isRepo;
});
document.getElementById('save-post').style.display = isRepo ? 'none' : 'inline-block';
} else {
titleEl.textContent = 'New Post';
document.getElementById('post-form').reset();
document.getElementById('post-id').value = '';
document.getElementById('post-status').value = 'draft';
document.getElementById('post-type').value = 'news';
sourceEl.textContent = 'New Post';
sourceEl.className = 'source-badge source-database';
document.querySelectorAll('#post-form input, #post-form select, #post-form textarea').forEach(el => {
el.disabled = false;
});
document.getElementById('save-post').style.display = 'inline-block';
}
modal.classList.remove('hidden');
// Initialize Editor.js
initEditor(post?.content || null);
}
// Close modal
function closeModal() {
document.getElementById('post-modal').classList.add('hidden');
if (editor) {
editor.destroy();
editor = null;
}
currentEditingPost = null;
}
// Initialize Editor.js
function initEditor(savedContent = null) {
if (editor) {
editor.destroy();
}
// Check if Editor.js and plugins are loaded
if (typeof EditorJS === 'undefined') {
console.error('EditorJS is not loaded');
showStatus('Editor failed to load. Please refresh the page.', 'error');
return;
}
// Map plugin names to their global variables with multiple possible names
const HeaderClass = window.Header || window.EditorJSHeader;
const ParagraphClass = window.Paragraph || window.EditorJSParagraph;
const ListClass = window.List || window.EditorjsList || window.EditorJSList;
const LinkToolClass = window.LinkTool || window.Link || window.EditorJSLink;
const ImageToolClass = window.ImageTool || window.Image || window.EditorJSImage;
const EmbedClass = window.Embed || window.EditorJSEmbed;
const CodeToolClass = window.CodeTool || window.Code || window.EditorJSCode;
const QuoteClass = window.Quote || window.EditorJSQuote;
const TableClass = window.Table || window.EditorJSTable;
const MarkerClass = window.Marker || window.EditorJSMarker;
// Log available plugins for debugging
console.log('Editor.js plugins availability:', {
EditorJS: typeof EditorJS,
Header: typeof HeaderClass,
Paragraph: typeof ParagraphClass,
List: typeof ListClass,
LinkTool: typeof LinkToolClass,
ImageTool: typeof ImageToolClass,
Embed: typeof EmbedClass,
CodeTool: typeof CodeToolClass,
Quote: typeof QuoteClass,
Table: typeof TableClass,
Marker: typeof MarkerClass
});
// Build tools object dynamically, only including available plugins
const tools = {};
if (HeaderClass) {
tools.header = {
class: HeaderClass,
config: {
levels: [2, 3, 4, 5, 6],
defaultLevel: 2
}
};
}
if (ParagraphClass) {
tools.paragraph = {
class: ParagraphClass,
inlineToolbar: true
};
}
if (ListClass) {
tools.list = {
class: ListClass,
inlineToolbar: true
};
}
if (LinkToolClass) {
tools.link = {
class: LinkToolClass,
config: {
endpoint: '/api/fetch-url'
}
};
}
if (ImageToolClass) {
tools.image = {
class: ImageToolClass,
config: {
endpoints: {
byFile: '/api/admin/blogs/upload-image'
},
field: 'image',
types: 'image/*'
}
};
}
if (EmbedClass) {
tools.embed = {
class: EmbedClass,
config: {
services: {
youtube: true,
vimeo: true,
twitter: true,
codepen: true
}
}
};
}
if (CodeToolClass) {
tools.code = {
class: CodeToolClass
};
}
if (QuoteClass) {
tools.quote = {
class: QuoteClass,
inlineToolbar: true,
config: {
quotePlaceholder: 'Enter a quote',
captionPlaceholder: 'Quote\'s author'
}
};
}
if (TableClass) {
tools.table = {
class: TableClass,
inlineToolbar: true,
config: {
rows: 2,
cols: 2
}
};
}
if (MarkerClass) {
tools.marker = {
class: MarkerClass
};
}
// Ensure at least paragraph tool is available
if (!tools.paragraph) {
console.error('No paragraph tool available');
showStatus('Editor tools failed to load. Please refresh the page.', 'error');
return;
}
const editorConfig = {
holder: 'editor',
tools: tools,
placeholder: 'Start writing your post...',
data: savedContent || {
blocks: [
{
type: 'paragraph',
data: {
text: ''
}
}
]
},
readOnly: currentEditingPost?.source === 'repo'
};
try {
editor = new EditorJS(editorConfig);
console.log('Editor.js initialized successfully');
} catch (error) {
console.error('Failed to initialize Editor.js:', error);
showStatus('Failed to initialize editor: ' + error.message, 'error');
}
}
// Handle post form submission
async function handlePostSubmit(e) {
e.preventDefault();
if (!editor) {
showStatus('Editor not initialized', 'error');
return;
}
try {
const content = await editor.save();
const postId = document.getElementById('post-id').value;
const postData = {
title: document.getElementById('post-title').value,
slug: document.getElementById('post-slug').value,
type: document.getElementById('post-type').value,
status: document.getElementById('post-status').value,
category: document.getElementById('post-category').value,
author: document.getElementById('post-author').value,
excerpt: document.getElementById('post-excerpt').value,
meta_title: document.getElementById('post-meta-title').value,
meta_description: document.getElementById('post-meta-description').value,
featured_image: document.getElementById('post-featured-image').value,
tags: document.getElementById('post-tags').value.split(',').map(t => t.trim()).filter(Boolean),
content: content
};
let response;
if (postId) {
// Update existing post
response = await fetch(`/api/admin/blogs/${postId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData)
});
} else {
// Create new post
response = await fetch('/api/admin/blogs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData)
});
}
const result = await response.json();
if (response.ok) {
showStatus(postId ? 'Post updated successfully' : 'Post created successfully', 'success');
closeModal();
await loadPosts();
} else {
showStatus(result.error || 'Failed to save post', 'error');
}
} catch (error) {
console.error('Failed to save post:', error);
showStatus('Failed to save post', 'error');
}
}
// Handle category form submission
async function handleCategorySubmit(e) {
e.preventDefault();
const categoryData = {
name: document.getElementById('category-name').value,
slug: document.getElementById('category-slug').value,
description: document.getElementById('category-description').value
};
try {
const response = await fetch('/api/admin/blogs/categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(categoryData)
});
const result = await response.json();
if (response.ok) {
showStatus('Category created successfully', 'success');
document.getElementById('category-form').reset();
await loadCategories();
} else {
showStatus(result.error || 'Failed to create category', 'error');
}
} catch (error) {
console.error('Failed to create category:', error);
showStatus('Failed to create category', 'error');
}
}
// Edit post (global function for onclick)
window.editPost = function(postId) {
const post = posts.find(p => p.id === postId);
if (post) {
openModal(post);
}
};
// Delete post (global function for onclick)
window.deletePost = async function(postId) {
if (!confirm('Are you sure you want to delete this post? This cannot be undone.')) {
return;
}
try {
const response = await fetch(`/api/admin/blogs/${postId}`, {
method: 'DELETE'
});
if (response.ok) {
showStatus('Post deleted successfully', 'success');
await loadPosts();
} else {
const result = await response.json();
showStatus(result.error || 'Failed to delete post', 'error');
}
} catch (error) {
console.error('Failed to delete post:', error);
showStatus('Failed to delete post', 'error');
}
};
// Delete category (global function for onclick)
window.deleteCategory = async function(categoryId) {
if (!confirm('Are you sure you want to delete this category?')) {
return;
}
try {
const response = await fetch(`/api/admin/blogs/categories/${categoryId}`, {
method: 'DELETE'
});
if (response.ok) {
showStatus('Category deleted successfully', 'success');
await loadCategories();
} else {
const result = await response.json();
showStatus(result.error || 'Failed to delete category', 'error');
}
} catch (error) {
console.error('Failed to delete category:', error);
showStatus('Failed to delete category', 'error');
}
};
// Utility functions
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
function showStatus(message, type = 'info') {
// Use the existing showStatus function from admin.js if available
if (typeof window.showStatus === 'function' && window.showStatus !== showStatus) {
window.showStatus(message, type);
return;
}
// Simple fallback
console.log(`[${type}] ${message}`);
alert(message);
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', init);