// pgv3-repos.js — lazy "view more" for the /repos landing page.
//
// The page used to render every repo the daemon serves: 131 cards and 207KB
// of HTML on the live forge, each carrying an inline SVG sparkline. The server
// now renders the 25 most recently pushed and this script pulls the rest on
// demand, at a size the reader picks (next 25 / 50 / 100 / all remaining).
//
// Fragments come back as server-rendered HTML from GET /api/repos/page, not
// as JSON we re-render here — the card markup then has exactly one definition
// and a later page cannot drift from the first.
//
// Paging state lives in data-attributes on #repo-more, seeded server-side, so
// the first "view more" continues from where the SSR page stopped rather than
// re-deriving an offset from what happens to be in the DOM.
//
// Progressive enhancement: with this script blocked the first 25 still render
// and the nav search still reaches everything below them.
(function () {
'use strict'
function init () {
var more = document.getElementById('repo-more')
var grid = document.getElementById('repo-grid')
var btn = document.getElementById('repo-more-btn')
var sizeSel = document.getElementById('repo-more-size')
var status = document.getElementById('repo-more-status')
if (!more || !grid || !btn || !sizeSel) return
var prefix = document.body.getAttribute('data-prefix') || ''
var total = parseInt(more.getAttribute('data-total'), 10) || 0
var offset = parseInt(more.getAttribute('data-next-offset'), 10) || 0
var busy = false
function setStatus (text) {
if (status) status.textContent = text
}
async function loadMore () {
// A second click while a fetch is in flight would append the SAME page
// twice — the offset only advances on response.
if (busy) return
busy = true
btn.disabled = true
var label = btn.textContent
btn.textContent = 'Loading…'
try {
var limit = sizeSel.value // "0" is the explicit "all remaining"
var url = prefix + '/api/repos/page?offset=' + encodeURIComponent(offset) +
'&limit=' + encodeURIComponent(limit)
var res = await fetch(url, { headers: { accept: 'application/json' } })
if (!res.ok) throw new Error('HTTP ' + res.status)
var page = await res.json()
if (page.html) grid.insertAdjacentHTML('beforeend', page.html)
offset = typeof page.nextOffset === 'number' ? page.nextOffset : offset
if (typeof page.total === 'number') total = page.total
more.setAttribute('data-next-offset', String(offset))
more.setAttribute('data-remaining', String(page.remaining || 0))
if (!page.remaining) {
// Nothing left — remove the control rather than leaving a button
// that does nothing.
more.hidden = true
return
}
setStatus('showing ' + offset + ' of ' + total)
} catch (err) {
// Surface it inline: a silently dead button reads as a broken page.
setStatus('could not load more repos — ' + (err && err.message ? err.message : 'try again'))
} finally {
busy = false
btn.disabled = false
btn.textContent = label
}
}
btn.addEventListener('click', loadMore)
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init()
}
})()