/** * 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; } // Translate document if (target.hasAttribute('data-action') && target.getAttribute('data-action') === 'translate-document') { const docType = target.getAttribute('data-doc-type'); const fromLang = target.getAttribute('data-from-lang'); const toLang = target.getAttribute('data-to-lang'); translateDocument(docType, fromLang, toLang, target); 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 token = localStorage.getItem('admin_token'); const response = await fetch(`/api/blog/admin/${articleId}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) throw new Error('Failed to load article'); const articleData = await response.json(); currentArticle = articleData.post; // Extract post from wrapper // Try to load existing submission const submissionResponse = await fetch(`/api/submissions/by-blog-post/${articleId}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (submissionResponse.ok) { const submissionData = await submissionResponse.json(); currentSubmission = submissionData.data; // Extract data from wrapper } 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; // Handle standalone submissions (no article) const isStandalone = !article; const title = isStandalone ? submission.title : article.title; const subtitle = isStandalone ? '' : (article.subtitle || ''); const wordCount = isStandalone ? (submission.wordCount || 0) : (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; // Get content/excerpt - for standalone, use document content if available let excerptText = 'No content available'; if (isStandalone) { const mainArticle = submission.documents?.mainArticle?.versions?.[0]; if (mainArticle?.content) { excerptText = mainArticle.content.substring(0, 300) + '...'; } } else { excerptText = article.excerpt || (article.content?.substring(0, 300) + '...') || 'No content available'; } const publishedDate = isStandalone ? (submission.submittedAt ? new Date(submission.submittedAt).toLocaleDateString() : 'Not submitted') : (article.published_at ? new Date(article.published_at).toLocaleDateString() : 'N/A'); return `
${isStandalone ? `
πŸ“¦ Standalone Submission Package This is a direct submission without an associated blog post
` : ''}

${isStandalone ? 'Submission' : 'Article'} Preview

${escapeHtml(title)}

${subtitle ? `
${escapeHtml(subtitle)}
` : ''}
${escapeHtml(excerptText)}
Word Count: ${wordCount.toLocaleString()} ${isStandalone ? 'Submitted' : '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 isStandalone = !article; return `

🌍 Multilingual Support

You can create and edit multiple language versions of each document. Each version is saved independently.

${renderMultilingualDocument('mainArticle', 'Main Article', submission, article, isStandalone)} ${renderMultilingualDocument('coverLetter', 'Cover Letter / Pitch', submission, null, false)} ${renderMultilingualDocument('authorBio', 'Author Bio', submission, null, false)} ${renderMultilingualDocument('pitchEmail', 'Pitch Email (Optional)', submission, null, 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 multilingual document editor * Supports multiple language versions with translation */ function renderMultilingualDocument(docType, title, submission, article, isStandalone) { const doc = submission.documents?.[docType] || {}; const versions = doc.versions || []; const primaryLang = doc.primaryLanguage || 'en'; // Get English and French versions const enVersion = versions.find(v => v.language === 'en'); const frVersion = versions.find(v => v.language === 'fr'); // For main article, use blog content as English version if no submission version exists let enContent = enVersion?.content || ''; if (docType === 'mainArticle' && !enContent && article?.content) { enContent = article.content; } const frContent = frVersion?.content || ''; const enWordCount = enContent ? enContent.split(/\s+/).filter(w => w.length > 0).length : 0; const frWordCount = frContent ? frContent.split(/\s+/).filter(w => w.length > 0).length : 0; const readonly = docType === 'mainArticle' && !isStandalone && article; return `

${escapeHtml(title)}

πŸ‡¬πŸ‡§ English Version${primaryLang === 'en' ? ' (Primary)' : ''} ${enWordCount.toLocaleString()} words
${readonly ? '

πŸ”’ Linked from blog post - edit the blog post to change this content

' : ''}
πŸ‡«πŸ‡· French Version${primaryLang === 'fr' ? ' (Primary)' : ''} ${frWordCount.toLocaleString()} words
${frVersion?.translatedBy ? `

Translated by: ${frVersion.translatedBy}${frVersion.approved ? ' βœ“ Approved' : ''}

` : ''}
`; } /** * Helper: Render document editor (legacy single-language version) */ 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 token = localStorage.getItem('admin_token'); const response = await fetch(`/api/submissions/${currentSubmission._id}/export?format=${format}`, { headers: { 'Authorization': `Bearer ${token}` } }); 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'); } } /** * Translate document using DeepL */ async function translateDocument(docType, fromLang, toLang, buttonEl) { const sourceTextarea = document.getElementById(`doc-${docType}-${fromLang}`); const targetTextarea = document.getElementById(`doc-${docType}-${toLang}`); if (!sourceTextarea || !targetTextarea) { alert('Translation error: Cannot find document fields'); return; } const sourceText = sourceTextarea.value.trim(); if (!sourceText) { alert('No text to translate. Please enter content in the source language first.'); return; } // Disable button and show loading state const originalText = buttonEl.textContent; buttonEl.disabled = true; buttonEl.textContent = '⏳ Translating...'; buttonEl.classList.add('opacity-50', 'cursor-not-allowed'); try { const token = localStorage.getItem('admin_token'); const submissionId = currentSubmission._id; const response = await fetch(`/api/submissions/${submissionId}/translate`, { method: 'POST', cache: 'no-store', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ docType, fromLang, toLang, text: sourceText }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Translation failed'); } const data = await response.json(); // Update target textarea with translation targetTextarea.value = data.translatedText; // Update word count const wordCount = data.translatedText.split(/\s+/).filter(w => w.length > 0).length; const wordCountEl = targetTextarea.closest('.bg-white').querySelector('.text-xs.text-gray-600'); if (wordCountEl) { wordCountEl.textContent = `${wordCount.toLocaleString()} words`; } // Show success message const statusEl = document.getElementById('modal-status'); statusEl.textContent = `βœ“ Translated to ${toLang.toUpperCase()} using DeepL`; statusEl.className = 'text-sm text-green-600'; setTimeout(() => { statusEl.textContent = ''; }, 5000); } catch (error) { console.error('Translation error:', error); alert(`Translation failed: ${error.message}`); } finally { // Re-enable button buttonEl.disabled = false; buttonEl.textContent = originalText; buttonEl.classList.remove('opacity-50', 'cursor-not-allowed'); } } /** * 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(); }