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:
+184
-1
@@ -1 +1,184 @@
|
||||
/* palette static assets placeholder */
|
||||
/* Palette UI tokens (mirrors sketches/themes/tokens.css, midnight approved preset) */
|
||||
:root {
|
||||
--bg: #241B30; --surface: #2D2340; --surface-2: #3A2D52;
|
||||
--muted: #7A6A9E; --muted-fg: #C0B2DE; --fg: #F2EDF8;
|
||||
--accent: #C4A8F0; --border: #42355C;
|
||||
--radius: 14px;
|
||||
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-mono: ui-monospace, "JetBrains Mono", "Fira Code", monospace;
|
||||
}
|
||||
[data-preset="smooth"] {
|
||||
--bg: #F6F5FA; --surface: #FFFFFF; --surface-2: #DAD7E6;
|
||||
--muted: #B5B1C9; --muted-fg: #7A7796; --fg: #2A2A36;
|
||||
--accent: #7A7796; --border: #DAD7E6;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
line-height: 1.45;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100vh;
|
||||
font-size: 14px;
|
||||
}
|
||||
.topbar {
|
||||
display: flex; align-items: center; gap: 20px;
|
||||
padding: 0 20px; height: 52px;
|
||||
background: var(--surface); border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.logo { font-weight: 700; font-size: 16px; letter-spacing: -0.02em; text-decoration: none; color: var(--fg); }
|
||||
.logo em { font-style: normal; color: var(--muted-fg); font-weight: 400; }
|
||||
.topbar nav { display: flex; gap: 4px; }
|
||||
.topbar nav a { color: var(--muted-fg); text-decoration: none; padding: 6px 12px; border-radius: 8px; font-size: 13.5px; }
|
||||
.topbar nav a:hover { background: var(--surface-2); color: var(--fg); }
|
||||
.topbar nav a.on { background: var(--accent); color: var(--bg); }
|
||||
.topbar .spacer { flex: 1; }
|
||||
.kbd { font-family: var(--font-mono); font-size: 11px; border: 1px solid var(--border); border-radius: 5px; padding: 2px 6px; color: var(--muted-fg); }
|
||||
|
||||
.float {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
box-shadow: 0 2px 6px rgba(20,14,32,.25), 0 12px 32px rgba(20,14,32,.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* new paste page */
|
||||
.deck {
|
||||
display: grid; grid-template-columns: 1fr 300px; gap: 16px;
|
||||
padding: 16px 20px; height: calc(100vh - 52px);
|
||||
max-width: 1400px; margin: 0 auto;
|
||||
}
|
||||
.pane-r { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; padding-bottom: 4px; }
|
||||
.side-section { padding: 14px 16px; flex-shrink: 0; }
|
||||
.side-section h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted-fg); margin-bottom: 10px; }
|
||||
.pane-l { display: flex; flex-direction: column; }
|
||||
.editor-head {
|
||||
display: flex; align-items: center; gap: 12px; padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.editor-head input {
|
||||
border: none; outline: none; background: transparent; color: var(--fg); font: inherit; font-size: 13.5px; flex: 1;
|
||||
}
|
||||
.editor-head select {
|
||||
border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg);
|
||||
border-radius: 7px; padding: 4px 10px; font: inherit; font-size: 12.5px; cursor: pointer;
|
||||
}
|
||||
.editor-wrap { flex: 1; display: flex; min-height: 0; }
|
||||
.gutter {
|
||||
padding: 14px 10px; text-align: right; color: var(--muted); font-family: var(--font-mono);
|
||||
font-size: 12.5px; line-height: 1.7; user-select: none; white-space: pre; overflow: hidden;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
.editor {
|
||||
flex: 1; padding: 14px 16px; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.7;
|
||||
white-space: pre; outline: none; overflow: auto; border: none; background: transparent; color: var(--fg);
|
||||
resize: none; width: 100%;
|
||||
}
|
||||
.editor::placeholder { color: var(--muted); }
|
||||
.actionbar {
|
||||
display: flex; align-items: center; gap: 14px; padding: 12px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.btn {
|
||||
background: var(--accent); color: var(--bg); border: none; cursor: pointer;
|
||||
padding: 8px 18px; border-radius: 8px; font: inherit; font-size: 13px; font-weight: 600;
|
||||
}
|
||||
.btn:hover { filter: brightness(1.08); }
|
||||
.hint { font-size: 12px; color: var(--muted-fg); }
|
||||
.hint b { color: var(--fg); font-weight: 550; }
|
||||
.seg { display: flex; flex-direction: column; gap: 2px; }
|
||||
.seg label { display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 7px; cursor: pointer; font-size: 13px; }
|
||||
.seg label:hover { background: var(--surface-2); }
|
||||
.seg input { accent-color: var(--accent); }
|
||||
.toggle { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; padding: 5px 8px; border-radius: 7px; }
|
||||
.toggle:hover { background: var(--surface-2); }
|
||||
.toggle input { accent-color: var(--accent); }
|
||||
.row { display: flex; justify-content: space-between; align-items: center; font-size: 13px; padding: 4px 0; }
|
||||
.row input[type="text"] {
|
||||
border: 1px solid var(--border); border-radius: 7px; padding: 5px 8px; background: var(--bg);
|
||||
color: var(--fg); font: inherit; font-size: 12.5px; width: 130px;
|
||||
}
|
||||
.created-banner {
|
||||
display: none; padding: 10px 16px; font-size: 13px; background: var(--surface-2);
|
||||
border-bottom: 1px solid var(--border); word-break: break-all;
|
||||
}
|
||||
.created-banner a { color: var(--accent); }
|
||||
|
||||
/* paste view */
|
||||
.meta-bar { display: flex; align-items: center; gap: 12px; padding: 12px 18px; flex-wrap: wrap; }
|
||||
.meta-bar h1 { font-size: 17px; font-weight: 600; }
|
||||
.slug { font-family: var(--font-mono); font-size: 12.5px; color: var(--muted-fg); background: var(--surface-2); padding: 3px 9px; border-radius: 7px; }
|
||||
.tag { font-size: 11.5px; color: var(--muted-fg); border: 1px solid var(--border); border-radius: 999px; padding: 2px 9px; }
|
||||
.meta-bar .spacer { flex: 1; }
|
||||
.iconbtn { border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg); border-radius: 8px; padding: 5px 12px; font: inherit; font-size: 12.5px; cursor: pointer; text-decoration: none; }
|
||||
.iconbtn:hover { color: var(--fg); border-color: var(--muted); }
|
||||
.iconbtn.danger:hover { color: #ff8fa3; border-color: #ff8fa3; }
|
||||
.code-head {
|
||||
display: flex; align-items: center; gap: 10px; padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--border); font-size: 12.5px; color: var(--muted-fg);
|
||||
}
|
||||
.code-head .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
|
||||
.code {
|
||||
font-family: var(--font-mono); font-size: 13px; line-height: 1.7;
|
||||
padding: 14px 0; display: flex; overflow-x: auto;
|
||||
}
|
||||
.code .gutter { flex-shrink: 0; }
|
||||
.codebody { padding: 0 18px; white-space: pre; }
|
||||
.footnote { display: flex; gap: 20px; padding: 10px 18px; font-size: 12px; color: var(--muted-fg); border-top: 1px solid var(--border); flex-wrap: wrap; }
|
||||
|
||||
/* history */
|
||||
.page { max-width: 860px; margin: 0 auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
|
||||
.head-row { display: flex; align-items: baseline; gap: 14px; }
|
||||
.head-row h1 { font-size: 20px; font-weight: 600; }
|
||||
.search {
|
||||
display: flex; align-items: center; gap: 8px; background: var(--surface);
|
||||
border: 1px solid var(--border); border-radius: 10px; padding: 8px 14px; width: 260px;
|
||||
}
|
||||
.search input { border: none; outline: none; background: transparent; color: var(--fg); font: inherit; font-size: 13px; width: 100%; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
|
||||
th:nth-child(1), td:nth-child(1) { width: 130px; }
|
||||
th:nth-child(2), td:nth-child(2) { width: auto; }
|
||||
th:nth-child(3), td:nth-child(3) { width: 80px; }
|
||||
th:nth-child(4), td:nth-child(4) { width: 80px; }
|
||||
th:nth-child(5), td:nth-child(5) { width: 64px; }
|
||||
th:nth-child(6), td:nth-child(6) { width: 90px; }
|
||||
th {
|
||||
text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
|
||||
color: var(--muted-fg); padding: 10px 16px; border-bottom: 1px solid var(--border); font-weight: 600;
|
||||
}
|
||||
td { padding: 10px 16px; border-bottom: 1px solid var(--border); }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr.row:hover td { background: var(--surface-2); }
|
||||
td a.slug { font-family: var(--font-mono); font-size: 12.5px; color: var(--fg); text-decoration: none; }
|
||||
td a.slug:hover { color: var(--accent); }
|
||||
.badge { font-size: 11px; border: 1px solid var(--border); color: var(--muted-fg); border-radius: 999px; padding: 1px 8px; }
|
||||
.badge.lock { color: var(--accent); border-color: var(--accent); }
|
||||
.dim { color: var(--muted-fg); white-space: nowrap; }
|
||||
.pager { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; font-size: 12.5px; color: var(--muted-fg); }
|
||||
.pager .pg { display: flex; gap: 6px; }
|
||||
.pager button { border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg); border-radius: 7px; padding: 4px 11px; font: inherit; font-size: 12.5px; cursor: pointer; }
|
||||
.pager button:hover:not(:disabled) { color: var(--fg); border-color: var(--muted); }
|
||||
.pager button.on { background: var(--accent); color: var(--bg); border-color: var(--accent); }
|
||||
.pager button:disabled { opacity: .4; cursor: default; }
|
||||
.empty { text-align: center; padding: 40px 16px; color: var(--muted-fg); font-size: 13px; }
|
||||
|
||||
/* unlock */
|
||||
.center { display: flex; align-items: center; justify-content: center; padding: 20px; height: calc(100vh - 52px); }
|
||||
.center .float { width: 400px; max-width: 100%; }
|
||||
.inner { padding: 28px; text-align: center; }
|
||||
.lockring {
|
||||
width: 56px; height: 56px; margin: 0 auto 16px; border-radius: 50%;
|
||||
background: var(--surface-2); display: flex; align-items: center; justify-content: center; font-size: 24px;
|
||||
}
|
||||
.inner h1 { font-size: 17px; font-weight: 600; margin-bottom: 6px; }
|
||||
.inner .sub { font-size: 13px; color: var(--muted-fg); margin-bottom: 20px; }
|
||||
.pwinput {
|
||||
width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: 9px;
|
||||
background: var(--bg); color: var(--fg); font: inherit; font-size: 13.5px; outline: none; text-align: center;
|
||||
letter-spacing: .12em;
|
||||
}
|
||||
.pwinput:focus { border-color: var(--accent); }
|
||||
.center .btn { width: 100%; margin-top: 12px; }
|
||||
.err { display: none; margin-top: 12px; font-size: 12.5px; color: #ff8fa3; }
|
||||
.center .foot { font-size: 12px; color: var(--muted-fg); padding: 14px; border-top: 1px solid var(--border); }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{{define "foot"}}{{end}}
|
||||
@@ -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" .}}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{define "head"}}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
{{end}}
|
||||
|
||||
{{define "topbar"}}
|
||||
<div class="topbar">
|
||||
<a class="logo" href="/history">Palette <em>/ beta</em></a>
|
||||
<nav>
|
||||
<a href="/new" {{if eq .Page "new"}}class="on"{{end}}>new</a>
|
||||
<a href="/history" {{if eq .Page "history"}}class="on"{{end}}>history</a>
|
||||
<a href="https://git.archfox.org/poslop/palette">git</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,104 @@
|
||||
{{template "head" .}}
|
||||
{{template "topbar" .}}
|
||||
<div class="deck">
|
||||
<div class="float pane-l">
|
||||
<div class="editor-head">
|
||||
<input id="title" placeholder="title (optional)">
|
||||
<select id="language">
|
||||
<option value="">auto</option>
|
||||
<option>go</option><option>python</option><option>javascript</option>
|
||||
<option>rust</option><option>c</option><option>cpp</option><option>java</option>
|
||||
<option>bash</option><option>sql</option><option>yaml</option><option>json</option>
|
||||
<option>markdown</option><option>text</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="editor-wrap">
|
||||
<div class="gutter" id="gutter">1</div>
|
||||
<textarea class="editor" id="content" placeholder="paste your code, text, or notes here…" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="created-banner" id="created"></div>
|
||||
<div class="actionbar">
|
||||
<span class="hint">Ctrl+Enter to create</span>
|
||||
<div class="spacer" style="flex:1"></div>
|
||||
<button class="btn" id="create">Create ⇧</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane-r">
|
||||
<div class="float side-section">
|
||||
<h3>Expiry</h3>
|
||||
<div class="seg">
|
||||
<label><input type="radio" name="exp" value=""> Never</label>
|
||||
<label><input type="radio" name="exp" value="1h"> 1 hour</label>
|
||||
<label><input type="radio" name="exp" value="24h"> 1 day</label>
|
||||
<label><input type="radio" name="exp" value="168h" checked> 1 week</label>
|
||||
<label><input type="radio" name="exp" value="720h"> 30 days</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="float side-section">
|
||||
<h3>Protection</h3>
|
||||
<label class="toggle"><input type="checkbox" id="haspw"> Password lock</label>
|
||||
<input type="password" id="password" class="pwinput" placeholder="password" style="display:none; margin: 6px 8px 0; width: auto;">
|
||||
<label class="toggle"><input type="checkbox" id="burn"> Burn after read</label>
|
||||
<label class="toggle"><input type="checkbox" id="unlisted"> Unlisted</label>
|
||||
</div>
|
||||
<div class="float side-section">
|
||||
<h3>Custom URL</h3>
|
||||
<div class="row"><span>/</span><input type="text" id="custom" placeholder="my-snippet"></div>
|
||||
</div>
|
||||
<div class="float side-section">
|
||||
<h3>Result</h3>
|
||||
<div class="hint" id="result" style="word-break:break-all">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const $ = id => document.getElementById(id);
|
||||
const content = $('content'), gutter = $('gutter');
|
||||
|
||||
function updateGutter() {
|
||||
const lines = content.value.split('\n').length;
|
||||
let s = '';
|
||||
for (let i = 1; i <= Math.max(lines, 1); i++) s += i + '\n';
|
||||
gutter.textContent = s;
|
||||
}
|
||||
content.addEventListener('input', updateGutter);
|
||||
updateGutter();
|
||||
|
||||
$('haspw').addEventListener('change', e => { $('password').style.display = e.target.checked ? 'block' : 'none'; });
|
||||
|
||||
async function create() {
|
||||
const body = {
|
||||
content: content.value,
|
||||
title: $('title').value || null,
|
||||
language: $('language').value || null,
|
||||
custom_slug: $('custom').value || null,
|
||||
burn_after_read: $('burn').checked,
|
||||
};
|
||||
if ($('haspw').checked) body.password = $('password').value;
|
||||
const exp = document.querySelector('input[name="exp"]:checked').value;
|
||||
if (exp) body.expires_in = exp;
|
||||
|
||||
const res = await fetch('/api/pastes', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
$('result').textContent = 'Error: ' + (data.error || res.status);
|
||||
return;
|
||||
}
|
||||
const url = location.origin + '/' + (data.custom_slug || data.id);
|
||||
$('result').innerHTML = '<a href="' + url + '">' + url + '</a>';
|
||||
$('result').dataset.token = data.deletion_token || '';
|
||||
try { navigator.clipboard.writeText(url); } catch(e) {}
|
||||
// show the paste
|
||||
location.href = '/' + data.id + '?created=1&token=' + encodeURIComponent(data.deletion_token || '');
|
||||
}
|
||||
$('create').addEventListener('click', create);
|
||||
document.addEventListener('keydown', e => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); create(); }
|
||||
});
|
||||
</script>
|
||||
{{template "foot" .}}
|
||||
@@ -0,0 +1,45 @@
|
||||
{{template "head" .}}
|
||||
{{template "topbar" .}}
|
||||
<div class="page">
|
||||
<div class="float">
|
||||
<div class="meta-bar">
|
||||
<h1>{{if .Title}}{{.Title}}{{else}}Untitled paste{{end}}</h1>
|
||||
<span class="slug">/{{.ID}}</span>
|
||||
{{if .Language}}<span class="tag">{{.Language}}</span>{{end}}
|
||||
{{if .ExpiresAt}}<span class="tag">expires in {{.ExpiresIn}}</span>{{end}}
|
||||
<div class="spacer"></div>
|
||||
<a class="iconbtn" href="/raw/{{.ID}}">raw</a>
|
||||
<a class="iconbtn" href="#" onclick="copyContent(); return false;">copy</a>
|
||||
{{if .DeletionToken}}<a class="iconbtn danger" href="#" onclick="redeem('{{.DeletionToken}}'); return false;">delete</a>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if .JustCreated}}
|
||||
<div class="float">
|
||||
<div class="created-banner" style="display:block">
|
||||
Paste created. Link copied to clipboard: <a href="/{{.ID}}">{{.Host}}/{{.ID}}</a>
|
||||
{{if .DeletionToken}} · deletion token: <code>{{.DeletionToken}}</code>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="float">
|
||||
<div class="code-head"><span class="dot"></span> {{.LineCount}} lines · {{.SizeBytes}} bytes</div>
|
||||
<div class="code"><div class="gutter">{{.Gutter}}</div><div class="codebody" id="codebody">{{.ContentHTML}}</div></div>
|
||||
<div class="footnote">
|
||||
<span>Created {{.CreatedAgo}}</span>
|
||||
<span>{{.ViewCount}} views</span>
|
||||
<span>{{.Visibility}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="raw-content" value="{{.ContentAttr}}">
|
||||
<script>
|
||||
function copyContent() {
|
||||
navigator.clipboard.writeText(document.getElementById('raw-content').value);
|
||||
}
|
||||
function redeem(token) {
|
||||
if (!confirm('Hard delete this paste immediately?')) return;
|
||||
fetch('/api/pastes/{{.ID}}/redeem?token=' + encodeURIComponent(token), {method: 'DELETE'})
|
||||
.then(r => { if (r.ok) location.href = '/history'; else alert('delete failed'); });
|
||||
}
|
||||
</script>
|
||||
{{template "foot" .}}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{template "head" .}}
|
||||
{{template "topbar" .}}
|
||||
<div class="center">
|
||||
<div class="float">
|
||||
<div class="inner">
|
||||
<div class="lockring">🔒</div>
|
||||
<h1>This paste is locked</h1>
|
||||
<p class="sub">Enter the password to view <span class="slug">/{{.ID}}</span></p>
|
||||
<form method="post" action="/unlock/{{.ID}}">
|
||||
<input type="password" name="password" class="pwinput" placeholder="••••••••" autofocus>
|
||||
{{if .Wrong}}<p class="err" style="display:block">Wrong password. Try again.</p>{{end}}
|
||||
<button class="btn" type="submit">Unlock</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="foot">Created {{.CreatedAgo}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "foot" .}}
|
||||
Reference in New Issue
Block a user