Implements comprehensive submission tracking workflow for blog posts targeting external publications. This feature enables systematic management of submission packages and progress monitoring. Frontend: - Add submission-modal.js with complete modal implementation - Modal includes publication selector (22 ranked publications) - 4-item submission checklist (cover letter, pitch, notes, bio) - Auto-save on blur with success indicators - Progress bar (0-100%) tracking completion - Requirements display per publication - Update blog-validation.js with event handlers - Update cache versions (HTML, service worker, version.json) Backend: - Add GET /api/blog/:id/submissions endpoint - Add PUT /api/blog/:id/submissions endpoint (upsert logic) - Implement getSubmissions and updateSubmission controllers - Fix publications controller to use config helper functions - Integration with SubmissionTracking MongoDB model Version: 1.8.4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
157 lines
4.4 KiB
JavaScript
157 lines
4.4 KiB
JavaScript
/**
|
|
* Publications Controller
|
|
* API endpoints for publication targets metadata
|
|
*/
|
|
|
|
const publicationTargets = require('../config/publication-targets.config');
|
|
|
|
/**
|
|
* GET /api/publications
|
|
* Get all publication targets with optional filtering
|
|
*/
|
|
async function getPublications(req, res) {
|
|
try {
|
|
const {
|
|
type, // letter, oped, social
|
|
tier, // premier, specialist, regional, digital
|
|
culture, // global-north, asia, developing-world
|
|
minRank,
|
|
maxRank,
|
|
language // en, de, fr, pt, zh, etc.
|
|
} = req.query;
|
|
|
|
// Get all publications
|
|
let publications = publicationTargets.getAllPublications();
|
|
|
|
// Apply filters
|
|
if (type) {
|
|
publications = publications.filter(p => p.type === type);
|
|
}
|
|
|
|
if (tier) {
|
|
publications = publications.filter(p => p.tier === tier);
|
|
}
|
|
|
|
if (culture) {
|
|
publications = publications.filter(p =>
|
|
p.culture && p.culture.includes(culture)
|
|
);
|
|
}
|
|
|
|
if (minRank) {
|
|
publications = publications.filter(p => p.rank >= parseInt(minRank, 10));
|
|
}
|
|
|
|
if (maxRank) {
|
|
publications = publications.filter(p => p.rank <= parseInt(maxRank, 10));
|
|
}
|
|
|
|
if (language) {
|
|
publications = publications.filter(p =>
|
|
p.requirements && p.requirements.language === language
|
|
);
|
|
}
|
|
|
|
// Sort by rank
|
|
publications.sort((a, b) => a.rank - b.rank);
|
|
|
|
res.json({
|
|
success: true,
|
|
count: publications.length,
|
|
data: publications
|
|
});
|
|
} catch (error) {
|
|
console.error('[Publications] Get publications error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch publications'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/publications/:id
|
|
* Get specific publication by ID
|
|
*/
|
|
async function getPublicationById(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const publication = publicationTargets.getPublicationById(id);
|
|
|
|
if (!publication) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Publication not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: publication
|
|
});
|
|
} catch (error) {
|
|
console.error('[Publications] Get publication by ID error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch publication'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/publications/summary
|
|
* Get summary statistics about publications
|
|
*/
|
|
async function getPublicationsSummary(req, res) {
|
|
try {
|
|
const publications = publicationTargets.getAllPublications();
|
|
|
|
// Calculate summary statistics
|
|
const summary = {
|
|
total: publications.length,
|
|
byType: {
|
|
letter: publications.filter(p => p.type === 'letter').length,
|
|
oped: publications.filter(p => p.type === 'oped').length,
|
|
social: publications.filter(p => p.type === 'social').length
|
|
},
|
|
byTier: {
|
|
premier: publications.filter(p => p.tier === 'premier').length,
|
|
specialist: publications.filter(p => p.tier === 'specialist').length,
|
|
regional: publications.filter(p => p.tier === 'regional').length,
|
|
digital: publications.filter(p => p.tier === 'digital').length
|
|
},
|
|
byCulture: {
|
|
'global-north': publications.filter(p => p.culture && p.culture.includes('global-north')).length,
|
|
'asia': publications.filter(p => p.culture && p.culture.includes('asia')).length,
|
|
'developing-world': publications.filter(p => p.culture && p.culture.includes('developing-world')).length
|
|
},
|
|
byLanguage: {
|
|
en: publications.filter(p => !p.requirements?.language || p.requirements.language === 'en').length,
|
|
de: publications.filter(p => p.requirements?.language === 'de').length,
|
|
fr: publications.filter(p => p.requirements?.language === 'fr').length,
|
|
pt: publications.filter(p => p.requirements?.language === 'pt').length,
|
|
zh: publications.filter(p => p.requirements?.language === 'zh').length
|
|
},
|
|
verified: publications.filter(p => p.verified).length,
|
|
unverified: publications.filter(p => !p.verified).length
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
data: summary
|
|
});
|
|
} catch (error) {
|
|
console.error('[Publications] Get summary error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch publication summary'
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getPublications,
|
|
getPublicationById,
|
|
getPublicationsSummary
|
|
};
|