Files
dickendock/backend/upload-page.html

189 lines
8.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Datei hochladen DickenDock</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#0d0d0f;color:#fff;font-family:'Courier New',monospace;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
.card{background:#1a1a1e;border:1px solid rgba(255,255,255,0.1);border-radius:16px;padding:28px;width:100%;max-width:440px}
h1{font-size:18px;margin-bottom:6px;color:#4ecdc4}
.sub{color:rgba(255,255,255,0.4);font-size:12px;margin-bottom:20px}
label{display:block;color:rgba(255,255,255,0.5);font-size:10px;letter-spacing:2px;margin-bottom:4px;margin-top:12px}
input[type=password],input[type=file]{width:100%;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.12);border-radius:8px;padding:10px 12px;color:#fff;font-family:inherit;font-size:14px;outline:none;transition:border 0.2s}
input[type=password]:focus{border-color:#4ecdc4}
input[type=file]{cursor:pointer;padding:8px}
input[type=file]::file-selector-button{background:#4ecdc4;border:none;border-radius:6px;color:#0d0d0f;cursor:pointer;font-family:inherit;font-weight:700;padding:4px 12px;margin-right:10px}
.btn{width:100%;background:#4ecdc4;border:none;border-radius:8px;color:#0d0d0f;cursor:pointer;font-family:inherit;font-size:14px;font-weight:700;padding:12px;margin-top:12px;transition:opacity 0.2s}
.btn:hover:not(:disabled){opacity:0.85}
.btn:disabled{opacity:0.4;cursor:default}
.info{background:rgba(78,205,196,0.08);border:1px solid rgba(78,205,196,0.2);border-radius:8px;padding:10px 12px;font-size:12px;color:rgba(255,255,255,0.6);margin-bottom:16px;margin-top:4px}
.msg{border-radius:8px;padding:10px 12px;font-size:13px;margin-top:10px}
.msg.ok{background:rgba(78,205,196,0.12);border:1px solid rgba(78,205,196,0.3);color:#4ecdc4}
.msg.err{background:rgba(255,107,157,0.12);border:1px solid rgba(255,107,157,0.3);color:#ff6b9d}
.msg.warn{background:rgba(255,230,109,0.12);border:1px solid rgba(255,230,109,0.3);color:#ffe66d}
.hidden{display:none}
.done-list{margin-top:10px;font-size:12px;color:rgba(255,255,255,0.5)}
.done-list li{padding:2px 0;list-style:none}
.done-list li::before{content:'✓ ';color:#4ecdc4}
#progress{height:4px;background:rgba(78,205,196,0.15);border-radius:2px;margin-top:10px;overflow:hidden}
#progress-bar{height:100%;background:#4ecdc4;width:0;transition:width 0.3s}
.attempts{color:#ffe66d;font-size:11px;margin-top:6px;font-family:monospace}
</style>
</head>
<body>
<div class="card">
<h1>📤 Datei hochladen</h1>
<p class="sub" id="sub">Dateien senden</p>
<div id="pw-section">
<div class="info" id="share-info">⏳ Link wird geprüft…</div>
<label>PASSWORT</label>
<input type="password" id="pw" placeholder="Passwort eingeben" autocomplete="off" onkeydown="if(event.key==='Enter')verify()"/>
<button class="btn" id="pw-btn" onclick="verify()">Zugang bestätigen</button>
<div id="pw-msg" class="hidden"></div>
<div id="attempts-info" class="attempts hidden"></div>
</div>
<div id="upload-section" class="hidden">
<div class="info" id="upload-info"></div>
<label>DATEI(EN) AUSWÄHLEN</label>
<input type="file" id="file-input" multiple/>
<div id="progress"><div id="progress-bar"></div></div>
<button class="btn" id="upload-btn" onclick="uploadFiles()">⬆ Hochladen</button>
<div id="upload-msg" class="hidden"></div>
<ul class="done-list" id="done-list"></ul>
</div>
</div>
<script>
const token = location.pathname.split('/u/')[1] || '';
let password = '';
let maxMb = 10;
async function init() {
if (!token) return showFatal('Ungültiger Link-Aufbau');
try {
const r = await fetch('/api/upload-shares/public/' + encodeURIComponent(token));
const d = await r.json();
if (!r.ok) return showFatal(d.error || 'Link ungültig');
maxMb = d.max_size_mb;
const exp = new Date(d.expires_at).toLocaleString('de-DE',{timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
document.getElementById('share-info').textContent = '🔐 Gültig bis ' + exp + ' · max ' + d.max_size_mb + ' MB pro Datei';
} catch(e) {
showFatal('Verbindung fehlgeschlagen: ' + e.message);
}
}
function showFatal(msg) {
document.getElementById('pw-section').innerHTML = '<div class="msg err">' + msg + '</div>';
}
async function verify() {
const pw = document.getElementById('pw').value;
if (!pw) { document.getElementById('pw').focus(); return; }
const btn = document.getElementById('pw-btn');
btn.disabled = true;
btn.textContent = '…';
const msg = document.getElementById('pw-msg');
try {
const r = await fetch('/api/upload-shares/public/' + encodeURIComponent(token) + '/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pw })
});
const d = await r.json();
if (!r.ok) {
msg.className = 'msg err';
msg.textContent = d.error;
msg.classList.remove('hidden');
if (d.locked) {
showFatal(d.error);
return;
}
if (d.attempts_left !== undefined) {
const ai = document.getElementById('attempts-info');
ai.textContent = '⚠ Noch ' + d.attempts_left + ' Versuch' + (d.attempts_left===1?'':'e') + ' übrig';
ai.classList.remove('hidden');
}
btn.disabled = false;
btn.textContent = 'Zugang bestätigen';
return;
}
password = pw;
maxMb = d.max_size_mb;
document.getElementById('pw-section').classList.add('hidden');
const us = document.getElementById('upload-section');
us.classList.remove('hidden');
const exp = new Date(d.expires_at).toLocaleString('de-DE',{timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
document.getElementById('upload-info').textContent = '✓ Zugang bestätigt · Gültig bis ' + exp + ' · max ' + d.max_size_mb + ' MB pro Datei';
document.getElementById('sub').textContent = 'Bereit zum Hochladen';
} catch(e) {
msg.className = 'msg err';
msg.textContent = 'Verbindung fehlgeschlagen: ' + e.message;
msg.classList.remove('hidden');
btn.disabled = false;
btn.textContent = 'Zugang bestätigen';
}
}
async function uploadFiles() {
const files = document.getElementById('file-input').files;
if (!files.length) { document.getElementById('file-input').click(); return; }
const btn = document.getElementById('upload-btn');
btn.disabled = true;
const msg = document.getElementById('upload-msg');
const bar = document.getElementById('progress-bar');
const list = document.getElementById('done-list');
msg.classList.add('hidden');
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.size > maxMb * 1024 * 1024) {
msg.className = 'msg warn';
msg.textContent = '"' + file.name + '" ist zu groß (max ' + maxMb + ' MB)';
msg.classList.remove('hidden');
continue;
}
bar.style.width = ((i + 0.5) / files.length * 100) + '%';
const fd = new FormData();
fd.append('file', file);
try {
const r = await fetch('/api/upload-shares/public/' + encodeURIComponent(token) + '/upload', {
method: 'POST',
headers: { 'x-upload-password': password },
body: fd
});
const d = await r.json();
if (r.ok) {
const li = document.createElement('li');
li.textContent = file.name;
list.appendChild(li);
} else {
msg.className = 'msg err';
msg.textContent = d.error;
msg.classList.remove('hidden');
}
} catch(e) {
msg.className = 'msg err';
msg.textContent = 'Upload fehlgeschlagen: ' + e.message;
msg.classList.remove('hidden');
}
}
bar.style.width = '100%';
setTimeout(() => bar.style.width = '0', 1500);
if (list.children.length > 0) {
msg.className = 'msg ok';
msg.textContent = list.children.length + ' Datei' + (list.children.length !== 1 ? 'en' : '') + ' erfolgreich hochgeladen';
msg.classList.remove('hidden');
}
btn.disabled = false;
document.getElementById('file-input').value = '';
}
init();
</script>
</body>
</html>