History: working pagination over full 2431-row dataset, live filter

This commit is contained in:
2026-09-08 16:36:45 -05:00
parent 76aab13fcd
commit 80c9a4af86
+114 -15
View File
@@ -63,40 +63,139 @@
.pager { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; font-size: 12.5px; color: var(--muted-fg); } .pager { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; font-size: 12.5px; color: var(--muted-fg); }
.pager .pg { display: flex; gap: 6px; } .pager .pg { display: flex; gap: 6px; }
.pager button { border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg); border-radius: 7px; padding: 4px 11px; font: inherit; font-size: 12.5px; cursor: pointer; } .pager button { border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg); border-radius: 7px; padding: 4px 11px; font: inherit; font-size: 12.5px; cursor: pointer; }
.pager button:hover:not(:disabled) { color: var(--fg); border-color: var(--muted); }
.pager button.on { background: var(--accent); color: var(--bg); border-color: var(--accent); } .pager button.on { background: var(--accent); color: var(--bg); border-color: var(--accent); }
.pager button:disabled { opacity: .4; cursor: default; }
.empty { text-align: center; padding: 40px 16px; color: var(--muted-fg); font-size: 13px; }
</style> </style>
</head> </head>
<body data-preset="midnight"> <body data-preset="midnight">
<div class="topbar"> <div class="topbar">
<a class="logo" href="index.html">Palette <em>/ v0</em></a> <a class="logo" href="/history/">Palette <em>/ v0</em></a>
<nav><a href="/new/v2.html">new</a><a class="on" href="index.html">history</a><a href="https://git.archfox.org/poslop/palette">git</a></nav> <nav><a href="/new/">new</a><a class="on" href="/history/">history</a><a href="https://git.archfox.org/poslop/palette">git</a></nav>
<div class="spacer"></div> <div class="spacer"></div>
</div> </div>
<div class="deck"> <div class="deck">
<div class="head-row"> <div class="head-row">
<h1>Public pastes</h1> <h1>Public pastes</h1>
<span class="count">2,431 total</span> <span class="count" id="count"></span>
<div class="spacer"></div> <div class="spacer"></div>
<div class="search"><input placeholder="filter by language or slug…"></div> <div class="search"><input id="filter" placeholder="filter by language or slug…"></div>
</div> </div>
<div class="float"> <div class="float">
<table> <table>
<thead><tr><th>Paste</th><th>Description</th><th>Language</th><th>Size</th><th>Views</th><th>Created</th></tr></thead> <thead><tr><th>Paste</th><th>Description</th><th>Language</th><th>Size</th><th>Views</th><th>Created</th></tr></thead>
<tbody> <tbody id="rows"></tbody>
<tr class="row"><td><a class="slug" href="/paste/index.html">k3sYt2</a></td><td class="title-cell">traefik redirect middleware snippet</td><td><span class="badge">go</span></td><td class="dim">412 B</td><td class="dim">12</td><td class="dim">2m ago</td></tr>
<tr class="row"><td><a class="slug" href="/paste/index.html">b7QwRn</a></td><td class="title-cell">nginx notes for staging</td><td><span class="badge">txt</span></td><td class="dim">1.2 KB</td><td class="dim">3</td><td class="dim">14m ago</td></tr>
<tr class="row"><td><a class="slug" href="/paste/index.html">xT8vZc</a></td><td class="title-cell">csv parse helper</td><td><span class="badge">py</span></td><td class="dim">890 B</td><td class="dim">41</td><td class="dim">3h ago</td></tr>
<tr class="row"><td><a class="slug" href="/unlock/index.html">9mQkLp</a><span class="badge lock">🔒</span></td><td class="title-cell">uptime kuma compose</td><td><span class="badge">yaml</span></td><td class="dim">2.4 KB</td><td class="dim"></td><td class="dim">5h ago</td></tr>
<tr class="row"><td><a class="slug" href="/can-demo/index.html">can/9fL2m</a></td><td class="title-cell">incident screenshots + logs (4 items)</td><td><span class="badge">can</span></td><td class="dim">3.8 MB</td><td class="dim">7</td><td class="dim">1h ago</td></tr>
<tr class="row"><td><a class="slug" href="/paste/index.html">vR4nWq</a></td><td class="title-cell">sql index check</td><td><span class="badge">sql</span></td><td class="dim">220 B</td><td class="dim">2</td><td class="dim">6h ago</td></tr>
<tr class="row"><td><a class="slug" href="/unlock/index.html">pL2xCa</a><span class="badge lock">🔒</span></td><td class="title-cell">todo: rewrite auth flow</td><td><span class="badge">md</span></td><td class="dim">560 B</td><td class="dim"></td><td class="dim">1d ago</td></tr>
</tbody>
</table> </table>
<div class="empty" id="empty" style="display:none">No pastes match that filter.</div>
<div class="pager"> <div class="pager">
<span>Showing 17 of 2,431 · page 1 of 348</span> <span id="showing"></span>
<div class="pg"><button class="on">1</button><button>2</button><button>3</button><button></button><button>348</button></div> <div class="pg" id="pg"></div>
</div> </div>
</div> </div>
</div> </div>
<script>
// Demo dataset: 7 real entries + generated filler to simulate 2,431 total
const real = [
{slug:"k3sYt2", href:"/paste/index.html", desc:"traefik redirect middleware snippet", lang:"go", size:"412 B", views:"12", created:"2m ago", lock:false},
{slug:"b7QwRn", href:"/paste/index.html", desc:"nginx notes for staging", lang:"txt", size:"1.2 KB", views:"3", created:"14m ago",lock:false},
{slug:"xT8vZc", href:"/paste/index.html", desc:"csv parse helper", lang:"py", size:"890 B", views:"41", created:"3h ago", lock:false},
{slug:"9mQkLp", href:"/unlock/index.html", desc:"uptime kuma compose", lang:"yaml", size:"2.4 KB", views:"—", created:"5h ago", lock:true},
{slug:"can/9fL2m", href:"/can-demo/index.html",desc:"incident screenshots + logs (4 items)",lang:"can", size:"3.8 MB", views:"7", created:"1h ago", lock:false},
{slug:"vR4nWq", href:"/paste/index.html", desc:"sql index check", lang:"sql", size:"220 B", views:"2", created:"6h ago", lock:false},
{slug:"pL2xCa", href:"/unlock/index.html", desc:"todo: rewrite auth flow", lang:"md", size:"560 B", views:"—", created:"1d ago", lock:true},
];
const langs = ["go","py","txt","js","sh","yaml","md","sql","rs","c"];
const TOTAL = 2431, PER_PAGE = 7;
const PAGES = Math.ceil(TOTAL / PER_PAGE); // 348
// deterministic pseudo-random filler so pages look stable between renders
function filler(i) {
const lang = langs[(i * 7 + 3) % langs.length];
const locked = i % 11 === 0;
const slug = locked ? String(100000 + (i * 3719) % 899999).slice(0,6) : genSlugDemo(i);
return {
slug,
href: locked ? "/unlock/index.html" : "/paste/index.html",
desc: ["scratch notes","config diff","error log","snippet dump","test output","benchmark results","query plan","draft changelog"][i % 8] + " #" + (i+1),
lang, locked,
size: (0.2 + (i % 40) / 10).toFixed(1) + " KB",
views: locked ? "—" : String((i * 13) % 97),
created: (i % 24) + "h ago",
};
}
function genSlugDemo(i) {
const a = "23456789abcdefghjkmnpqrstuvwxyz";
let s = "";
let x = i * 2654435761 % 4294967296;
for (let j = 0; j < 6; j++) { s += a[x % a.length]; x = Math.floor(x / a.length) + 7; }
return s;
}
const data = [...real];
for (let i = real.length; i < TOTAL; i++) data.push(filler(i));
let page = 1;
const rowsEl = document.getElementById('rows');
const pgEl = document.getElementById('pg');
const showingEl = document.getElementById('showing');
const countEl = document.getElementById('count');
const filterEl = document.getElementById('filter');
function filtered() {
const q = filterEl.value.trim().toLowerCase();
if (!q) return data;
return data.filter(d => d.slug.toLowerCase().includes(q) || d.lang.includes(q) || d.desc.toLowerCase().includes(q));
}
function render() {
const all = filtered();
const totalPages = Math.max(1, Math.ceil(all.length / PER_PAGE));
if (page > totalPages) page = totalPages;
const start = (page - 1) * PER_PAGE;
const slice = all.slice(start, start + PER_PAGE);
if (slice.length === 0) {
rowsEl.innerHTML = '';
document.getElementById('empty').style.display = 'block';
} else {
document.getElementById('empty').style.display = 'none';
rowsEl.innerHTML = slice.map(d =>
`<tr class="row"><td><a class="slug" href="${d.href}">${d.slug}</a>${d.lock ? '<span class="badge lock">🔒</span>' : ''}</td>` +
`<td class="title-cell">${d.desc}</td>` +
`<td><span class="badge">${d.lang}</span></td>` +
`<td class="dim">${d.size}</td><td class="dim">${d.views}</td><td class="dim">${d.created}</td></tr>`
).join('');
}
countEl.textContent = all.length.toLocaleString() + " total";
const lo = all.length === 0 ? 0 : start + 1;
const hi = Math.min(start + PER_PAGE, all.length);
showingEl.textContent = `Showing ${lo}${hi} of ${all.length.toLocaleString()} · page ${page} of ${totalPages}`;
// pager: 1, prev-1, [cur], next+1, last, plus ellipses
const btns = [];
const add = (label, target, opts={}) => btns.push(`<button ${opts.on?'class="on"':''} ${opts.dis?'disabled':''} data-p="${target}">${label}</button>`);
add('', page-1, {dis: page===1});
const window = new Set([1, 2, page-1, page, page+1, totalPages]);
let last = 0;
for (let i = 1; i <= totalPages; i++) {
if (window.has(i)) {
if (last && i - last > 1) btns.push('<span class="dim">…</span>');
add(String(i), i, {on: i===page});
last = i;
}
}
add('', page+1, {dis: page===totalPages});
pgEl.innerHTML = btns.join('');
}
pgEl.addEventListener('click', e => {
const b = e.target.closest('button[data-p]');
if (!b || b.disabled) return;
page = parseInt(b.dataset.p);
render();
});
filterEl.addEventListener('input', () => { page = 1; render(); });
render();
</script>
</body> </body>
</html> </html>