This commit is contained in:
Anaïs Saraiva
2026-08-15 00:35:22 +02:00
parent 954384d1e4
commit d8b9fbe993
8 changed files with 578 additions and 388 deletions
+98 -73
View File
@@ -6,7 +6,11 @@ const STORAGE_KEY = "filehost.uploads";
const dropzone = $("dropzone");
const fileInput = $("fileInput");
const chosenFile = $("chosenFile");
const pwInput = $("pwInput");
const uploadBtn = $("uploadBtn");
const newUploadBtn = $("newUploadBtn");
const clearListBtn = $("clearListBtn");
const progress = $("progress");
const progressBar = $("progressBar");
const progressPct = $("progressPct");
@@ -14,20 +18,17 @@ const progressName = $("progressName");
const result = $("result");
const resultUrl = $("resultUrl");
const resultName = $("resultName");
const resultSize = $("resultSize");
const resultLock = $("resultLock");
const copyBtn = $("copyBtn");
const deleteBtn = $("deleteBtn");
const againBtn = $("againBtn");
const notice = $("notice");
const fileListWrap = $("fileListWrap");
const fileList = $("fileList");
const clearListBtn = $("clearListBtn");
const fileCount = $("fileCount");
const emptyState = $("emptyState");
let current = null;
let currentFile = null;
let uploading = false;
function formatBytes(bytes) {
if (!Number.isFinite(bytes) || bytes < 0) return "-";
if (bytes < 1024) return `${bytes} B`;
const units = ["KB", "MB", "GB", "TB"];
let value = bytes;
@@ -57,18 +58,21 @@ function setProgress(loaded, total) {
function reset() {
uploading = false;
current = null;
currentFile = null;
result.classList.add("hidden");
resultLock.classList.add("hidden");
progress.classList.add("hidden");
progressBar.style.width = "0%";
progressPct.textContent = "0%";
chosenFile.classList.add("hidden");
chosenFile.textContent = "";
fileInput.value = "";
pwInput.value = "";
uploadBtn.disabled = false;
uploadBtn.textContent = "Upload";
clearNotice();
}
// ---- client-side upload history (localStorage) ----
// ---- client-side history (localStorage) ----
function loadUploads() {
try {
@@ -84,7 +88,7 @@ function saveUploads(list) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
} catch {
/* storage unavailable (e.g. private mode) — ignore */
/* storage unavailable — ignore */
}
}
@@ -142,80 +146,79 @@ function setCopyFeedback(btn, label) {
}, 1500);
}
function formatDate(iso) {
try {
return new Date(iso).toLocaleString();
} catch {
return "";
}
}
function renderList() {
const list = loadUploads();
if (list.length === 0) {
fileListWrap.classList.add("hidden");
fileList.innerHTML = "";
return;
}
fileListWrap.classList.remove("hidden");
fileList.innerHTML = "";
emptyState.classList.toggle("hidden", list.length > 0);
fileCount.textContent = list.length === 0 ? "" : `${list.length} file${list.length === 1 ? "" : "s"}`;
for (const item of list) {
const li = document.createElement("li");
li.className = "file-item";
const meta = document.createElement("div");
meta.className = "file-item-meta";
const tr = document.createElement("tr");
const nameTd = document.createElement("td");
const nameWrap = document.createElement("div");
nameWrap.className = "file-name";
const name = document.createElement("span");
name.className = "file-item-name";
name.textContent = item.originalName;
name.title = item.originalName;
const lock = document.createElement("span");
lock.className = "lock";
lock.textContent = "Protected";
nameWrap.appendChild(name);
nameWrap.appendChild(lock);
nameTd.appendChild(nameWrap);
if (item.protected) {
const lock = document.createElement("span");
lock.className = "lock";
lock.textContent = "Password protected";
name.appendChild(lock);
}
const sizeTd = document.createElement("td");
sizeTd.className = "file-size";
sizeTd.textContent = formatBytes(item.size);
const sub = document.createElement("span");
sub.className = "file-item-sub";
let subText = formatBytes(item.size || 0);
if (item.createdAt) {
try {
subText += ` \u00b7 ${new Date(item.createdAt).toLocaleString()}`;
} catch {
/* ignore date formatting */
}
}
sub.textContent = subText;
meta.appendChild(name);
meta.appendChild(sub);
const dateTd = document.createElement("td");
dateTd.className = "file-date";
dateTd.textContent = formatDate(item.createdAt);
const actionsTd = document.createElement("td");
const actions = document.createElement("div");
actions.className = "file-item-actions";
actions.className = "row-actions";
const dl = document.createElement("a");
dl.className = "btn-link";
dl.className = "btn btn-primary";
dl.href = downloadUrl(item);
dl.textContent = "Download";
dl.title = "Open download link";
const copy = document.createElement("button");
copy.type = "button";
copy.className = "btn btn-ghost";
copy.textContent = "Copy";
copy.addEventListener("click", async () => {
const ok = await copyText(downloadUrl(item));
setCopyFeedback(copy, ok ? "Copied" : "Copy failed");
setCopyFeedback(copy, ok ? "Copied" : "Failed");
});
const del = document.createElement("button");
del.type = "button";
del.className = "danger";
del.className = "btn btn-danger";
del.textContent = "Delete";
del.addEventListener("click", () => deleteFile(item, () => removeUpload(item.id)));
actions.appendChild(dl);
actions.appendChild(copy);
actions.appendChild(del);
actionsTd.appendChild(actions);
li.appendChild(meta);
li.appendChild(actions);
fileList.appendChild(li);
tr.appendChild(nameTd);
tr.appendChild(sizeTd);
tr.appendChild(dateTd);
tr.appendChild(actionsTd);
fileList.appendChild(tr);
}
}
@@ -251,32 +254,30 @@ function showResult(data) {
progress.classList.add("hidden");
result.classList.remove("hidden");
resultName.textContent = data.originalName;
resultSize.textContent = formatBytes(data.size);
resultUrl.value = downloadUrl(data);
resultLock.classList.toggle("hidden", !data.protected);
current = data;
addUpload({
id: data.id,
key: data.key,
url: data.url,
originalName: data.originalName,
size: data.size,
protected: !!data.protected,
createdAt: new Date().toISOString(),
});
}
function startUpload(file) {
function startUpload(file, password) {
if (uploading) return;
clearNotice();
progress.classList.remove("hidden");
progressName.textContent = file.name;
setProgress(0, file.size);
uploading = true;
uploadBtn.disabled = true;
uploadBtn.textContent = "Uploading\u2026";
const xhr = new XMLHttpRequest();
const form = new FormData();
if (pwInput.value) form.append("password", pwInput.value);
form.append("password", password);
form.append("file", file);
xhr.upload.addEventListener("progress", (e) => {
@@ -285,11 +286,13 @@ function startUpload(file) {
xhr.addEventListener("load", () => {
uploading = false;
uploadBtn.disabled = false;
uploadBtn.textContent = "Upload";
if (xhr.status >= 200 && xhr.status < 300) {
progress.classList.add("hidden");
try {
showResult(JSON.parse(xhr.responseText));
} catch {
progress.classList.add("hidden");
showNotice("Invalid server response.", "error");
}
return;
@@ -307,12 +310,16 @@ function startUpload(file) {
xhr.addEventListener("error", () => {
uploading = false;
uploadBtn.disabled = false;
uploadBtn.textContent = "Upload";
progress.classList.add("hidden");
showNotice("Network error during upload.", "error");
});
xhr.addEventListener("abort", () => {
uploading = false;
uploadBtn.disabled = false;
uploadBtn.textContent = "Upload";
progress.classList.add("hidden");
});
@@ -320,13 +327,30 @@ function startUpload(file) {
xhr.send(form);
}
function handleFile(file) {
function selectFile(file) {
if (!file) return;
if (file.size === 0) {
showNotice("Empty files are not supported.", "error");
return;
}
startUpload(file);
currentFile = file;
chosenFile.textContent = file.name;
chosenFile.classList.remove("hidden");
clearNotice();
}
function submitUpload() {
if (!currentFile) {
showNotice("Choose a file first.", "error");
return;
}
const password = pwInput.value;
if (!password) {
showNotice("A password is required to protect this file.", "error");
pwInput.focus();
return;
}
startUpload(currentFile, password);
}
dropzone.addEventListener("click", () => {
@@ -342,7 +366,18 @@ dropzone.addEventListener("keydown", (e) => {
fileInput.addEventListener("change", () => {
const file = fileInput.files ? fileInput.files[0] : null;
handleFile(file);
selectFile(file);
});
pwInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") submitUpload();
});
uploadBtn.addEventListener("click", submitUpload);
newUploadBtn.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
pwInput.focus();
});
let dragDepth = 0;
@@ -365,28 +400,18 @@ document.addEventListener("drop", (e) => {
dropzone.classList.remove("drag");
if (uploading) return;
const file = e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files[0] : null;
if (file) handleFile(file);
selectFile(file);
});
copyBtn.addEventListener("click", async () => {
const ok = await copyText(resultUrl.value);
setCopyFeedback(copyBtn, ok ? "Copied" : "Copy failed");
setCopyFeedback(copyBtn, ok ? "Copied" : "Failed");
});
deleteBtn.addEventListener("click", () => {
if (!current) return;
deleteFile(current, () => {
removeUpload(current.id);
reset();
});
});
againBtn.addEventListener("click", reset);
clearListBtn.addEventListener("click", () => {
if (loadUploads().length === 0) return;
const ok = window.confirm(
"Clear this list? This only removes the entries from THIS browser. The uploaded files remain on the server.",
"Clear this list? This only removes the entries from THIS browser. The files remain on the server.",
);
if (!ok) return;
saveUploads([]);
+86 -56
View File
@@ -5,69 +5,99 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="referrer" content="no-referrer" />
<link rel="icon" href="data:," />
<title>File drop</title>
<title>File Drop</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<main class="card">
<h1>File drop</h1>
<p class="tagline">Drop a file to get a private download link. Files are kept forever.</p>
<div id="dropzone" class="dropzone" role="button" tabindex="0" aria-label="Upload a file">
<input type="file" id="fileInput" hidden />
<p class="dz-main">Drag &amp; drop a file here</p>
<p class="dz-sub">or click to choose</p>
</div>
<div class="pw-row">
<input
type="password"
id="pwInput"
placeholder="Optional password (protects the download)"
autocomplete="off"
aria-label="Optional download password"
/>
</div>
<div id="progress" class="progress hidden">
<div class="progress-label">
<span id="progressName"></span>
<span id="progressPct">0%</span>
<div class="app">
<header class="topbar">
<div class="brand">
<div class="logo" aria-hidden="true">FD</div>
<div class="brand-text">
<h1>File Drop</h1>
<span class="sub">Private, password-protected file hosting</span>
</div>
</div>
<div class="progress-track">
<div id="progressBar" class="progress-bar"></div>
<div class="topbar-actions">
<button id="newUploadBtn" type="button" class="btn btn-primary">New upload</button>
<button id="clearListBtn" type="button" class="btn btn-ghost">Clear list</button>
</div>
</div>
</header>
<div id="result" class="result hidden">
<div class="result-meta">
<strong id="resultName"></strong>
<span id="resultSize"></span>
<span id="resultLock" class="lock hidden">Password protected</span>
</div>
<div class="url-row">
<input id="resultUrl" type="text" readonly aria-label="Download URL" />
<button id="copyBtn" type="button">Copy</button>
</div>
<div class="result-actions">
<button id="deleteBtn" type="button" class="danger">Delete file</button>
<button id="againBtn" type="button" class="ghost">Upload another</button>
</div>
</div>
<main class="layout">
<aside class="panel upload-panel">
<h2>Upload a file</h2>
<div id="notice" class="hidden"></div>
<div id="dropzone" class="dropzone" role="button" tabindex="0" aria-label="Choose a file">
<input type="file" id="fileInput" hidden />
<p class="dz-main">Drag &amp; drop a file here</p>
<p class="dz-sub">or click to choose</p>
<p id="chosenFile" class="dz-file hidden"></p>
</div>
<section id="fileListWrap" class="files hidden" aria-label="Your uploaded files">
<div class="files-head">
<h2>Your files</h2>
<button id="clearListBtn" type="button" class="ghost">Clear list</button>
</div>
<p class="files-note">
Stored locally in this browser only. Other visitors cannot see them.
</p>
<ul id="fileList"></ul>
</section>
</main>
<div class="field">
<label for="pwInput">Password</label>
<input
type="password"
id="pwInput"
placeholder="Required — protects the download"
autocomplete="new-password"
/>
</div>
<div id="progress" class="progress hidden">
<div class="progress-label">
<span id="progressName"></span>
<span id="progressPct">0%</span>
</div>
<div class="progress-track">
<div id="progressBar" class="progress-bar"></div>
</div>
</div>
<button id="uploadBtn" type="button" class="btn btn-primary btn-block">Upload</button>
<div id="result" class="result hidden">
<div class="result-head">
<span id="resultName"></span>
<span class="lock">Protected</span>
</div>
<div class="url-row">
<input id="resultUrl" type="text" readonly aria-label="Download URL" />
<button id="copyBtn" type="button" class="btn btn-primary">Copy</button>
</div>
</div>
<div id="notice" class="hidden"></div>
</aside>
<section class="panel files-panel">
<div class="panel-head">
<h2>Your files</h2>
<span id="fileCount" class="count"></span>
</div>
<p class="files-note">
Stored locally in this browser only. Every file requires its password.
</p>
<div class="table-wrap">
<table class="files-table">
<thead>
<tr>
<th class="col-name">Name</th>
<th class="col-size">Size</th>
<th class="col-date">Uploaded</th>
<th class="col-actions">Actions</th>
</tr>
</thead>
<tbody id="fileList"></tbody>
</table>
</div>
<p id="emptyState" class="empty">No files yet. Upload your first file.</p>
</section>
</main>
</div>
<script src="/app.js"></script>
</body>
</html>
</html>
+335 -208
View File
@@ -1,13 +1,15 @@
:root {
--bg: #0f1115;
--card: #171a21;
--border: #2a2f3a;
--text: #e6e8ee;
--muted: #9aa3b2;
--accent: #4f8cff;
--accent-hover: #3a74e0;
--danger: #e05252;
--bg: #0e1013;
--surface: #15181d;
--surface-2: #1a1e24;
--border: #262b33;
--text: #e7eaf0;
--muted: #98a1af;
--accent: #3f7bf6;
--accent-strong: #2f63d8;
--danger: #e05555;
--ok: #46b076;
--radius: 8px;
}
* {
@@ -20,37 +22,151 @@ body {
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
button,
input {
font: inherit;
}
button {
cursor: pointer;
}
.app {
max-width: 1180px;
margin: 0 auto;
padding: 0 20px 48px;
}
/* ---- top bar ---- */
.topbar {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
justify-content: space-between;
gap: 16px;
padding: 20px 0 18px;
border-bottom: 1px solid var(--border);
margin-bottom: 22px;
}
.card {
width: 100%;
max-width: 560px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 28px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
.brand {
display: flex;
align-items: center;
gap: 12px;
}
h1 {
margin: 0 0 4px;
font-size: 24px;
.logo {
width: 40px;
height: 40px;
border-radius: var(--radius);
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
color: #fff;
font-weight: 700;
display: grid;
place-items: center;
letter-spacing: 0.5px;
}
.tagline {
margin: 0 0 20px;
.brand-text h1 {
margin: 0;
font-size: 18px;
font-weight: 650;
line-height: 1.1;
}
.sub {
font-size: 12px;
color: var(--muted);
font-size: 14px;
}
.topbar-actions {
display: flex;
gap: 8px;
}
/* ---- buttons ---- */
.btn {
border: 1px solid transparent;
border-radius: var(--radius);
padding: 9px 14px;
font-size: 13px;
font-weight: 600;
transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.btn-primary {
background: var(--accent);
color: #fff;
}
.btn-primary:hover {
background: var(--accent-strong);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-ghost {
background: transparent;
border-color: var(--border);
color: var(--muted);
}
.btn-ghost:hover {
color: var(--text);
border-color: var(--muted);
}
.btn-danger {
background: transparent;
border: 1px solid var(--danger);
color: var(--danger);
}
.btn-danger:hover {
background: rgba(224, 85, 85, 0.1);
}
.btn-block {
width: 100%;
margin-top: 14px;
}
/* ---- layout ---- */
.layout {
display: grid;
grid-template-columns: 340px 1fr;
gap: 20px;
align-items: start;
}
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
}
.panel h2 {
margin: 0 0 16px;
font-size: 15px;
font-weight: 650;
letter-spacing: 0.2px;
}
/* ---- upload panel ---- */
.upload-panel {
position: sticky;
top: 20px;
}
.dropzone {
border: 2px dashed var(--border);
border-radius: 12px;
padding: 40px 20px;
border: 1.5px dashed var(--border);
border-radius: var(--radius);
padding: 26px 16px;
text-align: center;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
@@ -61,61 +177,67 @@ h1 {
.dropzone.drag,
.dropzone:focus-visible {
border-color: var(--accent);
background: rgba(79, 140, 255, 0.06);
background: rgba(63, 123, 246, 0.06);
}
.dz-main {
margin: 0;
font-weight: 600;
font-size: 14px;
}
.dz-sub {
margin: 6px 0 0;
color: var(--muted);
font-size: 12px;
}
.dz-file {
margin: 12px 0 0;
font-size: 13px;
font-weight: 600;
color: var(--accent);
word-break: break-all;
}
.pw-row {
margin-top: 12px;
.field {
margin-top: 16px;
}
.pw-row input {
.field label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--muted);
margin-bottom: 6px;
letter-spacing: 0.3px;
}
.field input {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
border-radius: 8px;
padding: 10px 12px;
font-size: 13px;
border-radius: var(--radius);
padding: 11px 12px;
font-size: 14px;
}
.pw-row input:focus {
.field input:focus {
outline: none;
border-color: var(--accent);
}
.lock {
display: inline-block;
margin-left: 8px;
font-size: 11px;
font-weight: 600;
color: var(--muted);
background: rgba(79, 140, 255, 0.12);
border: 1px solid rgba(79, 140, 255, 0.3);
border-radius: 99px;
padding: 2px 8px;
vertical-align: middle;
}
/* ---- progress ---- */
.progress {
margin-top: 20px;
margin-top: 16px;
}
.progress-label {
display: flex;
justify-content: space-between;
gap: 12px;
font-size: 13px;
font-size: 12px;
color: var(--muted);
margin-bottom: 8px;
}
@@ -127,8 +249,8 @@ h1 {
}
.progress-track {
height: 8px;
background: var(--border);
height: 6px;
background: var(--surface-2);
border-radius: 99px;
overflow: hidden;
}
@@ -141,20 +263,27 @@ h1 {
transition: width 0.15s;
}
/* ---- result banner ---- */
.result {
margin-top: 20px;
margin-top: 16px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface-2);
padding: 12px;
}
.result-meta {
font-size: 14px;
.result-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
color: var(--muted);
word-break: break-all;
}
.result-meta strong {
color: var(--text);
.result-head span:first-child {
font-size: 13px;
font-weight: 600;
word-break: break-all;
}
.url-row {
@@ -168,65 +297,159 @@ h1 {
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
border-radius: 8px;
border-radius: var(--radius);
padding: 10px 12px;
font-size: 13px;
}
.result-actions {
/* ---- files panel ---- */
.panel-head {
display: flex;
gap: 8px;
margin-top: 10px;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
button {
font: inherit;
cursor: pointer;
border: none;
border-radius: 8px;
padding: 9px 14px;
font-size: 13px;
background: var(--accent);
color: #fff;
transition: background 0.15s;
.panel-head h2 {
margin-bottom: 4px;
}
button:hover {
background: var(--accent-hover);
.count {
font-size: 12px;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
button.danger {
background: transparent;
border: 1px solid var(--danger);
color: var(--danger);
}
button.danger:hover {
background: rgba(224, 82, 82, 0.1);
}
button.ghost {
background: transparent;
border: 1px solid var(--border);
.files-note {
margin: 0 0 16px;
font-size: 12px;
color: var(--muted);
}
button.ghost:hover {
color: var(--text);
border-color: var(--muted);
.table-wrap {
border: 1px solid var(--border);
border-radius: var(--radius);
overflow-x: auto;
}
#notice {
margin-top: 16px;
.files-table {
width: 100%;
border-collapse: collapse;
min-width: 520px;
}
.files-table th,
.files-table td {
text-align: left;
padding: 12px 14px;
border-bottom: 1px solid var(--border);
font-size: 13px;
border-radius: 8px;
vertical-align: middle;
}
.files-table thead th {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.6px;
color: var(--muted);
background: var(--surface-2);
border-bottom: 1px solid var(--border);
font-weight: 600;
}
.files-table tbody tr:last-child td {
border-bottom: none;
}
.files-table tbody tr:hover td {
background: rgba(255, 255, 255, 0.015);
}
.col-name {
width: 40%;
}
.col-size {
width: 12%;
}
.col-date {
width: 20%;
}
.col-actions {
width: 28%;
text-align: right;
}
.file-name {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
word-break: break-all;
}
.lock {
flex-shrink: 0;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.4px;
color: var(--muted);
background: rgba(63, 123, 246, 0.12);
border: 1px solid rgba(63, 123, 246, 0.3);
border-radius: 99px;
padding: 2px 8px;
}
.file-sub {
display: block;
font-size: 12px;
color: var(--muted);
font-weight: 400;
margin-top: 2px;
word-break: break-all;
}
.file-size,
.file-date {
color: var(--muted);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.row-actions {
display: flex;
justify-content: flex-end;
gap: 6px;
}
.row-actions .btn {
padding: 7px 10px;
font-size: 12px;
}
.empty {
text-align: center;
color: var(--muted);
font-size: 13px;
padding: 28px 0 6px;
margin: 0;
}
/* ---- notice ---- */
#notice {
margin-top: 14px;
font-size: 13px;
border-radius: var(--radius);
padding: 10px 12px;
}
#notice.error {
color: var(--danger);
background: rgba(224, 82, 82, 0.08);
border: 1px solid rgba(224, 82, 82, 0.3);
background: rgba(224, 85, 85, 0.08);
border: 1px solid rgba(224, 85, 85, 0.3);
}
#notice.info {
@@ -239,119 +462,23 @@ button.ghost:hover {
display: none !important;
}
/* Client-side upload list */
.files {
margin-top: 24px;
border-top: 1px solid var(--border);
padding-top: 18px;
}
.files-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.files-head h2 {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.files-note {
margin: 6px 0 12px;
font-size: 12px;
color: var(--muted);
}
#fileList {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 12px;
}
.file-item-meta {
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.file-item-name {
font-size: 13px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-item-sub {
font-size: 12px;
color: var(--muted);
white-space: nowrap;
}
.file-item-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.btn-link {
font: inherit;
font-size: 13px;
color: #fff;
background: var(--accent);
border-radius: 8px;
padding: 8px 12px;
text-decoration: none;
transition: background 0.15s;
}
.btn-link:hover {
background: var(--accent-hover);
}
.file-item-actions button {
font-size: 12px;
padding: 8px 10px;
}
@media (max-width: 480px) {
.card {
padding: 20px;
/* ---- responsive ---- */
@media (max-width: 860px) {
.layout {
grid-template-columns: 1fr;
}
.dropzone {
padding: 28px 16px;
}
.url-row {
flex-direction: column;
}
.file-item {
flex-direction: column;
align-items: stretch;
}
.file-item-actions {
justify-content: flex-end;
.upload-panel {
position: static;
}
}
@media (max-width: 520px) {
.app {
padding: 0 12px 32px;
}
.topbar {
flex-wrap: wrap;
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ export async function handleDownload(
if (!meta) return notFound();
if (!secretMatches(meta.downloadKeyHash, rawKey)) return notFound();
if (meta.passwordHash && !hasValidAuth(req, secret, rawKey)) {
if (!hasValidAuth(req, secret, rawKey)) {
const url = new URL(req.url);
return renderPasswordForm(rawKey, meta.originalName, url.searchParams.has("e"));
}
+4 -7
View File
@@ -63,24 +63,21 @@ export async function handleUpload(
}
if (!hasFile) return jsonError(400, "no file part in the upload");
if (!password.trim()) return jsonError(400, "a password is required");
const originalName = sanitizeOriginalName(filename);
const protected_ = password.trim().length > 0;
await storage.writeMetadata({
id,
originalName,
size,
mimeType: mime,
downloadKeyHash: hashSecret(key),
passwordHash: protected_ ? hashPassword(password) : undefined,
passwordHash: hashPassword(password),
createdAt: new Date().toISOString(),
});
log(
"info",
`upload ok id=${id} size=${size} name=${JSON.stringify(originalName)} protected=${protected_}`,
);
return json({ id, key, url: `/d/${key}`, originalName, size, protected: protected_ });
log("info", `upload ok id=${id} size=${size} name=${JSON.stringify(originalName)}`);
return json({ id, key, url: `/d/${key}`, originalName, size, protected: true });
} catch (err) {
await storage.delete(id);
await storage.deletePartial(id);