/** * Blog Article Validation * Two-level validation: Content Similarity + Title Similarity */ // Helper to safely convert ObjectId to string function toStringId(id) { if (!id) { console.warn('[toStringId] Received empty id'); return ''; } if (typeof id === 'string') return id; // Handle Buffer object (MongoDB ObjectId as buffer) if (typeof id === 'object' && id.buffer) { console.log('[toStringId] Converting buffer to hex string'); const bytes = []; for (let i = 0; i < 12; i++) { if (id.buffer[i] !== undefined) { bytes.push(id.buffer[i].toString(16).padStart(2, '0')); } } const hexString = bytes.join(''); console.log('[toStringId] Hex string:', hexString); return hexString; } // Check for other common MongoDB ObjectId representations if (typeof id === 'object') { if (id.$oid) return id.$oid; if (id.id) return id.id; if (id._id) return id._id; } const result = String(id); console.log('[toStringId] Fallback String():', result); return result; } // Get auth token function getAuthToken() { return localStorage.getItem('admin_token'); } // API call helper async function apiCall(endpoint, options = {}) { const token = getAuthToken(); const defaultOptions = { cache: 'no-store', // Force fresh requests - prevent cached 500 errors headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }; const response = await fetch(endpoint, { ...defaultOptions, ...options }); if (response.status === 401) { localStorage.removeItem('admin_token'); window.location.href = '/admin/login.html'; throw new Error('Unauthorized'); } return response; } // Load all pending review articles async function loadValidationArticles() { const listDiv = document.getElementById('validation-list'); if (!listDiv) return; listDiv.innerHTML = '
Loading articles and submission packages...
'; try { // Load blog posts with status=pending_review const articlesResponse = await apiCall('/api/blog/admin/posts?status=pending_review&limit=100'); if (!articlesResponse.ok) { throw new Error('Failed to load articles'); } const articlesData = await articlesResponse.json(); const articles = articlesData.posts || []; console.log('[Validation] Loaded articles:', articles.length); // ALSO load standalone submission packages (those without blogPostId) const submissionsResponse = await apiCall('/api/submissions?limit=100'); const standaloneSubmissions = []; console.log('[Validation] Submissions response ok:', submissionsResponse.ok); if (submissionsResponse.ok) { const submissionsData = await submissionsResponse.json(); console.log('[Validation] Submissions data:', submissionsData); const allSubmissions = submissionsData.data || []; console.log('[Validation] Total submissions found:', allSubmissions.length); // Filter for standalone submissions (no blogPostId) allSubmissions.forEach(sub => { console.log('[Validation] Checking submission:', sub.title, 'blogPostId:', sub.blogPostId); if (!sub.blogPostId) { const standaloneSub = { _id: typeof sub._id === 'object' ? toStringId(sub._id) : sub._id.toString(), title: sub.title, status: 'submission_package', publicationName: sub.publicationName, publicationId: sub.publicationId, wordCount: sub.wordCount, contentType: sub.contentType, submissionStatus: sub.status, isStandalone: true, submissionData: sub }; console.log('[Validation] Adding standalone submission:', standaloneSub.title); standaloneSubmissions.push(standaloneSub); } }); console.log('[Validation] Loaded standalone submissions:', standaloneSubmissions.length); } else { console.error('[Validation] Failed to load submissions'); } // Combine articles and standalone submissions const allItems = [...articles, ...standaloneSubmissions]; if (allItems.length === 0) { listDiv.innerHTML = '
No articles or packages pending review
'; return; } // Load submission tracking for each blog post article (not standalone submissions) const submissionData = {}; for (const article of articles) { const articleId = toStringId(article._id); try { const subResponse = await apiCall(`/api/submissions/by-blog-post/${articleId}`); if (subResponse.ok) { const subData = await subResponse.json(); if (subData.data) { submissionData[articleId] = subData.data; } } } catch (err) { // Silently ignore if no submission found } } // Render all items (blog posts + standalone submissions) listDiv.innerHTML = allItems.map(item => { const itemId = toStringId(item._id); const isStandalone = item.isStandalone; // For standalone submissions, use submissionData directly const submission = isStandalone ? item.submissionData : submissionData[itemId]; // Calculate word count const wordCount = isStandalone ? (item.wordCount || 0) : (item.content ? item.content.split(/\s+/).length : 0); // Calculate checklist completion let checklistItems = 0; let checklistCompleted = 0; if (submission && submission.submissionPackage) { const pkg = submission.submissionPackage; if (pkg.coverLetter) { checklistItems++; if (pkg.coverLetter.completed) checklistCompleted++; } if (pkg.notesToEditor) { checklistItems++; if (pkg.notesToEditor.completed) checklistCompleted++; } if (pkg.authorBio) { checklistItems++; if (pkg.authorBio.completed) checklistCompleted++; } if (pkg.pitchEmail) { checklistItems++; if (pkg.pitchEmail.completed) checklistCompleted++; } } const checklistColor = checklistCompleted === checklistItems && checklistItems > 0 ? 'text-green-600' : 'text-yellow-600'; return `

