- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
379 lines
14 KiB
JavaScript
379 lines
14 KiB
JavaScript
/**
|
|
* Manage Submission Modal
|
|
* Handles submission tracking for blog posts to external publications
|
|
*/
|
|
|
|
// Create the modal HTML structure dynamically
|
|
function createManageSubmissionModal() {
|
|
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-4xl w-full max-h-[90vh] overflow-y-auto mx-4">
|
|
<div class="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center">
|
|
<h2 class="text-2xl font-bold text-gray-900">Manage Submission</h2>
|
|
<button id="close-submission-modal" class="text-gray-400 hover:text-gray-600">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-6 space-y-6">
|
|
<!-- Article Info -->
|
|
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-r">
|
|
<h3 id="article-title" class="font-semibold text-gray-900 mb-1"></h3>
|
|
<p id="article-excerpt" class="text-sm text-gray-600"></p>
|
|
<div class="mt-2 text-xs text-gray-500">
|
|
<span id="article-word-count"></span> words
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Publication Selection -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Target Publication
|
|
</label>
|
|
<select id="publication-select"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
<option value="">Select a publication...</option>
|
|
</select>
|
|
<div id="publication-requirements" class="mt-3 hidden">
|
|
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-3 text-sm">
|
|
<h4 class="font-semibold text-gray-900 mb-2">Requirements</h4>
|
|
<div id="requirements-content"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress Indicator -->
|
|
<div>
|
|
<div class="flex justify-between text-sm text-gray-600 mb-2">
|
|
<span>Submission Package Progress</span>
|
|
<span id="progress-percentage">0%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
|
<div id="progress-bar" class="bg-green-600 h-2.5 rounded-full transition-all duration-300" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Checklist Items -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-semibold text-gray-900">Submission Package</h3>
|
|
|
|
<!-- Cover Letter -->
|
|
<div class="border border-gray-200 rounded-lg p-4">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div class="flex items-center">
|
|
<input type="checkbox" id="check-coverLetter"
|
|
class="w-5 h-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
|
onchange="saveChecklistItem('coverLetter')">
|
|
<label for="check-coverLetter" class="ml-3 font-medium text-gray-900">
|
|
Cover Letter
|
|
</label>
|
|
</div>
|
|
<span id="saved-coverLetter" class="text-xs text-green-600 hidden">✓ Saved</span>
|
|
</div>
|
|
<textarea id="content-coverLetter"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
rows="4"
|
|
placeholder="Write your cover letter here..."
|
|
onblur="saveChecklistItem('coverLetter')"></textarea>
|
|
</div>
|
|
|
|
<!-- Pitch Email -->
|
|
<div class="border border-gray-200 rounded-lg p-4">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div class="flex items-center">
|
|
<input type="checkbox" id="check-pitchEmail"
|
|
class="w-5 h-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
|
onchange="saveChecklistItem('pitchEmail')">
|
|
<label for="check-pitchEmail" class="ml-3 font-medium text-gray-900">
|
|
Pitch Email
|
|
</label>
|
|
</div>
|
|
<span id="saved-pitchEmail" class="text-xs text-green-600 hidden">✓ Saved</span>
|
|
</div>
|
|
<textarea id="content-pitchEmail"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
rows="4"
|
|
placeholder="Write your pitch email here..."
|
|
onblur="saveChecklistItem('pitchEmail')"></textarea>
|
|
</div>
|
|
|
|
<!-- Notes to Editor -->
|
|
<div class="border border-gray-200 rounded-lg p-4">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div class="flex items-center">
|
|
<input type="checkbox" id="check-notesToEditor"
|
|
class="w-5 h-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
|
onchange="saveChecklistItem('notesToEditor')">
|
|
<label for="check-notesToEditor" class="ml-3 font-medium text-gray-900">
|
|
Notes to Editor
|
|
</label>
|
|
</div>
|
|
<span id="saved-notesToEditor" class="text-xs text-green-600 hidden">✓ Saved</span>
|
|
</div>
|
|
<textarea id="content-notesToEditor"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
rows="4"
|
|
placeholder="Any additional notes for the editor..."
|
|
onblur="saveChecklistItem('notesToEditor')"></textarea>
|
|
</div>
|
|
|
|
<!-- Author Bio -->
|
|
<div class="border border-gray-200 rounded-lg p-4">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div class="flex items-center">
|
|
<input type="checkbox" id="check-authorBio"
|
|
class="w-5 h-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
|
onchange="saveChecklistItem('authorBio')">
|
|
<label for="check-authorBio" class="ml-3 font-medium text-gray-900">
|
|
Author Bio
|
|
</label>
|
|
</div>
|
|
<span id="saved-authorBio" class="text-xs text-green-600 hidden">✓ Saved</span>
|
|
</div>
|
|
<textarea id="content-authorBio"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
rows="3"
|
|
placeholder="Your author biography..."
|
|
onblur="saveChecklistItem('authorBio')"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return modal;
|
|
}
|
|
|
|
// Open modal and load data
|
|
async function openManageSubmissionModal(articleId, submissionId) {
|
|
currentSubmissionArticleId = articleId;
|
|
currentSubmissionId = submissionId;
|
|
|
|
// Create modal if it doesn't exist
|
|
let modal = document.getElementById('manage-submission-modal');
|
|
if (!modal) {
|
|
modal = createManageSubmissionModal();
|
|
document.getElementById('modal-container').appendChild(modal);
|
|
|
|
// Add close handler
|
|
document.getElementById('close-submission-modal').addEventListener('click', closeManageSubmissionModal);
|
|
|
|
// Load publication targets
|
|
await loadPublicationTargets();
|
|
|
|
// Add publication change handler
|
|
document.getElementById('publication-select').addEventListener('change', onPublicationChange);
|
|
}
|
|
|
|
// Load article and submission data
|
|
await loadSubmissionData(articleId);
|
|
|
|
// Show modal
|
|
modal.classList.remove('hidden');
|
|
}
|
|
|
|
// Close modal
|
|
function closeManageSubmissionModal() {
|
|
const modal = document.getElementById('manage-submission-modal');
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
clearSubmissionForm();
|
|
}
|
|
currentSubmissionArticleId = null;
|
|
currentSubmissionId = null;
|
|
currentArticleData = null;
|
|
}
|
|
|
|
// Load publication targets into dropdown
|
|
async function loadPublicationTargets() {
|
|
try {
|
|
const response = await fetch('/api/publications');
|
|
const data = await response.json();
|
|
|
|
const select = document.getElementById('publication-select');
|
|
if (!select) return; // Modal not created yet
|
|
const publications = data.data || [];
|
|
|
|
publications.forEach(pub => {
|
|
const option = document.createElement('option');
|
|
option.value = pub.id;
|
|
option.textContent = `${pub.name} (${pub.type})`;
|
|
option.dataset.requirements = JSON.stringify(pub.requirements);
|
|
option.dataset.submission = JSON.stringify(pub.submission);
|
|
select.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load publication targets:', error);
|
|
}
|
|
}
|
|
|
|
// Handle publication selection change
|
|
function onPublicationChange(e) {
|
|
const select = e.target;
|
|
const selectedOption = select.options[select.selectedIndex];
|
|
const requirementsDiv = document.getElementById('publication-requirements');
|
|
const requirementsContent = document.getElementById('requirements-content');
|
|
|
|
if (!selectedOption.value) {
|
|
requirementsDiv.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
const requirements = JSON.parse(selectedOption.dataset.requirements || '{}');
|
|
const submission = JSON.parse(selectedOption.dataset.submission || '{}');
|
|
|
|
let html = '<ul class="space-y-1">';
|
|
|
|
if (requirements.wordCount) {
|
|
html += `<li><strong>Word Count:</strong> ${requirements.wordCount.min}-${requirements.wordCount.max} words</li>`;
|
|
}
|
|
if (requirements.exclusivity) {
|
|
html += '<li><strong>Exclusivity:</strong> Must not be published elsewhere</li>';
|
|
}
|
|
if (submission.method) {
|
|
html += `<li><strong>Method:</strong> ${submission.method}`;
|
|
if (submission.email) {
|
|
html += ` (${submission.email})`;
|
|
}
|
|
html += '</li>';
|
|
}
|
|
if (submission.responseTime) {
|
|
html += `<li><strong>Response Time:</strong> ${submission.responseTime.min}-${submission.responseTime.max} ${submission.responseTime.unit}</li>`;
|
|
}
|
|
|
|
html += '</ul>';
|
|
requirementsContent.innerHTML = html;
|
|
requirementsDiv.classList.remove('hidden');
|
|
}
|
|
|
|
// Load submission data
|
|
async function loadSubmissionData(articleId) {
|
|
try {
|
|
// Load article data
|
|
const articleResponse = await fetch(`/api/blog/${articleId}`);
|
|
const articleData = await articleResponse.json();
|
|
currentArticleData = articleData;
|
|
|
|
// Populate article info
|
|
document.getElementById('article-title').textContent = articleData.title;
|
|
document.getElementById('article-excerpt').textContent = articleData.excerpt || '';
|
|
document.getElementById('article-word-count').textContent = articleData.wordCount || 0;
|
|
|
|
// Load existing submission if any
|
|
const submissionResponse = await fetch(`/api/blog/${articleId}/submissions`);
|
|
if (submissionResponse.ok) {
|
|
const submissionData = await submissionResponse.json();
|
|
|
|
if (submissionData.submissions && submissionData.submissions.length > 0) {
|
|
const submission = submissionData.submissions[0];
|
|
currentSubmissionId = submission._id;
|
|
|
|
// Populate publication select
|
|
if (submission.publicationId) {
|
|
document.getElementById('publication-select').value = submission.publicationId;
|
|
onPublicationChange({ target: document.getElementById('publication-select') });
|
|
}
|
|
|
|
// Populate checklist items
|
|
const fields = ['coverLetter', 'pitchEmail', 'notesToEditor', 'authorBio'];
|
|
fields.forEach(field => {
|
|
const packageData = submission.submissionPackage?.[field];
|
|
if (packageData) {
|
|
document.getElementById(`check-${field}`).checked = packageData.completed || false;
|
|
document.getElementById(`content-${field}`).value = packageData.content || '';
|
|
}
|
|
});
|
|
|
|
updateProgress();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load submission data:', error);
|
|
}
|
|
}
|
|
|
|
// Clear submission form
|
|
function clearSubmissionForm() {
|
|
document.getElementById('publication-select').value = '';
|
|
document.getElementById('publication-requirements').classList.add('hidden');
|
|
|
|
const fields = ['coverLetter', 'pitchEmail', 'notesToEditor', 'authorBio'];
|
|
fields.forEach(field => {
|
|
document.getElementById(`check-${field}`).checked = false;
|
|
document.getElementById(`content-${field}`).value = '';
|
|
});
|
|
|
|
updateProgress();
|
|
}
|
|
|
|
// Save checklist item
|
|
async function saveChecklistItem(field) {
|
|
if (!currentSubmissionArticleId) return;
|
|
|
|
const publicationId = document.getElementById('publication-select').value;
|
|
if (!publicationId) {
|
|
alert('Please select a publication first');
|
|
return;
|
|
}
|
|
|
|
const completed = document.getElementById(`check-${field}`).checked;
|
|
const content = document.getElementById(`content-${field}`).value;
|
|
|
|
try {
|
|
const response = await fetch(`/api/blog/${currentSubmissionArticleId}/submissions`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
publicationId,
|
|
submissionPackage: {
|
|
[field]: {
|
|
completed,
|
|
content,
|
|
lastUpdated: new Date().toISOString()
|
|
}
|
|
}
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
currentSubmissionId = data.submission._id;
|
|
|
|
// Show saved indicator
|
|
const savedIndicator = document.getElementById(`saved-${field}`);
|
|
savedIndicator.classList.remove('hidden');
|
|
setTimeout(() => {
|
|
savedIndicator.classList.add('hidden');
|
|
}, 2000);
|
|
|
|
updateProgress();
|
|
} else {
|
|
console.error('Failed to save checklist item');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving checklist item:', error);
|
|
}
|
|
}
|
|
|
|
// Update progress indicator
|
|
function updateProgress() {
|
|
const fields = ['coverLetter', 'pitchEmail', 'notesToEditor', 'authorBio'];
|
|
let completed = 0;
|
|
|
|
fields.forEach(field => {
|
|
if (document.getElementById(`check-${field}`).checked) {
|
|
completed++;
|
|
}
|
|
});
|
|
|
|
const percentage = Math.round((completed / fields.length) * 100);
|
|
document.getElementById('progress-percentage').textContent = `${percentage}%`;
|
|
document.getElementById('progress-bar').style.width = `${percentage}%`;
|
|
}
|