- vwr cookie middleware: random browser id set on first visit (reused by #49) - pastes table gains viewer_id column, set server-side at creation from the cookie - GET /api/mine lists pastes for the requesting browser (title/lang/size/created) - DELETE enforcement: 403 when client-sent vwr doesn't match the paste's viewer_id - /mine page reuses history table styling, delete buttons, empty state - nav: 'Saved' item between Public and Git; Git gets external-link arrow (#56) - tests: create-with-cookie appears in /mine, other cookie doesn't, delete enforcement Closes #37
90 lines
3.5 KiB
HTML
90 lines
3.5 KiB
HTML
{{template "head" .}}
|
|
{{template "topbar" .}}
|
|
<div class="page">
|
|
<div class="head-row">
|
|
<h1>Saved pastes</h1>
|
|
<span class="count" id="count"></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>
|
|
<thead><tr>
|
|
<th>Paste</th>
|
|
<th>Language</th>
|
|
<th>Size</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 from this browser yet.</div>
|
|
</div>
|
|
</div>
|
|
<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); }
|
|
t.textContent = msg;
|
|
t.classList.remove('success', 'error');
|
|
if (kind === 'success') t.classList.add('success');
|
|
if (kind === 'error') t.classList.add('error');
|
|
t.classList.add('show');
|
|
clearTimeout(t._h);
|
|
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><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" 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;
|
|
});
|
|
|
|
load();
|
|
</script>
|
|
{{template "foot" .}}
|