// NextSnap - Polish & UX Enhancements 'use strict'; const Polish = { init() { this.setupLoadingIndicator(); this.setupSmoothScroll(); this.setupFormValidation(); this.setupKeyboardShortcuts(); this.setupOfflineDetection(); this.setupImageLazyLoading(); console.log('Polish enhancements loaded'); }, // Global loading indicator setupLoadingIndicator() { this.loadingBar = document.createElement('div'); this.loadingBar.className = 'loading-bar'; this.loadingBar.style.display = 'none'; document.body.appendChild(this.loadingBar); }, showLoading() { if (this.loadingBar) { this.loadingBar.style.display = 'block'; } }, hideLoading() { if (this.loadingBar) { this.loadingBar.style.display = 'none'; } }, // Smooth scroll for anchor links setupSmoothScroll() { document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', (e) => { const href = anchor.getAttribute('href'); if (href === '#') return; e.preventDefault(); const target = document.querySelector(href); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); }, // Enhanced form validation setupFormValidation() { document.querySelectorAll('form').forEach(form => { form.addEventListener('submit', (e) => { const invalidInputs = form.querySelectorAll(':invalid'); if (invalidInputs.length > 0) { invalidInputs[0].focus(); this.shake(invalidInputs[0]); } }); }); // Real-time validation feedback document.querySelectorAll('input[required], textarea[required]').forEach(input => { input.addEventListener('blur', () => { if (!input.validity.valid) { this.shake(input); } }); }); }, // Shake animation for errors shake(element) { element.style.animation = 'inputShake 0.3s'; setTimeout(() => { element.style.animation = ''; }, 300); }, // Keyboard shortcuts setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Alt+1: Go to capture if (e.altKey && e.key === '1') { e.preventDefault(); window.location.href = '/capture'; } // Alt+2: Go to queue if (e.altKey && e.key === '2') { e.preventDefault(); window.location.href = '/queue'; } // Alt+3: Go to files if (e.altKey && e.key === '3') { e.preventDefault(); window.location.href = '/browser'; } // Alt+R: Refresh/sync if (e.altKey && e.key === 'r') { e.preventDefault(); if (window.SyncEngine && window.SyncEngine.triggerSync) { window.SyncEngine.triggerSync(); } } }); }, // Enhanced offline detection setupOfflineDetection() { let wasOffline = !navigator.onLine; window.addEventListener('online', () => { if (wasOffline) { this.showToast('✓ Back online', 'success', 2000); wasOffline = false; } }); window.addEventListener('offline', () => { this.showToast('⚠️ You are offline', 'warning', 3000); wasOffline = true; }); }, // Lazy loading for images setupImageLazyLoading() { if ('IntersectionObserver' in window) { const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; if (img.dataset.src) { img.src = img.dataset.src; img.removeAttribute('data-src'); imageObserver.unobserve(img); } } }); }); document.querySelectorAll('img[data-src]').forEach(img => { imageObserver.observe(img); }); } }, // Toast notification helper showToast(message, type = 'info', duration = 3000) { let toast = document.getElementById('global-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'global-toast'; toast.className = 'toast'; document.body.appendChild(toast); } toast.textContent = message; toast.className = `toast ${type}`; toast.style.display = 'block'; setTimeout(() => { toast.style.display = 'none'; }, duration); }, // Confirm dialog with better UX async confirm(message, title = 'Confirm') { return new Promise((resolve) => { const modal = this.createConfirmModal(message, title); document.body.appendChild(modal); modal.querySelector('.confirm-yes').onclick = () => { document.body.removeChild(modal); resolve(true); }; modal.querySelector('.confirm-no').onclick = () => { document.body.removeChild(modal); resolve(false); }; modal.onclick = (e) => { if (e.target === modal) { document.body.removeChild(modal); resolve(false); } }; }); }, createConfirmModal(message, title) { const modal = document.createElement('div'); modal.className = 'modal'; modal.innerHTML = ` `; return modal; }, // Copy to clipboard with feedback async copyToClipboard(text) { try { await navigator.clipboard.writeText(text); this.showToast('✓ Copied to clipboard', 'success', 1500); return true; } catch (err) { this.showToast('❌ Failed to copy', 'error', 2000); return false; } }, // Debounce helper debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, // Throttle helper throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }, // Escape HTML escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }, // Format file size formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; }, // Format date relative formatRelativeTime(date) { const now = new Date(); const diffMs = now - date; const diffSecs = Math.floor(diffMs / 1000); const diffMins = Math.floor(diffSecs / 60); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffSecs < 60) return 'just now'; if (diffMins < 60) return `${diffMins} min ago`; if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; return date.toLocaleDateString(); }, // Vibrate if supported (for mobile feedback) vibrate(pattern = 50) { if ('vibrate' in navigator) { navigator.vibrate(pattern); } } }; // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => Polish.init()); } else { Polish.init(); } // Make Polish globally available window.Polish = Polish;