Update peer.paste

This commit is contained in:
Raven Scott
2026-05-27 22:34:35 -04:00
parent e802bbcf64
commit 8b6789805c
3 changed files with 309 additions and 91 deletions
+54 -32
View File
@@ -7,40 +7,62 @@
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<main class="wrap">
<h1>peer.paste</h1>
<p class="subtitle">Temporary P2P snippets with expiration and burn-after-read options.</p>
<section class="card">
<h2>Create Paste</h2>
<form id="create-form">
<label>Title (optional)
<input type="text" id="title" maxlength="120" />
</label>
<label>Language (optional)
<input type="text" id="language" maxlength="32" placeholder="plain / js / json / md" />
</label>
<label>Expires in hours
<input type="number" id="expiresHours" min="1" max="168" value="24" />
</label>
<label>Max reads (0 = unlimited until expiry)
<input type="number" id="maxReads" min="0" max="10000" value="0" />
</label>
<label class="row">
<input type="checkbox" id="destroyOnRead" />
Burn after first read
</label>
<label>Content
<textarea id="content" rows="14" required></textarea>
</label>
<button type="submit">Create Paste</button>
</form>
<div id="create-result" class="result"></div>
<main class="page">
<section class="hero">
<h1>peer.paste</h1>
<p>Share temporary text with one clean link.</p>
</section>
<section class="card">
<h2>Active Pastes</h2>
<button id="refresh-btn" type="button">Refresh</button>
<section class="panel">
<form id="create-form" class="create-form">
<label for="content" class="label">Secret / Paste</label>
<textarea id="content" rows="14" required placeholder="Paste text here..."></textarea>
<details class="advanced">
<summary>Advanced options</summary>
<div class="advanced-grid">
<label>Title (optional)
<input type="text" id="title" maxlength="120" />
</label>
<label>Language (optional)
<input type="text" id="language" maxlength="32" placeholder="plain / js / json / md" />
</label>
<label>Expires in hours
<input type="number" id="expiresHours" min="1" max="168" value="24" />
</label>
<label>Max reads (0 = unlimited)
<input type="number" id="maxReads" min="0" max="1000000" value="0" />
</label>
<label class="check-row">
<input type="checkbox" id="destroyOnRead" />
Burn after first read
</label>
</div>
</details>
<div class="actions">
<button type="submit" id="create-btn">Create secure link</button>
<button type="button" id="clear-btn" class="ghost">Clear</button>
</div>
</form>
<div id="create-result" class="result hidden">
<h2>Share this link</h2>
<p class="hint">Anyone with this link can read the paste.</p>
<div class="link-row">
<input id="share-link" type="text" readonly />
<button id="copy-link-btn" type="button" class="ghost">Copy</button>
<a id="open-link-btn" class="ghost link-btn" href="#" target="_blank" rel="noopener">Open</a>
</div>
<div class="meta-row" id="result-meta"></div>
</div>
</section>
<section class="panel compact">
<div class="list-head">
<h2>Recent active pastes</h2>
<button id="refresh-btn" type="button" class="ghost">Refresh</button>
</div>
<div id="list"></div>
</section>
</main>
+72 -12
View File
@@ -16,27 +16,40 @@ function msToHuman(ms) {
return `${h}h ${m}m`;
}
function setCreateState(message, isError = false) {
const result = document.getElementById('create-result');
const meta = document.getElementById('result-meta');
if (message) {
result.classList.remove('hidden');
meta.textContent = message;
meta.style.color = isError ? '#fca5a5' : '#9fb0cb';
} else {
result.classList.add('hidden');
meta.textContent = '';
}
}
async function refreshList() {
const list = document.getElementById('list');
list.textContent = 'Loading...';
try {
const data = await api('/api/pastes');
const data = await api('/api/pastes/mine');
if (!data.pastes || data.pastes.length === 0) {
list.textContent = 'No active pastes.';
list.textContent = 'No active pastes yet.';
return;
}
list.innerHTML = data.pastes.map((p) => `
<div class="paste-item">
<div><strong>${escapeHtml(p.title || '(untitled)')}</strong></div>
<div class="meta">
<div class="paste-title">${escapeHtml(p.title || '(untitled)')}</div>
<div class="paste-meta">
id: ${p.id} |
reads: ${p.readCount}${p.maxReads ? `/${p.maxReads}` : ''} |
expires: ${msToHuman(p.expiresInMs)}
</div>
<div>
<div class="paste-actions">
<a href="/p/${p.id}" target="_blank" rel="noopener">Open</a>
&nbsp;|&nbsp;
<a href="/api/pastes/${p.id}/raw" target="_blank" rel="noopener">Raw JSON</a>
<a href="/api/pastes/${p.id}/raw" target="_blank" rel="noopener">Raw</a>
<a href="#" data-copy-url="/p/${p.id}">Copy link</a>
</div>
</div>
`).join('');
@@ -47,8 +60,14 @@ async function refreshList() {
document.getElementById('create-form').addEventListener('submit', async (e) => {
e.preventDefault();
const result = document.getElementById('create-result');
result.textContent = 'Creating...';
const createBtn = document.getElementById('create-btn');
const shareLink = document.getElementById('share-link');
const openBtn = document.getElementById('open-link-btn');
const content = document.getElementById('content');
createBtn.disabled = true;
createBtn.textContent = 'Creating...';
setCreateState('Creating secure link...');
try {
const payload = {
title: document.getElementById('title').value,
@@ -63,11 +82,52 @@ document.getElementById('create-form').addEventListener('submit', async (e) => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
result.innerHTML = `Created: <a href="${data.viewUrl}" target="_blank" rel="noopener">${data.viewUrl}</a>`;
document.getElementById('content').value = '';
shareLink.value = `${window.location.origin}${data.viewUrl}`;
openBtn.href = data.viewUrl;
setCreateState(`Created. Expires in about ${msToHuman(data.paste.expiresInMs)}.`);
content.value = '';
content.focus();
await refreshList();
} catch (err) {
result.textContent = err.message;
setCreateState(err.message, true);
} finally {
createBtn.disabled = false;
createBtn.textContent = 'Create secure link';
}
});
document.getElementById('copy-link-btn').addEventListener('click', async () => {
const shareLink = document.getElementById('share-link');
if (!shareLink.value) return;
try {
await navigator.clipboard.writeText(shareLink.value);
setCreateState('Link copied to clipboard.');
} catch (err) {
setCreateState('Copy failed. You can copy the link manually.', true);
}
});
document.getElementById('clear-btn').addEventListener('click', () => {
document.getElementById('title').value = '';
document.getElementById('language').value = '';
document.getElementById('expiresHours').value = '24';
document.getElementById('maxReads').value = '0';
document.getElementById('destroyOnRead').checked = false;
document.getElementById('content').value = '';
document.getElementById('share-link').value = '';
setCreateState('');
});
document.getElementById('list').addEventListener('click', async (e) => {
const link = e.target.closest('[data-copy-url]');
if (!link) return;
e.preventDefault();
try {
const full = `${window.location.origin}${link.getAttribute('data-copy-url')}`;
await navigator.clipboard.writeText(full);
setCreateState('Recent paste link copied.');
} catch (err) {
setCreateState('Could not copy recent paste link.', true);
}
});
+183 -47
View File
@@ -1,86 +1,222 @@
:root {
color-scheme: dark;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background: #0f172a;
color: #e2e8f0;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
background: radial-gradient(circle at top, #1e293b 0%, #0b1220 45%, #070d18 100%);
color: #e6edf8;
}
.wrap {
max-width: 1000px;
margin: 1.5rem auto;
padding: 0 1rem;
.page {
max-width: 920px;
margin: 2.25rem auto;
padding: 0 1rem 2.5rem;
}
h1, h2 {
margin: 0 0 0.75rem;
.hero h1 {
margin: 0;
font-size: 2rem;
letter-spacing: -0.02em;
}
.subtitle {
color: #94a3b8;
margin: 0 0 1.25rem;
.hero p {
margin: 0.35rem 0 1.25rem;
color: #9fb0cb;
}
.card {
background: #111827;
border: 1px solid #334155;
border-radius: 12px;
.panel {
background: rgba(12, 18, 32, 0.84);
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 16px;
padding: 1rem;
margin-bottom: 1rem;
backdrop-filter: blur(6px);
}
label {
.compact {
padding: 0.9rem;
}
.label {
display: block;
margin-bottom: 0.75rem;
font-size: 0.95rem;
font-size: 0.92rem;
color: #9fb0cb;
margin-bottom: 0.4rem;
}
textarea,
input[type="text"],
input[type="number"],
textarea {
input[type="number"] {
width: 100%;
margin-top: 0.35rem;
padding: 0.6rem;
border-radius: 8px;
border: 1px solid #475569;
background: #020617;
color: #e2e8f0;
border-radius: 10px;
border: 1px solid #334155;
background: #070d18;
color: #edf3ff;
padding: 0.68rem 0.72rem;
font: inherit;
}
.row {
display: flex;
gap: 0.5rem;
align-items: center;
textarea {
resize: vertical;
min-height: 240px;
}
button {
background: #2563eb;
color: white;
border: 0;
border-radius: 8px;
padding: 0.55rem 0.9rem;
textarea:focus,
input:focus,
button:focus,
a:focus {
outline: 2px solid #3b82f6;
outline-offset: 1px;
}
.advanced {
margin-top: 0.85rem;
border-top: 1px solid #1e293b;
padding-top: 0.8rem;
}
.advanced summary {
cursor: pointer;
color: #9fb0cb;
}
button:hover {
background: #1d4ed8;
.advanced-grid {
margin-top: 0.85rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.75rem;
}
.advanced-grid label {
display: block;
font-size: 0.88rem;
color: #9fb0cb;
}
.check-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding-top: 1.7rem;
}
.actions {
display: flex;
gap: 0.6rem;
margin-top: 0.95rem;
}
button,
.link-btn {
border: 0;
border-radius: 10px;
background: #3b82f6;
color: #f8fbff;
padding: 0.62rem 0.95rem;
font-weight: 600;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
}
button:hover,
.link-btn:hover {
background: #2563eb;
}
.ghost {
background: transparent;
border: 1px solid #334155;
color: #d6e2f7;
}
.ghost:hover {
background: #122039;
}
.hidden {
display: none;
}
.result {
margin-top: 0.75rem;
color: #93c5fd;
word-break: break-all;
margin-top: 1rem;
border-top: 1px solid #1e293b;
padding-top: 1rem;
}
.result h2 {
margin: 0 0 0.2rem;
font-size: 1.02rem;
}
.hint {
margin: 0 0 0.65rem;
color: #9fb0cb;
font-size: 0.9rem;
}
.link-row {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 0.5rem;
}
.meta-row {
margin-top: 0.7rem;
color: #9fb0cb;
font-size: 0.9rem;
}
.list-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.list-head h2 {
margin: 0;
font-size: 1rem;
}
#list {
color: #9fb0cb;
}
.paste-item {
padding: 0.72rem 0;
border-top: 1px solid #1e293b;
padding: 0.75rem 0;
}
.meta {
color: #94a3b8;
font-size: 0.85rem;
.paste-item:first-child {
border-top: 0;
}
.paste-title {
color: #eff5ff;
font-weight: 600;
}
.paste-meta {
margin-top: 0.2rem;
font-size: 0.88rem;
}
.paste-actions {
margin-top: 0.35rem;
display: flex;
gap: 0.65rem;
font-size: 0.9rem;
}
a {
color: #60a5fa;
color: #7db1ff;
}