uploader / public /app.js
vyles's picture
feat: enhance ui with consistent scrollbar and remove api docs tab
2590c3f
/**
* 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 = '<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'; // 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 = `
<span class="toast-message">${message}</span>
`;
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();