/** * 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 = '

Failed to load posts

'; } } // Populate category select dropdown function populateCategorySelect() { const select = document.getElementById('post-category'); select.innerHTML = ''; 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 = '

No posts found

'; return; } container.innerHTML = filtered.map(post => `
${escapeHtml(post.title)} ${post.type} ${post.status} ${post.source}
By ${escapeHtml(post.author || 'Unknown')} ${formatDate(post.published_at || post.created_at)} ${post.category ? `Category: ${escapeHtml(post.category)}` : ''} ${post.tags?.length ? `Tags: ${post.tags.join(', ')}` : ''}
${post.source === 'database' ? ` ` : ''}
`).join(''); } // Render categories list function renderCategoriesList() { const container = document.getElementById('category-list'); if (categories.length === 0) { container.innerHTML = '

No categories found

'; return; } container.innerHTML = categories.map(cat => `
${escapeHtml(cat.name)} /${escapeHtml(cat.slug)} ${cat.description ? `

${escapeHtml(cat.description)}

` : ''}
`).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);