diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000..8b0fd1f --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,4 @@ + +## 2024-05-18 - Safe Async Form Submissions +**Learning:** When adding loading states to vanilla JS form buttons, `e.submitter` must be null-checked to avoid TypeErrors. Additionally, state restoration (restoring text, disabling `aria-busy`, and re-enabling the button) must be placed in a `finally` block to ensure the UI remains usable regardless of network success or failure. +**Action:** Always wrap `e.submitter` state changes in a conditional and use `finally` for UI recovery in async DOM operations. diff --git a/web-demo/css/style.css b/web-demo/css/style.css index e0d4506..f1365ad 100644 --- a/web-demo/css/style.css +++ b/web-demo/css/style.css @@ -229,6 +229,18 @@ body { box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); } +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; + box-shadow: none !important; +} + +.btn:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + .btn-google { background: white; color: #3c4043; diff --git a/web-demo/js/app.js b/web-demo/js/app.js index 11508de..515531e 100644 --- a/web-demo/js/app.js +++ b/web-demo/js/app.js @@ -145,6 +145,15 @@ class ClimaAI { 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; + submitBtn.setAttribute('aria-busy', 'true'); + } + try { this.showToast('Logging in...', 'info'); const response = await api.login(email, password); @@ -155,6 +164,12 @@ class ClimaAI { this.checkSubscription(); } catch (error) { this.showToast(error.message || 'Login failed', 'error'); + } finally { + if (submitBtn) { + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + submitBtn.removeAttribute('aria-busy'); + } } }