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 @@
## 2026-06-12 - Adding async loading states to vanilla JS forms
**Learning:** When managing loading states for asynchronous form submissions in vanilla JavaScript, it's crucial to safely access `e.submitter` inside a null-check to prevent `TypeError`s, as `submitter` can be null depending on how the submission was triggered. Furthermore, wrapping the state restoration (e.g., removing `aria-busy`, re-enabling the button) in a `finally` block ensures the UI recovers cleanly regardless of API success or failure, avoiding permanently disabled buttons.
**Action:** Always wrap original element state restoration in a `finally` block, and robustly null-check event properties like `submitter` before accessing or modifying them during async operations.
94 changes: 69 additions & 25 deletions web-demo/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class ClimaAI {

setupEventListeners() {
// Auth
document.getElementById('googleSignInBtn').addEventListener('click', () => this.handleGoogleSignIn());
document.getElementById('googleSignInBtn').addEventListener('click', (e) => this.handleGoogleSignIn(e));
document.getElementById('loginForm').addEventListener('submit', (e) => this.handleLogin(e));
document.getElementById('registerForm').addEventListener('submit', (e) => this.handleRegister(e));
document.getElementById('showRegister').addEventListener('click', (e) => {
Expand Down 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 originalContent = '';
if (submitBtn) {
originalContent = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.setAttribute('aria-busy', 'true');
submitBtn.innerHTML = '⏳ Loading...';
}

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.disabled = false;
submitBtn.removeAttribute('aria-busy');
submitBtn.innerHTML = originalContent;
}
}
}

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 originalContent = '';
if (submitBtn) {
originalContent = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.setAttribute('aria-busy', 'true');
submitBtn.innerHTML = '⏳ Loading...';
}

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.disabled = false;
submitBtn.removeAttribute('aria-busy');
submitBtn.innerHTML = originalContent;
}
}
}

Expand All @@ -185,7 +215,16 @@ class ClimaAI {
this.showToast('Logged out successfully', 'info');
}

async handleGoogleSignIn() {
async handleGoogleSignIn(e) {
const btn = e ? e.currentTarget : null;
let originalContent = '';
if (btn) {
originalContent = btn.innerHTML;
btn.disabled = true;
btn.setAttribute('aria-busy', 'true');
btn.innerHTML = '⏳ Loading...';
}

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

Expand All @@ -197,31 +236,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.disabled = false;
btn.removeAttribute('aria-busy');
btn.innerHTML = originalContent;
}
}
}

Expand Down