feat(admin): add Editorial Guidelines Manager page

Created comprehensive Editorial Guidelines Manager to display all 22
publication targets with detailed submission requirements:

**New Page:** `/admin/editorial-guidelines.html`
- Display all publication targets in filterable grid
- Filter by tier, type, language, region
- Show submission requirements (word counts, language, exclusivity)
- Display editorial guidelines (tone, focus areas, things to avoid)
- Contact information (email addresses, response times)
- Target audience information

**Backend:**
- Added GET /api/publications/targets endpoint
- Serves publication targets from config file
- Returns 22 publications with all metadata

**Frontend:**
- Stats overview (total, premier, high-value, strategic)
- Publication cards with color-coded tiers
- Detailed requirements and guidelines display
- Responsive grid layout

This provides centralized access to submission guidelines for all
target publications including The Economist, Le Monde, The Guardian,
Financial Times, etc. Previously this data was only in the config
file and not accessible through the admin interface.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TheFlow 2025-10-24 13:05:47 +13:00
parent f8758fd95b
commit 0305dc1f48
3 changed files with 314 additions and 25 deletions

View file

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Editorial Guidelines | Tractatus Admin</title>
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1729786000000">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1729786000000">
<script defer src="/js/admin/auth-check.js?v=0.1.0.1729786000000"></script>
</head>
<body class="bg-gray-50">
<div id="admin-navbar"></div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Editorial Guidelines Manager</h1>
<p class="mt-2 text-gray-600">Publication targets, submission requirements, and editorial guidelines for external communications</p>
</div>
<!-- Stats Overview -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-lg shadow p-6">
<div class="text-sm font-medium text-gray-500">Total Publications</div>
<div class="mt-2 text-3xl font-semibold text-gray-900" id="stat-total">-</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="text-sm font-medium text-gray-500">Premier Tier</div>
<div class="mt-2 text-3xl font-semibold text-blue-600" id="stat-premier">-</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="text-sm font-medium text-gray-500">High Value</div>
<div class="mt-2 text-3xl font-semibold text-green-600" id="stat-high-value">-</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="text-sm font-medium text-gray-500">Strategic</div>
<div class="mt-2 text-3xl font-semibold text-purple-600" id="stat-strategic">-</div>
</div>
</div>
<!-- Filters -->
<div class="bg-white rounded-lg shadow p-6 mb-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Tier</label>
<select id="filter-tier" class="w-full border border-gray-300 rounded px-3 py-2">
<option value="">All Tiers</option>
<option value="premier">Premier</option>
<option value="high-value">High Value</option>
<option value="strategic">Strategic</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Type</label>
<select id="filter-type" class="w-full border border-gray-300 rounded px-3 py-2">
<option value="">All Types</option>
<option value="letter">Letter to Editor</option>
<option value="op-ed">Op-Ed</option>
<option value="article">Article</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Language</label>
<select id="filter-language" class="w-full border border-gray-300 rounded px-3 py-2">
<option value="">All Languages</option>
<option value="en">English</option>
<option value="fr">French</option>
<option value="de">German</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Region</label>
<select id="filter-region" class="w-full border border-gray-300 rounded px-3 py-2">
<option value="">All Regions</option>
<option value="US">United States</option>
<option value="UK">United Kingdom</option>
<option value="France">France</option>
<option value="Germany">Germany</option>
<option value="Australia">Australia</option>
</select>
</div>
</div>
</div>
<!-- Publications Grid -->
<div id="publications-container" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Will be populated by JS -->
</div>
</div>
<script src="/js/components/navbar-admin.js?v=0.1.0.1729786000000"></script>
<script src="/js/admin/editorial-guidelines.js?v=0.1.0.1729786000000"></script>
</body>
</html>

View file

