133 lines
5.1 KiB
HTML
133 lines
5.1 KiB
HTML
{{template "head" .}}
|
||
{{template "topbar" .}}
|
||
<div class="page">
|
||
<div class="head-row">
|
||
<h1>Public pastes</h1>
|
||
<span class="count" id="count"></span>
|
||
</div>
|
||
<div class="search"><input id="filter" placeholder="Search…"><span class="search-spinner" id="search-spinner"></span></div>
|
||
<div class="float">
|
||
<table>
|
||
<colgroup><col style="width:260px"><col style="width:140px"><col style="width:120px"><col style="width:96px"><col style="width:140px"><col style="width:190px"><col style="width:100px"></colgroup>
|
||
<thead><tr><th>Paste</th><th>Language</th><th>Size</th><th>Views</th><th>Created</th><th>URL</th><th>ID</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>
|
||
<div class="pager float">
|
||
<span id="showing"></span>
|
||
<div class="pg" id="pg"></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 'none'; 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';
|
||
}
|
||
|
||
let filter = '';
|
||
|
||
function matchesFilter(it) {
|
||
if (!filter) return true;
|
||
const f = filter.toLowerCase();
|
||
return (it.title || '').toLowerCase().includes(f) || (it.id || '').toLowerCase().includes(f);
|
||
}
|
||
|
||
async function load() {
|
||
const spinner = document.getElementById('search-spinner');
|
||
spinner.style.visibility = 'visible';
|
||
try {
|
||
const filtered = filter.length > 0;
|
||
const off = (page - 1) * PER;
|
||
const url = filtered
|
||
? '/api/public?limit=200&offset=0'
|
||
: '/api/public?limit=' + PER + '&offset=' + off;
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
total = data.total;
|
||
|
||
const items = filtered ? data.items.filter(matchesFilter) : data.items;
|
||
|
||
$('count').textContent = filtered
|
||
? items.length.toLocaleString() + ' matches (of ' + total.toLocaleString() + ' total)'
|
||
: total.toLocaleString() + ' total';
|
||
const rows = $('rows');
|
||
if (items.length === 0) {
|
||
rows.innerHTML = '';
|
||
$('empty').style.display = 'block';
|
||
$('empty').textContent = filtered ? 'No pastes match your search.' : 'No pastes yet. Create the first one.';
|
||
} else {
|
||
$('empty').style.display = 'none';
|
||
rows.innerHTML = items.map(it =>
|
||
`<tr class="row" data-href="/${esc(it.id)}"><td><a class="slug" href="/${esc(it.id)}">${esc(it.id)}</a>` +
|
||
(it.title ? `<div class="paste-sub">${esc(it.title)}</div>` : `<div class="paste-sub dim">none</div>`) + `</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>` +
|
||
(it.custom_slug ? `<td><a class="slug url-link" href="/${esc(it.custom_slug)}">/${esc(it.custom_slug)}</a></td>` : `<td class="dim">none</td>`) +
|
||
`<td class="dim"><a class="id-link" href="/${esc(it.id)}">${esc(it.id)}</a></td></tr>`
|
||
).join('');
|
||
}
|
||
|
||
const pages = Math.max(1, Math.ceil(total / PER));
|
||
if (filtered) {
|
||
$('showing').textContent = 'Showing ' + items.length.toLocaleString() + ' matches for "' + filter + '"';
|
||
$('pg').innerHTML = '';
|
||
} else {
|
||
$('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('');
|
||
}
|
||
} finally {
|
||
spinner.style.visibility = 'hidden';
|
||
}
|
||
}
|
||
|
||
$('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);
|
||
});
|
||
$('rows').addEventListener('click', e => {
|
||
const tr = e.target.closest('tr.row[data-href]');
|
||
if (!tr || e.target.closest('a')) return; // let real links (e.g. middle-click) work
|
||
window.location.href = tr.dataset.href;
|
||
});
|
||
let filterTimer = null;
|
||
$('filter').addEventListener('input', e => {
|
||
clearTimeout(filterTimer);
|
||
filterTimer = setTimeout(() => {
|
||
filter = e.target.value.trim();
|
||
page = 1;
|
||
load();
|
||
}, 200);
|
||
});
|
||
load();
|
||
setInterval(load, 30000); // auto-refresh history every 30s
|
||
</script>
|
||
{{template "foot" .}}
|