- Modified loadValidationArticles() to load standalone submissions (no blogPostId) - Updated rendering to handle both blog posts and standalone packages - Fixed API endpoint from /api/blog/posts/:id to /api/blog/admin/:id - Standalone packages show with purple 'STANDALONE PACKAGE' badge - Button text changes to 'View Package' for standalone submissions - Cache version bumped to 0.1.1
775 lines
27 KiB
JavaScript
775 lines
27 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<div class="bg-white rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] flex flex-col mx-4">
|
|
<!-- Header -->
|
|
<div class="border-b px-6 py-4 flex items-center justify-between">
|
|
<h2 class="text-2xl font-bold text-gray-900" id="modal-title">Manage Submission</h2>
|
|
<button data-action="close-modal" class="text-gray-400 hover:text-gray-600 text-2xl leading-none">
|
|
×
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tab Navigation -->
|
|
<div class="border-b px-6">
|
|
<nav class="flex space-x-8" aria-label="Tabs">
|
|
<button
|
|
data-tab="overview"
|
|
id="tab-overview"
|
|
class="tab-button whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm border-blue-500 text-blue-600">
|
|
Overview
|
|
</button>
|
|
<button
|
|
data-tab="documents"
|
|
id="tab-documents"
|
|
class="tab-button whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
|
Documents
|
|
</button>
|
|
<button
|
|
data-tab="validation"
|
|
id="tab-validation"
|
|
class="tab-button whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
|
Validation & Export
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="flex-1 overflow-y-auto px-6 py-4" id="modal-content">
|
|
<!-- Content will be dynamically loaded here -->
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="border-t px-6 py-4 flex justify-between items-center bg-gray-50">
|
|
<div id="modal-status" class="text-sm text-gray-600"></div>
|
|
<div class="flex space-x-3">
|
|
<button
|
|
data-action="close-modal"
|
|
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
|
Close
|
|
</button>
|
|
<button
|
|
data-action="save-submission"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
|
Save Changes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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/admin/${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 `
|
|
<div class="space-y-6">
|
|
<!-- Article Preview -->
|
|
<div class="bg-gray-50 rounded-lg p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Article Preview</h3>
|
|
<div class="prose max-w-none">
|
|
<h4 class="text-xl font-bold mb-2">${escapeHtml(article.title)}</h4>
|
|
<div class="text-sm text-gray-600 mb-4">
|
|
${escapeHtml(article.subtitle || '')}
|
|
</div>
|
|
<div class="text-gray-700 line-clamp-6">
|
|
${escapeHtml(excerptText)}
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 flex items-center space-x-4 text-sm text-gray-600">
|
|
<span><strong>Word Count:</strong> ${wordCount.toLocaleString()}</span>
|
|
<span><strong>Published:</strong> ${publishedDate}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submission Status -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="bg-white border rounded-lg p-4">
|
|
<div class="text-sm text-gray-600 mb-1">Target Publication</div>
|
|
<div class="text-lg font-semibold text-gray-900">${escapeHtml(publicationName)}</div>
|
|
</div>
|
|
<div class="bg-white border rounded-lg p-4">
|
|
<div class="text-sm text-gray-600 mb-1">Status</div>
|
|
<div class="text-lg font-semibold ${getStatusColor(status)}">
|
|
${status.charAt(0).toUpperCase() + status.slice(1)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress Indicator -->
|
|
<div class="bg-white border rounded-lg p-4">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<div class="text-sm font-medium text-gray-700">Completion Progress</div>
|
|
<div class="text-sm font-semibold text-gray-900">${completionScore}%</div>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-2">
|
|
<div class="bg-blue-600 h-2 rounded-full transition-all duration-300" data-progress-bar data-progress="${completionScore}"></div>
|
|
</div>
|
|
<div class="mt-4 space-y-2 text-sm">
|
|
${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)}
|
|
</div>
|
|
</div>
|
|
|
|
${submission.publicationId ? renderPublicationRequirements(submission.publicationId) : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 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 `
|
|
<div class="space-y-6">
|
|
${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)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 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 `
|
|
<div class="space-y-6">
|
|
<!-- Validation Checks -->
|
|
<div class="bg-white border rounded-lg p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Validation Checks</h3>
|
|
<div class="space-y-3">
|
|
${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)}
|
|
</div>
|
|
</div>
|
|
|
|
${renderWarnings(wordCountWarnings, formatWarnings)}
|
|
${allChecksPassed ? renderSuccessMessage() : ''}
|
|
|
|
<!-- Export Options -->
|
|
${renderExportOptions(allChecksPassed)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Helper: Render checklist item
|
|
*/
|
|
function renderChecklistItem(label, completed) {
|
|
const icon = completed ? '✓' : '○';
|
|
const color = completed ? 'text-green-600' : 'text-gray-400';
|
|
return `
|
|
<div class="flex items-center">
|
|
<span class="${color}">${icon}</span>
|
|
<span class="ml-2 text-gray-700">${escapeHtml(label)}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 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 `
|
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<h4 class="text-sm font-semibold text-blue-900 mb-2">Publication Requirements</h4>
|
|
<div class="text-sm text-blue-800">${escapeHtml(text)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Helper: Render document editor
|
|
*/
|
|
function renderDocumentEditor(docType, title, content, wordCount, readonly) {
|
|
const readonlyAttr = readonly ? 'readonly' : '';
|
|
const readonlyNote = readonly ? '<p class="text-xs text-gray-500 mt-2">Linked from blog post - edit the blog post to change this content</p>' : '';
|
|
const placeholder = readonly ? '' : `placeholder="Enter ${title.toLowerCase()} content..."`;
|
|
|
|
return `
|
|
<div class="border rounded-lg overflow-hidden">
|
|
<div class="bg-gray-100 px-4 py-3 flex items-center justify-between">
|
|
<h4 class="font-semibold text-gray-900">${escapeHtml(title)}</h4>
|
|
<span class="text-sm text-gray-600" id="wordcount-${docType}">${wordCount.toLocaleString()} words</span>
|
|
</div>
|
|
<div class="p-4">
|
|
<textarea
|
|
id="doc-${docType}"
|
|
rows="${readonly ? '8' : '6'}"
|
|
class="w-full border rounded-md p-3 text-sm font-mono resize-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
${readonlyAttr}
|
|
${placeholder}
|
|
>${escapeHtml(content)}</textarea>
|
|
${readonlyNote}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 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 `
|
|
<div class="flex items-center">
|
|
<span class="${color} text-xl mr-3">${icon}</span>
|
|
<span class="text-gray-700">${escapeHtml(label)}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Helper: Render warnings
|
|
*/
|
|
function renderWarnings(wordCountWarnings, formatWarnings) {
|
|
if (wordCountWarnings.length === 0 && formatWarnings.length === 0) return '';
|
|
|
|
const allWarnings = [...wordCountWarnings, ...formatWarnings];
|
|
|
|
return `
|
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
|
<h4 class="text-sm font-semibold text-yellow-900 mb-2">⚠ Warnings</h4>
|
|
<ul class="text-sm text-yellow-800 space-y-1 list-disc list-inside">
|
|
${allWarnings.map(w => `<li>${escapeHtml(w)}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Helper: Render success message
|
|
*/
|
|
function renderSuccessMessage() {
|
|
return `
|
|
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<h4 class="text-sm font-semibold text-green-900 mb-2">✓ Ready for Export</h4>
|
|
<p class="text-sm text-green-800">All validation checks passed. Your submission package is ready.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Helper: Render export options
|
|
*/
|
|
function renderExportOptions(enabled) {
|
|
const disabledAttr = enabled ? '' : 'disabled';
|
|
|
|
return `
|
|
<div class="bg-white border rounded-lg p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Export Package</h3>
|
|
<div class="space-y-3">
|
|
<button
|
|
data-export="json"
|
|
class="w-full flex items-center justify-between px-4 py-3 border rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
${disabledAttr}
|
|
>
|
|
<div class="flex items-center">
|
|
<svg class="w-5 h-5 text-gray-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
|
</svg>
|
|
<div class="text-left">
|
|
<div class="font-medium text-gray-900">Export as JSON</div>
|
|
<div class="text-xs text-gray-500">Complete package with all metadata</div>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
data-export="text"
|
|
class="w-full flex items-center justify-between px-4 py-3 border rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
${disabledAttr}
|
|
>
|
|
<div class="flex items-center">
|
|
<svg class="w-5 h-5 text-gray-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
<div class="text-left">
|
|
<div class="font-medium text-gray-900">Export Individual Documents</div>
|
|
<div class="text-xs text-gray-500">Separate text files for each document</div>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
data-action="copy-clipboard"
|
|
class="w-full flex items-center justify-between px-4 py-3 border rounded-lg hover:bg-gray-50 transition-colors"
|
|
>
|
|
<div class="flex items-center">
|
|
<svg class="w-5 h-5 text-gray-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path>
|
|
</svg>
|
|
<div class="text-left">
|
|
<div class="font-medium text-gray-900">Copy Cover Letter to Clipboard</div>
|
|
<div class="text-xs text-gray-500">Quick copy for email submissions</div>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 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, """)
|
|
.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();
|
|
}
|