diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000..9d598cf --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,6 @@ +## 2024-05-18 - Missing Loading State Feedback on Async Forms +**Learning:** In the ClimaAI login and registration forms, async network requests are triggered but the submit button lacks visual loading states and remains interactive. This allows multiple submissions and leaves the user wondering if the button click registered. +**Action:** Always provide immediate visual feedback and disable the submit button during async form submissions to improve perceived performance and prevent duplicate requests. Added disabled styles and original text restoration in `app.js`. +## 2026-05-30 - Added Missing Async Form Loading States +**Learning:** Async forms natively do not provide user feedback or prevent duplicate requests during high-latency network actions. This can lead to frustration and accidental duplicate submissions. +**Action:** Handled async submit buttons by storing their original innerHTML, substituting a loading text, and applying a `disabled=true` state wrapped within a `try...finally` block. Used CSS `.btn:disabled` styling to visually communicate the locked interaction state. diff --git a/web-demo/css/style.css b/web-demo/css/style.css index e0d4506..67f8f36 100644 --- a/web-demo/css/style.css +++ b/web-demo/css/style.css @@ -229,6 +229,13 @@ body { box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); } +.btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + .btn-google { background: white; color: #3c4043; diff --git a/web-demo/js/app.js b/web-demo/js/app.js index 11508de..e00ce84 100644 --- a/web-demo/js/app.js +++ b/web-demo/js/app.js @@ -144,6 +144,14 @@ class ClimaAI { e.preventDefault(); const email = document.getElementById('loginEmail').value; const password = document.getElementById('loginPassword').value; + const submitBtn = e.submitter; + let originalText = ''; + + if (submitBtn) { + originalText = submitBtn.innerHTML; + submitBtn.innerHTML = 'Logging in...'; + submitBtn.disabled = true; + } try { this.showToast('Logging in...', 'info'); @@ -155,6 +163,11 @@ class ClimaAI { this.checkSubscription(); } catch (error) { this.showToast(error.message || 'Login failed', 'error'); + } finally { + if (submitBtn) { + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + } } } @@ -163,6 +176,14 @@ class ClimaAI { const name = document.getElementById('registerName').value; const email = document.getElementById('registerEmail').value; const password = document.getElementById('registerPassword').value; + const submitBtn = e.submitter; + let originalText = ''; + + if (submitBtn) { + originalText = submitBtn.innerHTML; + submitBtn.innerHTML = 'Signing up...'; + submitBtn.disabled = true; + } try { this.showToast('Creating account...', 'info'); @@ -174,6 +195,11 @@ class ClimaAI { this.checkSubscription(); } catch (error) { this.showToast(error.message || 'Registration failed', 'error'); + } finally { + if (submitBtn) { + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + } } }