mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2025-11-29 10:47:56 +08:00
chore: add button loading state (#95)
* chore: add button loading state * chore: waiting loading
This commit is contained in:
@@ -3104,5 +3104,8 @@
|
|||||||
"continueToSignIn": "Continue to sign in",
|
"continueToSignIn": "Continue to sign in",
|
||||||
"requireCode": "Please enter a valid verification code.",
|
"requireCode": "Please enter a valid verification code.",
|
||||||
"signing": "Signing in...",
|
"signing": "Signing in...",
|
||||||
"invalidOTPCode": "The code is invalid or has expired. Please try again."
|
"invalidOTPCode": "The code is invalid or has expired. Please try again.",
|
||||||
|
"verifying": "Verifying...",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"tooManyRequests": "Too many requests, please try again later."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys';
|
|||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { ReactComponent as Logo } from '@/assets/icons/logo.svg';
|
import { ReactComponent as Logo } from '@/assets/icons/logo.svg';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
function CheckEmail ({ email, redirectTo }: {
|
function CheckEmail ({ email, redirectTo }: {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -26,7 +25,6 @@ function CheckEmail ({ email, redirectTo }: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const id = toast.loading(t('signing'));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await service?.signInOTP({
|
await service?.signInOTP({
|
||||||
@@ -43,7 +41,6 @@ function CheckEmail ({ email, redirectTo }: {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toast.dismiss(id);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,11 +85,12 @@ function CheckEmail ({ email, redirectTo }: {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
loading={loading}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
size={'lg'}
|
size={'lg'}
|
||||||
className={'w-[320px]'}
|
className={'w-[320px]'}
|
||||||
>
|
>
|
||||||
{t('continueToSignIn')}
|
{loading ? t('verifying') : t('continueToSignIn')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : <Button
|
) : <Button
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys';
|
import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import isEmail from 'validator/lib/isEmail';
|
import isEmail from 'validator/lib/isEmail';
|
||||||
|
|
||||||
function MagicLink ({ redirectTo }: { redirectTo: string }) {
|
function MagicLink ({ redirectTo }: { redirectTo: string }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [email, setEmail] = React.useState<string>('');
|
const [email, setEmail] = React.useState<string>('');
|
||||||
const [, setLoading] = React.useState<boolean>(false);
|
const [loading, setLoading] = React.useState<boolean>(false);
|
||||||
|
const [error, setError] = React.useState<string>('');
|
||||||
|
const [, setSearch] = useSearchParams();
|
||||||
const service = useContext(AFConfigContext)?.service;
|
const service = useContext(AFConfigContext)?.service;
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
if (loading) return;
|
||||||
const isValidEmail = isEmail(email);
|
const isValidEmail = isEmail(email);
|
||||||
|
|
||||||
if (!isValidEmail) {
|
if (!isValidEmail) {
|
||||||
@@ -20,29 +24,47 @@ function MagicLink ({ redirectTo }: { redirectTo: string }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setError('');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
void (async () => {
|
||||||
void service?.signInMagicLink({
|
try {
|
||||||
email,
|
await service?.signInMagicLink({
|
||||||
redirectTo,
|
email,
|
||||||
});
|
redirectTo,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.code === 429 || e.response?.status === 429) {
|
||||||
|
toast.error(t('tooManyRequests'));
|
||||||
|
} else {
|
||||||
|
toast.error(e.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
setSearch(prev => {
|
||||||
|
prev.set('email', email);
|
||||||
|
prev.set('action', 'checkEmail');
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
|
||||||
window.location.href = `/login?action=checkEmail&email=${email}&redirectTo=${redirectTo}`;
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(t('web.signInError'));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex w-full flex-col items-center justify-center gap-3'}>
|
<div className={'flex w-full flex-col items-center justify-center gap-3'}>
|
||||||
<Input
|
<Input
|
||||||
size={'md'}
|
size={'md'}
|
||||||
|
variant={error ? 'destructive' : 'default'}
|
||||||
|
helpText={error}
|
||||||
type={'email'}
|
type={'email'}
|
||||||
className={'w-[320px]'}
|
className={'w-[320px]'}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setError('');
|
||||||
|
setEmail(e.target.value);
|
||||||
|
}}
|
||||||
value={email}
|
value={email}
|
||||||
placeholder={t('signIn.pleaseInputYourEmail')}
|
placeholder={t('signIn.pleaseInputYourEmail')}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
@@ -56,8 +78,9 @@ function MagicLink ({ redirectTo }: { redirectTo: string }) {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
size={'lg'}
|
size={'lg'}
|
||||||
className={'w-[320px]'}
|
className={'w-[320px]'}
|
||||||
|
loading={loading}
|
||||||
>
|
>
|
||||||
{t('signIn.signInWithEmail')}
|
{loading ? t('loading') : t('signIn.signInWithEmail')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
@@ -20,6 +21,7 @@ const buttonVariants = cva(
|
|||||||
ghost:
|
ghost:
|
||||||
'hover:bg-fill-primary-alpha-5 text-text-primary disabled:bg-fill-transparent disabled:text-text-tertiary',
|
'hover:bg-fill-primary-alpha-5 text-text-primary disabled:bg-fill-transparent disabled:text-text-tertiary',
|
||||||
link: 'hover:bg-transparent text-text-theme hover:text-text-theme-hover !h-fit',
|
link: 'hover:bg-transparent text-text-theme hover:text-text-theme-hover !h-fit',
|
||||||
|
loading: 'opacity-50 cursor-not-allowed',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
sm: 'h-7 text-sm px-4 rounded-300 gap-2 font-normal',
|
sm: 'h-7 text-sm px-4 rounded-300 gap-2 font-normal',
|
||||||
@@ -29,22 +31,29 @@ const buttonVariants = cva(
|
|||||||
icon: 'size-7 p-1 text-icon-primary disabled:text-icon-tertiary',
|
icon: 'size-7 p-1 text-icon-primary disabled:text-icon-tertiary',
|
||||||
'icon-lg': 'size-10 p-[10px] text-icon-primary disabled:text-icon-tertiary',
|
'icon-lg': 'size-10 p-[10px] text-icon-primary disabled:text-icon-tertiary',
|
||||||
},
|
},
|
||||||
|
loading: {
|
||||||
|
true: 'opacity-70 cursor-not-allowed hover:bg-fill-theme-thick',
|
||||||
|
false: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
size: 'default',
|
size: 'default',
|
||||||
|
loading: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'> &
|
const Button = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}>(({
|
}>(({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
|
loading,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const Comp = asChild ? Slot : 'button';
|
const Comp = asChild ? Slot : 'button';
|
||||||
@@ -53,9 +62,22 @@ const Button = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'
|
|||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className, loading }))}
|
||||||
|
onClick={e => {
|
||||||
|
if (loading) return;
|
||||||
|
if (props.onClick) {
|
||||||
|
props.onClick(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{loading && (
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore
|
||||||
|
<Progress variant={variant} />
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ const progressVariants = cva(
|
|||||||
'relative block',
|
'relative block',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
|
||||||
sm: 'h-4 w-4',
|
|
||||||
md: 'h-6 w-6',
|
|
||||||
lg: 'h-8 w-8',
|
|
||||||
xl: 'h-10 w-10',
|
|
||||||
},
|
|
||||||
variant: {
|
variant: {
|
||||||
default: '',
|
default: '',
|
||||||
success: '',
|
success: '',
|
||||||
@@ -25,7 +19,6 @@ const progressVariants = cva(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
size: 'md',
|
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
isIndeterminate: false,
|
isIndeterminate: false,
|
||||||
},
|
},
|
||||||
@@ -33,7 +26,7 @@ const progressVariants = cva(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const circleVariants = cva(
|
const circleVariants = cva(
|
||||||
'fill-fill-transparent ',
|
'fill-fill-transparent h-5 w-5',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -51,7 +44,7 @@ const circleVariants = cva(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const progressCircleVariants = cva(
|
const progressCircleVariants = cva(
|
||||||
'fill-fill-transparent transition-all',
|
'fill-fill-transparent transition-all h-5 w-5',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -79,22 +72,16 @@ export interface ProgressProps
|
|||||||
|
|
||||||
export function Progress ({
|
export function Progress ({
|
||||||
value,
|
value,
|
||||||
size = 'md',
|
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
strokeLinecap = 'round',
|
strokeLinecap = 'round',
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ProgressProps) {
|
}: ProgressProps) {
|
||||||
// Calculate dimensions based on size variant
|
// Calculate dimensions based on size variant
|
||||||
const dimensions: number = {
|
const dimensions: number = 20;
|
||||||
sm: 16,
|
|
||||||
md: 24,
|
const strokeWidth = 2.5;
|
||||||
lg: 32,
|
|
||||||
xl: 40,
|
|
||||||
}[size as string] || 24;
|
|
||||||
|
|
||||||
const strokeWidth = Math.ceil(dimensions * 0.125); // 12.5% of dimensions
|
|
||||||
|
|
||||||
const radius = dimensions / 2 - strokeWidth;
|
const radius = dimensions / 2 - strokeWidth;
|
||||||
const circumference = Math.ceil(2 * Math.PI * radius);
|
const circumference = Math.ceil(2 * Math.PI * radius);
|
||||||
const isIndeterminate = value === undefined;
|
const isIndeterminate = value === undefined;
|
||||||
@@ -111,7 +98,7 @@ export function Progress ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(progressVariants({ size, variant, isIndeterminate, className }))}
|
className={cn(progressVariants({ variant, isIndeterminate, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
Reference in New Issue
Block a user