Skip to content

Commit

Permalink
#121 Improve UX of forgot password view
Browse files Browse the repository at this point in the history
  • Loading branch information
wongchito committed Sep 15, 2024
1 parent baf8623 commit 8c41405
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 72 deletions.
177 changes: 115 additions & 62 deletions src/components/menu/account-view/forgot-password-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,35 @@ import {
AlertIcon,
Button,
Flex,
FormControl,
FormLabel,
Heading,
Input,
InputGroup,
InputRightElement,
Stack,
Text,
useToast,
} from '@chakra-ui/react';
import { RmgSection, RmgSectionHeader } from '@railmapgen/rmg-components';
import React from 'react';
import { RmgDebouncedInput, RmgFields, RmgSection, RmgSectionHeader } from '@railmapgen/rmg-components';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRootDispatch } from '../../../redux';
import { fetchLogin } from '../../../redux/account/account-slice';
import { API_ENDPOINT, API_URL } from '../../../util/constants';
import { emailValidator, passwordValidator } from './account-utils';
import { MdCheck } from 'react-icons/md';

const ForgotPasswordView = (props: { setLoginState: (_: 'login' | 'register' | 'forgot-password') => void }) => {
const toast = useToast();
const { t } = useTranslation();
const dispatch = useRootDispatch();

const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
const [resetPasswordToken, setResetPasswordToken] = React.useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [resetPasswordToken, setResetPasswordToken] = useState('');
const [emailResetPasswordSent, setEmailResetPasswordSent] = useState('');
const [isLoading, setIsLoading] = useState(false);

const [emailResetPasswordSent, setEmailResetPasswordSent] = React.useState('');
const isEmailValid = !!email && emailValidator(email);
const areFieldsValid = isEmailValid && !!resetPasswordToken && !!password && passwordValidator(password);

const showErrorToast = (msg: string) =>
toast({
Expand Down Expand Up @@ -59,39 +62,44 @@ const ForgotPasswordView = (props: { setLoginState: (_: 'login' | 'register' | '
};

const handleResetPassword = async () => {
setIsLoading(true);
const resetPasswordParam = new URLSearchParams({ token: resetPasswordToken });
const resetPasswordURL = `${API_URL}${API_ENDPOINT.AUTH_RESET_PASSWORD}?${resetPasswordParam.toString()}`;
const resetPasswordRep = await fetch(resetPasswordURL, {
method: 'POST',
headers: {
accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ password }),
});
if (resetPasswordRep.status !== 204) {
showErrorToast(await resetPasswordRep.text());
return;
}

const { error, username } = (await dispatch(fetchLogin({ email, password }))).payload as {
error?: string;
username?: string;
};
if (error) {
toast({
title: error,
status: 'error' as const,
duration: 9000,
isClosable: true,
});
} else {
toast({
title: t('Welcome ') + username,
status: 'success' as const,
duration: 5000,
isClosable: true,
try {
const resetPasswordRep = await fetch(resetPasswordURL, {
method: 'POST',
headers: {
accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ password }),
});
if (resetPasswordRep.status !== 204) {
showErrorToast(await resetPasswordRep.text());
return;
}

const { error, username } = (await dispatch(fetchLogin({ email, password }))).payload as {
error?: string;
username?: string;
};
if (error) {
toast({
title: error,
status: 'error' as const,
duration: 9000,
isClosable: true,
});
} else {
toast({
title: t('Welcome ') + username,
status: 'success' as const,
duration: 5000,
isClosable: true,
});
}
} finally {
setIsLoading(false);
}
};

Expand All @@ -114,30 +122,75 @@ const ForgotPasswordView = (props: { setLoginState: (_: 'login' | 'register' | '
</Alert>
)}

<Flex p="3" flexDirection="column">
<FormControl>
<FormLabel>{t('Email')}</FormLabel>
<InputGroup size="md">
<Input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<InputRightElement width="4.5rem">
<Button h="1.75rem" size="sm" onClick={handleSendResetPasswordEmail}>
{t('Send reset link')}
</Button>
</InputRightElement>
</InputGroup>
</FormControl>
<FormControl>
<FormLabel>{t('Reset password token')}</FormLabel>
<Input value={resetPasswordToken} onChange={e => setResetPasswordToken(e.target.value)} />
</FormControl>
<FormControl>
<FormLabel>{t('Password')}</FormLabel>
<Input type="password" value={password} onChange={e => setPassword(e.target.value)} />
</FormControl>
<Flex p={2} flexDirection="column">
<RmgFields
fields={[
{
label: t('Email'),
type: 'custom',
component: (
<InputGroup size="sm">
<RmgDebouncedInput
type="email"
value={email}
onDebouncedChange={setEmail}
delay={0}
validator={emailValidator}
/>
<InputRightElement width="auto" bottom={0} top="unset">
{emailResetPasswordSent ? (
<>
<MdCheck />
<Text fontSize="xs" ml={1}>
{t('Reset link sent')}
</Text>
</>
) : (
<Button
size="xs"
onClick={handleSendResetPasswordEmail}
isDisabled={!isEmailValid}
>
{t('Send reset link')}
</Button>
)}
</InputRightElement>
</InputGroup>
),
},
{
label: t('Reset password token'),
type: 'input',
value: resetPasswordToken,
onChange: setResetPasswordToken,
debouncedDelay: 0,
},
{
label: t('Password'),
type: 'input',
variant: 'password',
value: password,
onChange: setPassword,
debouncedDelay: 0,
validator: passwordValidator,
helper: t('Mininum 8 characters. Contain at least 1 letter and 1 number.'),
},
]}
minW="full"
/>

<Stack mt="10">
<Button onClick={handleResetPassword}>{t('Reset password')}</Button>
<Button onClick={() => props.setLoginState('login')}>{t('Back to log in')}</Button>
<Stack mt={1}>
<Button
colorScheme="primary"
onClick={handleResetPassword}
isLoading={isLoading}
isDisabled={!areFieldsValid || isLoading}
>
{t('Reset password')}
</Button>
<Button onClick={() => props.setLoginState('login')} isDisabled={isLoading}>
{t('Back to log in')}
</Button>
</Stack>
</Flex>
</RmgSection>
Expand Down
17 changes: 14 additions & 3 deletions src/components/menu/account-view/register-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
InputGroup,
InputRightElement,
Stack,
Text,
useToast,
} from '@chakra-ui/react';
import { RmgDebouncedInput, RmgFields, RmgSection, RmgSectionHeader } from '@railmapgen/rmg-components';
Expand All @@ -17,6 +18,7 @@ import { useRootDispatch } from '../../../redux';
import { fetchLogin } from '../../../redux/account/account-slice';
import { API_ENDPOINT, API_URL } from '../../../util/constants';
import { emailValidator, passwordValidator } from './account-utils';
import { MdCheck } from 'react-icons/md';

const RegisterView = (props: { setLoginState: (_: 'login' | 'register') => void }) => {
const toast = useToast();
Expand Down Expand Up @@ -144,9 +146,18 @@ const RegisterView = (props: { setLoginState: (_: 'login' | 'register') => void
validator={emailValidator}
/>
<InputRightElement width="auto" bottom={0} top="unset">
<Button size="xs" onClick={handleVerifyEmail} isDisabled={!isEmailValid}>
{t('Send verification code')}
</Button>
{emailVerificationSent ? (
<>
<MdCheck />
<Text fontSize="xs" ml={1}>
{t('Verification code sent')}
</Text>
</>
) : (
<Button size="xs" onClick={handleVerifyEmail} isDisabled={!isEmailValid}>
{t('Send verification code')}
</Button>
)}
</InputRightElement>
</InputGroup>
),
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/translations/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"Email with reset link is sent to: ":"重置密码的邮件已发送至:",
"Email":"邮件地址",
"Send reset link":"发送重置邮件",
"Reset link sent":"重置邮件已发送",
"Reset password token":"重置密码的验证码",
"Password":"密码",
"Reset password":"重置密码",
Expand All @@ -100,6 +101,7 @@
"Name":"名称",
"You may always change it later.":"之后可随时修改",
"Send verification code":"发送验证码",
"Verification code sent": "验证码已发送",
"Verification code":"验证码",
"Mininum 8 characters. Contain at least 1 letter and 1 number.": "8个字符以上,包含至少1个字母和1个数字。",
"Failed to get the RMP save!":"无法获取RMP存档",
Expand Down
14 changes: 8 additions & 6 deletions src/i18n/translations/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,23 @@
"Welcome": "歡迎",
"Forgot password": "忘記密碼",
"Check your email again!": "請再次檢查您的電郵地址!",
"Email with reset link is sent to: ": "重置密碼的電郵已發送至",
"Email with reset link is sent to: ": "重設密碼的連結已傳送至",
"Email": "電郵地址",
"Send reset link": "發送重置電郵",
"Reset password token": "重置密碼驗證碼",
"Send reset link": "傳送重設連結",
"Reset link": "重設連結已傳送",
"Reset password token": "重設密碼驗證碼",
"Password": "密碼",
"Reset password": "重置密碼",
"Reset password": "重設密碼",
"Back to log in": "返回登入",
"Log in": "登入",
"Create an account": "建立新帳戶",
"Sign up": "註冊",
"The email is not valid!": "電郵地址無效!",
"Verification email is sent to: ": "驗證電郵已發送至",
"Verification email is sent to: ": "驗證電郵已傳送至",
"Name": "名稱",
"You may always change it later.": "您可以隨時更改。",
"Send verification code": "發送驗證碼",
"Send verification code": "傳送驗證碼",
"Verification code sent": "驗證碼已傳送",
"Verification code": "驗證碼",
"Mininum 8 characters. Contain at least 1 letter and 1 number.": "8个字元以上,包含至少1個字母和1個數字。",
"Failed to get the RMP save!": "無法獲取RMP存檔",
Expand Down
2 changes: 1 addition & 1 deletion vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default defineConfig({
},
server: {
proxy: {
'^(/rmg/|/rmp/|/rmg-palette/|/rmg-template/|/rmg-templates/|/rmp-gallery/|/rmg-components/|/svg-assets/|/rmg-translate/|/seed-project/|/rmg-runtime/)':
'^(/rmg/|/rmp/|/rmg-palette/|/rmg-template/|/rmg-templates/|/rmp-gallery/|/rmp-designer/|/rmg-components/|/svg-assets/|/rmg-translate/|/seed-project/|/rmg-runtime/)':
{
target: 'https://railmapgen.github.io',
changeOrigin: true,
Expand Down

0 comments on commit 8c41405

Please sign in to comment.