Real web UI: /new editor, /history with live pagination, paste view with gutter and deletion banner, /unlock page; size in public API

This commit is contained in:
2026-09-08 16:50:44 -05:00
parent 9ff4d158ad
commit c6233dbbec
12 changed files with 658 additions and 7 deletions
+89
View File
@@ -0,0 +1,89 @@
{{template "head" .}}
{{template "topbar" .}}
<div class="page">
<div class="head-row">
<h1>Public pastes</h1>
<span class="count" id="count"></span>
<div class="spacer"></div>
<div class="search"><input id="filter" placeholder="filter…"></div>
</div>
<div class="float">
<table>
<thead><tr><th>Paste</th><th>Description</th><th>Language</th><th>Size</th><th>Views</th><th>Created</th></tr></thead>
<tbody id="rows"></tbody>
</table>
<div class="empty" id="empty" style="display:none">No pastes yet. Create the first one.</div>
<div class="pager">
<span id="showing"></span>
<div class="pg" id="pg"></div>
</div>
</div>
</div>
<script>
const PER = 25;
let page = 1, total = 0;
const $ = id => document.getElementById(id);
function esc(s) { const d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; }
function fmtSize(n) { if (n == null) return '—'; if (n < 1024) return n + ' B'; if (n < 1048576) return (n/1024).toFixed(1) + ' KB'; return (n/1048576).toFixed(1) + ' MB'; }
function ago(ts) {
const s = Math.floor(Date.now()/1000) - ts;
if (s < 60) return s + 's ago';
if (s < 3600) return Math.floor(s/60) + 'm ago';
if (s < 86400) return Math.floor(s/3600) + 'h ago';
return Math.floor(s/86400) + 'd ago';
}
async function load() {
const off = (page - 1) * PER;
const res = await fetch('/api/public?limit=' + PER + '&offset=' + off);
const data = await res.json();
total = data.total;
$('count').textContent = total.toLocaleString() + ' total';
const rows = $('rows');
if (data.items.length === 0) {
rows.innerHTML = '';
$('empty').style.display = 'block';
} else {
$('empty').style.display = 'none';
rows.innerHTML = data.items.map(it =>
`<tr class="row"><td><a class="slug" href="/${esc(it.id)}">${esc(it.id)}</a></td>` +
`<td class="title-cell">${it.title ? esc(it.title) : '<span class=dim>—</span>'}</td>` +
`<td><span class="badge">${esc(it.language || 'text')}</span></td>` +
`<td class="dim">${fmtSize(it.size)}</td><td class="dim">${it.view_count}</td><td class="dim">${ago(it.created_at)}</td></tr>`
).join('');
}
const pages = Math.max(1, Math.ceil(total / PER));
$('showing').textContent = total === 0 ? 'Nothing here yet' :
`Showing ${off+1}${Math.min(off+PER, total)} of ${total.toLocaleString()} · page ${page} of ${pages}`;
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 win = new Set([1, 2, page-1, page, page+1, pages]);
let last = 0;
for (let i = 1; i <= pages; i++) {
if (win.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===pages});
$('pg').innerHTML = btns.join('');
}
$('pg').addEventListener('click', e => {
const b = e.target.closest('button[data-p]');
if (!b || b.disabled) return;
page = parseInt(b.dataset.p);
load();
window.scrollTo(0, 0);
});
$('filter').addEventListener('input', () => { page = 1; load(); });
load();
setInterval(load, 30000); // auto-refresh history every 30s
</script>
{{template "foot" .}}