Skip to content

Commit f937234

Browse files
feat: standardize button loading state
Refactors the Button component to support an `isLoading` prop, ensuring consistent loading feedback across the application. Replaces manual "Processing..." text in Auth page with this new standard. - Adds `isLoading` prop to `Button` component - Creates `Spinner` component - Updates Auth page to use `isLoading` - Improves accessibility with `aria-disabled` and `role="status"`
1 parent 22f39f1 commit f937234

File tree

4 files changed

+45
-6
lines changed

4 files changed

+45
-6
lines changed

.Jules/palette.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# Palette Journal
2+
3+
## 2024-05-22 - Standardized Button Loading State
4+
**Learning:** The application previously used manual text updates (e.g., "Processing...") for loading states, which causes layout shifts and lacks visual consistency.
5+
**Action:** Implemented a standardized `isLoading` prop on the `Button` component that automatically disables the button and renders a `Spinner`. This pattern should be used for all async actions going forward to ensure a consistent Neobrutalism look and prevent multiple submissions.

web/components/ui/Button.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import React from 'react';
22
import { THEMES } from '../../constants';
33
import { useTheme } from '../../contexts/ThemeContext';
4+
import { Spinner } from './Spinner';
45

56
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
67
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
78
size?: 'sm' | 'md' | 'lg';
9+
isLoading?: boolean;
810
}
911

1012
export const Button: React.FC<ButtonProps> = ({
1113
children,
1214
variant = 'primary',
1315
size = 'md',
16+
isLoading = false,
1417
className = '',
18+
disabled,
1519
...props
1620
}) => {
1721
const { style } = useTheme();
@@ -47,8 +51,11 @@ export const Button: React.FC<ButtonProps> = ({
4751
return (
4852
<button
4953
className={`${baseStyles} ${sizeStyles[size]} ${themeStyles} ${className}`}
54+
disabled={isLoading || disabled}
55+
aria-disabled={isLoading || disabled}
5056
{...props}
5157
>
58+
{isLoading && <Spinner size={size === 'sm' ? 16 : 20} />}
5259
{children}
5360
</button>
5461
);

web/components/ui/Spinner.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import { Loader2 } from 'lucide-react';
3+
4+
interface SpinnerProps {
5+
size?: number;
6+
className?: string;
7+
ariaLabel?: string;
8+
}
9+
10+
export const Spinner: React.FC<SpinnerProps> = ({
11+
size = 24,
12+
className = '',
13+
ariaLabel = 'Loading'
14+
}) => {
15+
return (
16+
<Loader2
17+
size={size}
18+
className={`animate-spin ${className}`}
19+
aria-label={ariaLabel}
20+
role="status"
21+
/>
22+
);
23+
};

web/pages/Auth.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, { useState } from 'react';
44
import { useNavigate } from 'react-router-dom';
55
import { Button } from '../components/ui/Button';
66
import { Input } from '../components/ui/Input';
7+
import { Spinner } from '../components/ui/Spinner';
78
import { THEMES } from '../constants';
89
import { useAuth } from '../contexts/AuthContext';
910
import { useTheme } from '../contexts/ThemeContext';
@@ -170,10 +171,10 @@ export const Auth = () => {
170171
}`}
171172
>
172173
{googleLoading ? (
173-
<div
174-
className="w-5 h-5 border-2 border-black/20 border-t-black rounded-full animate-spin"
175-
role="status"
176-
aria-label="Signing in with Google"
174+
<Spinner
175+
size={20}
176+
className={isNeo ? 'text-black' : 'text-gray-600'}
177+
ariaLabel="Signing in with Google"
177178
/>
178179
) : (
179180
<svg className="w-5 h-5" viewBox="0 0 24 24" role="img" aria-labelledby="google-logo-title">
@@ -251,8 +252,12 @@ export const Auth = () => {
251252
</motion.div>
252253
)}
253254

254-
<Button type="submit" disabled={loading} className={`w-full py-4 text-lg ${isNeo ? 'rounded-none' : ''}`}>
255-
{loading ? 'Processing...' : isLogin ? 'Log In' : 'Create Account'} <ArrowRight size={20} />
255+
<Button
256+
type="submit"
257+
isLoading={loading}
258+
className={`w-full py-4 text-lg ${isNeo ? 'rounded-none' : ''}`}
259+
>
260+
{isLogin ? 'Log In' : 'Create Account'} <ArrowRight size={20} />
256261
</Button>
257262
</form>
258263

0 commit comments

Comments
 (0)