|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
|
|
|
|
|
|
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'); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function getHistory() { |
|
|
try { |
|
|
return JSON.parse(localStorage.getItem('uploadHistory') || '[]'); |
|
|
} catch { |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
function saveToHistory(newFiles) { |
|
|
const history = getHistory(); |
|
|
const updatedHistory = [...newFiles, ...history].slice(0, 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 = '<p class="empty-state" style="text-align: center; color: var(--text-secondary); padding: 2rem;">No upload history yet.</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
history.forEach(file => { |
|
|
const item = document.createElement('div'); |
|
|
item.className = 'result-item'; |
|
|
|
|
|
if (isImageFile(file.originalName)) { |
|
|
|
|
|
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'; |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function showToast(message, type = 'info') { |
|
|
const toast = document.createElement('div'); |
|
|
toast.className = `toast ${type}`; |
|
|
|
|
|
toast.innerHTML = ` |
|
|
<span class="toast-message">${message}</span> |
|
|
`; |
|
|
|
|
|
elements.toastContainer.appendChild(toast); |
|
|
|
|
|
|
|
|
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(); |
|
|
|