quickdrop / templates /index.html
parthmax's picture
Update templates/index.html
7d73e9e verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>DropVault Β· Instant File Share</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=Instrument+Sans:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
<style>
:root {
--bg: #f5f2eb;
--ink: #1a1814;
--muted: #8a8478;
--accent: #d4481a;
--accent2: #2a5caa;
--card: #ffffff;
--border: #ddd9d0;
--success: #2a7a4b;
--warn: #c07a1a;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Instrument Sans', sans-serif;
background: var(--bg);
color: var(--ink);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 3rem 1.5rem 5rem;
background-image:
radial-gradient(circle at 10% 20%, rgba(212,72,26,0.06) 0%, transparent 50%),
radial-gradient(circle at 90% 80%, rgba(42,92,170,0.06) 0%, transparent 50%);
}
/* ── Header ── */
header {
text-align: center;
margin-bottom: 3.5rem;
animation: fadeDown 0.6s ease both;
}
.logo {
font-family: 'Syne', sans-serif;
font-weight: 800;
font-size: clamp(2.4rem, 6vw, 3.8rem);
letter-spacing: -0.04em;
color: var(--ink);
line-height: 1;
}
.logo span { color: var(--accent); }
.tagline {
margin-top: 0.5rem;
font-size: 0.85rem;
color: var(--muted);
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 500;
}
/* ── Drop zone ── */
.drop-zone {
width: min(580px, 100%);
background: var(--card);
border: 2px dashed var(--border);
border-radius: 1.4rem;
padding: 3.5rem 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.1rem;
cursor: pointer;
transition: border-color 0.2s, background 0.2s, transform 0.15s;
animation: fadeUp 0.6s 0.1s ease both;
position: relative;
overflow: hidden;
}
.drop-zone::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at 50% 0%, rgba(212,72,26,0.04), transparent 60%);
pointer-events: none;
}
.drop-zone:hover, .drop-zone.drag-over {
border-color: var(--accent);
background: #fff8f6;
transform: translateY(-2px);
}
.drop-zone.drag-over { border-style: solid; }
.drop-icon {
width: 4.5rem;
height: 4.5rem;
background: linear-gradient(135deg, #fff0ec, #fde4db);
border-radius: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
transition: transform 0.2s;
border: 1.5px solid rgba(212,72,26,0.15);
}
.drop-zone:hover .drop-icon { transform: scale(1.08) rotate(-4deg); }
.drop-text {
font-family: 'Syne', sans-serif;
font-size: 1.1rem;
font-weight: 600;
color: var(--ink);
}
.drop-sub {
font-size: 0.8rem;
color: var(--muted);
text-align: center;
line-height: 1.6;
}
.browse-btn {
background: var(--accent);
color: white;
border: none;
padding: 0.65rem 1.8rem;
border-radius: 2rem;
font-family: 'Syne', sans-serif;
font-weight: 600;
font-size: 0.88rem;
cursor: pointer;
letter-spacing: 0.02em;
transition: background 0.15s, transform 0.1s;
}
.browse-btn:hover { background: #bc3e14; transform: scale(1.03); }
.browse-btn:active { transform: scale(0.98); }
#fileInput { display: none; }
/* ── Progress ── */
.progress-wrap {
width: min(580px, 100%);
display: none;
flex-direction: column;
gap: 0.6rem;
animation: fadeUp 0.4s ease both;
}
.progress-wrap.show { display: flex; }
.progress-label {
font-size: 0.82rem;
color: var(--muted);
display: flex;
justify-content: space-between;
}
.progress-bar-bg {
height: 6px;
background: var(--border);
border-radius: 99px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--accent), #e8703a);
border-radius: 99px;
width: 0%;
transition: width 0.3s ease;
}
/* ── Result card ── */
.result-card {
width: min(580px, 100%);
background: var(--card);
border: 1.5px solid var(--border);
border-radius: 1.4rem;
padding: 1.8rem;
display: none;
flex-direction: column;
gap: 1.2rem;
animation: fadeUp 0.5s ease both;
position: relative;
overflow: hidden;
}
.result-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
background: linear-gradient(90deg, var(--accent), var(--accent2));
}
.result-card.show { display: flex; }
.result-header {
display: flex;
align-items: center;
gap: 0.8rem;
}
.result-icon {
width: 2.8rem;
height: 2.8rem;
background: #eef6f1;
border-radius: 0.8rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
flex-shrink: 0;
}
.result-name {
font-family: 'Syne', sans-serif;
font-weight: 700;
font-size: 1rem;
word-break: break-all;
}
.result-meta { font-size: 0.78rem; color: var(--muted); margin-top: 0.1rem; }
.link-row {
display: flex;
gap: 0.6rem;
align-items: center;
}
.link-box {
flex: 1;
background: #f7f5f0;
border: 1.5px solid var(--border);
border-radius: 0.7rem;
padding: 0.65rem 0.9rem;
font-size: 0.82rem;
color: var(--accent2);
word-break: break-all;
font-family: 'Instrument Sans', monospace;
}
.copy-btn {
background: var(--ink);
color: white;
border: none;
padding: 0.65rem 1.2rem;
border-radius: 0.7rem;
font-family: 'Syne', sans-serif;
font-weight: 600;
font-size: 0.8rem;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s, transform 0.1s;
flex-shrink: 0;
}
.copy-btn:hover { background: #333; }
.copy-btn.copied { background: var(--success); }
/* ── Timer ── */
.timer-row {
display: flex;
align-items: center;
gap: 0.7rem;
}
.timer-label {
font-size: 0.78rem;
color: var(--muted);
white-space: nowrap;
}
.timer-bar-bg {
flex: 1;
height: 5px;
background: var(--border);
border-radius: 99px;
overflow: hidden;
}
.timer-bar {
height: 100%;
background: linear-gradient(90deg, var(--success), #6fcf97);
border-radius: 99px;
width: 100%;
transition: width 1s linear, background 1s;
}
.timer-count {
font-family: 'Syne', sans-serif;
font-weight: 700;
font-size: 0.85rem;
color: var(--ink);
min-width: 2.5rem;
text-align: right;
}
.download-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
background: transparent;
color: var(--accent2);
border: 1.5px solid var(--accent2);
padding: 0.65rem 1.4rem;
border-radius: 0.7rem;
font-family: 'Syne', sans-serif;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
text-decoration: none;
transition: background 0.15s, color 0.15s;
}
.download-btn:hover { background: var(--accent2); color: white; }
.upload-another {
background: none;
border: none;
color: var(--muted);
font-size: 0.8rem;
cursor: pointer;
text-align: center;
font-family: 'Instrument Sans', sans-serif;
text-decoration: underline;
transition: color 0.15s;
}
.upload-another:hover { color: var(--ink); }
/* ── How it works ── */
.steps {
width: min(580px, 100%);
margin-top: 2.5rem;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
animation: fadeUp 0.6s 0.25s ease both;
}
.step {
background: var(--card);
border: 1.5px solid var(--border);
border-radius: 1rem;
padding: 1.2rem 1rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.step-num {
font-family: 'Syne', sans-serif;
font-weight: 800;
font-size: 1.5rem;
color: var(--border);
line-height: 1;
}
.step-title {
font-family: 'Syne', sans-serif;
font-weight: 600;
font-size: 0.85rem;
}
.step-desc { font-size: 0.75rem; color: var(--muted); line-height: 1.5; }
@media (max-width: 480px) {
.steps { grid-template-columns: 1fr; }
.link-row { flex-direction: column; }
.copy-btn { width: 100%; }
}
/* ── Animations ── */
@keyframes fadeDown {
from { opacity: 0; transform: translateY(-18px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(18px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%) translateY(4rem);
background: var(--ink);
color: white;
padding: 0.7rem 1.5rem;
border-radius: 2rem;
font-size: 0.85rem;
font-family: 'Syne', sans-serif;
font-weight: 600;
transition: transform 0.35s cubic-bezier(0.34,1.56,0.64,1);
pointer-events: none;
z-index: 100;
white-space: nowrap;
}
.toast.show { transform: translateX(-50%) translateY(0); }
</style>
</head>
<body>
<header>
<div class="logo">Drop<span>Vault</span></div>
<div class="tagline">Instant Β· Secure Β· 5-Minute Links</div>
</header>
<!-- Drop Zone -->
<div class="drop-zone" id="dropZone">
<div class="drop-icon">πŸ“¦</div>
<div class="drop-text">Drop your file here</div>
<div class="drop-sub">Any file type supported<br>Link auto-expires in 5 minutes</div>
<button class="browse-btn" onclick="document.getElementById('fileInput').click()">Browse Files</button>
<input type="file" id="fileInput"/>
</div>
<!-- Progress -->
<div class="progress-wrap" id="progressWrap" style="margin-top:1rem">
<div class="progress-label">
<span id="progressFile">Uploading…</span>
<span id="progressPct">0%</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar" id="progressBar"></div>
</div>
</div>
<!-- Result -->
<div class="result-card" id="resultCard" style="margin-top:1rem">
<div class="result-header">
<div class="result-icon">βœ…</div>
<div>
<div class="result-name" id="resultName">filename.txt</div>
<div class="result-meta">Ready to share Β· expires in <span id="metaTimer">5:00</span></div>
</div>
</div>
<div class="link-row">
<div class="link-box" id="linkBox">–</div>
<button class="copy-btn" id="copyBtn" onclick="copyLink()">Copy</button>
</div>
<div class="timer-row">
<span class="timer-label">Expires in</span>
<div class="timer-bar-bg">
<div class="timer-bar" id="timerBar"></div>
</div>
<span class="timer-count" id="timerCount">5:00</span>
</div>
<a class="download-btn" id="downloadBtn" href="#" target="_blank">⬇ Download File</a>
<button class="upload-another" onclick="resetUI()">Upload another file</button>
</div>
<!-- Steps -->
<div class="steps" id="steps">
<div class="step">
<div class="step-num">01</div>
<div class="step-title">Upload</div>
<div class="step-desc">Drop or browse any file β€” no account needed</div>
</div>
<div class="step">
<div class="step-num">02</div>
<div class="step-title">Share</div>
<div class="step-desc">Copy the link and send it to anyone instantly</div>
</div>
<div class="step">
<div class="step-num">03</div>
<div class="step-title">Auto-Delete</div>
<div class="step-desc">File vanishes after 5 minutes β€” zero clutter</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast">Link copied!</div>
<script>
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const progressWrap = document.getElementById('progressWrap');
const progressBar = document.getElementById('progressBar');
const progressPct = document.getElementById('progressPct');
const progressFile = document.getElementById('progressFile');
const resultCard = document.getElementById('resultCard');
const resultName = document.getElementById('resultName');
const linkBox = document.getElementById('linkBox');
const copyBtn = document.getElementById('copyBtn');
const downloadBtn= document.getElementById('downloadBtn');
const timerBar = document.getElementById('timerBar');
const timerCount = document.getElementById('timerCount');
const metaTimer = document.getElementById('metaTimer');
const steps = document.getElementById('steps');
const toast = document.getElementById('toast');
let timerInterval = null;
// ── Drag & Drop ──────────────────────────────────────────────────────────────
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const file = e.dataTransfer.files[0];
if (file) uploadFile(file);
});
dropZone.addEventListener('click', e => {
if (e.target.classList.contains('browse-btn')) return;
fileInput.click();
});
fileInput.addEventListener('change', () => {
if (fileInput.files[0]) uploadFile(fileInput.files[0]);
});
// ── Upload ───────────────────────────────────────────────────────────────────
function uploadFile(file) {
dropZone.style.display = 'none';
steps.style.display = 'none';
progressWrap.classList.add('show');
progressFile.textContent = file.name;
progressBar.style.width = '0%';
progressPct.textContent = '0%';
const form = new FormData();
form.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
const pct = Math.round(e.loaded / e.total * 100);
progressBar.style.width = pct + '%';
progressPct.textContent = pct + '%';
}
};
xhr.onload = () => {
progressWrap.classList.remove('show');
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
showResult(data, file.name);
} else {
alert('Upload failed. Please try again.');
resetUI();
}
};
xhr.onerror = () => { alert('Network error.'); resetUI(); };
xhr.send(form);
}
// ── Show result ───────────────────────────────────────────────────────────────
function showResult(data, originalName) {
const fullLink = window.location.origin + data.link;
resultName.textContent = originalName;
linkBox.textContent = fullLink;
downloadBtn.href = data.link;
resultCard.classList.add('show');
startTimer(300);
}
// ── Countdown timer ───────────────────────────────────────────────────────────
function startTimer(seconds) {
if (timerInterval) clearInterval(timerInterval);
let remaining = seconds;
function tick() {
const m = Math.floor(remaining / 60);
const s = remaining % 60;
const label = `${m}:${s.toString().padStart(2, '0')}`;
timerCount.textContent = label;
metaTimer.textContent = label;
const pct = (remaining / 300) * 100;
timerBar.style.width = pct + '%';
// Color shift: green β†’ orange β†’ red
if (pct > 50) {
timerBar.style.background = 'linear-gradient(90deg,#2a7a4b,#6fcf97)';
} else if (pct > 20) {
timerBar.style.background = 'linear-gradient(90deg,#c07a1a,#f4a742)';
} else {
timerBar.style.background = 'linear-gradient(90deg,#d4481a,#f07050)';
}
if (remaining <= 0) {
clearInterval(timerInterval);
timerCount.textContent = 'Expired';
linkBox.textContent = 'Link has expired';
downloadBtn.style.opacity = '0.4';
downloadBtn.style.pointerEvents = 'none';
copyBtn.style.opacity = '0.4';
copyBtn.style.pointerEvents = 'none';
}
remaining--;
}
tick();
timerInterval = setInterval(tick, 1000);
}
// ── Copy link ─────────────────────────────────────────────────────────────────
function copyLink() {
const link = linkBox.textContent;
if (!link || link === '–' || link.includes('expired')) return;
navigator.clipboard.writeText(link).then(() => {
copyBtn.textContent = 'Copied!';
copyBtn.classList.add('copied');
showToast('Link copied to clipboard!');
setTimeout(() => {
copyBtn.textContent = 'Copy';
copyBtn.classList.remove('copied');
}, 2000);
});
}
// ── Toast ─────────────────────────────────────────────────────────────────────
function showToast(msg) {
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2500);
}
// ── Reset ─────────────────────────────────────────────────────────────────────
function resetUI() {
if (timerInterval) clearInterval(timerInterval);
resultCard.classList.remove('show');
progressWrap.classList.remove('show');
dropZone.style.display = '';
steps.style.display = '';
fileInput.value = '';
progressBar.style.width = '0%';
copyBtn.textContent = 'Copy';
copyBtn.classList.remove('copied');
downloadBtn.style.opacity = '';
downloadBtn.style.pointerEvents = '';
copyBtn.style.opacity = '';
copyBtn.style.pointerEvents = '';
timerBar.style.width = '100%';
}
</script>
</body>
</html>