${item.title}

${isStandalone ? `STANDALONE PACKAGE` : ''} ${submission ? `${item.publicationName || submission.publicationName}` : ''}
${!isStandalone ? `Slug: ${item.slug}` : ''} Words: ${wordCount} ${!isStandalone && item.published_at ? `Created: ${new Date(item.published_at).toLocaleDateString()}` : ''} ${submission ? `${(item.contentType || submission.contentType || 'article').toUpperCase()}` : ''}
${submission ? `
📋 Checklist: ${checklistCompleted}/${checklistItems} complete ${submission.submissionMethod ? `via ${submission.submissionMethod}` : ''}
` : '
⚠️ No submission tracking - click "Manage Submission"
'}
${isStandalone ? `✓ Submission Package Ready` : `Not validated yet` }
${!isStandalone ? ` ` : ''} ${!isStandalone ? `
`; }).join(''); // Add edit handlers listDiv.querySelectorAll('.edit-article').forEach(btn => { btn.addEventListener('click', () => { const articleId = btn.dataset.articleId; openEditModal(articleId); }); }); // Add validate handlers listDiv.querySelectorAll('.validate-article').forEach(btn => { btn.addEventListener('click', () => { const articleId = btn.dataset.articleId; const articleTitle = btn.dataset.articleTitle; runValidation(articleId, articleTitle); }); }); } catch (error) { console.error('Failed to load validation articles:', error); listDiv.innerHTML = '
Error loading articles
'; } } // Run validation on an article async function runValidation(articleId, articleTitle) { const statusDiv = document.querySelector(`.validation-status-${articleId}`); const btn = document.querySelector(`.validate-article[data-article-id="${articleId}"]`); if (!statusDiv || !btn) return; // Update UI btn.disabled = true; btn.textContent = 'Validating...'; statusDiv.innerHTML = 'Validating...'; try { const response = await apiCall('/api/blog/validate-article', { method: 'POST', body: JSON.stringify({ articleId }) }); const result = await response.json(); if (response.ok && result.validation) { const v = result.validation; // Count passes and fails const contentFails = v.contentChecks.filter(c => !c.pass).length; const titleFails = v.titleChecks.filter(c => !c.pass).length; // Build status badges let badges = []; // Content badge if (contentFails === 0) { badges.push('✅ Content: Pass'); } else { badges.push(`❌ Content: ${contentFails} conflict(s)`); } // Title badge if (titleFails === 0) { badges.push('✅ Title: Pass'); } else { badges.push(`🚫 Title: ${titleFails} conflict(s)`); } statusDiv.innerHTML = badges.join(' '); // Show detailed modal showValidationModal(articleTitle, result.validation); } else { statusDiv.innerHTML = 'Error'; alert(`Validation error: ${result.message || 'Unknown error'}`); } } catch (error) { console.error('Validation error:', error); statusDiv.innerHTML = 'Error'; alert(`Validation error: ${error.message}`); } finally { btn.disabled = false; btn.textContent = 'Validate'; } } // Show validation results modal function showValidationModal(articleTitle, validation) { const { contentChecks, titleChecks, overallStatus, errors, warnings, summary } = validation; const contentChecksHtml = contentChecks.length > 0 ? `

Content Similarity Checks (${summary.contentPasses} pass, ${summary.contentFails} fail)

