diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000..79451ff --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,3 @@ +## 2024-05-14 - explicit loading states for asynchronous ui interactions +**Learning:** For asynchronous ui interactions without obvious native loading indicators (like form submissions or oauth redirects), explicit visual states are crucial. Users quickly assume the app is frozen or ignore repeated clicks if feedback isn't immediate, especially when network latency or simulated delays are involved. +**Action:** When implementing async form submissions or simulated delays (e.g. auth flows), always add a visual loading state (like '⏳ Loading...' and disabled status) to the interactive element, ensuring safe property access (`e.submitter`) and reliable state restoration in `finally` blocks by using awaitable Promises. diff --git a/web-demo/js/app.js b/web-demo/js/app.js index 11508de..38628ef 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; + let originalText = ''; + + if (e.submitter) { + originalText = e.submitter.innerHTML; + e.submitter.innerHTML = '⏳ Loading...'; + e.submitter.disabled = true; + e.submitter.style.opacity = '0.7'; + } try { this.showToast('Logging in...', 'info'); @@ -155,6 +163,12 @@ class ClimaAI { this.checkSubscription(); } catch (error) { this.showToast(error.message || 'Login failed', 'error'); + } finally { + if (e.submitter) { + e.submitter.innerHTML = originalText; + e.submitter.disabled = false; + e.submitter.style.opacity = '1'; + } } } @@ -163,6 +177,14 @@ class ClimaAI { const name = document.getElementById('registerName').value; const email = document.getElementById('registerEmail').value; const password = document.getElementById('registerPassword').value; + let originalText = ''; + + if (e.submitter) { + originalText = e.submitter.innerHTML; + e.submitter.innerHTML = '⏳ Loading...'; + e.submitter.disabled = true; + e.submitter.style.opacity = '0.7'; + } try { this.showToast('Creating account...', 'info'); @@ -174,6 +196,12 @@ class ClimaAI { this.checkSubscription(); } catch (error) { this.showToast(error.message || 'Registration failed', 'error'); + } finally { + if (e.submitter) { + e.submitter.innerHTML = originalText; + e.submitter.disabled = false; + e.submitter.style.opacity = '1'; + } } } @@ -186,6 +214,15 @@ class ClimaAI { } async handleGoogleSignIn() { + const btn = document.getElementById('googleSignInBtn'); + let originalText = ''; + if (btn) { + originalText = btn.innerHTML; + btn.innerHTML = '⏳ Loading...'; + btn.disabled = true; + btn.style.opacity = '0.7'; + } + try { this.showToast('🔐 Signing in with Google...', 'info'); @@ -197,31 +234,36 @@ class ClimaAI { // 5. Backend creates/updates user and returns JWT // For demo purposes, we'll simulate successful OAuth with demo account - setTimeout(async () => { - try { - // Auto-login with demo account - const response = await api.login('demo@climaai.com', 'Test1234'); - this.user = response.user; - this.showToast('✅ Welcome! Signed in with Google', 'success'); - this.showScreen('homeScreen'); - this.loadWeatherData(); - this.checkSubscription(); - } catch (error) { - this.showToast('Google Sign-In succeeded! Welcome!', 'success'); - // Create a demo user object - this.user = { - email: 'google-user@gmail.com', - full_name: 'Google User', - is_premium: true - }; - this.isPremium = true; - this.showScreen('homeScreen'); - this.loadWeatherData(); - } - }, 1500); // Simulate OAuth redirect delay + await new Promise(resolve => setTimeout(resolve, 1500)); + try { + // Auto-login with demo account + const response = await api.login('demo@climaai.com', 'Test1234'); + this.user = response.user; + this.showToast('✅ Welcome! Signed in with Google', 'success'); + this.showScreen('homeScreen'); + this.loadWeatherData(); + this.checkSubscription(); + } catch (error) { + this.showToast('Google Sign-In succeeded! Welcome!', 'success'); + // Create a demo user object + this.user = { + email: 'google-user@gmail.com', + full_name: 'Google User', + is_premium: true + }; + this.isPremium = true; + this.showScreen('homeScreen'); + this.loadWeatherData(); + } } catch (error) { this.showToast(error.message || 'Google Sign-In failed', 'error'); + } finally { + if (btn) { + btn.innerHTML = originalText; + btn.disabled = false; + btn.style.opacity = '1'; + } } }