90 lines
4.6 KiB
JavaScript
90 lines
4.6 KiB
JavaScript
// SvxPortal — web front-end for SvxReflector
|
||
// Copyright (C) 2026 SA7AUX (Björn)
|
||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||
//
|
||
// This program is free software: you can redistribute it and/or modify it under
|
||
// the terms of the GNU Affero General Public License as published by the Free
|
||
// Software Foundation, either version 3 of the License, or (at your option) any
|
||
// later version. See the LICENSE file for details.
|
||
|
||
// changelog.js — shared "recent updates" viewer (dmr-web, svx-web).
|
||
// Canonical copy: <dmr-portal>/lib/changelog.js; other repos carry a COPY.
|
||
// Reads changelog.json (newest first, {date, repo, hash, type, summary}) and
|
||
// renders a client-side paginated list. Needs lib/fmt.js (escHtml).
|
||
//
|
||
// const ch = changelogViewer({ body: 'chBody', pager: 'chPager', fetchAt: 'chFetchAt',
|
||
// url: 'changelog.json', perPage: 6 });
|
||
// ch.load(); // fetch once, render; later calls just re-render
|
||
(function () {
|
||
'use strict';
|
||
function changelogViewer(opts) {
|
||
opts = opts || {};
|
||
const ids = { body: opts.body || 'chBody', pager: opts.pager || 'chPager', fetchAt: opts.fetchAt || 'chFetchAt' };
|
||
const url = opts.url || 'changelog.json';
|
||
const perPage = opts.perPage || 6;
|
||
const $ = id => document.getElementById(id);
|
||
let entries = null, page = 0, wired = false;
|
||
|
||
function typeBadge(t) {
|
||
const m = { feature: ['Added', 'bg-success'], fix: ['Fix', 'bg-danger'],
|
||
change: ['Change', 'bg-info text-dark'] };
|
||
const x = m[String(t || '').toLowerCase()] || [t || '—', 'bg-secondary'];
|
||
return `<span class="badge ${x[1]}" style="font-size:9px;">${escHtml(x[0])}</span>`;
|
||
}
|
||
function render() {
|
||
const body = $(ids.body), pager = $(ids.pager);
|
||
if (!body || !entries) return;
|
||
const n = entries.length, pages = Math.max(1, Math.ceil(n / perPage));
|
||
page = Math.min(Math.max(0, page), pages - 1);
|
||
const start = page * perPage;
|
||
const rows = entries.slice(start, start + perPage).map(e => `
|
||
<div class="d-flex align-items-start gap-2 py-1" style="border-bottom:1px solid rgba(128,128,128,.15);">
|
||
<span class="text-secondary mono" style="min-width:78px;">${escHtml(e.date || '')}</span>
|
||
${typeBadge(e.type)}
|
||
<span class="flex-grow-1">${escHtml(e.summary || '')}</span>
|
||
<span class="text-secondary mono" style="font-size:10px;" title="${escHtml(e.repo || '')} ${escHtml(e.hash || '')}">${escHtml(e.repo || '')} ${escHtml(e.hash || '')}</span>
|
||
</div>`).join('');
|
||
body.innerHTML = rows || '<div class="text-secondary">No entries.</div>';
|
||
if (!pager) return;
|
||
if (pages <= 1) { pager.innerHTML = ''; return; }
|
||
const btn = (label, p, dis, act) =>
|
||
`<button class="btn btn-sm ${act ? 'btn-primary' : 'btn-outline-secondary'}" style="padding:1px 8px;" ${dis ? 'disabled' : ''} data-chpage="${p}">${label}</button>`;
|
||
let h = btn('«', 0, page === 0) + btn('‹', page - 1, page === 0);
|
||
const lo = Math.max(0, page - 2), hi = Math.min(pages - 1, page + 2);
|
||
for (let p = lo; p <= hi; p++) h += btn(String(p + 1), p, false, p === page);
|
||
h += btn('›', page + 1, page === pages - 1) + btn('»', pages - 1, page === pages - 1);
|
||
h += `<select class="form-select form-select-sm" style="width:auto;padding:1px 6px;" id="chPageSel" title="Jump to page">` +
|
||
Array.from({ length: pages }, (_, p) => `<option value="${p}" ${p === page ? 'selected' : ''}>${p + 1}/${pages}</option>`).join('') + `</select>`;
|
||
pager.innerHTML = h;
|
||
}
|
||
async function load() {
|
||
if (!wired) {
|
||
wired = true;
|
||
const pager = $(ids.pager);
|
||
if (pager) {
|
||
pager.addEventListener('click', ev => {
|
||
const b = ev.target.closest('[data-chpage]'); if (!b) return;
|
||
page = parseInt(b.dataset.chpage, 10) || 0; render();
|
||
});
|
||
pager.addEventListener('change', ev => {
|
||
if (ev.target && ev.target.id === 'chPageSel') { page = parseInt(ev.target.value, 10) || 0; render(); }
|
||
});
|
||
}
|
||
}
|
||
if (entries) { render(); return; }
|
||
try {
|
||
const r = await fetch(url + '?_=' + Date.now(), { credentials: 'omit' });
|
||
const j = await r.json();
|
||
entries = Array.isArray(j) ? j : (j.entries || []);
|
||
const fa = $(ids.fetchAt);
|
||
if (fa && entries[0]) fa.textContent = 'latest ' + (entries[0].date || '');
|
||
render();
|
||
} catch (_) {
|
||
const b = $(ids.body); if (b) b.innerHTML = '<div class="text-secondary">Changelog unavailable.</div>';
|
||
}
|
||
}
|
||
return { load, render };
|
||
}
|
||
window.changelogViewer = changelogViewer;
|
||
})();
|