summaryrefslogtreecommitdiff
path: root/static/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/app.js')
-rw-r--r--static/app.js108
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';
+ }
+});