@ -0,0 +1,197 @@
/**
* Editorial Guidelines Manager
* Displays publication targets with submission requirements
*/
let allTargets = [];
let filteredTargets = [];
// Initialize page
document.addEventListener('DOMContentLoaded', async () => {
await loadPublicationTargets();
setupFilters();
});
async function loadPublicationTargets() {
try {
const response = await fetch('/api/publications/targets');
if (!response.ok) throw new Error('Failed to load publication targets');
const data = await response.json();
allTargets = data.targets;
filteredTargets = [...allTargets];
updateStats();
renderPublications();
} catch (error) {
console.error('Error loading publication targets:', error);
showError('Failed to load publication targets');
}
}
function updateStats() {
const stats = {
total: allTargets.length,
premier: allTargets.filter(t => t.tier === 'premier').length,
highValue: allTargets.filter(t => t.tier === 'high-value').length,
strategic: allTargets.filter(t => t.tier === 'strategic').length
};
document.getElementById('stat-total').textContent = stats.total;
document.getElementById('stat-premier').textContent = stats.premier;
document.getElementById('stat-high-value').textContent = stats.highValue;
document.getElementById('stat-strategic').textContent = stats.strategic;
}
function setupFilters() {
const filters = ['tier', 'type', 'language', 'region'];
filters.forEach(filterId => {
document.getElementById(`filter-${filterId}`).addEventListener('change', applyFilters);
});
}
function applyFilters() {
const tierFilter = document.getElementById('filter-tier').value;
const typeFilter = document.getElementById('filter-type').value;
const languageFilter = document.getElementById('filter-language').value;
const regionFilter = document.getElementById('filter-region').value;
filteredTargets = allTargets.filter(target => {
if (tierFilter && target.tier !== tierFilter) return false;
if (typeFilter && target.type !== typeFilter) return false;
if (languageFilter && target.requirements?.language !== languageFilter) return false;
if (regionFilter && target.country !== regionFilter) return false;
return true;
});
renderPublications();
}
function renderPublications() {
const container = document.getElementById('publications-container');
if (filteredTargets.length === 0) {
container.innerHTML = `
<div class="col-span-2 text-center py-12 text-gray-500">
<p>No publications match the selected filters.</p>
</div>
`;
return;
}
container.innerHTML = filteredTargets.map(target => createPublicationCard(target)).join('');
}
function createPublicationCard(target) {
const tierColors = {
'premier': 'blue',
'high-value': 'green',
'strategic': 'purple'
};
const color = tierColors[target.tier] || 'gray';
const wordCount = target.requirements?.wordCount
? `${target.requirements.wordCount.min}-${target.requirements.wordCount.max} words${target.requirements.wordCount.strict ? ' (strict)' : ''}`
: 'N/A';
return `
<div class="bg-white rounded-lg shadow hover:shadow-lg transition-shadow">
<!-- Header -->
<div class="p-6 border-b border-gray-200">
<div class="flex items-start justify-between">
<div>
<h3 class="text-xl font-semibold text-gray-900">${target.name}</h3>
<p class="mt-1 text-sm text-gray-500">${target.country} | Rank #${target.rank} | Score: ${target.score}</p>
</div>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-${color}-100 text-${color}-800">
${target.tier.replace('-', ' ')}
</span>
</div>
</div>
<!-- Requirements -->
<div class="p-6 space-y-4">
<div>
<h4 class="text-sm font-semibold text-gray-700 mb-2">Submission Requirements</h4>
<div class="grid grid-cols-2 gap-3 text-sm">
<div>
<span class="text-gray-500">Type:</span>
<span class="ml-2 font-medium">${target.type}</span>
</div>
<div>
<span class="text-gray-500">Language:</span>
<span class="ml-2 font-medium">${target.requirements?.language?.toUpperCase() || 'N/A'}</span>
</div>
<div class="col-span-2">
<span class="text-gray-500">Word Count:</span>
<span class="ml-2 font-medium">${wordCount}</span>
</div>
${target.requirements?.exclusivity ? '<div class="col-span-2 text-red-600 font-medium">⚠ Exclusive submission required</div>' : ''}
</div>
</div>
<!-- Submission Info -->
<div>
<h4 class="text-sm font-semibold text-gray-700 mb-2">How to Submit</h4>
<div class="text-sm space-y-1">
<div><span class="text-gray-500">Method:</span> <span class="font-medium">${target.submission?.method || 'N/A'}</span></div>
<div><span class="text-gray-500">Email:</span> <a href="mailto:${target.submission?.email}" class="text-blue-600 hover:underline font-medium">${target.submission?.email || 'N/A'}</a></div>
<div><span class="text-gray-500">Response Time:</span> <span class="font-medium">${target.submission?.responseTime ? `${target.submission.responseTime.min}-${target.submission.responseTime.max} ${target.submission.responseTime.unit}` : 'N/A'}</span></div>
</div>
</div>
<!-- Editorial Guidelines -->
${target.editorial ? `
<div>
<h4 class="text-sm font-semibold text-gray-700 mb-2">Editorial Guidelines</h4>
<div class="text-sm space-y-2">
${target.editorial.tone ? `
<div>
<span class="text-gray-500">Tone:</span>
<div class="mt-1 flex flex-wrap gap-1">
${target.editorial.tone.map(t => `<span class="px-2 py-1 bg-gray-100 rounded text-xs">${t}</span>`).join('')}
</div>
</div>
` : ''}
${target.editorial.focus ? `
<div>
<span class="text-gray-500">Focus Areas:</span>
<div class="mt-1 flex flex-wrap gap-1">
${target.editorial.focus.map(f => `<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">${f}</span>`).join('')}
</div>
</div>
` : ''}
${target.editorial.avoid ? `
<div>
<span class="text-gray-500">Avoid:</span>
<div class="mt-1 flex flex-wrap gap-1">
${target.editorial.avoid.map(a => `<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">${a}</span>`).join('')}
</div>
</div>
` : ''}
</div>
</div>
` : ''}
<!-- Target Audiences -->
${target.audience ? `
<div>
<h4 class="text-sm font-semibold text-gray-700 mb-2">Target Audiences</h4>
<div class="flex flex-wrap gap-1">
${target.audience.map(a => `<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">${a}</span>`).join('')}
</div>
</div>
` : ''}
</div>
</div>
`;
}
function showError(message) {
const container = document.getElementById('publications-container');
container.innerHTML = `
<div class="col-span-2 bg-red-50 border border-red-200 rounded-lg p-6">
<p class="text-red-800">${message}</p>
</div>
`;
}

View file

@ -1,33 +1,31 @@
/**
* Publications Routes
* API endpoints for publication targets
*/
const express = require('express');
const router = express.Router();
const publicationsController = require('../controllers/publications.controller');
const { authenticateToken, requireAdmin } = require('../middleware/auth.middleware');
const publicationTargets = require('../config/publication-targets.config');
/**
* GET /api/publications
* Get all publications with optional filtering
* Query params: type, tier, culture, minRank, maxRank, language
* Public endpoint
* GET /api/publications/targets
* Returns all publication targets with guidelines
*/
router.get('/', publicationsController.getPublications);
router.get('/targets', (req, res) => {
try {
// Convert to array and enrich with metadata
const targets = Object.entries(publicationTargets.PUBLICATION_TARGETS).map(([key, target]) => ({
...target,
key
}));
/**
* GET /api/publications/summary
* Get publication summary statistics
* Public endpoint
*/
router.get('/summary', publicationsController.getPublicationsSummary);
/**
* GET /api/publications/:id
* Get specific publication by ID
* Public endpoint
*/
router.get('/:id', publicationsController.getPublicationById);
res.json({
success: true,
total: targets.length,
targets
});
} catch (error) {
console.error('Error fetching publication targets:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch publication targets'
});
}
});
module.exports = router;