feat: add browse-at-scale — date ranges, load more, month navigator

CLI: --month YYYY-MM, --from/--to date range, --limit override
API: from/to query params for date range filtering
Web: load more button for pagination, month nav (◂/▸) in stream view
This commit is contained in:
2026-05-14 14:03:45 -04:00
parent 2d62199705
commit 949ccaca59
6 changed files with 226 additions and 8 deletions
+76 -3
View File
@@ -14,12 +14,16 @@
link: 'glyph-link',
};
const PAGE_SIZE = 50;
const state = {
view: 'stream',
entities: [],
tags: [],
selectedIndex: -1,
activeTag: null,
hasMore: false,
activeMonth: null,
};
const $ = (sel) => document.querySelector(sel);
@@ -32,9 +36,13 @@
const q = new URLSearchParams();
if (params.tag) q.set('tag', params.tag);
if (params.date) q.set('date', params.date);
if (params.from) q.set('from', params.from);
if (params.to) q.set('to', params.to);
if (params.cards_only) q.set('cards_only', 'true');
if (params.sort) q.set('sort', params.sort);
if (params.order) q.set('order', params.order);
if (params.limit) q.set('limit', String(params.limit));
if (params.offset) q.set('offset', String(params.offset));
const resp = await fetch('/api/entities?' + q);
return resp.json();
},
@@ -226,6 +234,10 @@
});
}
if (state.hasMore) {
html += '<div class="load-more-wrap"><button class="load-more-btn">load more</button></div>';
}
list.innerHTML = html;
list.querySelectorAll('.entity-item').forEach(el => {
@@ -233,6 +245,9 @@
selectEntity(parseInt(el.dataset.index));
});
});
const loadMoreBtn = list.querySelector('.load-more-btn');
if (loadMoreBtn) loadMoreBtn.addEventListener('click', loadMore);
}
function renderEntityItem(e, idx) {
@@ -388,8 +403,8 @@
renderDetailPane();
}
async function loadEntities() {
const params = {};
function buildListParams(offset) {
const params = { limit: PAGE_SIZE, offset: offset || 0 };
if (state.activeTag) params.tag = state.activeTag;
if (state.view === 'cards') {
params.cards_only = true;
@@ -399,13 +414,68 @@
params.sort = 'created';
params.order = 'desc';
}
if (state.activeMonth) {
const [y, m] = state.activeMonth.split('-').map(Number);
params.from = state.activeMonth + '-01';
const last = new Date(y, m, 0).getDate();
params.to = state.activeMonth + '-' + String(last).padStart(2, '0');
}
return params;
}
state.entities = await api.listEntities(params);
async function loadEntities() {
const params = buildListParams(0);
const results = await api.listEntities(params);
state.entities = results;
state.hasMore = results.length === PAGE_SIZE;
state.selectedIndex = -1;
renderEntityList();
renderDetailPane();
}
async function loadMore() {
const params = buildListParams(state.entities.length);
const results = await api.listEntities(params);
state.entities = state.entities.concat(results);
state.hasMore = results.length === PAGE_SIZE;
renderEntityList();
}
function renderMonthNav() {
const nav = $('#month-nav');
if (state.view !== 'stream') { nav.innerHTML = ''; return; }
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
const label = state.activeMonth
? (() => { const [y, m] = state.activeMonth.split('-'); return MONTHS[parseInt(m) - 1] + ' ' + y; })()
: 'all time';
nav.innerHTML = `
<button class="month-nav-btn" id="month-prev">◂</button>
<span class="month-nav-label">${label}</span>
<button class="month-nav-btn" id="month-next">▸</button>
${state.activeMonth ? '<button class="month-nav-clear">clear</button>' : ''}
`;
$('#month-prev').addEventListener('click', () => shiftMonth(-1));
$('#month-next').addEventListener('click', () => shiftMonth(1));
const clearBtn = nav.querySelector('.month-nav-clear');
if (clearBtn) clearBtn.addEventListener('click', () => { state.activeMonth = null; loadEntities(); renderMonthNav(); });
}
function shiftMonth(dir) {
if (!state.activeMonth) {
const now = new Date();
state.activeMonth = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
} else {
const [y, m] = state.activeMonth.split('-').map(Number);
const d = new Date(y, m - 1 + dir, 1);
state.activeMonth = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
}
loadEntities();
renderMonthNav();
}
async function loadTags() {
state.tags = await api.listTags();
renderTagRail();
@@ -413,9 +483,11 @@
function switchView(view) {
state.view = view;
state.activeMonth = null;
$$('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
window.location.hash = view === 'cards' ? '/cards' : '/';
loadEntities();
renderMonthNav();
}
// ========== Public API (for inline handlers) ==========
@@ -687,6 +759,7 @@
async function init() {
await Promise.all([loadEntities(), loadTags()]);
handleHash();
renderMonthNav();
}
init();