${contentChecks.slice(0, 5).map(check => { const bgClass = check.pass ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'; const iconClass = check.pass ? 'text-green-600' : 'text-red-600'; const icon = check.pass ? '✅' : '❌'; return `
${icon}
vs. "${check.comparedWith}"
Similarity: ${Math.round(check.similarity * 100)}%
${check.message}
`; }).join('')}
` : '
No other articles to compare
'; const titleChecksHtml = titleChecks.length > 0 ? `

Title Similarity Checks (${summary.titlePasses} pass, ${summary.titleFails} fail)

${titleChecks.slice(0, 5).map(check => { const bgClass = check.pass ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'; const iconClass = check.pass ? 'text-green-600' : 'text-red-600'; const icon = check.pass ? '✅' : '🚫'; return `
${icon}
vs. "${check.comparedWith}"
Similarity: ${Math.round(check.similarity * 100)}% ${check.sharedWords && check.sharedWords.length > 0 ? ` | Shared: ${check.sharedWords.join(', ')}` : ''}
${check.message}
`; }).join('')}
` : '
No other articles to compare
'; const errorsHtml = errors.length > 0 ? `
❌ Errors - Cannot Submit
` : ''; const warningsHtml = warnings.length > 0 ? `
⚠️ Warnings
` : ''; const summaryClass = overallStatus === 'pass' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'; const summaryText = overallStatus === 'pass' ? '✅ PASS - Ready for submission' : '❌ FAIL - Revisions required'; const modal = `

Validation: "${articleTitle}"

${summaryText} Checked against ${summary.totalChecks} article(s)
${errorsHtml} ${warningsHtml} ${contentChecksHtml} ${titleChecksHtml}
`; const container = document.getElementById('modal-container'); if (!container) return; container.innerHTML = modal; // Close modal handlers container.querySelectorAll('.close-validation-modal').forEach(btn => { btn.addEventListener('click', () => { container.innerHTML = ''; }); }); } // Open edit modal async function openEditModal(articleId) { const container = document.getElementById('modal-container'); container.innerHTML = '
Loading article...
'; try { const response = await apiCall(`/api/blog/admin/${articleId}`); if (!response.ok) { throw new Error('Failed to load article'); } const data = await response.json(); const article = data.post; const modal = `

Edit Article

Change this if there's a title conflict

URL-friendly version (lowercase, hyphens)

Markdown format

Comma-separated

`; container.innerHTML = modal; // Close handlers container.querySelectorAll('.close-edit-modal').forEach(btn => { btn.addEventListener('click', () => { container.innerHTML = ''; }); }); // Save handler container.querySelector('#save-article-btn').addEventListener('click', () => { saveArticle(articleId); }); } catch (error) { console.error('Failed to load article:', error); container.innerHTML = ''; alert('Failed to load article: ' + error.message); } } // Save article changes async function saveArticle(articleId) { const statusDiv = document.getElementById('edit-status-message'); const saveBtn = document.getElementById('save-article-btn'); const form = document.getElementById('edit-article-form'); const formData = new FormData(form); const updates = { title: formData.get('title'), slug: formData.get('slug'), excerpt: formData.get('excerpt'), content: formData.get('content'), status: formData.get('status'), tags: formData.get('tags').split(',').map(t => t.trim()).filter(t => t) }; saveBtn.disabled = true; saveBtn.textContent = 'Saving...'; statusDiv.textContent = 'Saving changes...'; statusDiv.className = 'mt-4 text-sm text-blue-600'; try { const response = await apiCall(`/api/blog/${articleId}`, { method: 'PUT', body: JSON.stringify(updates) }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to save changes'); } statusDiv.textContent = '✓ Changes saved successfully!'; statusDiv.className = 'mt-4 text-sm text-green-600'; // Close modal and refresh list after short delay setTimeout(() => { document.getElementById('modal-container').innerHTML = ''; loadValidationArticles(); }, 1000); } catch (error) { console.error('Save error:', error); statusDiv.textContent = `✗ Error: ${error.message}`; statusDiv.className = 'mt-4 text-sm text-red-600'; saveBtn.disabled = false; saveBtn.textContent = 'Save Changes'; } } // Load published posts async function loadPublishedPosts() { const listDiv = document.getElementById('published-list'); if (!listDiv) return; listDiv.innerHTML = '
Loading published posts...
'; try { const response = await apiCall('/api/blog/admin/posts?status=published&limit=100'); if (!response.ok) { throw new Error('Failed to load published posts'); } const data = await response.json(); const posts = data.posts || []; if (posts.length === 0) { listDiv.innerHTML = '
No published posts found.
'; return; } listDiv.innerHTML = posts.map(post => { const articleId = toStringId(post._id); const wordCount = post.content ? post.content.split(/\s+/).length : 0; const excerpt = post.excerpt || (post.content ? post.content.substring(0, 150) + '...' : 'No excerpt'); const tags = (post.tags || []).slice(0, 3).map(tag => `${tag}` ).join(' '); return `

${post.title}

${excerpt}

${wordCount} words Slug: ${post.slug} ${post.publishedAt ? `Published: ${new Date(post.publishedAt).toLocaleDateString()}` : ''}
${tags ? `
${tags}
` : ''}
`; }).join(''); // Attach read button handlers document.querySelectorAll('.read-article').forEach(btn => { btn.addEventListener('click', async (e) => { const articleId = e.target.dataset.articleId; const post = posts.find(p => toStringId(p._id) === articleId); if (post) { showReadModal(post); } }); }); } catch (error) { console.error('Failed to load published posts:', error); listDiv.innerHTML = '
Failed to load published posts. Please try again.
'; } } // Show read modal for a post function showReadModal(post) { const modal = document.createElement('div'); modal.className = 'fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50'; modal.innerHTML = `

${post.title}

Slug: ${post.slug} ${post.publishedAt ? `Published: ${new Date(post.publishedAt).toLocaleDateString()}` : ''}
${post.tags && post.tags.length > 0 ? `
${post.tags.map(tag => `${tag}`).join(' ')}
` : ''}
${post.excerpt ? `

${post.excerpt}

` : ''}
${post.content || '

No content available.

'}
View Live
`; document.body.appendChild(modal); // Close modal handlers modal.querySelectorAll('.close-modal').forEach(btn => { btn.addEventListener('click', () => modal.remove()); }); // Close on background click modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); } // Initialize section navigation function initSectionNavigation() { const tabs = document.querySelectorAll('.section-tab'); const sections = { validation: document.getElementById('validation-section'), draft: document.getElementById('draft-section'), queue: document.getElementById('queue-section'), guidelines: document.getElementById('guidelines-section'), published: document.getElementById('published-section') }; tabs.forEach(tab => { tab.addEventListener('click', () => { const targetSection = tab.dataset.section; // Update tab styles tabs.forEach(t => { t.classList.remove('border-blue-500', 'text-blue-600'); t.classList.add('border-transparent', 'text-gray-500'); }); tab.classList.remove('border-transparent', 'text-gray-500'); tab.classList.add('border-blue-500', 'text-blue-600'); // Show/hide sections Object.keys(sections).forEach(key => { if (key === targetSection) { sections[key].classList.remove('hidden'); // Load data for specific sections if (key === 'validation') { loadValidationArticles(); } else if (key === 'published') { loadPublishedPosts(); } } else { sections[key].classList.add('hidden'); } }); }); }); } // Initialize validation section function initValidation() { console.log('[Validation] Initializing validation section'); const refreshBtn = document.getElementById('refresh-validation-btn'); if (refreshBtn) { refreshBtn.addEventListener('click', loadValidationArticles); } const refreshPublishedBtn = document.getElementById('refresh-published-btn'); if (refreshPublishedBtn) { refreshPublishedBtn.addEventListener('click', loadPublishedPosts); } // Auto-load validation articles const validationList = document.getElementById('validation-list'); if (validationList) { console.log('[Validation] Loading validation articles...'); loadValidationArticles(); } else { console.warn('[Validation] validation-list element not found'); } // Initialize section navigation initSectionNavigation(); } // Run on page load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initValidation); } else { initValidation(); } // =========================================== // MANAGE SUBMISSION MODAL // =========================================== // Global state for submission modal let currentSubmissionArticleId = null; let currentSubmissionId = null; let currentArticleData = null; /** * Initialize Manage Submission button handlers */ function initManageSubmissionHandlers() { // Event delegation for manage submission buttons document.addEventListener('click', (e) => { if (e.target.classList.contains('manage-submission')) { const articleId = e.target.dataset.articleId; const submissionId = e.target.dataset.submissionId; openManageSubmissionModal(articleId, submissionId || null); } }); } // Call this when page loads initManageSubmissionHandlers();