/**
* 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 = `
Manage Submission
×
Overview
Documents
Validation & Export
`;
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
${allWarnings.map(w => `${escapeHtml(w)} `).join('')}
`;
}
/**
* 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
Export as JSON
Complete package with all metadata
Export Individual Documents
Separate text files for each document
Copy Cover Letter to Clipboard
Quick copy for email submissions
`;
}
/**
* 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();
}