tractatus/public/js/admin/submission-modal-enhanced.js
TheFlow d3074f87a3 fix(blog-validation): show Le Monde standalone submission package
- 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
2025-10-24 09:50:42 +13:00

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">
&times;
</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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* 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();
}