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:
@@ -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" .}}
|
||||
Reference in New Issue
Block a user