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 @@
## 2024-05-18 - Async Loading States with Mock APIs
**Learning:** When adding UI loading states (spinners, disabled buttons) to applications using an in-memory mock client that bypasses the native `fetch` API, transitions happen instantly, making the loading state imperceptible to users and untestable. Furthermore, using `e.submitter` to manipulate the submit button requires carefully handling the original state restoration within a `finally` block to guarantee recovery regardless of API outcome.
**Action:** Always introduce an explicit awaitable delay (e.g., `await new Promise(resolve => setTimeout(resolve, delay))`) when simulating mock async behavior so UX loading states are visible. When manipulating submit buttons during forms, capture the `e.submitter` context safely, apply states (`disabled`, `aria-busy`), and strictly reset those states in a `finally` block.
32 changes: 32 additions & 0 deletions web-demo/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,19 @@ class ClimaAI {
e.preventDefault();
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 = 'Signing in...';
}

try {
this.showToast('Logging in...', 'info');
await new Promise(resolve => setTimeout(resolve, 500)); // Make mock loading perceptible
const response = await api.login(email, password);
this.user = response.user;
this.showToast('Welcome back! 🌤️', 'success');
Expand All @@ -155,6 +165,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 @@ -163,9 +179,19 @@ 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 originalContent = '';

if (submitBtn) {
originalContent = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.setAttribute('aria-busy', 'true');
submitBtn.innerHTML = 'Signing up...';
}

try {
this.showToast('Creating account...', 'info');
await new Promise(resolve => setTimeout(resolve, 500)); // Make mock loading perceptible
const response = await api.register(email, password, name);
this.user = response.user;
this.showToast('Account created! Welcome! 🎉', 'success');
Expand All @@ -174,6 +200,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 Down