Saved page: live search + sortable columns via shared table module (#57)
- new web/static/table.js (PaletteTable): shared live search, client-side sort with indicators, pagination, row rendering + click-through - history.html and mine.html both consume it; data endpoint, columns, empty-state text and row extras (delete buttons) are page-supplied - verified in-browser: search, sort, delete on /mine; sort, pager on /history Closes #57
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// Shared table logic for history (/api/public) and saved (/api/mine) pages (#57).
|
||||
// Provides: live search, client-side sort with indicators, row rendering via
|
||||
// a page-supplied rowHtml(), pagination state, and row click-through.
|
||||
const PaletteTable = (() => {
|
||||
const $ = id => document.getElementById(id);
|
||||
const esc = s => { const d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; };
|
||||
const 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'; };
|
||||
const 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';
|
||||
};
|
||||
|
||||
const sortVal = (it, k) => {
|
||||
let v = it[k];
|
||||
if (k === 'title' || k === 'custom_slug') v = (v == null || v === '') ? null : String(v).toLowerCase();
|
||||
if (k === 'language') v = (v == null || v === '') ? 'text' : String(v).toLowerCase();
|
||||
if (k === 'size' || k === 'view_count' || k === 'created_at') return v == null ? -1 : v;
|
||||
return v == null ? null : v;
|
||||
};
|
||||
|
||||
function init(opts) {
|
||||
// opts: {endpoint, perPage, hasPager, rowHtml(it), emptyFiltered, emptyAll}
|
||||
const state = { filter: '', sortKey: null, sortDir: 1, page: 1, total: 0 };
|
||||
let timer = null;
|
||||
|
||||
function sortItems(items) {
|
||||
if (!state.sortKey) return items;
|
||||
const k = state.sortKey, dir = state.sortDir;
|
||||
return items.slice().sort((a, b) => {
|
||||
const va = sortVal(a, k), vb = sortVal(b, k);
|
||||
const na = va == null, nb = vb == null;
|
||||
if (na && nb) return 0;
|
||||
if (na) return 1;
|
||||
if (nb) return -1;
|
||||
if (va < vb) return -1 * dir;
|
||||
if (va > vb) return 1 * dir;
|
||||
return (a.created_at || 0) < (b.created_at || 0) ? 1 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
function matches(it) {
|
||||
if (!state.filter) return true;
|
||||
const f = state.filter.toLowerCase();
|
||||
return (it.title || '').toLowerCase().includes(f) || (it.id || '').toLowerCase().includes(f);
|
||||
}
|
||||
|
||||
function renderSortIndicators() {
|
||||
document.querySelectorAll('th.sortable').forEach(th => {
|
||||
th.classList.toggle('sorted', th.dataset.sort === state.sortKey);
|
||||
th.classList.toggle('asc', th.dataset.sort === state.sortKey && state.sortDir === 1);
|
||||
th.classList.toggle('desc', th.dataset.sort === state.sortKey && state.sortDir === -1);
|
||||
});
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const spinner = $('search-spinner');
|
||||
if (spinner) spinner.style.visibility = 'visible';
|
||||
try {
|
||||
const filtered = state.filter.length > 0;
|
||||
const off = (state.page - 1) * opts.perPage;
|
||||
const url = (filtered || state.sortKey)
|
||||
? opts.endpoint + '?limit=500&offset=0'
|
||||
: opts.endpoint + '?limit=' + opts.perPage + '&offset=' + off;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
state.total = data.total;
|
||||
let items = filtered ? data.items.filter(matches) : data.items;
|
||||
items = sortItems(items);
|
||||
|
||||
const count = $('count');
|
||||
if (count) count.textContent = filtered
|
||||
? items.length.toLocaleString() + ' matches (of ' + state.total.toLocaleString() + ' total)'
|
||||
: state.total.toLocaleString() + ' total';
|
||||
|
||||
const rows = $('rows'), empty = $('empty');
|
||||
if (!items.length) {
|
||||
rows.innerHTML = '';
|
||||
empty.style.display = 'block';
|
||||
empty.textContent = filtered ? opts.emptyFiltered : opts.emptyAll;
|
||||
} else {
|
||||
empty.style.display = 'none';
|
||||
rows.innerHTML = items.map(opts.rowHtml).join('');
|
||||
}
|
||||
|
||||
const pager = $('pg'), showing = $('showing');
|
||||
if (opts.hasPager && pager && showing) {
|
||||
const pages = Math.max(1, Math.ceil(state.total / opts.perPage));
|
||||
if (filtered || state.sortKey) {
|
||||
showing.textContent = state.sortKey
|
||||
? 'Sorted by ' + state.sortKey + ' (' + (state.sortDir === 1 ? 'ascending' : 'descending') + ') · ' + items.length.toLocaleString() + ' of ' + state.total.toLocaleString()
|
||||
: 'Showing ' + items.length.toLocaleString() + ' matches for "' + state.filter + '"';
|
||||
pager.innerHTML = '';
|
||||
} else {
|
||||
showing.textContent = state.total === 0 ? 'Nothing here yet' :
|
||||
`Showing ${off+1}–${Math.min(off+opts.perPage, state.total)} of ${state.total.toLocaleString()} · page ${state.page} of ${pages}`;
|
||||
const btns = [];
|
||||
const add = (label, target, o={}) => btns.push(`<button ${o.on?'class="on"':''} ${o.dis?'disabled':''} data-p="${target}">${label}</button>`);
|
||||
add('‹', state.page-1, {dis: state.page===1});
|
||||
const win = new Set([1, 2, state.page-1, state.page, state.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===state.page});
|
||||
last = i;
|
||||
}
|
||||
}
|
||||
add('›', state.page+1, {dis: state.page===pages});
|
||||
pager.innerHTML = btns.join('');
|
||||
}
|
||||
} else if (showing) {
|
||||
showing.textContent = '';
|
||||
}
|
||||
renderSortIndicators();
|
||||
} finally {
|
||||
if (spinner) spinner.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector('thead').addEventListener('click', e => {
|
||||
const th = e.target.closest('th.sortable');
|
||||
if (!th) return;
|
||||
const k = th.dataset.sort;
|
||||
if (state.sortKey === k) { state.sortDir = -state.sortDir; } else { state.sortKey = k; state.sortDir = 1; }
|
||||
renderSortIndicators();
|
||||
load();
|
||||
});
|
||||
|
||||
const rows = $('rows');
|
||||
if (rows) rows.addEventListener('click', e => {
|
||||
const tr = e.target.closest('tr.row[data-href]');
|
||||
if (!tr || e.target.closest('a') || e.target.closest('button')) return;
|
||||
window.location.href = tr.dataset.href;
|
||||
});
|
||||
|
||||
const pg = $('pg');
|
||||
if (pg) pg.addEventListener('click', e => {
|
||||
const b = e.target.closest('button[data-p]');
|
||||
if (!b || b.disabled) return;
|
||||
state.page = parseInt(b.dataset.p);
|
||||
load();
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
|
||||
const filter = $('filter');
|
||||
if (filter) filter.addEventListener('input', e => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
state.filter = e.target.value.trim();
|
||||
state.page = 1;
|
||||
load();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
return { load, state, esc, fmtSize, ago };
|
||||
}
|
||||
|
||||
return { init, esc, fmtSize, ago };
|
||||
})();
|
||||
+19
-152
@@ -27,159 +27,26 @@
|
||||
<div class="pg" id="pg"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/table.js"></script>
|
||||
<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 = '';
|
||||
let sortKey = null, sortDir = 1;
|
||||
|
||||
const sortVal = (it, k) => {
|
||||
let v = it[k];
|
||||
if (k === 'title' || k === 'custom_slug') v = (v == null || v === '') ? null : String(v).toLowerCase();
|
||||
if (k === 'language') v = (v == null || v === '') ? 'text' : String(v).toLowerCase();
|
||||
if (k === 'size' || k === 'view_count' || k === 'created_at') return v == null ? -1 : v;
|
||||
return v == null ? null : v;
|
||||
};
|
||||
function sortItems(items) {
|
||||
if (!sortKey) return items;
|
||||
const k = sortKey;
|
||||
return items.slice().sort((a, b) => {
|
||||
let va = sortVal(a, k), vb = sortVal(b, k);
|
||||
const na = va == null, nb = vb == null;
|
||||
if (na && nb) return 0;
|
||||
if (na) return 1; // nulls always last
|
||||
if (nb) return -1;
|
||||
if (va < vb) return -1 * sortDir;
|
||||
if (va > vb) return 1 * sortDir;
|
||||
return (a.created_at || 0) < (b.created_at || 0) ? 1 : -1; // stable tiebreak: newest first
|
||||
});
|
||||
}
|
||||
function renderSortIndicators() {
|
||||
document.querySelectorAll('th.sortable').forEach(th => {
|
||||
th.classList.toggle('sorted', th.dataset.sort === sortKey);
|
||||
th.classList.toggle('asc', th.dataset.sort === sortKey && sortDir === 1);
|
||||
th.classList.toggle('desc', th.dataset.sort === sortKey && sortDir === -1);
|
||||
});
|
||||
}
|
||||
document.querySelector('thead').addEventListener('click', e => {
|
||||
const th = e.target.closest('th.sortable');
|
||||
if (!th) return;
|
||||
const k = th.dataset.sort;
|
||||
if (sortKey === k) { sortDir = -sortDir; } else { sortKey = k; sortDir = 1; }
|
||||
renderSortIndicators();
|
||||
load();
|
||||
const t = PaletteTable.init({
|
||||
endpoint: '/api/public',
|
||||
perPage: 25,
|
||||
hasPager: true,
|
||||
rowHtml: it =>
|
||||
`<tr class="row" data-href="/${t.esc(it.id)}"><td>` +
|
||||
(it.title
|
||||
? `${t.esc(it.title)}`
|
||||
: `<a class="slug" href="/${t.esc(it.id)}">${t.esc(it.id)}</a>`) +
|
||||
`</td>` +
|
||||
`<td><span class="badge">${t.esc(it.language || 'text')}</span></td>` +
|
||||
`<td class="dim">${t.fmtSize(it.size)}</td><td class="dim">${it.view_count}</td><td class="dim" data-ts="${it.created_at}">${t.ago(it.created_at)}</td>` +
|
||||
(it.custom_slug ? `<td><a class="slug url-link" href="/${t.esc(it.custom_slug)}">/${t.esc(it.custom_slug)}</a></td>` : `<td class="dim">none</td>`) +
|
||||
`<td class="dim"><a class="id-link" href="/${t.esc(it.id)}">${t.esc(it.id)}</a></td></tr>`,
|
||||
emptyFiltered: 'No pastes match your search.',
|
||||
emptyAll: 'No pastes yet. Create the first one.',
|
||||
});
|
||||
|
||||
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 || sortKey)
|
||||
? '/api/public?limit=500&offset=0'
|
||||
: '/api/public?limit=' + PER + '&offset=' + off;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
total = data.total;
|
||||
|
||||
let items = filtered ? data.items.filter(matchesFilter) : data.items;
|
||||
items = sortItems(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>` +
|
||||
(it.title
|
||||
? `${esc(it.title)}`
|
||||
: `<a class="slug" href="/${esc(it.id)}">${esc(it.id)}</a>`) +
|
||||
`</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" data-ts="${it.created_at}">${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 || sortKey) {
|
||||
$('showing').textContent = sortKey
|
||||
? 'Sorted by ' + sortKey + ' (' + (sortDir === 1 ? 'ascending' : 'descending') + ') · ' + items.length.toLocaleString() + ' of ' + total.toLocaleString()
|
||||
: '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('');
|
||||
}
|
||||
renderSortIndicators();
|
||||
} 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
|
||||
t.load();
|
||||
setInterval(t.load, 30000); // auto-refresh history every 30s
|
||||
</script>
|
||||
{{template "foot" .}}
|
||||
|
||||
+40
-56
@@ -5,34 +5,25 @@
|
||||
<h1>Saved 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:140px"><col style="width:190px"><col style="width:100px"></colgroup>
|
||||
<colgroup><col style="width:260px"><col style="width:140px"><col style="width:120px"><col style="width:150px"><col style="width:190px"><col style="width:100px"></colgroup>
|
||||
<thead><tr>
|
||||
<th>Paste</th>
|
||||
<th>Language</th>
|
||||
<th>Size</th>
|
||||
<th>Created</th>
|
||||
<th>URL</th>
|
||||
<th>ID</th>
|
||||
<th data-sort="title" class="sortable"><span class="sort-ind"></span>Paste</th>
|
||||
<th data-sort="language" class="sortable"><span class="sort-ind"></span>Language</th>
|
||||
<th data-sort="size" class="sortable"><span class="sort-ind"></span>Size</th>
|
||||
<th data-sort="created_at" class="sortable"><span class="sort-ind"></span>Created</th>
|
||||
<th data-sort="custom_slug" class="sortable"><span class="sort-ind"></span>URL</th>
|
||||
<th data-sort="id" class="sortable"><span class="sort-ind"></span>ID</th>
|
||||
</tr></thead>
|
||||
<tbody id="rows"></tbody>
|
||||
</table>
|
||||
<div class="empty" id="empty" style="display:none">No pastes from this browser yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/table.js"></script>
|
||||
<script>
|
||||
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';
|
||||
}
|
||||
function toast(msg, kind) {
|
||||
let t = document.querySelector('.toast');
|
||||
if (!t) { t = document.createElement('div'); t.className = 'toast'; document.body.appendChild(t); }
|
||||
@@ -45,45 +36,38 @@ function toast(msg, kind) {
|
||||
t._h = setTimeout(() => t.classList.remove('show'), 2000);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const res = await fetch('/api/mine');
|
||||
const data = await res.json();
|
||||
$('count').textContent = data.total.toLocaleString() + ' total';
|
||||
const rows = $('rows');
|
||||
if (!data.items.length) {
|
||||
rows.innerHTML = '';
|
||||
$('empty').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
$('empty').style.display = 'none';
|
||||
rows.innerHTML = data.items.map(it =>
|
||||
`<tr class="row" data-href="/${esc(it.id)}"><td>` +
|
||||
(it.title ? `${esc(it.title)}` : `<a class="slug" href="/${esc(it.id)}">${esc(it.id)}</a>`) + `</td>` +
|
||||
`<td><span class="badge">${esc(it.language || 'text')}</span></td>` +
|
||||
`<td class="dim">${fmtSize(it.size)}</td><td class="dim" data-ts="${it.created_at}">${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>` +
|
||||
`<td><button class="btn btn-icon del" data-id="${esc(it.id)}" title="Delete paste" aria-label="Delete paste">×</button></td></tr>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
$('rows').addEventListener('click', async e => {
|
||||
const del = e.target.closest('button.del');
|
||||
if (del) {
|
||||
e.stopPropagation();
|
||||
del.disabled = true;
|
||||
try {
|
||||
const res = await fetch('/api/pastes/' + del.dataset.id, { method: 'DELETE' });
|
||||
if (res.ok) { toast('Deleted', 'success'); load(); }
|
||||
else { toast('Delete failed', 'error'); del.disabled = false; }
|
||||
} catch (err) { toast('Delete failed', 'error'); del.disabled = false; }
|
||||
return;
|
||||
}
|
||||
const tr = e.target.closest('tr.row[data-href]');
|
||||
if (!tr || e.target.closest('a')) return;
|
||||
window.location.href = tr.dataset.href;
|
||||
const t = PaletteTable.init({
|
||||
endpoint: '/api/mine',
|
||||
perPage: 50,
|
||||
hasPager: false,
|
||||
rowHtml: it =>
|
||||
`<tr class="row" data-href="/${t.esc(it.id)}"><td>` +
|
||||
(it.title
|
||||
? `${t.esc(it.title)}`
|
||||
: `<a class="slug" href="/${t.esc(it.id)}">${t.esc(it.id)}</a>`) +
|
||||
`</td>` +
|
||||
`<td><span class="badge">${t.esc(it.language || 'text')}</span></td>` +
|
||||
`<td class="dim">${t.fmtSize(it.size)}</td><td class="dim" data-ts="${it.created_at}">${t.ago(it.created_at)}</td>` +
|
||||
(it.custom_slug ? `<td><a class="slug url-link" href="/${t.esc(it.custom_slug)}">/${t.esc(it.custom_slug)}</a></td>` : `<td class="dim">none</td>`) +
|
||||
`<td class="dim"><a class="id-link" href="/${t.esc(it.id)}">${t.esc(it.id)}</a></td>` +
|
||||
`<td><button class="btn btn-icon del" data-id="${t.esc(it.id)}" title="Delete paste" aria-label="Delete paste">×</button></td></tr>`,
|
||||
emptyFiltered: 'No pastes from this browser match your search.',
|
||||
emptyAll: 'No pastes from this browser yet.',
|
||||
});
|
||||
|
||||
load();
|
||||
// delete buttons (viewer-scoped, enforced server-side #37)
|
||||
document.getElementById('rows').addEventListener('click', async e => {
|
||||
const del = e.target.closest('button.del');
|
||||
if (!del) return;
|
||||
e.stopPropagation();
|
||||
del.disabled = true;
|
||||
try {
|
||||
const res = await fetch('/api/pastes/' + del.dataset.id, { method: 'DELETE' });
|
||||
if (res.ok) { toast('Deleted', 'success'); t.load(); }
|
||||
else { toast('Delete failed', 'error'); del.disabled = false; }
|
||||
} catch (err) { toast('Delete failed', 'error'); del.disabled = false; }
|
||||
});
|
||||
|
||||
t.load();
|
||||
</script>
|
||||
{{template "foot" .}}
|
||||
|
||||
Reference in New Issue
Block a user