feat: publish UI rewrite — tabs, calendar widget, multi-week and single-article support
Made-with: Cursor
This commit is contained in:
@@ -64,6 +64,7 @@ def build_epub(
|
||||
article_ids: list[int],
|
||||
cover_path: str,
|
||||
output_dir: str,
|
||||
issue_type: str = "weekly",
|
||||
) -> str:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
@@ -74,6 +75,16 @@ def build_epub(
|
||||
.all()
|
||||
)
|
||||
|
||||
if issue_type == "single_article" and len(articles) == 1:
|
||||
title = f"Plymouth Independent \u2014 {articles[0].title}"
|
||||
elif issue_type == "multi_week":
|
||||
w1 = week_start.isocalendar()[1]
|
||||
w2 = week_end.isocalendar()[1]
|
||||
title = (
|
||||
f"Plymouth Independent \u2014 "
|
||||
f"Weeks {w1}\u2013{w2}, {week_start.strftime('%b %d')}\u2013{week_end.strftime('%b %d, %Y')}"
|
||||
)
|
||||
else:
|
||||
title = (
|
||||
f"Plymouth Independent \u2014 "
|
||||
f"Week of {week_start.strftime('%b %d')}\u2013{week_end.strftime('%b %d, %Y')}"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
from datetime import date, timedelta
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from calendar import monthrange
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
|
||||
from app import db
|
||||
from src.models import Article, Issue
|
||||
@@ -11,19 +12,64 @@ import config
|
||||
publish_bp = Blueprint("publish", __name__)
|
||||
|
||||
|
||||
def _get_week_bounds(d: date) -> tuple[date, date]:
|
||||
week_start = d - timedelta(days=d.weekday())
|
||||
week_end = week_start + timedelta(days=6)
|
||||
return week_start, week_end
|
||||
|
||||
|
||||
def _calendar_data(year: int, month: int) -> list[dict]:
|
||||
"""Build calendar grid data for a given month."""
|
||||
first_day = date(year, month, 1)
|
||||
_, days_in_month = monthrange(year, month)
|
||||
|
||||
weeks = []
|
||||
current = first_day - timedelta(days=first_day.weekday())
|
||||
|
||||
while current.month <= month or (current + timedelta(days=6)).month <= month or current < first_day:
|
||||
week_start = current
|
||||
week_end = current + timedelta(days=6)
|
||||
iso_week = week_start.isocalendar()[1]
|
||||
|
||||
article_count = Article.query.filter(
|
||||
Article.pub_date >= str(week_start),
|
||||
Article.pub_date < str(week_end + timedelta(days=1)),
|
||||
).count()
|
||||
|
||||
days = []
|
||||
for i in range(7):
|
||||
d = current + timedelta(days=i)
|
||||
days.append({
|
||||
"day": d.day,
|
||||
"date": d.isoformat(),
|
||||
"in_month": d.month == month,
|
||||
})
|
||||
|
||||
weeks.append({
|
||||
"iso_week": iso_week,
|
||||
"week_start": week_start.isoformat(),
|
||||
"week_end": week_end.isoformat(),
|
||||
"days": days,
|
||||
"article_count": article_count,
|
||||
})
|
||||
|
||||
current += timedelta(days=7)
|
||||
if current.month > month and current.year >= year:
|
||||
if not any(d["in_month"] for d in days):
|
||||
weeks.pop()
|
||||
break
|
||||
|
||||
return weeks
|
||||
|
||||
|
||||
@publish_bp.route("/publish", methods=["GET"])
|
||||
def index():
|
||||
week_str = request.args.get("week")
|
||||
if week_str:
|
||||
try:
|
||||
year, week_num = week_str.split("-W")
|
||||
week_start = date.fromisocalendar(int(year), int(week_num), 1)
|
||||
except (ValueError, TypeError):
|
||||
week_start = date.today() - timedelta(days=date.today().weekday())
|
||||
else:
|
||||
week_start = date.today() - timedelta(days=date.today().weekday())
|
||||
today = date.today()
|
||||
cal_year = request.args.get("cal_year", today.year, type=int)
|
||||
cal_month = request.args.get("cal_month", today.month, type=int)
|
||||
|
||||
week_end = week_start + timedelta(days=6)
|
||||
week_start, week_end = _get_week_bounds(today)
|
||||
calendar_weeks = _calendar_data(cal_year, cal_month)
|
||||
|
||||
articles = (
|
||||
Article.query
|
||||
@@ -35,20 +81,75 @@ def index():
|
||||
.all()
|
||||
)
|
||||
|
||||
all_articles = (
|
||||
Article.query
|
||||
.order_by(Article.pub_date.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"publish.html",
|
||||
articles=articles,
|
||||
all_articles=all_articles,
|
||||
week_start=week_start,
|
||||
week_end=week_end,
|
||||
week_str=f"{week_start.year}-W{week_start.isocalendar()[1]:02d}",
|
||||
calendar_weeks=calendar_weeks,
|
||||
cal_year=cal_year,
|
||||
cal_month=cal_month,
|
||||
cal_month_name=date(cal_year, cal_month, 1).strftime("%B %Y"),
|
||||
)
|
||||
|
||||
|
||||
@publish_bp.route("/publish/articles", methods=["GET"])
|
||||
def articles_api():
|
||||
start = request.args.get("start")
|
||||
end = request.args.get("end")
|
||||
if not start or not end:
|
||||
return jsonify([])
|
||||
|
||||
try:
|
||||
start_date = date.fromisoformat(start)
|
||||
end_date = date.fromisoformat(end)
|
||||
except ValueError:
|
||||
return jsonify([])
|
||||
|
||||
articles = (
|
||||
Article.query
|
||||
.filter(
|
||||
Article.pub_date >= str(start_date),
|
||||
Article.pub_date < str(end_date + timedelta(days=1)),
|
||||
)
|
||||
.order_by(Article.pub_date.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
return jsonify([
|
||||
{
|
||||
"id": a.id,
|
||||
"title": a.title,
|
||||
"author": a.author,
|
||||
"pub_date": a.pub_date.strftime("%b %d, %Y"),
|
||||
"categories": json.loads(a.categories),
|
||||
}
|
||||
for a in articles
|
||||
])
|
||||
|
||||
|
||||
@publish_bp.route("/publish/calendar", methods=["GET"])
|
||||
def calendar_api():
|
||||
year = request.args.get("year", type=int)
|
||||
month = request.args.get("month", type=int)
|
||||
if not year or not month:
|
||||
return jsonify([])
|
||||
return jsonify(_calendar_data(year, month))
|
||||
|
||||
|
||||
@publish_bp.route("/publish", methods=["POST"])
|
||||
def create_issue():
|
||||
week_start_str = request.form.get("week_start")
|
||||
week_end_str = request.form.get("week_end")
|
||||
cover_method = request.form.get("cover_method", "text")
|
||||
issue_type = request.form.get("issue_type", "weekly")
|
||||
included_ids = request.form.getlist("article_ids", type=int)
|
||||
|
||||
if not included_ids:
|
||||
@@ -58,17 +159,6 @@ def create_issue():
|
||||
week_start = date.fromisoformat(week_start_str)
|
||||
week_end = date.fromisoformat(week_end_str)
|
||||
|
||||
all_week_articles = (
|
||||
Article.query
|
||||
.filter(
|
||||
Article.pub_date >= str(week_start),
|
||||
Article.pub_date < str(week_end + timedelta(days=1)),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
all_ids = {a.id for a in all_week_articles}
|
||||
excluded_ids = list(all_ids - set(included_ids))
|
||||
|
||||
headlines = [
|
||||
a.title for a in Article.query.filter(Article.id.in_(included_ids))
|
||||
.order_by(Article.pub_date.asc()).all()
|
||||
@@ -84,12 +174,24 @@ def create_issue():
|
||||
headlines, categories_list
|
||||
)
|
||||
epub_path = build_epub(
|
||||
week_start, week_end, included_ids, cover_path, config.ISSUES_DIR
|
||||
week_start, week_end, included_ids, cover_path,
|
||||
config.ISSUES_DIR, issue_type=issue_type
|
||||
)
|
||||
except Exception as e:
|
||||
flash(f"Error generating issue: {e}", "error")
|
||||
return redirect(url_for("publish.index"))
|
||||
|
||||
all_week_articles = (
|
||||
Article.query
|
||||
.filter(
|
||||
Article.pub_date >= str(week_start),
|
||||
Article.pub_date < str(week_end + timedelta(days=1)),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
all_ids = {a.id for a in all_week_articles}
|
||||
excluded_ids = list(all_ids - set(included_ids))
|
||||
|
||||
issue = Issue(
|
||||
week_start=week_start,
|
||||
week_end=week_end,
|
||||
@@ -99,6 +201,7 @@ def create_issue():
|
||||
article_ids=json.dumps(included_ids),
|
||||
excluded_article_ids=json.dumps(excluded_ids),
|
||||
status="published",
|
||||
issue_type=issue_type,
|
||||
)
|
||||
db.session.add(issue)
|
||||
db.session.commit()
|
||||
|
||||
@@ -3,73 +3,272 @@
|
||||
{% block content %}
|
||||
<h1>Publish Issue</h1>
|
||||
|
||||
<form method="get" action="/publish" style="margin-bottom: 1rem;">
|
||||
<label>
|
||||
Target Week
|
||||
<input type="week" name="week" value="{{ week_str }}">
|
||||
</label>
|
||||
<button type="submit" class="outline">Load Week</button>
|
||||
</form>
|
||||
<div class="tab-bar">
|
||||
<button class="tab active" data-tab="weekly" onclick="switchTab('weekly')">Weekly Issue</button>
|
||||
<button class="tab" data-tab="multi-week" onclick="switchTab('multi-week')">Multi-Week</button>
|
||||
<button class="tab" data-tab="single-article" onclick="switchTab('single-article')">Single Article</button>
|
||||
</div>
|
||||
|
||||
<p>{{ week_start.strftime('%b %d') }} – {{ week_end.strftime('%b %d, %Y') }} · {{ articles|length }} articles</p>
|
||||
|
||||
{% if articles %}
|
||||
<form method="post" action="/publish" id="publish-form">
|
||||
<input type="hidden" name="week_start" value="{{ week_start.isoformat() }}">
|
||||
<input type="hidden" name="week_end" value="{{ week_end.isoformat() }}">
|
||||
|
||||
<table>
|
||||
<!-- WEEKLY TAB -->
|
||||
<div id="tab-weekly" class="tab-content active">
|
||||
<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><input type="checkbox" id="select-all" checked></th>
|
||||
<th>Date</th>
|
||||
<th>Title</th>
|
||||
<th>Author</th>
|
||||
<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>
|
||||
{% for article in articles %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="article_ids" value="{{ article.id }}" checked
|
||||
class="article-checkbox">
|
||||
</td>
|
||||
<td>{{ article.pub_date.strftime('%b %d') }}</td>
|
||||
<td>{{ article.title }}</td>
|
||||
<td>{{ article.author }}</td>
|
||||
<tbody id="cal-body">
|
||||
{% for week in calendar_weeks %}
|
||||
<tr class="cal-week" data-start="{{ week.week_start }}" data-end="{{ week.week_end }}"
|
||||
onclick="selectWeek(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 %}">{{ 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" value="weekly">
|
||||
<div id="weekly-checkboxes"></div>
|
||||
<div class="publish-actions">
|
||||
<select name="cover_method">
|
||||
<option value="ai">AI Cover (themed)</option>
|
||||
<option value="text">Text Cover</option>
|
||||
</select>
|
||||
<button type="submit">Generate Issue</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Cover</legend>
|
||||
<label>
|
||||
<input type="radio" name="cover_method" value="ai" checked>
|
||||
AI Cover (Pollinations.ai)
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="cover_method" value="text">
|
||||
Text Cover (fallback)
|
||||
</label>
|
||||
</fieldset>
|
||||
<!-- MULTI-WEEK TAB -->
|
||||
<div id="tab-multi-week" class="tab-content">
|
||||
<div class="calendar-widget">
|
||||
<div class="calendar-nav">
|
||||
<button class="outline" onclick="changeMonthMulti(-1)">◀</button>
|
||||
<strong id="cal-month-label-multi">{{ cal_month_name }}</strong>
|
||||
<button class="outline" onclick="changeMonthMulti(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-multi">
|
||||
{% for week in calendar_weeks %}
|
||||
<tr class="cal-week-multi" 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 %}">{{ day.day }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="multi-summary" class="publish-summary"></div>
|
||||
<form method="post" action="/publish" id="multi-form">
|
||||
<input type="hidden" name="week_start" id="multi-start">
|
||||
<input type="hidden" name="week_end" id="multi-end">
|
||||
<input type="hidden" name="issue_type" value="multi_week">
|
||||
<div id="multi-checkboxes"></div>
|
||||
<div class="publish-actions">
|
||||
<select name="cover_method">
|
||||
<option value="ai">AI Cover (themed)</option>
|
||||
<option value="text">Text Cover</option>
|
||||
</select>
|
||||
<button type="submit">Generate Issue</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="publish-btn">Generate Issue</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>No articles found for this week. Try fetching articles first from the <a href="/">Dashboard</a>.</p>
|
||||
{% endif %}
|
||||
<!-- 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="ai">AI Cover (themed)</option>
|
||||
<option value="text">Text Cover</option>
|
||||
</select>
|
||||
<button type="submit">Generate Single Issue</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.getElementById('select-all')?.addEventListener('change', function() {
|
||||
document.querySelectorAll('.article-checkbox').forEach(cb => cb.checked = this.checked);
|
||||
});
|
||||
let calYear = {{ cal_year }};
|
||||
let calMonth = {{ cal_month }};
|
||||
let calYearMulti = {{ cal_year }};
|
||||
let calMonthMulti = {{ cal_month }};
|
||||
let multiStart = null;
|
||||
let multiEnd = null;
|
||||
|
||||
document.getElementById('publish-form')?.addEventListener('submit', function() {
|
||||
document.getElementById('publish-btn').setAttribute('aria-busy', 'true');
|
||||
document.getElementById('publish-btn').textContent = 'Generating...';
|
||||
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 selectWeek(row) {
|
||||
document.querySelectorAll('.cal-week').forEach(r => r.classList.remove('selected'));
|
||||
row.classList.add('selected');
|
||||
const start = row.dataset.start;
|
||||
const end = row.dataset.end;
|
||||
document.getElementById('weekly-start').value = start;
|
||||
document.getElementById('weekly-end').value = end;
|
||||
const count = await loadArticles(start, end, 'weekly-checkboxes');
|
||||
const ws = new Date(start + 'T00:00:00');
|
||||
const we = new Date(end + 'T00:00:00');
|
||||
const opts = { month: 'short', day: 'numeric' };
|
||||
const optsYear = { month: 'short', day: 'numeric', year: 'numeric' };
|
||||
document.getElementById('weekly-summary').innerHTML =
|
||||
`<strong>Week ${row.cells[0].textContent.trim().split(' ')[0]}:</strong> ${ws.toLocaleDateString('en-US', opts)} – ${we.toLocaleDateString('en-US', optsYear)} · <strong>${count} articles</strong>`;
|
||||
}
|
||||
|
||||
async function selectWeekMulti(row) {
|
||||
const start = row.dataset.start;
|
||||
const end = row.dataset.end;
|
||||
|
||||
if (!multiStart || (multiStart && multiEnd)) {
|
||||
multiStart = { start, end, row };
|
||||
multiEnd = null;
|
||||
document.querySelectorAll('.cal-week-multi').forEach(r => r.classList.remove('selected', 'in-range'));
|
||||
row.classList.add('selected');
|
||||
} else {
|
||||
multiEnd = { start, end, row };
|
||||
if (multiEnd.start < multiStart.start) {
|
||||
[multiStart, multiEnd] = [multiEnd, multiStart];
|
||||
}
|
||||
document.querySelectorAll('.cal-week-multi').forEach(r => {
|
||||
r.classList.remove('selected', 'in-range');
|
||||
if (r.dataset.start >= multiStart.start && r.dataset.end <= multiEnd.end) {
|
||||
r.classList.add('in-range');
|
||||
}
|
||||
});
|
||||
multiStart.row.classList.add('selected');
|
||||
multiEnd.row.classList.add('selected');
|
||||
}
|
||||
|
||||
const rangeStart = multiStart.start;
|
||||
const rangeEnd = (multiEnd || multiStart).end;
|
||||
document.getElementById('multi-start').value = rangeStart;
|
||||
document.getElementById('multi-end').value = rangeEnd;
|
||||
const count = await loadArticles(rangeStart, rangeEnd, 'multi-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' };
|
||||
document.getElementById('multi-summary').innerHTML =
|
||||
`<strong>Range:</strong> ${ws.toLocaleDateString('en-US', opts)} – ${we.toLocaleDateString('en-US', optsYear)} · <strong>${count} articles</strong>`;
|
||||
}
|
||||
|
||||
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', 'selectWeek(this)');
|
||||
document.getElementById('cal-month-label').textContent =
|
||||
new Date(calYear, calMonth - 1).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
async function changeMonthMulti(delta) {
|
||||
calMonthMulti += delta;
|
||||
if (calMonthMulti > 12) { calMonthMulti = 1; calYearMulti++; }
|
||||
if (calMonthMulti < 1) { calMonthMulti = 12; calYearMulti--; }
|
||||
const resp = await fetch(`/publish/calendar?year=${calYearMulti}&month=${calMonthMulti}`);
|
||||
const weeks = await resp.json();
|
||||
renderCalendar(weeks, 'cal-body-multi', 'cal-week-multi', 'selectWeekMulti(this)');
|
||||
document.getElementById('cal-month-label-multi').textContent =
|
||||
new Date(calYearMulti, calMonthMulti - 1).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||
multiStart = null;
|
||||
multiEnd = null;
|
||||
}
|
||||
|
||||
function renderCalendar(weeks, tbodyId, rowClass, onclickFn) {
|
||||
const tbody = document.getElementById(tbodyId);
|
||||
tbody.innerHTML = weeks.map(w => {
|
||||
const days = w.days.map(d =>
|
||||
`<td class="${d.in_month ? '' : 'cal-dim'}">${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 %}
|
||||
|
||||
Reference in New Issue
Block a user