diff --git a/internal/server/templates/dashboard.html b/internal/server/templates/dashboard.html index c86fc78..593027d 100644 --- a/internal/server/templates/dashboard.html +++ b/internal/server/templates/dashboard.html @@ -29,16 +29,53 @@
@@ -78,41 +115,84 @@ (function () { const zone = document.getElementById('dropZone'); const input = document.getElementById('dropInput'); - const hint = document.getElementById('dropHint'); - const form = document.getElementById('publishForm'); + const filename = document.getElementById('dropFilename'); + const sizeEl = document.getElementById('dropSize'); + const errorEl = document.getElementById('dropError'); + const clearBtn = document.getElementById('dropClear'); if (!zone || !input) return; - const ACTIVE = ['border-indigo-500', 'bg-indigo-500/10']; + const panels = { + empty: zone.querySelector('[data-state="empty"]'), + ready: zone.querySelector('[data-state="ready"]'), + error: zone.querySelector('[data-state="error"]'), + }; + + const READY = ['border-emerald-500', '!border-solid', 'bg-emerald-500/5']; + const OVER = ['border-indigo-500', 'bg-indigo-500/10', 'scale-[1.01]']; + const ERROR = ['border-red-500', 'bg-red-500/5']; + + function setState(name) { + for (const key of Object.keys(panels)) { + const show = key === name; + panels[key].classList.toggle('hidden', !show); + panels[key].classList.toggle('flex', show); + } + zone.classList.remove(...READY, ...OVER, ...ERROR); + if (name === 'ready') zone.classList.add(...READY); + else if (name === 'error') zone.classList.add(...ERROR); + } + const prevent = e => { e.preventDefault(); e.stopPropagation(); }; ['dragenter', 'dragover'].forEach(ev => - zone.addEventListener(ev, e => { prevent(e); zone.classList.add(...ACTIVE); })); + zone.addEventListener(ev, e => { prevent(e); zone.classList.add(...OVER); })); ['dragleave', 'drop'].forEach(ev => - zone.addEventListener(ev, e => { prevent(e); zone.classList.remove(...ACTIVE); })); + zone.addEventListener(ev, e => { prevent(e); zone.classList.remove(...OVER); })); zone.addEventListener('drop', e => { const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; if (!f) return; - if (!/\.html?$/i.test(f.name) && f.type !== 'text/html') { - hint.textContent = 'Only .html files are accepted'; - hint.classList.add('text-red-400'); - return; - } - // DataTransfer into the input so form submit picks it up. - const dt = new DataTransfer(); - dt.items.add(f); - input.files = dt.files; - showSelection(f); + handleFile(f); }); input.addEventListener('change', () => { - if (input.files && input.files[0]) showSelection(input.files[0]); + const f = input.files && input.files[0]; + if (f) handleFile(f); }); - function showSelection(f) { - const kb = (f.size / 1024).toFixed(1); - hint.textContent = `${f.name} · ${kb} KB`; - hint.classList.remove('text-red-400'); - hint.classList.add('text-indigo-300'); + clearBtn && clearBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + input.value = ''; + setState('empty'); + }); + + function handleFile(f) { + if (!/\.html?$/i.test(f.name) && f.type !== 'text/html') { + errorEl.textContent = `Not an HTML file: ${f.name}`; + setState('error'); + setTimeout(() => { if (!input.files[0]) setState('empty'); }, 2500); + return; + } + if (f.size > 5 * 1024 * 1024) { + errorEl.textContent = `Too large (${fmtSize(f.size)}, max 5 MB)`; + setState('error'); + return; + } + // Reflect drop selection back into the input so the form submits it. + try { + const dt = new DataTransfer(); + dt.items.add(f); + input.files = dt.files; + } catch (_) { /* no-op: input.files already set if via click */ } + filename.textContent = f.name; + sizeEl.textContent = fmtSize(f.size); + setState('ready'); + } + + function fmtSize(n) { + if (n < 1024) return `${n} B`; + if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; + return `${(n / (1024 * 1024)).toFixed(1)} MB`; } })();