/** * Web Uploader - Frontend Application * Handles file uploads with drag-and-drop, progress tracking, and image previews. */ const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i; const PROGRESS_INTERVAL_MS = 100; const PROGRESS_INCREMENT = 5; const MODAL_TRANSITION_MS = 300; const COPY_FEEDBACK_MS = 2000; const elements = { dropZone: document.getElementById('dropZone'), fileInput: document.getElementById('fileInput'), browseBtn: document.getElementById('browseBtn'), progressContainer: document.getElementById('progressContainer'), progressBar: document.getElementById('progressBar'), fileName: document.getElementById('fileName'), uploadStatus: document.getElementById('uploadStatus'), statusMessage: document.getElementById('statusMessage'), resultsContainer: document.getElementById('resultsContainer'), modal: document.getElementById('imageModal'), modalImage: document.getElementById('modalImage'), closeModal: document.querySelector('.close-modal'), tabBtns: document.querySelectorAll('.tab-btn'), tabContents: document.querySelectorAll('.tab-content'), expirationSelect: document.getElementById('expiration'), historyList: document.getElementById('historyList'), clearHistoryBtn: document.getElementById('clearHistoryBtn'), toastContainer: document.getElementById('toastContainer'), confirmModal: document.getElementById('confirmModal'), cancelConfirmBtn: document.getElementById('cancelConfirmBtn'), confirmDeleteBtn: document.getElementById('confirmDeleteBtn') }; function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } function highlight() { elements.dropZone.classList.add('dragover'); } function unhighlight() { elements.dropZone.classList.remove('dragover'); } function handleDrop(e) { const files = e.dataTransfer.files; handleFiles(files); } function initDragAndDrop() { const dragEvents = ['dragenter', 'dragover', 'dragleave', 'drop']; dragEvents.forEach(eventName => { elements.dropZone.addEventListener(eventName, preventDefaults, false); document.body.addEventListener(eventName, preventDefaults, false); }); ['dragenter', 'dragover'].forEach(eventName => { elements.dropZone.addEventListener(eventName, highlight, false); }); ['dragleave', 'drop'].forEach(eventName => { elements.dropZone.addEventListener(eventName, unhighlight, false); }); elements.dropZone.addEventListener('drop', handleDrop, false); } function initFileInput() { elements.browseBtn.addEventListener('click', () => { elements.fileInput.click(); }); elements.fileInput.addEventListener('change', function () { handleFiles(this.files); }); // Init History renderHistory(); elements.clearHistoryBtn.addEventListener('click', showConfirmModal); initConfirmModal(); } function handleFiles(files) { if (files.length > 0) { uploadFiles(files); } } function resetUploadUI() { elements.progressContainer.hidden = false; elements.statusMessage.hidden = true; elements.resultsContainer.hidden = true; elements.resultsContainer.innerHTML = ''; elements.progressBar.style.width = '0%'; } function createProgressSimulator() { let progress = 0; return setInterval(() => { progress += PROGRESS_INCREMENT; if (progress > 90) return; elements.progressBar.style.width = `${progress}%`; }, PROGRESS_INTERVAL_MS); } async function uploadFiles(files) { resetUploadUI(); const fileLabel = files.length > 1 ? `${files.length} files selected` : files[0].name; elements.fileName.textContent = fileLabel; elements.uploadStatus.textContent = 'Uploading...'; const progressInterval = createProgressSimulator(); const formData = new FormData(); Array.from(files).forEach(file => { formData.append('files', file); }); // Add expiration to formData formData.append('expiration', elements.expirationSelect.value); try { const response = await fetch('/api/upload', { method: 'POST', body: formData }); clearInterval(progressInterval); elements.progressBar.style.width = '100%'; if (!response.ok) { throw new Error('Upload failed'); } const data = await response.json(); setTimeout(() => { elements.uploadStatus.textContent = 'Completed'; displayResults(data.files); saveToHistory(data.files); showToast('Files uploaded successfully!', 'success'); }, 500); } catch (error) { clearInterval(progressInterval); console.error('Upload error:', error); elements.uploadStatus.textContent = 'Failed'; showToast('Upload failed. Please try again.', 'error'); } finally { elements.fileInput.value = ''; } } function isImageFile(filename) { return IMAGE_EXTENSIONS.test(filename); } function createImagePreview(file) { const trigger = document.createElement('div'); trigger.className = 'preview-link'; trigger.style.cursor = 'pointer'; const img = document.createElement('img'); img.src = file.url; img.className = 'result-preview'; trigger.appendChild(img); trigger.onclick = () => openModal(file.url); return trigger; } function createFileLink(file) { const link = document.createElement('a'); link.href = file.url; link.textContent = file.originalName; link.className = 'file-link'; link.target = '_blank'; return link; } function createCopyButton(url) { const btn = document.createElement('button'); btn.className = 'copy-btn'; btn.textContent = 'Copy'; btn.setAttribute('aria-label', `Copy link for ${url}`); btn.onclick = () => copyToClipboard(url, btn); return btn; } function displayResults(files) { elements.resultsContainer.innerHTML = ''; files.forEach(file => { const item = document.createElement('div'); item.className = 'result-item'; if (isImageFile(file.originalName)) { item.appendChild(createImagePreview(file)); } const content = document.createElement('div'); content.className = 'result-content'; content.appendChild(createFileLink(file)); content.appendChild(createCopyButton(file.url)); item.appendChild(content); elements.resultsContainer.appendChild(item); }); elements.resultsContainer.hidden = false; } async function copyToClipboard(text, btn) { try { await navigator.clipboard.writeText(text); const originalText = btn.textContent; btn.textContent = 'Copied!'; btn.classList.add('copied'); showToast('Link copied to clipboard!', 'success'); setTimeout(() => { btn.textContent = originalText; btn.classList.remove('copied'); }, COPY_FEEDBACK_MS); } catch (err) { console.error('Failed to copy:', err); showToast('Failed to copy link', 'error'); } } function initTabs() { elements.tabBtns.forEach(btn => { btn.addEventListener('click', () => { const targetTab = btn.dataset.tab; elements.tabBtns.forEach(b => b.classList.remove('active')); elements.tabContents.forEach(c => c.classList.remove('active')); btn.classList.add('active'); document.getElementById(targetTab).classList.add('active'); }); }); } // History Management function getHistory() { try { return JSON.parse(localStorage.getItem('uploadHistory') || '[]'); } catch { return []; } } function saveToHistory(newFiles) { const history = getHistory(); const updatedHistory = [...newFiles, ...history].slice(0, 50); // Keep last 50 localStorage.setItem('uploadHistory', JSON.stringify(updatedHistory)); renderHistory(); } function showConfirmModal() { elements.confirmModal.hidden = false; requestAnimationFrame(() => elements.confirmModal.classList.add('show')); } function hideConfirmModal() { elements.confirmModal.classList.remove('show'); setTimeout(() => { elements.confirmModal.hidden = true; }, 300); } function initConfirmModal() { elements.cancelConfirmBtn.onclick = hideConfirmModal; elements.confirmDeleteBtn.onclick = () => { localStorage.removeItem('uploadHistory'); renderHistory(); hideConfirmModal(); showToast('History cleared', 'success'); }; elements.confirmModal.addEventListener('click', (e) => { if (e.target === elements.confirmModal) hideConfirmModal(); }); } function renderHistory() { const history = getHistory(); elements.historyList.innerHTML = ''; if (history.length === 0) { elements.historyList.innerHTML = '

No upload history yet.

'; return; } history.forEach(file => { const item = document.createElement('div'); item.className = 'result-item'; // Reuse result item styling if (isImageFile(file.originalName)) { // Create small preview const trigger = document.createElement('div'); trigger.className = 'preview-link'; trigger.style.cursor = 'pointer'; const img = document.createElement('img'); img.src = file.url; img.className = 'result-preview'; trigger.appendChild(img); trigger.setAttribute('role', 'button'); trigger.setAttribute('aria-label', `Preview ${file.originalName}`); trigger.setAttribute('tabindex', '0'); trigger.onkeypress = (e) => { if (e.key === 'Enter') openModal(file.url); }; trigger.onclick = () => openModal(file.url); item.appendChild(trigger); } const content = document.createElement('div'); content.className = 'result-content'; // Wrapper for link and expiration info const infoWrapper = document.createElement('div'); infoWrapper.style.flex = '1'; infoWrapper.style.minWidth = '0'; infoWrapper.style.display = 'flex'; infoWrapper.style.flexDirection = 'column'; infoWrapper.appendChild(createFileLink(file)); if (file.expiresAt) { const expirationSpan = document.createElement('span'); expirationSpan.style.fontSize = '0.75rem'; expirationSpan.style.color = 'var(--text-muted)'; expirationSpan.style.marginTop = '4px'; const expDate = new Date(file.expiresAt); const now = new Date(); const isExpired = expDate < now; if (isExpired) { expirationSpan.textContent = 'Expired'; expirationSpan.style.color = 'var(--error-color)'; } else { expirationSpan.textContent = `Expires: ${expDate.toLocaleString()}`; } infoWrapper.appendChild(expirationSpan); } else { const permanentSpan = document.createElement('span'); permanentSpan.style.fontSize = '0.75rem'; permanentSpan.style.color = 'var(--success-color)'; permanentSpan.style.marginTop = '4px'; permanentSpan.textContent = 'Permanent'; infoWrapper.appendChild(permanentSpan); } content.appendChild(infoWrapper); content.appendChild(createCopyButton(file.url)); item.appendChild(content); elements.historyList.appendChild(item); }); } // Toast Notification System function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = ` ${message} `; elements.toastContainer.appendChild(toast); // Remove after 3 seconds setTimeout(() => { toast.classList.add('hiding'); toast.addEventListener('animationend', () => toast.remove()); }, 3000); } function openModal(src) { elements.modal.hidden = false; requestAnimationFrame(() => { elements.modal.classList.add('show'); }); elements.modalImage.src = src; } function handleCloseModal() { elements.modal.classList.remove('show'); setTimeout(() => { elements.modal.hidden = true; elements.modalImage.src = ''; }, MODAL_TRANSITION_MS); } function initModal() { elements.closeModal.addEventListener('click', handleCloseModal); elements.modal.addEventListener('click', (e) => { if (e.target === elements.modal) { handleCloseModal(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && elements.modal.classList.contains('show')) { handleCloseModal(); } }); } function init() { initDragAndDrop(); initFileInput(); initTabs(); initModal(); } init();