tractatus/public/js/admin/editorial-guidelines.js
TheFlow cdd7109b73 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>
2025-10-24 13:05:47 +13:00

197 lines
7.4 KiB
JavaScript

/**
* 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>
`;
}