Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2023-11-20 - Ensure Safe Button State Restoration
**Learning:** Using `try/finally` blocks for simulated asynchronous operations in vanilla JavaScript is crucial for ensuring that temporary UI states (like loading text, `disabled=true`, and `aria-busy`) are robustly cleaned up. When using callbacks inside `setTimeout`, converting them into an awaitable Promise allows the `finally` block to effectively guarantee state cleanup regardless of success or failure.
**Action:** Always wrap state restoration logic in a `finally` block, and convert non-awaitable asynchronous callbacks (e.g., in `handleGoogleSignIn`) into awaitable Promises when they impact UI state.
4 changes: 4 additions & 0 deletions plan.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
1. Implement loading states for async operations.
Specifically, when logging in or registering, we should show a loading indicator on the submit button, disable it to prevent multiple clicks, and restore it once the operation is done. This also needs to apply to the Google Sign-in button.
2. I will modify `web-demo/css/style.css` to add styling for a disabled button and potentially an inline spinner class.
3. I will modify `web-demo/js/app.js`'s `handleLogin`, `handleRegister`, and `handleGoogleSignIn` to implement the loading state.
1 change: 1 addition & 0 deletions test_auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const { test, expect } = require('@playwright/test');
89 changes: 67 additions & 22 deletions web-demo/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '⏳ Loading...';
submitBtn.disabled = true;
submitBtn.setAttribute('aria-busy', 'true');
}

try {
this.showToast('Logging in...', 'info');
const response = await api.login(email, password);
Expand All @@ -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');
}
}
}

Expand All @@ -164,6 +179,15 @@ class ClimaAI {
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 = '⏳ Loading...';
submitBtn.disabled = true;
submitBtn.setAttribute('aria-busy', 'true');
}

try {
this.showToast('Creating account...', 'info');
const response = await api.register(email, password, name);
Expand All @@ -174,6 +198,12 @@ class ClimaAI {
this.checkSubscription();
} catch (error) {
this.showToast(error.message || 'Registration failed', 'error');
} finally {
if (submitBtn) {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
submitBtn.removeAttribute('aria-busy');
}
}
}

Expand All @@ -186,6 +216,15 @@ class ClimaAI {
}

async handleGoogleSignIn() {
const btn = document.getElementById('googleSignInBtn');
let originalText = '';
if (btn) {
originalText = btn.innerHTML;
btn.innerHTML = '⏳ Loading...';
btn.disabled = true;
btn.setAttribute('aria-busy', 'true');
}

try {
this.showToast('πŸ” Signing in with Google...', 'info');

Expand All @@ -197,31 +236,37 @@ 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)); // Simulate OAuth redirect delay

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.removeAttribute('aria-busy');
}
}
}

Expand Down