diff options
Diffstat (limited to 'static/app.js')
| -rw-r--r-- | static/app.js | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..bb09c86 --- /dev/null +++ b/static/app.js @@ -0,0 +1,108 @@ +const MAX_BYTES = 10 * 1024 * 1024; +const ALLOWED_EXTENSIONS = /\.(jpe?g|png|heic)$/i; + +const form = document.querySelector('#upload-form'); +const input = document.querySelector('#image-input'); +const dropZone = document.querySelector('#drop-zone'); +const fileRow = document.querySelector('#file-row'); +const fileName = document.querySelector('#file-name'); +const fileSize = document.querySelector('#file-size'); +const preview = document.querySelector('#preview'); +const removeButton = document.querySelector('#remove-button'); +const submitButton = document.querySelector('#submit-button'); +const status = document.querySelector('#status'); +let previewUrl = null; + +function setStatus(message = '', type = '') { + status.textContent = message; + status.className = `status ${type}`.trim(); +} + +function clearSelection() { + input.value = ''; + fileRow.hidden = true; + dropZone.hidden = false; + submitButton.disabled = true; + if (previewUrl) URL.revokeObjectURL(previewUrl); + previewUrl = null; + preview.removeAttribute('src'); +} + +function validate(file) { + if (!file) return 'Choose an image to continue.'; + if (!ALLOWED_EXTENSIONS.test(file.name)) return 'Choose a JPG, PNG, or HEIC image.'; + if (file.size === 0) return 'The selected image is empty.'; + if (file.size > MAX_BYTES) return 'The image must be 10 MB or smaller.'; + return ''; +} + +function showFile(file) { + const error = validate(file); + if (error) { + clearSelection(); + setStatus(error, 'error'); + return; + } + setStatus(); + fileName.textContent = file.name; + fileSize.textContent = `${(file.size / 1024 / 1024).toFixed(2)} MB`; + if (previewUrl) URL.revokeObjectURL(previewUrl); + previewUrl = URL.createObjectURL(file); + preview.src = previewUrl; + fileRow.hidden = false; + dropZone.hidden = true; + submitButton.disabled = false; +} + +input.addEventListener('change', () => showFile(input.files[0])); +removeButton.addEventListener('click', () => { clearSelection(); setStatus(); }); + +for (const eventName of ['dragenter', 'dragover']) { + dropZone.addEventListener(eventName, (event) => { + event.preventDefault(); + dropZone.classList.add('is-dragging'); + }); +} +for (const eventName of ['dragleave', 'drop']) { + dropZone.addEventListener(eventName, (event) => { + event.preventDefault(); + dropZone.classList.remove('is-dragging'); + }); +} +dropZone.addEventListener('drop', (event) => { + const files = event.dataTransfer.files; + if (files.length !== 1) { + setStatus('Drop exactly one image.', 'error'); + return; + } + const transfer = new DataTransfer(); + transfer.items.add(files[0]); + input.files = transfer.files; + showFile(files[0]); +}); + +form.addEventListener('submit', async (event) => { + event.preventDefault(); + const file = input.files[0]; + const error = validate(file); + if (error) { setStatus(error, 'error'); return; } + + submitButton.disabled = true; + submitButton.querySelector('span').textContent = 'Submitting…'; + setStatus('Securely sending your image…'); + const body = new FormData(); + body.append('image', file); + + try { + const response = await fetch(window.location.pathname, { method: 'POST', body }); + const result = await response.json(); + if (!response.ok) throw new Error(result.message || 'Upload failed.'); + clearSelection(); + setStatus(`${result.message} Receipt: ${result.receipt}`, 'success'); + } catch (error) { + submitButton.disabled = false; + setStatus(error.message || 'Upload failed. Please try again.', 'error'); + } finally { + submitButton.querySelector('span').textContent = 'Submit image'; + } +}); |
