- Merged Weekly and Multi-Week tabs into a single "By Week" tab - Updated calendar to visually indicate days with articles via a dot indicator - Improved calendar week selection to allow toggling multiple individual weeks - Enhanced calendar hover states to invert foreground text for readability - Fixed active tab styling to remove clashing bottom borders and focus outlines Made-with: Cursor
228 lines
9.6 KiB
HTML
228 lines
9.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Publish{% endblock %}
|
|
{% block content %}
|
|
<h1>Publish Issue</h1>
|
|
|
|
<div class="tab-bar">
|
|
<button class="tab active" data-tab="weekly" onclick="switchTab('weekly')">By Week</button>
|
|
<button class="tab" data-tab="single-article" onclick="switchTab('single-article')">Single Article</button>
|
|
</div>
|
|
|
|
<!-- WEEKLY TAB -->
|
|
<div id="tab-weekly" class="tab-content active">
|
|
<p><small><em>Select a single week, or click two different weeks to select a range.</em></small></p>
|
|
<div class="calendar-widget">
|
|
<div class="calendar-nav">
|
|
<button class="outline" onclick="changeMonth(-1)">◀</button>
|
|
<strong id="cal-month-label">{{ cal_month_name }}</strong>
|
|
<button class="outline" onclick="changeMonth(1)">▶</button>
|
|
</div>
|
|
<table class="calendar-grid">
|
|
<thead>
|
|
<tr>
|
|
<th>Wk</th><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th><th>Sun</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="cal-body">
|
|
{% for week in calendar_weeks %}
|
|
<tr class="cal-week" data-start="{{ week.week_start }}" data-end="{{ week.week_end }}"
|
|
onclick="selectWeekMulti(this)">
|
|
<td class="cal-wk">{{ week.iso_week }}{% if week.article_count %} <small>({{ week.article_count }})</small>{% endif %}</td>
|
|
{% for day in week.days %}
|
|
<td class="{% if not day.in_month %}cal-dim{% endif %} {% if day.has_articles %}has-articles{% endif %}">{{ day.day }}</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div id="weekly-summary" class="publish-summary"></div>
|
|
<div id="weekly-articles"></div>
|
|
<form method="post" action="/publish" id="weekly-form">
|
|
<input type="hidden" name="week_start" id="weekly-start">
|
|
<input type="hidden" name="week_end" id="weekly-end">
|
|
<input type="hidden" name="issue_type" id="weekly-issue-type" value="weekly">
|
|
<div id="weekly-checkboxes"></div>
|
|
<div class="publish-actions">
|
|
<select name="cover_method">
|
|
<option value="mosaic">Mosaic Cover</option>
|
|
<option value="text">Text Cover</option>
|
|
</select>
|
|
<button type="submit">Generate Issue</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
|
|
<!-- SINGLE ARTICLE TAB -->
|
|
<div id="tab-single-article" class="tab-content">
|
|
<input type="search" id="article-search" placeholder="Search articles by title, author, or category..."
|
|
oninput="filterArticles()">
|
|
<form method="post" action="/publish" id="single-form">
|
|
<input type="hidden" name="issue_type" value="single_article">
|
|
<input type="hidden" name="week_start" id="single-start">
|
|
<input type="hidden" name="week_end" id="single-end">
|
|
<div id="single-articles" class="article-radio-list">
|
|
{% for article in all_articles %}
|
|
<label class="article-radio-item" data-search="{{ article.title|lower }} {{ article.author|lower }} {{ article.categories|lower }}">
|
|
<input type="radio" name="article_ids" value="{{ article.id }}"
|
|
data-date="{{ article.pub_date.strftime('%Y-%m-%d') }}"
|
|
onchange="selectSingleArticle(this)">
|
|
<div>
|
|
<strong>{{ article.title }}</strong><br>
|
|
<small>{{ article.pub_date.strftime('%b %d, %Y') }} · {{ article.author }} · {{ article.categories | replace('[', '') | replace(']', '') | replace('"', '') }}</small>
|
|
</div>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="publish-actions">
|
|
<select name="cover_method">
|
|
<option value="mosaic">Mosaic Cover</option>
|
|
<option value="text">Text Cover</option>
|
|
</select>
|
|
<button type="submit">Generate Single Issue</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let calYear = {{ cal_year }};
|
|
let calMonth = {{ cal_month }};
|
|
let selectedWeeks = [];
|
|
|
|
function switchTab(tab) {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
|
|
document.getElementById(`tab-${tab}`).classList.add('active');
|
|
}
|
|
|
|
async function loadArticles(start, end, targetId) {
|
|
const resp = await fetch(`/publish/articles?start=${start}&end=${end}`);
|
|
const articles = await resp.json();
|
|
const container = document.getElementById(targetId);
|
|
container.innerHTML = articles.map(a =>
|
|
`<label class="article-check-item">
|
|
<input type="checkbox" name="article_ids" value="${a.id}" checked>
|
|
<span><strong>${a.title}</strong> <small>${a.pub_date} · ${a.author}</small></span>
|
|
</label>`
|
|
).join('');
|
|
return articles.length;
|
|
}
|
|
|
|
async function selectWeekMulti(row) {
|
|
const start = row.dataset.start;
|
|
const end = row.dataset.end;
|
|
|
|
// Toggle selection
|
|
const existingIndex = selectedWeeks.findIndex(w => w.start === start);
|
|
if (existingIndex >= 0) {
|
|
selectedWeeks.splice(existingIndex, 1);
|
|
row.classList.remove('selected');
|
|
} else {
|
|
selectedWeeks.push({ start, end, row });
|
|
row.classList.add('selected');
|
|
}
|
|
|
|
if (selectedWeeks.length === 0) {
|
|
document.getElementById('weekly-start').value = '';
|
|
document.getElementById('weekly-end').value = '';
|
|
document.getElementById('weekly-checkboxes').innerHTML = '';
|
|
document.getElementById('weekly-summary').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
// Sort selected weeks by start date
|
|
selectedWeeks.sort((a, b) => a.start.localeCompare(b.start));
|
|
|
|
const rangeStart = selectedWeeks[0].start;
|
|
const rangeEnd = selectedWeeks[selectedWeeks.length - 1].end;
|
|
|
|
document.getElementById('weekly-start').value = rangeStart;
|
|
document.getElementById('weekly-end').value = rangeEnd;
|
|
|
|
// Update issue_type based on selection
|
|
const issueTypeInput = document.getElementById('weekly-issue-type');
|
|
if (rangeStart === rangeEnd) {
|
|
issueTypeInput.value = 'weekly';
|
|
} else {
|
|
issueTypeInput.value = 'multi_week';
|
|
}
|
|
|
|
const count = await loadArticles(rangeStart, rangeEnd, 'weekly-checkboxes');
|
|
const ws = new Date(rangeStart + 'T00:00:00');
|
|
const we = new Date(rangeEnd + 'T00:00:00');
|
|
const opts = { month: 'short', day: 'numeric' };
|
|
const optsYear = { month: 'short', day: 'numeric', year: 'numeric' };
|
|
|
|
let summaryHtml = '';
|
|
if (rangeStart === rangeEnd) {
|
|
summaryHtml = `<strong>Week ${selectedWeeks[0].row.cells[0].textContent.trim().split(' ')[0]}:</strong> ${ws.toLocaleDateString('en-US', opts)} – ${we.toLocaleDateString('en-US', optsYear)} · <strong>${count} articles</strong>`;
|
|
} else {
|
|
summaryHtml = `<strong>Range:</strong> ${ws.toLocaleDateString('en-US', opts)} – ${we.toLocaleDateString('en-US', optsYear)} · <strong>${count} articles</strong>`;
|
|
}
|
|
document.getElementById('weekly-summary').innerHTML = summaryHtml;
|
|
}
|
|
|
|
async function changeMonth(delta) {
|
|
calMonth += delta;
|
|
if (calMonth > 12) { calMonth = 1; calYear++; }
|
|
if (calMonth < 1) { calMonth = 12; calYear--; }
|
|
const resp = await fetch(`/publish/calendar?year=${calYear}&month=${calMonth}`);
|
|
const weeks = await resp.json();
|
|
renderCalendar(weeks, 'cal-body', 'cal-week', 'selectWeekMulti(this)');
|
|
document.getElementById('cal-month-label').textContent =
|
|
new Date(calYear, calMonth - 1).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
|
|
|
// Re-apply selected classes based on tracked selectedWeeks
|
|
document.querySelectorAll('.cal-week').forEach(row => {
|
|
if (selectedWeeks.some(w => w.start === row.dataset.start)) {
|
|
row.classList.add('selected');
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderCalendar(weeks, tbodyId, rowClass, onclickFn) {
|
|
const tbody = document.getElementById(tbodyId);
|
|
tbody.innerHTML = weeks.map(w => {
|
|
const days = w.days.map(d => {
|
|
let classes = [];
|
|
if (!d.in_month) classes.push('cal-dim');
|
|
if (d.has_articles) classes.push('has-articles');
|
|
return `<td class="${classes.join(' ')}">${d.day}</td>`;
|
|
}).join('');
|
|
const count = w.article_count ? ` <small>(${w.article_count})</small>` : '';
|
|
return `<tr class="${rowClass}" data-start="${w.week_start}" data-end="${w.week_end}" onclick="${onclickFn}">
|
|
<td class="cal-wk">${w.iso_week}${count}</td>${days}
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
function selectSingleArticle(radio) {
|
|
const pubDate = radio.dataset.date;
|
|
document.getElementById('single-start').value = pubDate;
|
|
document.getElementById('single-end').value = pubDate;
|
|
}
|
|
|
|
function filterArticles() {
|
|
const query = document.getElementById('article-search').value.toLowerCase();
|
|
document.querySelectorAll('.article-radio-item').forEach(item => {
|
|
const searchText = item.dataset.search;
|
|
item.style.display = searchText.includes(query) ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('form').forEach(form => {
|
|
form.addEventListener('submit', function() {
|
|
const btn = this.querySelector('button[type="submit"]');
|
|
if (btn) {
|
|
btn.setAttribute('aria-busy', 'true');
|
|
btn.textContent = 'Generating...';
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|