/** * Enhanced Submission Modal for Blog Post Submissions * World-class UI/UX with tabs, content preview, validation * CSP-compliant: Uses event delegation instead of inline handlers */ let currentArticle = null; let currentSubmission = null; let activeTab = 'overview'; /** * Create enhanced submission modal */ function createEnhancedSubmissionModal() { const modal = document.createElement('div'); modal.id = 'manage-submission-modal'; modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden'; modal.innerHTML = `
`; document.body.appendChild(modal); setupEventListeners(); } /** * Setup event listeners using event delegation */ function setupEventListeners() { const modal = document.getElementById('manage-submission-modal'); if (!modal) return; // Event delegation for all modal interactions modal.addEventListener('click', (e) => { const target = e.target; // Close modal if (target.hasAttribute('data-action') && target.getAttribute('data-action') === 'close-modal') { closeSubmissionModal(); return; } // Save submission if (target.hasAttribute('data-action') && target.getAttribute('data-action') === 'save-submission') { saveSubmission(); return; } // Tab switching if (target.hasAttribute('data-tab')) { switchTab(target.getAttribute('data-tab')); return; } // Export actions if (target.hasAttribute('data-export')) { exportPackage(target.getAttribute('data-export')); return; } // Copy to clipboard if (target.hasAttribute('data-action') && target.getAttribute('data-action') === 'copy-clipboard') { copyToClipboard(); return; } }); // Handle text input changes for word count modal.addEventListener('blur', (e) => { if (e.target.id && e.target.id.startsWith('doc-')) { const docType = e.target.id.replace('doc-', ''); updateDocumentWordCount(docType); } }, true); } /** * Open submission modal for article */ async function openManageSubmissionModal(articleId, submissionId) { const modal = document.getElementById('manage-submission-modal'); if (!modal) { createEnhancedSubmissionModal(); } // Load article and submission data try { const response = await fetch(`/api/blog/posts/${articleId}`); if (!response.ok) throw new Error('Failed to load article'); currentArticle = await response.json(); // Try to load existing submission const submissionResponse = await fetch(`/api/submissions/by-blog-post/${articleId}`); if (submissionResponse.ok) { currentSubmission = await submissionResponse.json(); } else { currentSubmission = null; } // Update modal title document.getElementById('modal-title').textContent = `Manage Submission: ${currentArticle.title}`; // Show modal document.getElementById('manage-submission-modal').classList.remove('hidden'); // Load overview tab switchTab('overview'); } catch (error) { console.error('Error loading submission data:', error); alert('Failed to load submission data. Please try again.'); } } /** * Close submission modal */ function closeSubmissionModal() { document.getElementById('manage-submission-modal').classList.add('hidden'); currentArticle = null; currentSubmission = null; activeTab = 'overview'; } /** * Switch between tabs */ function switchTab(tabName) { activeTab = tabName; // Update tab buttons document.querySelectorAll('.tab-button').forEach(btn => { btn.classList.remove('border-blue-500', 'text-blue-600'); btn.classList.add('border-transparent', 'text-gray-500'); }); const activeButton = document.getElementById(`tab-${tabName}`); activeButton.classList.remove('border-transparent', 'text-gray-500'); activeButton.classList.add('border-blue-500', 'text-blue-600'); // Load tab content const content = document.getElementById('modal-content'); switch(tabName) { case 'overview': content.innerHTML = renderOverviewTab(); // Set progress bar width after rendering requestAnimationFrame(() => { const progressBar = content.querySelector('[data-progress-bar]'); if (progressBar) { const width = progressBar.getAttribute('data-progress'); progressBar.style.width = width + '%'; } }); break; case 'documents': content.innerHTML = renderDocumentsTab(); break; case 'validation': content.innerHTML = renderValidationTab(); break; } } /** * Render Overview Tab */ function renderOverviewTab() { const submission = currentSubmission || {}; const article = currentArticle; const wordCount = article.content ? article.content.split(/\s+/).length : 0; const publicationName = submission.publicationName || 'Not assigned'; const status = submission.status || 'draft'; // Calculate completion percentage let completionScore = 0; if (submission.documents?.mainArticle?.versions?.length > 0) completionScore += 25; if (submission.documents?.coverLetter?.versions?.length > 0) completionScore += 25; if (submission.documents?.authorBio?.versions?.length > 0) completionScore += 25; if (submission.publicationId) completionScore += 25; const excerptText = article.excerpt || (article.content?.substring(0, 300) + '...') || 'No content available'; const publishedDate = article.published_at ? new Date(article.published_at).toLocaleDateString() : 'N/A'; return `

Article Preview

${escapeHtml(article.title)}

${escapeHtml(article.subtitle || '')}
${escapeHtml(excerptText)}
Word Count: ${wordCount.toLocaleString()} Published: ${publishedDate}
Target Publication
${escapeHtml(publicationName)}
Status
${status.charAt(0).toUpperCase() + status.slice(1)}
Completion Progress
${completionScore}%
${renderChecklistItem('Main Article', submission.documents?.mainArticle?.versions?.length > 0)} ${renderChecklistItem('Cover Letter', submission.documents?.coverLetter?.versions?.length > 0)} ${renderChecklistItem('Author Bio', submission.documents?.authorBio?.versions?.length > 0)} ${renderChecklistItem('Publication Target Set', !!submission.publicationId)}
${submission.publicationId ? renderPublicationRequirements(submission.publicationId) : ''}
`; } /** * Render Documents Tab */ function renderDocumentsTab() { const submission = currentSubmission || {}; const article = currentArticle; const mainArticle = submission.documents?.mainArticle?.versions?.[0]?.content || article.content || ''; const coverLetter = submission.documents?.coverLetter?.versions?.[0]?.content || ''; const authorBio = submission.documents?.authorBio?.versions?.[0]?.content || ''; const technicalBrief = submission.documents?.technicalBrief?.versions?.[0]?.content || ''; const mainWordCount = mainArticle.split(/\s+/).length; const coverWordCount = coverLetter.split(/\s+/).length; const bioWordCount = authorBio.split(/\s+/).length; const briefWordCount = technicalBrief.split(/\s+/).length; return `
${renderDocumentEditor('mainArticle', 'Main Article', mainArticle, mainWordCount, true)} ${renderDocumentEditor('coverLetter', 'Cover Letter / Pitch', coverLetter, coverWordCount, false)} ${renderDocumentEditor('authorBio', 'Author Bio', authorBio, bioWordCount, false)} ${renderDocumentEditor('technicalBrief', 'Technical Brief (Optional)', technicalBrief, briefWordCount, false)}
`; } /** * Render Validation Tab */ function renderValidationTab() { const submission = currentSubmission || {}; const article = currentArticle; // Get document word counts const mainArticle = submission.documents?.mainArticle?.versions?.[0]?.content || article.content || ''; const coverLetter = submission.documents?.coverLetter?.versions?.[0]?.content || ''; const authorBio = submission.documents?.authorBio?.versions?.[0]?.content || ''; const mainWordCount = mainArticle.split(/\s+/).filter(w => w.length > 0).length; const coverWordCount = coverLetter.split(/\s+/).filter(w => w.length > 0).length; const bioWordCount = authorBio.split(/\s+/).filter(w => w.length > 0).length; // Validation checks const hasPublication = !!submission.publicationId; const hasMainArticle = mainWordCount > 0; const hasCoverLetter = coverWordCount > 0; const hasAuthorBio = bioWordCount > 0; // Word count validation for specific publications const wordCountWarnings = []; if (submission.publicationId === 'economist-letter' && coverWordCount > 250) { wordCountWarnings.push('Cover letter exceeds The Economist letter limit (250 words)'); } if (submission.publicationId === 'lemonde-letter' && (coverWordCount < 150 || coverWordCount > 200)) { wordCountWarnings.push('Cover letter should be 150-200 words for Le Monde'); } // Format validation const formatWarnings = []; if (submission.publicationId === 'economist-letter' && !coverLetter.startsWith('SIR—')) { formatWarnings.push('Economist letters should start with "SIR—"'); } const allChecksPassed = hasPublication && hasMainArticle && hasCoverLetter && wordCountWarnings.length === 0 && formatWarnings.length === 0; return `

Validation Checks

${renderValidationCheck('Publication target assigned', hasPublication, true)} ${renderValidationCheck(`Main article has content (${mainWordCount.toLocaleString()} words)`, hasMainArticle, true)} ${renderValidationCheck(`Cover letter present (${coverWordCount.toLocaleString()} words)`, hasCoverLetter, false)} ${renderValidationCheck(`Author bio present (${bioWordCount.toLocaleString()} words)`, hasAuthorBio, false)}
${renderWarnings(wordCountWarnings, formatWarnings)} ${allChecksPassed ? renderSuccessMessage() : ''} ${renderExportOptions(allChecksPassed)}
`; } /** * Helper: Render checklist item */ function renderChecklistItem(label, completed) { const icon = completed ? '✓' : '○'; const color = completed ? 'text-green-600' : 'text-gray-400'; return `
${icon} ${escapeHtml(label)}
`; } /** * Helper: Render publication requirements */ function renderPublicationRequirements(publicationId) { const requirements = { 'economist-letter': 'Letters should be concise (max 250 words), start with "SIR—", and make a clear, compelling point. Include your credentials if relevant.', 'lemonde-letter': 'Lettres de 150-200 mots. Style formel mais accessible. Argument clair et bien structuré.', 'default': 'Check the publication\'s submission guidelines for specific requirements.' }; const text = requirements[publicationId] || requirements.default; return `

Publication Requirements

${escapeHtml(text)}
`; } /** * Helper: Render document editor */ function renderDocumentEditor(docType, title, content, wordCount, readonly) { const readonlyAttr = readonly ? 'readonly' : ''; const readonlyNote = readonly ? '

Linked from blog post - edit the blog post to change this content

' : ''; const placeholder = readonly ? '' : `placeholder="Enter ${title.toLowerCase()} content..."`; return `

${escapeHtml(title)}

${wordCount.toLocaleString()} words
${readonlyNote}
`; } /** * Helper: Render validation check */ function renderValidationCheck(label, passed, required) { const icon = passed ? '✓' : (required ? '✗' : '⚠'); const color = passed ? 'text-green-600' : (required ? 'text-red-600' : 'text-yellow-600'); return `
${icon} ${escapeHtml(label)}
`; } /** * Helper: Render warnings */ function renderWarnings(wordCountWarnings, formatWarnings) { if (wordCountWarnings.length === 0 && formatWarnings.length === 0) return ''; const allWarnings = [...wordCountWarnings, ...formatWarnings]; return `

⚠ Warnings

`; } /** * Helper: Render success message */ function renderSuccessMessage() { return `

✓ Ready for Export

All validation checks passed. Your submission package is ready.

`; } /** * Helper: Render export options */ function renderExportOptions(enabled) { const disabledAttr = enabled ? '' : 'disabled'; return `

Export Package

`; } /** * Update word count for document */ function updateDocumentWordCount(docType) { const textarea = document.getElementById(`doc-${docType}`); const wordCountSpan = document.getElementById(`wordcount-${docType}`); if (textarea && wordCountSpan) { const content = textarea.value; const wordCount = content.split(/\s+/).filter(w => w.length > 0).length; wordCountSpan.textContent = `${wordCount.toLocaleString()} words`; } } /** * Save submission changes */ async function saveSubmission() { if (!currentArticle) return; try { // Gather document content from textareas const coverLetter = document.getElementById('doc-coverLetter')?.value || ''; const authorBio = document.getElementById('doc-authorBio')?.value || ''; const technicalBrief = document.getElementById('doc-technicalBrief')?.value || ''; const submissionData = { blogPostId: currentArticle._id, publicationId: currentSubmission?.publicationId, publicationName: currentSubmission?.publicationName, title: currentArticle.title, wordCount: currentArticle.content?.split(/\s+/).length || 0, contentType: currentSubmission?.contentType || 'article', status: currentSubmission?.status || 'draft', documents: { mainArticle: { primaryLanguage: 'en', versions: [{ language: 'en', content: currentArticle.content, wordCount: currentArticle.content?.split(/\s+/).length || 0, translatedBy: 'manual', approved: true }] } } }; // Add cover letter if present if (coverLetter.trim()) { submissionData.documents.coverLetter = { primaryLanguage: 'en', versions: [{ language: 'en', content: coverLetter, wordCount: coverLetter.split(/\s+/).length, translatedBy: 'manual', approved: true }] }; } // Add author bio if present if (authorBio.trim()) { submissionData.documents.authorBio = { primaryLanguage: 'en', versions: [{ language: 'en', content: authorBio, wordCount: authorBio.split(/\s+/).length, translatedBy: 'manual', approved: true }] }; } // Add technical brief if present if (technicalBrief.trim()) { submissionData.documents.technicalBrief = { primaryLanguage: 'en', versions: [{ language: 'en', content: technicalBrief, wordCount: technicalBrief.split(/\s+/).length, translatedBy: 'manual', approved: true }] }; } // Save to server const url = currentSubmission?._id ? `/api/submissions/${currentSubmission._id}` : '/api/submissions'; const method = currentSubmission?._id ? 'PUT' : 'POST'; const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(submissionData) }); if (!response.ok) throw new Error('Failed to save submission'); const savedSubmission = await response.json(); currentSubmission = savedSubmission; const statusEl = document.getElementById('modal-status'); statusEl.textContent = '✓ Saved successfully'; statusEl.className = 'text-sm text-green-600'; setTimeout(() => { statusEl.textContent = ''; }, 3000); } catch (error) { console.error('Error saving submission:', error); const statusEl = document.getElementById('modal-status'); statusEl.textContent = '✗ Failed to save'; statusEl.className = 'text-sm text-red-600'; } } /** * Export package */ async function exportPackage(format) { if (!currentSubmission) { alert('No submission package to export'); return; } try { const response = await fetch(`/api/submissions/${currentSubmission._id}/export?format=${format}`); if (!response.ok) throw new Error('Export failed'); if (format === 'json') { const data = await response.json(); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${currentSubmission.publicationId}-package.json`; a.click(); URL.revokeObjectURL(url); } else if (format === 'text') { const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${currentSubmission.publicationId}-documents.zip`; a.click(); URL.revokeObjectURL(url); } } catch (error) { console.error('Error exporting package:', error); alert('Failed to export package. Please try again.'); } } /** * Copy cover letter to clipboard */ async function copyToClipboard() { const coverLetter = document.getElementById('doc-coverLetter')?.value; if (!coverLetter) { alert('No cover letter to copy'); return; } try { await navigator.clipboard.writeText(coverLetter); const statusEl = document.getElementById('modal-status'); statusEl.textContent = '✓ Copied to clipboard'; statusEl.className = 'text-sm text-green-600'; setTimeout(() => { statusEl.textContent = ''; }, 3000); } catch (error) { console.error('Error copying to clipboard:', error); alert('Failed to copy to clipboard'); } } /** * Utility: Escape HTML */ function escapeHtml(unsafe) { if (!unsafe) return ''; return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } /** * Utility: Get status color class */ function getStatusColor(status) { const colors = { 'ready': 'text-green-600', 'submitted': 'text-blue-600', 'published': 'text-purple-600', 'draft': 'text-gray-600' }; return colors[status] || 'text-gray-600'; } // Initialize modal on page load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createEnhancedSubmissionModal); } else { createEnhancedSubmissionModal(); }