Spaces:
Running
Running
Ade Surya Ananda
Enhance file upload functionality: improve drag-and-drop support and add loading state for form submission
79fa81f | /** | |
| * QR Code Generator - Modern UI Script | |
| */ | |
| document.addEventListener('DOMContentLoaded', function() { | |
| localStorage.clear(); | |
| sessionStorage.clear(); | |
| initFileUpload(); | |
| initFormSubmission(); | |
| initDonationModal(); | |
| clearQRResult(); | |
| }); | |
| window.addEventListener('pageshow', function(event) { | |
| if (event.persisted) { | |
| clearQRResult(); | |
| document.getElementById('qr-form').reset(); | |
| hideFileName(); | |
| } | |
| }); | |
| // File Upload | |
| function initFileUpload() { | |
| const fileInput = document.getElementById('logo'); | |
| const dropZone = document.getElementById('file-drop-zone'); | |
| const fileName = document.getElementById('file-name'); | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => { | |
| dropZone.addEventListener(event, e => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }); | |
| }); | |
| ['dragenter', 'dragover'].forEach(event => { | |
| dropZone.addEventListener(event, () => dropZone.classList.add('dragover')); | |
| }); | |
| ['dragleave', 'drop'].forEach(event => { | |
| dropZone.addEventListener(event, () => dropZone.classList.remove('dragover')); | |
| }); | |
| // enable click on the visible drop zone to open the hidden file input | |
| dropZone.addEventListener('click', () => { | |
| // delegate click to the actual input element | |
| fileInput.click(); | |
| }); | |
| dropZone.addEventListener('drop', e => { | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| // assign the dropped files to the file input in a cross‑browser safe way | |
| try { | |
| const dt = new DataTransfer(); // modern browsers support this | |
| for (let i = 0; i < files.length; i++) { | |
| dt.items.add(files[i]); | |
| } | |
| fileInput.files = dt.files; | |
| } catch (err) { | |
| // fallback: assignment may not be allowed in some browsers | |
| console.warn('Unable to directly assign dropped files to input', err); | |
| // we still keep the original files reference for display purposes | |
| } | |
| showFileName(files[0].name); | |
| } | |
| }); | |
| fileInput.addEventListener('change', function() { | |
| if (this.files.length > 0) { | |
| showFileName(this.files[0].name); | |
| } else { | |
| hideFileName(); | |
| } | |
| }); | |
| } | |
| function showFileName(name) { | |
| const el = document.getElementById('file-name'); | |
| el.textContent = name; | |
| el.style.display = 'flex'; | |
| } | |
| function hideFileName() { | |
| document.getElementById('file-name').style.display = 'none'; | |
| } | |
| // Form Submission | |
| function initFormSubmission() { | |
| const form = document.getElementById('qr-form'); | |
| const btn = document.getElementById('generate-btn'); | |
| form.addEventListener('submit', async function(e) { | |
| e.preventDefault(); | |
| const originalText = btn.innerHTML; | |
| // show loading state immediately so the user knows something is happening | |
| btn.innerHTML = '<span class="loading-spinner"></span>Generating...'; | |
| btn.disabled = true; | |
| try { | |
| clearQRResult(); | |
| removeErrors(); | |
| const formData = new FormData(this); | |
| const res = await fetch('/', { method: 'POST', body: formData }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| displayQRCode(data); | |
| } else { | |
| showError(data.error); | |
| } | |
| } catch (err) { | |
| // network failure or unexpected error should still clear the button state | |
| console.error('Submission error:', err); | |
| showError('An error occurred. Please try again.'); | |
| } finally { | |
| btn.innerHTML = originalText; | |
| btn.disabled = false; | |
| } | |
| }); | |
| } | |
| // QR Code Display | |
| function displayQRCode(data) { | |
| const mainContent = document.querySelector('.main-content'); | |
| const result = document.getElementById('qr-result'); | |
| const escaped = escapeHtml(data.text); | |
| // Set content | |
| result.innerHTML = ` | |
| <div class="qr-display"> | |
| <img src="/qr?id=${data.qr_id}&t=${Date.now()}" alt="QR Code" class="qr-code"> | |
| <div class="qr-info"> | |
| <div class="qr-info-label">Encoded Content</div> | |
| <div class="text-content">${escaped}</div> | |
| </div> | |
| <div class="download-section"> | |
| <a href="/qr?id=${data.qr_id}&download=1" download="qrcode.png" class="btn-action btn-download"> | |
| <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/> | |
| </svg> | |
| Download | |
| </a> | |
| <button type="button" class="btn-action" id="copy-btn"> | |
| <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/> | |
| </svg> | |
| Copy Text | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| document.getElementById('copy-btn').addEventListener('click', () => copyToClipboard(data.text)); | |
| // Show result section | |
| mainContent.classList.add('has-qr'); | |
| result.classList.add('visible'); | |
| // Scroll to QR code on mobile - wait for image to load | |
| if (window.innerWidth <= 768) { | |
| const qrImage = result.querySelector('.qr-code'); | |
| if (qrImage) { | |
| const performScroll = () => { | |
| // Scroll to the bottom of the result section in one smooth motion | |
| result.scrollIntoView({ behavior: 'smooth', block: 'end' }); | |
| }; | |
| qrImage.onload = performScroll; | |
| // Fallback if image is cached | |
| if (qrImage.complete) { | |
| performScroll(); | |
| } | |
| } | |
| } | |
| } | |
| function clearQRResult() { | |
| const mainContent = document.querySelector('.main-content'); | |
| const el = document.getElementById('qr-result'); | |
| if (mainContent) { | |
| mainContent.classList.remove('has-qr'); | |
| } | |
| if (el) { | |
| el.classList.remove('visible'); | |
| el.innerHTML = ''; | |
| } | |
| } | |
| // Clipboard | |
| async function copyToClipboard(text) { | |
| const btn = document.getElementById('copy-btn'); | |
| const original = btn.innerHTML; | |
| try { | |
| if (navigator.clipboard && window.isSecureContext) { | |
| await navigator.clipboard.writeText(text); | |
| } else { | |
| const ta = document.createElement('textarea'); | |
| ta.value = text; | |
| ta.style.cssText = 'position:fixed;left:-9999px;top:0'; | |
| document.body.appendChild(ta); | |
| ta.focus(); | |
| ta.select(); | |
| const success = document.execCommand('copy'); | |
| document.body.removeChild(ta); | |
| if (!success) throw new Error('Copy command failed'); | |
| } | |
| btn.innerHTML = `<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> Copied`; | |
| btn.style.background = 'var(--color-success)'; | |
| btn.style.color = 'white'; | |
| btn.style.border = 'none'; | |
| } catch (err) { | |
| console.error('Copy failed:', err); | |
| btn.innerHTML = `<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg> Failed`; | |
| btn.style.background = 'var(--color-error)'; | |
| btn.style.color = 'white'; | |
| btn.style.border = 'none'; | |
| } | |
| setTimeout(() => { | |
| btn.innerHTML = original; | |
| btn.style.background = ''; | |
| btn.style.color = ''; | |
| btn.style.border = ''; | |
| }, 2000); | |
| } | |
| // Errors | |
| function showError(msg) { | |
| const html = `<div class="message message-error">${escapeHtml(msg)}</div>`; | |
| document.querySelector('.main-content').insertAdjacentHTML('beforeend', html); | |
| } | |
| function removeErrors() { | |
| document.querySelectorAll('.message-error').forEach(el => el.remove()); | |
| } | |
| // Utility | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // Donation Modal | |
| function initDonationModal() { | |
| const modal = document.getElementById('donation-modal'); | |
| const openBtn = document.getElementById('donate-btn'); | |
| const closeBtn = document.getElementById('modal-close'); | |
| // QRIS Modal | |
| const qrisModal = document.getElementById('qris-modal'); | |
| const qrisCard = document.getElementById('qris-card'); | |
| const qrisCloseBtn = document.getElementById('qris-modal-close'); | |
| if (!modal || !openBtn) return; | |
| // Open donation modal | |
| openBtn.addEventListener('click', () => { | |
| modal.classList.add('visible'); | |
| document.body.style.overflow = 'hidden'; | |
| }); | |
| // Close donation modal | |
| function closeModal() { | |
| modal.classList.remove('visible'); | |
| document.body.style.overflow = ''; | |
| } | |
| closeBtn.addEventListener('click', closeModal); | |
| // Close on overlay click | |
| modal.addEventListener('click', (e) => { | |
| if (e.target === modal) { | |
| closeModal(); | |
| } | |
| }); | |
| // QRIS Modal handlers | |
| if (qrisCard && qrisModal) { | |
| qrisCard.addEventListener('click', () => { | |
| qrisModal.classList.add('visible'); | |
| }); | |
| function closeQrisModal() { | |
| qrisModal.classList.remove('visible'); | |
| } | |
| qrisCloseBtn.addEventListener('click', closeQrisModal); | |
| qrisModal.addEventListener('click', (e) => { | |
| if (e.target === qrisModal) { | |
| closeQrisModal(); | |
| } | |
| }); | |
| // QRIS Download handler | |
| const qrisDownloadBtn = document.getElementById('qris-download-btn'); | |
| if (qrisDownloadBtn) { | |
| qrisDownloadBtn.addEventListener('click', async () => { | |
| const imageUrl = 'https://purple-given-lark-169.mypinata.cloud/ipfs/bafkreihplwmmtmq6youvqcfpiks4mffrdzc54h5ymqhfiq63bzzjfdiugq'; | |
| const originalText = qrisDownloadBtn.innerHTML; | |
| try { | |
| qrisDownloadBtn.innerHTML = '<span class="loading-spinner"></span>Downloading...'; | |
| qrisDownloadBtn.disabled = true; | |
| const response = await fetch(imageUrl); | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'qris-ade.jpg'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| document.body.removeChild(a); | |
| qrisDownloadBtn.innerHTML = originalText; | |
| qrisDownloadBtn.disabled = false; | |
| } catch (error) { | |
| console.error('Download failed:', error); | |
| qrisDownloadBtn.innerHTML = originalText; | |
| qrisDownloadBtn.disabled = false; | |
| } | |
| }); | |
| } | |
| } | |
| // Close on Escape key | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| if (qrisModal && qrisModal.classList.contains('visible')) { | |
| qrisModal.classList.remove('visible'); | |
| } else if (modal.classList.contains('visible')) { | |
| closeModal(); | |
| } | |
| } | |
| }); | |
| // Copy crypto addresses | |
| const cryptoCards = document.querySelectorAll('.donation-card[data-crypto]'); | |
| cryptoCards.forEach(card => { | |
| card.addEventListener('click', () => { | |
| const address = card.dataset.address; | |
| if (address) { | |
| copyCryptoAddress(card, address); | |
| } | |
| }); | |
| }); | |
| } | |
| // Copy crypto address with visual feedback | |
| async function copyCryptoAddress(card, address) { | |
| const copyIcon = card.querySelector('.copy-icon'); | |
| const checkIcon = card.querySelector('.check-icon'); | |
| const badge = card.querySelector('.copy-badge'); | |
| try { | |
| if (navigator.clipboard && window.isSecureContext) { | |
| await navigator.clipboard.writeText(address); | |
| } else { | |
| const ta = document.createElement('textarea'); | |
| ta.value = address; | |
| ta.style.cssText = 'position:fixed;left:-9999px;top:0'; | |
| document.body.appendChild(ta); | |
| ta.focus(); | |
| ta.select(); | |
| const success = document.execCommand('copy'); | |
| document.body.removeChild(ta); | |
| if (!success) throw new Error('Copy command failed'); | |
| } | |
| // Show success feedback | |
| if (copyIcon) copyIcon.style.display = 'none'; | |
| if (checkIcon) { | |
| checkIcon.style.display = 'block'; | |
| checkIcon.style.color = 'var(--color-success)'; | |
| } | |
| if (badge) badge.style.display = 'inline-flex'; | |
| // Reset after delay | |
| setTimeout(() => { | |
| if (copyIcon) copyIcon.style.display = 'block'; | |
| if (checkIcon) checkIcon.style.display = 'none'; | |
| if (badge) badge.style.display = 'none'; | |
| }, 3000); | |
| } catch (err) { | |
| console.error('Failed to copy address:', err); | |
| } | |
| } |