Skip to content

Commit 74ed80e

Browse files
authored
chore: add button loading state (#95)
* chore: add button loading state * chore: waiting loading
1 parent a2e1da5 commit 74ed80e

File tree

5 files changed

+74
-41
lines changed

5 files changed

+74
-41
lines changed

src/@types/translations/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3104,5 +3104,8 @@
31043104
"continueToSignIn": "Continue to sign in",
31053105
"requireCode": "Please enter a valid verification code.",
31063106
"signing": "Signing in...",
3107-
"invalidOTPCode": "The code is invalid or has expired. Please try again."
3107+
"invalidOTPCode": "The code is invalid or has expired. Please try again.",
3108+
"verifying": "Verifying...",
3109+
"loading": "Loading...",
3110+
"tooManyRequests": "Too many requests, please try again later."
31083111
}

src/components/login/CheckEmail.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys';
55
import React, { useContext, useState } from 'react';
66
import { ReactComponent as Logo } from '@/assets/icons/logo.svg';
77
import { useTranslation } from 'react-i18next';
8-
import { toast } from 'sonner';
98

109
function CheckEmail ({ email, redirectTo }: {
1110
email: string;
@@ -26,7 +25,6 @@ function CheckEmail ({ email, redirectTo }: {
2625
}
2726

2827
setLoading(true);
29-
const id = toast.loading(t('signing'));
3028

3129
try {
3230
await service?.signInOTP({
@@ -43,7 +41,6 @@ function CheckEmail ({ email, redirectTo }: {
4341
}
4442
} finally {
4543
setLoading(false);
46-
toast.dismiss(id);
4744
}
4845
};
4946

@@ -88,11 +85,12 @@ function CheckEmail ({ email, redirectTo }: {
8885
/>
8986

9087
<Button
88+
loading={loading}
9189
onClick={handleSubmit}
9290
size={'lg'}
9391
className={'w-[320px]'}
9492
>
95-
{t('continueToSignIn')}
93+
{loading ? t('verifying') : t('continueToSignIn')}
9694
</Button>
9795
</div>
9896
) : <Button

src/components/login/MagicLink.tsx

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,67 @@ import { Input } from '@/components/ui/input';
44
import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys';
55
import React, { useContext } from 'react';
66
import { useTranslation } from 'react-i18next';
7+
import { useSearchParams } from 'react-router-dom';
78
import { toast } from 'sonner';
89
import isEmail from 'validator/lib/isEmail';
910

1011
function MagicLink ({ redirectTo }: { redirectTo: string }) {
1112
const { t } = useTranslation();
1213
const [email, setEmail] = React.useState<string>('');
13-
const [, setLoading] = React.useState<boolean>(false);
14+
const [loading, setLoading] = React.useState<boolean>(false);
15+
const [error, setError] = React.useState<string>('');
16+
const [, setSearch] = useSearchParams();
1417
const service = useContext(AFConfigContext)?.service;
1518
const handleSubmit = async () => {
19+
if (loading) return;
1620
const isValidEmail = isEmail(email);
1721

1822
if (!isValidEmail) {
1923
toast.error(t('signIn.invalidEmail'));
2024
return;
2125
}
2226

27+
setError('');
2328
setLoading(true);
2429

25-
try {
26-
void service?.signInMagicLink({
27-
email,
28-
redirectTo,
29-
});
30+
void (async () => {
31+
try {
32+
await service?.signInMagicLink({
33+
email,
34+
redirectTo,
35+
});
36+
// eslint-disable-next-line
37+
} catch (e: any) {
38+
if (e.code === 429 || e.response?.status === 429) {
39+
toast.error(t('tooManyRequests'));
40+
} else {
41+
toast.error(e.message);
42+
}
43+
} finally {
44+
setLoading(false);
45+
}
46+
})();
47+
48+
setSearch(prev => {
49+
prev.set('email', email);
50+
prev.set('action', 'checkEmail');
51+
return prev;
52+
});
3053

31-
window.location.href = `/login?action=checkEmail&email=${email}&redirectTo=${redirectTo}`;
32-
} catch (e) {
33-
toast.error(t('web.signInError'));
34-
} finally {
35-
setLoading(false);
36-
}
3754
};
3855

3956
return (
4057
<div className={'flex w-full flex-col items-center justify-center gap-3'}>
4158
<Input
4259
size={'md'}
60+
variant={error ? 'destructive' : 'default'}
61+
helpText={error}
4362
type={'email'}
4463
className={'w-[320px]'}
45-
onChange={(e) => setEmail(e.target.value)}
64+
onChange={(e) => {
65+
setError('');
66+
setEmail(e.target.value);
67+
}}
4668
value={email}
4769
placeholder={t('signIn.pleaseInputYourEmail')}
4870
onKeyDown={e => {
@@ -56,8 +78,9 @@ function MagicLink ({ redirectTo }: { redirectTo: string }) {
5678
onClick={handleSubmit}
5779
size={'lg'}
5880
className={'w-[320px]'}
81+
loading={loading}
5982
>
60-
{t('signIn.signInWithEmail')}
83+
{loading ? t('loading') : t('signIn.signInWithEmail')}
6184
</Button>
6285
</div>
6386
);

src/components/ui/button.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Progress } from '@/components/ui/progress';
12
import * as React from 'react';
23
import { Slot } from '@radix-ui/react-slot';
34
import { cva, type VariantProps } from 'class-variance-authority';
@@ -20,6 +21,7 @@ const buttonVariants = cva(
2021
ghost:
2122
'hover:bg-fill-primary-alpha-5 text-text-primary disabled:bg-fill-transparent disabled:text-text-tertiary',
2223
link: 'hover:bg-transparent text-text-theme hover:text-text-theme-hover !h-fit',
24+
loading: 'opacity-50 cursor-not-allowed',
2325
},
2426
size: {
2527
sm: 'h-7 text-sm px-4 rounded-300 gap-2 font-normal',
@@ -29,22 +31,29 @@ const buttonVariants = cva(
2931
icon: 'size-7 p-1 text-icon-primary disabled:text-icon-tertiary',
3032
'icon-lg': 'size-10 p-[10px] text-icon-primary disabled:text-icon-tertiary',
3133
},
34+
loading: {
35+
true: 'opacity-70 cursor-not-allowed hover:bg-fill-theme-thick',
36+
false: '',
37+
},
3238
},
3339
defaultVariants: {
3440
variant: 'default',
3541
size: 'default',
42+
loading: false,
3643
},
3744
},
3845
);
3946

4047
const Button = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'> &
4148
VariantProps<typeof buttonVariants> & {
42-
asChild?: boolean
49+
asChild?: boolean;
4350
}>(({
4451
className,
4552
variant,
4653
size,
54+
loading,
4755
asChild = false,
56+
children,
4857
...props
4958
}, ref) => {
5059
const Comp = asChild ? Slot : 'button';
@@ -53,9 +62,22 @@ const Button = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'
5362
<Comp
5463
ref={ref}
5564
data-slot="button"
56-
className={cn(buttonVariants({ variant, size, className }))}
65+
className={cn(buttonVariants({ variant, size, className, loading }))}
66+
onClick={e => {
67+
if (loading) return;
68+
if (props.onClick) {
69+
props.onClick(e);
70+
}
71+
}}
5772
{...props}
58-
/>
73+
>
74+
{loading && (
75+
// eslint-disable-next-line
76+
// @ts-ignore
77+
<Progress variant={variant} />
78+
)}
79+
{children}
80+
</Comp>
5981
);
6082
});
6183

src/components/ui/progress.tsx

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,6 @@ const progressVariants = cva(
66
'relative block',
77
{
88
variants: {
9-
size: {
10-
sm: 'h-4 w-4',
11-
md: 'h-6 w-6',
12-
lg: 'h-8 w-8',
13-
xl: 'h-10 w-10',
14-
},
159
variant: {
1610
default: '',
1711
success: '',
@@ -25,15 +19,14 @@ const progressVariants = cva(
2519
},
2620
},
2721
defaultVariants: {
28-
size: 'md',
2922
variant: 'default',
3023
isIndeterminate: false,
3124
},
3225
},
3326
);
3427

3528
const circleVariants = cva(
36-
'fill-fill-transparent ',
29+
'fill-fill-transparent h-5 w-5',
3730
{
3831
variants: {
3932
variant: {
@@ -51,7 +44,7 @@ const circleVariants = cva(
5144
);
5245

5346
const progressCircleVariants = cva(
54-
'fill-fill-transparent transition-all',
47+
'fill-fill-transparent transition-all h-5 w-5',
5548
{
5649
variants: {
5750
variant: {
@@ -79,22 +72,16 @@ export interface ProgressProps
7972

8073
export function Progress ({
8174
value,
82-
size = 'md',
8375
variant = 'default',
8476
strokeLinecap = 'round',
8577
className,
8678
...props
8779
}: ProgressProps) {
8880
// Calculate dimensions based on size variant
89-
const dimensions: number = {
90-
sm: 16,
91-
md: 24,
92-
lg: 32,
93-
xl: 40,
94-
}[size as string] || 24;
81+
const dimensions: number = 20;
82+
83+
const strokeWidth = 2.5;
9584

96-
const strokeWidth = Math.ceil(dimensions * 0.125); // 12.5% of dimensions
97-
9885
const radius = dimensions / 2 - strokeWidth;
9986
const circumference = Math.ceil(2 * Math.PI * radius);
10087
const isIndeterminate = value === undefined;
@@ -111,7 +98,7 @@ export function Progress ({
11198

11299
return (
113100
<div
114-
className={cn(progressVariants({ size, variant, isIndeterminate, className }))}
101+
className={cn(progressVariants({ variant, isIndeterminate, className }))}
115102
{...props}
116103
>
117104
<svg

0 commit comments

Comments
 (0)