forked from snxraven/p2ns
Update peer.paste
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
|
||||
<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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user