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
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

## 2026-06-03 - Form Submitters in Vanilla JS
**Learning:** In vanilla JavaScript, accessing the button that triggered a form submission using `e.target.querySelector('button[type="submit"]')` can be unreliable if elements intercept pointer events or multiple buttons exist.
**Action:** Use the native `e.submitter` property on the `submit` event to precisely and reliably capture the button that triggered the action, ensuring accurate DOM manipulation (like adding loading states) even if the button lacks a `type="submit"` attribute. Remember to also use a `finally` block to guarantee the state reset happens even if the simulated async operation fails.
92 changes: 68 additions & 24 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 @@ -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;
submitBtn.setAttribute('aria-busy', 'true');
}

try {
this.showToast('Logging in...', 'info');
Expand All @@ -155,6 +163,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 @@ -163,6 +177,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 = 'Creating account...';
submitBtn.disabled = true;
submitBtn.setAttribute('aria-busy', 'true');
}

try {
this.showToast('Creating account...', 'info');
Expand All @@ -174,6 +196,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 @@ -185,7 +213,17 @@ class ClimaAI {
this.showToast('Logged out successfully', 'info');
}

async handleGoogleSignIn() {
async handleGoogleSignIn(e) {
let btn = null;
let originalText = '';
if (e && e.currentTarget) {
btn = e.currentTarget;
originalText = btn.innerHTML;
btn.innerHTML = 'Connecting...';
btn.disabled = true;
btn.setAttribute('aria-busy', 'true');
}

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

Expand All @@ -197,31 +235,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