diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index 98eb16a48..c9fca1a8c 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -23,9 +23,9 @@ const translation = { }, register: { create_account: 'Create an Account', - action: 'Create', - loading: 'Creating Account...', + action: 'Create Account', have_account: 'Already have an account?', + confirm_password: 'Confirm Password', }, admin_console: { title: 'Admin Console', @@ -287,6 +287,8 @@ const errors = { }, user: { username_exists_register: 'The username has been registered.', + username_forbidden_initial_number: 'Username start with number is prohibited.', + username_invalid_character: 'The username should contain A-Za-z0-9_ only.', email_exists_register: 'The email address has been registered.', phone_exists_register: 'The phone number has been registered.', invalid_email: 'Invalid email address.', @@ -297,8 +299,10 @@ const errors = { identity_exists: 'The social account has been registered.', }, password: { + too_short: 'The password length should no less than {{min}}.', unsupported_encryption_method: 'The encryption method {{name}} is not supported.', pepper_not_found: 'Password pepper not found. Please check your core envs.', + inconsistent_password: 'Inconsistent password.', }, session: { not_found: 'Session not found. Please go back and sign in again.', diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index be913d745..b439df992 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -26,8 +26,8 @@ const translation = { register: { create_account: '创建新账户', action: '创建', - loading: '创建中...', have_account: '已经有账户?', + confirm_password: '确认密码', }, admin_console: { title: '管理面板', @@ -285,6 +285,8 @@ const errors = { }, user: { username_exists_register: '用户名已被注册。', + username_forbidden_initial_number: '用户名不能以数字开头。', + username_invalid_character: '用户名应只包含 A-Za-z0-9_ 字符。', email_exists_register: '邮箱地址已被注册。', phone_exists_register: '手机号码已被注册。', invalid_email: '邮箱地址不正确。', @@ -295,8 +297,10 @@ const errors = { identity_exists: '该社交账号已被注册。', }, password: { + too_short: '密码长度不得小于 {{min}}。', unsupported_encryption_method: '不支持的加密方法 {{name}}。', pepper_not_found: '密码 pepper 未找到。请检查 core 的环境变量。', + inconsistent_password: '密码不一致。', }, session: { not_found: 'Session not found. Please go back and sign in again.', diff --git a/packages/ui/src/assets/icons/arrow.svg b/packages/ui/src/assets/icons/arrow.svg index 5b77a8a0b..ea5d8cb45 100644 --- a/packages/ui/src/assets/icons/arrow.svg +++ b/packages/ui/src/assets/icons/arrow.svg @@ -2,4 +2,10 @@ + + + + + + diff --git a/packages/ui/src/components/Icons/NavArrowIcon.tsx b/packages/ui/src/components/Icons/NavArrowIcon.tsx new file mode 100644 index 000000000..9c4b6014c --- /dev/null +++ b/packages/ui/src/components/Icons/NavArrowIcon.tsx @@ -0,0 +1,17 @@ +import React, { SVGProps } from 'react'; + +import Arrow from '@/assets/icons/arrow.svg'; + +type Props = { + type?: 'prev' | 'next'; +} & SVGProps; + +const NavArrowIcon = ({ type = 'prev', ...rest }: Props) => { + return ( + + + + ); +}; + +export default NavArrowIcon; diff --git a/packages/ui/src/components/Input/PasswordInput.tsx b/packages/ui/src/components/Input/PasswordInput.tsx index d7d964208..98fcf1584 100644 --- a/packages/ui/src/components/Input/PasswordInput.tsx +++ b/packages/ui/src/components/Input/PasswordInput.tsx @@ -8,9 +8,18 @@ import * as styles from './index.module.scss'; export type Props = Omit, 'type'> & { className?: string; error?: ErrorType; + forceHidden?: boolean; }; -const PasswordInput = ({ className, value, error, onFocus, onBlur, ...rest }: Props) => { +const PasswordInput = ({ + className, + value, + error, + forceHidden = false, + onFocus, + onBlur, + ...rest +}: Props) => { // Toggle the password visibility const [type, setType] = useState('password'); const [onInputFocus, setOnInputFocus] = useState(false); @@ -36,7 +45,7 @@ const PasswordInput = ({ className, value, error, onFocus, onBlur, ...rest }: Pr }} {...rest} /> - {value && onInputFocus && ( + {!forceHidden && value && onInputFocus && ( * { + width: 100%; + } + + .inputField { + margin-bottom: _.unit(4); + + &.withError { + margin-bottom: _.unit(2); + } + } + + .terms { + margin: _.unit(7) 0 _.unit(6); + + &.withError { + margin: _.unit(7) 0 _.unit(2) 0; + } + } + + .inputField.withError + .terms { + margin-top: _.unit(9); + } +} diff --git a/packages/ui/src/containers/CreateAccount/index.test.tsx b/packages/ui/src/containers/CreateAccount/index.test.tsx new file mode 100644 index 000000000..637eb9db5 --- /dev/null +++ b/packages/ui/src/containers/CreateAccount/index.test.tsx @@ -0,0 +1,193 @@ +import { fireEvent, render, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { register } from '@/apis/register'; + +import CreateAccount from '.'; + +jest.mock('@/apis/register', () => ({ register: jest.fn(async () => Promise.resolve()) })); + +describe('', () => { + test('default render', () => { + const { queryByText, container } = render(); + expect(container.querySelector('input[name="username"]')).not.toBeNull(); + expect(container.querySelector('input[name="password"]')).not.toBeNull(); + expect(container.querySelector('input[name="confirm_password"]')).not.toBeNull(); + expect(queryByText('register.action')).not.toBeNull(); + expect(queryByText('sign_in.terms_of_use')).not.toBeNull(); + }); + + test('username and password are required', () => { + const { queryAllByText, getByText } = render(); + const submitButton = getByText('register.action'); + fireEvent.click(submitButton); + + expect(queryAllByText('errors:form.required')).toHaveLength(2); + + expect(register).not.toBeCalled(); + }); + + test('username with initial numeric char should throw', () => { + const { queryByText, getByText, container } = render(); + const submitButton = getByText('register.action'); + + const usernameInput = container.querySelector('input[name="username"]'); + + if (usernameInput) { + fireEvent.change(usernameInput, { target: { value: '1username' } }); + } + + fireEvent.click(submitButton); + + expect(queryByText('errors:user.username_forbidden_initial_number')).not.toBeNull(); + + expect(register).not.toBeCalled(); + + // Clear error + if (usernameInput) { + fireEvent.change(usernameInput, { target: { value: 'username' } }); + } + + expect(queryByText('errors:user.username_forbidden_initial_number')).toBeNull(); + }); + + test('username with special character should throw', () => { + const { queryByText, getByText, container } = render(); + const submitButton = getByText('register.action'); + const usernameInput = container.querySelector('input[name="username"]'); + + if (usernameInput) { + fireEvent.change(usernameInput, { target: { value: '@username' } }); + } + + fireEvent.click(submitButton); + + expect(queryByText('errors:user.username_invalid_character')).not.toBeNull(); + + expect(register).not.toBeCalled(); + + // Clear error + if (usernameInput) { + fireEvent.change(usernameInput, { target: { value: 'username' } }); + } + + expect(queryByText('errors:user.username_invalid_character')).toBeNull(); + }); + + test('password less than 6 chars should throw', () => { + const { queryByText, getByText, container } = render(); + const submitButton = getByText('register.action'); + const passwordInput = container.querySelector('input[name="password"]'); + + if (passwordInput) { + fireEvent.change(passwordInput, { target: { value: '12345' } }); + } + + fireEvent.click(submitButton); + + expect(queryByText('errors:password.too_short')).not.toBeNull(); + + expect(register).not.toBeCalled(); + + // Clear error + if (passwordInput) { + fireEvent.change(passwordInput, { target: { value: '123456' } }); + } + + expect(queryByText('errors:password.too_short')).toBeNull(); + }); + + test('password mismatch with confirmPassword should throw', () => { + const { queryByText, getByText, container } = render(); + const submitButton = getByText('register.action'); + const passwordInput = container.querySelector('input[name="password"]'); + const confirmPasswordInput = container.querySelector('input[name="confirm_password"]'); + const usernameInput = container.querySelector('input[name="username"]'); + + if (usernameInput) { + fireEvent.change(usernameInput, { target: { value: 'username' } }); + } + + if (passwordInput) { + fireEvent.change(passwordInput, { target: { value: '123456' } }); + } + + if (confirmPasswordInput) { + fireEvent.change(confirmPasswordInput, { target: { value: '012345' } }); + } + + fireEvent.click(submitButton); + + expect(queryByText('errors:password.inconsistent_password')).not.toBeNull(); + + expect(register).not.toBeCalled(); + + // Clear Error + if (confirmPasswordInput) { + fireEvent.change(confirmPasswordInput, { target: { value: '123456' } }); + } + + expect(queryByText('errors:password.inconsistent_password')).toBeNull(); + }); + + test('terms of use not checked should throw', () => { + const { queryByText, getByText, container } = render(); + const submitButton = getByText('register.action'); + const passwordInput = container.querySelector('input[name="password"]'); + const confirmPasswordInput = container.querySelector('input[name="confirm_password"]'); + const usernameInput = container.querySelector('input[name="username"]'); + + if (usernameInput) { + fireEvent.change(usernameInput, { target: { value: 'username' } }); + } + + if (passwordInput) { + fireEvent.change(passwordInput, { target: { value: '123456' } }); + } + + if (confirmPasswordInput) { + fireEvent.change(confirmPasswordInput, { target: { value: '123456' } }); + } + + fireEvent.click(submitButton); + + expect(queryByText('errors:form.terms_required')).not.toBeNull(); + + expect(register).not.toBeCalled(); + + // Clear Error + const termsButton = getByText('sign_in.terms_agreement_prefix'); + fireEvent.click(termsButton); + + expect(queryByText('errors:form.terms_required')).toBeNull(); + }); + + test('submit form properly', async () => { + const { getByText, container } = render(); + const submitButton = getByText('register.action'); + const passwordInput = container.querySelector('input[name="password"]'); + const confirmPasswordInput = container.querySelector('input[name="confirm_password"]'); + const usernameInput = container.querySelector('input[name="username"]'); + + if (usernameInput) { + fireEvent.change(usernameInput, { target: { value: 'username' } }); + } + + if (passwordInput) { + fireEvent.change(passwordInput, { target: { value: '123456' } }); + } + + if (confirmPasswordInput) { + fireEvent.change(confirmPasswordInput, { target: { value: '123456' } }); + } + + const termsButton = getByText('sign_in.terms_agreement_prefix'); + fireEvent.click(termsButton); + + await waitFor(() => { + fireEvent.click(submitButton); + }); + + expect(register).toBeCalledWith('username', '123456'); + }); +}); diff --git a/packages/ui/src/containers/CreateAccount/index.tsx b/packages/ui/src/containers/CreateAccount/index.tsx new file mode 100644 index 000000000..b5c9f6a89 --- /dev/null +++ b/packages/ui/src/containers/CreateAccount/index.tsx @@ -0,0 +1,230 @@ +/** + * TODO: + * 1. API redesign handle api error and loading status globally in PageContext + * 2. Input field validation, should move the validation rule to the input field scope + * 3. Forgot password URL + * 4. Read terms of use settings from SignInExperience Settings + */ + +import { LogtoErrorI18nKey } from '@logto/phrases'; +import classNames from 'classnames'; +import React, { useState, useEffect, useCallback, useMemo, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { register } from '@/apis/register'; +import Button from '@/components/Button'; +import { ErrorType } from '@/components/ErrorMessage'; +import Input from '@/components/Input'; +import PasswordInput from '@/components/Input/PasswordInput'; +import TermsOfUse from '@/components/TermsOfUse'; +import PageContext from '@/hooks/page-context'; +import useApi from '@/hooks/use-api'; + +import * as styles from './index.module.scss'; + +type FieldState = { + username: string; + password: string; + confirmPassword: string; + termsAgreement: boolean; +}; + +type ErrorState = { + [key in keyof FieldState]?: ErrorType; +}; + +type FieldValidations = { + [key in keyof FieldState]?: (state: FieldState) => ErrorType | undefined; +}; + +const defaultState = { + username: '', + password: '', + confirmPassword: '', + termsAgreement: false, +}; + +const usernameRegx = /^[A-Z_a-z-][\w-]*$/; + +const CreateAccount = () => { + const { t, i18n } = useTranslation(); + const [fieldState, setFieldState] = useState(defaultState); + const [fieldErrors, setFieldErrors] = useState({}); + + const { setToast } = useContext(PageContext); + + const { loading, error, result, run: asyncRegister } = useApi(register); + + const validations = useMemo( + () => ({ + username: ({ username }) => { + if (!username) { + return { code: 'form.required', data: { fieldName: t('sign_in.username') } }; + } + + if (/\d/.test(username.slice(0, 1))) { + return 'user.username_forbidden_initial_number'; + } + + if (!usernameRegx.test(username)) { + return 'user.username_invalid_character'; + } + }, + password: ({ password }) => { + if (!password) { + return { code: 'form.required', data: { fieldName: t('sign_in.password') } }; + } + + if (password.length < 6) { + return { code: 'password.too_short', data: { min: 6 } }; + } + }, + confirmPassword: ({ password, confirmPassword }) => { + if (password !== confirmPassword) { + return { code: 'password.inconsistent_password' }; + } + }, + termsAgreement: ({ termsAgreement }) => { + if (!termsAgreement) { + return 'form.terms_required'; + } + }, + }), + [t] + ); + + const onSubmitHandler = useCallback(() => { + // Should be removed after api redesign + if (loading) { + return; + } + + // Validates + const usernameError = validations.username?.(fieldState); + const passwordError = validations.password?.(fieldState); + + if (usernameError || passwordError) { + setFieldErrors((previous) => ({ + ...previous, + username: usernameError, + password: passwordError, + })); + + return; + } + + const confirmPasswordError = validations.confirmPassword?.(fieldState); + + if (confirmPasswordError) { + setFieldErrors((previous) => ({ ...previous, confirmPassword: confirmPasswordError })); + + return; + } + + const termsAgreementError = validations.termsAgreement?.(fieldState); + + if (termsAgreementError) { + setFieldErrors((previous) => ({ + ...previous, + termsAgreement: termsAgreementError, + })); + + return; + } + + void asyncRegister(fieldState.username, fieldState.password); + }, [fieldState, loading, validations, asyncRegister]); + + useEffect(() => { + if (result?.redirectTo) { + window.location.assign(result.redirectTo); + } + }, [result]); + + useEffect(() => { + // Clear errors + for (const key of Object.keys(fieldState) as [keyof FieldState]) { + setFieldErrors((previous) => { + if (!previous[key]) { + return previous; + } + const error = validations[key]?.(fieldState); + + return { ...previous, [key]: error }; + }); + } + }, [fieldState, validations]); + + useEffect(() => { + // TODO: username exist error message + if (error) { + setToast(i18n.t(`errors:${error.code}`)); + } + }, [error, i18n, setToast]); + + return ( +
+ { + if (target instanceof HTMLInputElement) { + const { value } = target; + setFieldState((state) => ({ ...state, username: value })); + } + }} + onClear={() => { + setFieldState((state) => ({ ...state, username: '' })); + }} + /> + { + if (target instanceof HTMLInputElement) { + const { value } = target; + setFieldState((state) => ({ ...state, password: value })); + } + }} + /> + { + if (target instanceof HTMLInputElement) { + const { value } = target; + setFieldState((state) => ({ ...state, confirmPassword: value })); + } + }} + /> + { + setFieldState((state) => ({ ...state, termsAgreement: checked })); + }} + /> + + + + ); +}; + +export default CreateAccount; diff --git a/packages/ui/src/containers/UsernameSignin/index.module.scss b/packages/ui/src/containers/UsernameSignin/index.module.scss index ab99dded5..9d50c4fbd 100644 --- a/packages/ui/src/containers/UsernameSignin/index.module.scss +++ b/packages/ui/src/containers/UsernameSignin/index.module.scss @@ -27,7 +27,7 @@ } .terms { - margin: _.unit(5) 0; + margin: _.unit(6) 0; &.withError { margin: _.unit(5) 0 _.unit(2) 0; diff --git a/packages/ui/src/containers/UsernameSignin/index.test.tsx b/packages/ui/src/containers/UsernameSignin/index.test.tsx index 0ddfb283a..3cd5c24b9 100644 --- a/packages/ui/src/containers/UsernameSignin/index.test.tsx +++ b/packages/ui/src/containers/UsernameSignin/index.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; import { signInBasic } from '@/apis/sign-in'; @@ -47,7 +47,7 @@ describe('', () => { expect(signInBasic).not.toBeCalled(); }); - test('submit form', () => { + test('submit form', async () => { const { getByText, container } = render(); const submitButton = getByText('sign_in.action'); @@ -65,7 +65,9 @@ describe('', () => { const termsButton = getByText('sign_in.terms_agreement_prefix'); fireEvent.click(termsButton); - fireEvent.click(submitButton); + await waitFor(() => { + fireEvent.click(submitButton); + }); expect(signInBasic).toBeCalledWith('username', 'password'); }); diff --git a/packages/ui/src/containers/UsernameSignin/index.tsx b/packages/ui/src/containers/UsernameSignin/index.tsx index a413f4cd8..233b0ac37 100644 --- a/packages/ui/src/containers/UsernameSignin/index.tsx +++ b/packages/ui/src/containers/UsernameSignin/index.tsx @@ -3,12 +3,12 @@ * 1. API redesign handle api error and loading status globally in PageContext * 2. Input field validation, should move the validation rule to the input field scope * 3. Forgot password URL - * 4. read terms of use settings from SignInExperience Settings + * 4. Read terms of use settings from SignInExperience Settings */ import { LogtoErrorI18nKey } from '@logto/phrases'; import classNames from 'classnames'; -import React, { FC, useState, useCallback, useEffect, useContext } from 'react'; +import React, { FC, useState, useCallback, useEffect, useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { signInBasic } from '@/apis/sign-in'; @@ -33,6 +33,10 @@ type ErrorState = { [key in keyof FieldState]?: ErrorType; }; +type FieldValidations = { + [key in keyof FieldState]?: (state: FieldState) => ErrorType | undefined; +}; + const defaultState: FieldState = { username: '', password: '', @@ -48,41 +52,60 @@ const UsernameSignin: FC = () => { const { error, loading, result, run: asyncSignInBasic } = useApi(signInBasic); + const validations = useMemo( + () => ({ + username: ({ username }) => { + if (!username) { + return { code: 'form.required', data: { fieldName: t('sign_in.username') } }; + } + }, + password: ({ password }) => { + if (!password) { + return { code: 'form.required', data: { fieldName: t('sign_in.password') } }; + } + }, + termsAgreement: ({ termsAgreement }) => { + if (!termsAgreement) { + return 'form.terms_required'; + } + }, + }), + [t] + ); + const onSubmitHandler = useCallback(async () => { // Should be removed after api redesign if (loading) { return; } - if (!fieldState.username) { + // Validates + const usernameError = validations.username?.(fieldState); + const passwordError = validations.password?.(fieldState); + + if (usernameError || passwordError) { setFieldErrors((previous) => ({ ...previous, - username: { code: 'form.required', data: { fieldName: t('sign_in.username') } }, + username: usernameError, + password: passwordError, })); - } - if (!fieldState.password) { - setFieldErrors((previous) => ({ - ...previous, - password: { code: 'form.required', data: { fieldName: t('sign_in.password') } }, - })); - } - - if (!fieldState.username || !fieldState.password) { return; } - if (!fieldState.termsAgreement) { + const termsAgreementError = validations.termsAgreement?.(fieldState); + + if (termsAgreementError) { setFieldErrors((previous) => ({ ...previous, - termsAgreement: 'form.terms_required', + termsAgreement: termsAgreementError, })); return; } void asyncSignInBasic(fieldState.username, fieldState.password); - }, [asyncSignInBasic, loading, t, fieldState]); + }, [loading, validations, fieldState, asyncSignInBasic]); useEffect(() => { if (result?.redirectTo) { @@ -93,13 +116,20 @@ const UsernameSignin: FC = () => { useEffect(() => { // Clear errors for (const key of Object.keys(fieldState) as [keyof FieldState]) { - if (fieldState[key] && fieldErrors[key]) { - setFieldErrors((previous) => ({ ...previous, [key]: undefined })); + if (fieldState[key]) { + setFieldErrors((previous) => { + if (!previous[key]) { + return previous; + } + + return { ...previous, [key]: undefined }; + }); } } - }, [fieldErrors, fieldState]); + }, [fieldState]); useEffect(() => { + // TODO: username password not correct error message if (error) { setToast(i18n.t(`errors:${error.code}`)); } diff --git a/packages/ui/src/jest.setup.ts b/packages/ui/src/jest.setup.ts index 94262d48e..52ca21b8f 100644 --- a/packages/ui/src/jest.setup.ts +++ b/packages/ui/src/jest.setup.ts @@ -15,11 +15,13 @@ Object.defineProperty(window, 'matchMedia', { })), }); +const translation = (key: string) => key; + jest.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => key, + t: translation, i18n: { - t: (key: string) => key, + t: translation, }, }), })); diff --git a/packages/ui/src/pages/Register/index.module.scss b/packages/ui/src/pages/Register/index.module.scss index f46f49b5e..4364e12e6 100644 --- a/packages/ui/src/pages/Register/index.module.scss +++ b/packages/ui/src/pages/Register/index.module.scss @@ -3,46 +3,20 @@ .wrapper { position: relative; padding: _.unit(8); - height: 100%; @include _.flex-column; } -.form { +.navBar { width: 100%; - @include _.flex-column; + margin-bottom: _.unit(6); - > * { - margin-bottom: _.unit(1.5); - } - - .title { - @include _.title; - margin-bottom: _.unit(9); - } - - .box { - margin-bottom: _.unit(-6); - } - - .box, - > input:not([type='button']) { - margin-top: _.unit(3); - width: 100%; - max-width: 320px; - } - - > input[type='button'] { - margin-top: _.unit(12); - } - - .haveAccount { - position: absolute; - bottom: _.unit(10); - } - - .prefix { - font: var(--font-body-bold); - color: var(--color-placeholder); - margin-right: _.unit(0.5); + svg { + margin-left: _.unit(-2); } } + +.title { + width: 100%; + @include _.title; + margin-bottom: _.unit(9); +} diff --git a/packages/ui/src/pages/Register/index.test.tsx b/packages/ui/src/pages/Register/index.test.tsx index cc89cac01..00cfc39b2 100644 --- a/packages/ui/src/pages/Register/index.test.tsx +++ b/packages/ui/src/pages/Register/index.test.tsx @@ -1,23 +1,14 @@ -import { render, fireEvent, waitFor } from '@testing-library/react'; +import { render } from '@testing-library/react'; import React from 'react'; -import { register } from '@/apis/register'; import Register from '@/pages/Register'; jest.mock('@/apis/register', () => ({ register: jest.fn(async () => Promise.resolve()) })); describe('', () => { test('renders without exploding', async () => { - const { queryByText, getByText } = render(); + const { queryByText } = render(); expect(queryByText('register.create_account')).not.toBeNull(); - expect(queryByText('register.have_account')).not.toBeNull(); - - const submit = getByText('register.action'); - fireEvent.click(submit); - - await waitFor(() => { - expect(register).toBeCalled(); - expect(queryByText('register.loading')).not.toBeNull(); - }); + expect(queryByText('register.action')).not.toBeNull(); }); }); diff --git a/packages/ui/src/pages/Register/index.tsx b/packages/ui/src/pages/Register/index.tsx index 2518dbff2..a5b62cbc7 100644 --- a/packages/ui/src/pages/Register/index.tsx +++ b/packages/ui/src/pages/Register/index.tsx @@ -1,76 +1,27 @@ -import classNames from 'classnames'; -import React, { FC, FormEventHandler, useState, useCallback, useEffect } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; -import { register } from '@/apis/register'; -import Button from '@/components/Button'; -import ErrorMessage from '@/components/ErrorMessage'; -import Input from '@/components/Input'; -import PasswordInput from '@/components/Input/PasswordInput'; -import TextLink from '@/components/TextLink'; -import useApi from '@/hooks/use-api'; +import NavArrowIcon from '@/components/Icons/NavArrowIcon'; +import CreateAccount from '@/containers/CreateAccount'; import * as styles from './index.module.scss'; -const Register: FC = () => { +const Register = () => { const { t } = useTranslation(); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - - const { loading, error, result, run: asyncRegister } = useApi(register); - - const signUp: FormEventHandler = useCallback( - async (event) => { - event.preventDefault(); - await asyncRegister(username, password); - }, - [username, password, asyncRegister] - ); - - useEffect(() => { - if (result?.redirectTo) { - window.location.assign(result.redirectTo); - } - }, [result]); + const history = useHistory(); return ( -
-
-
{t('register.create_account')}
- { - if (target instanceof HTMLInputElement) { - const { value } = target; - setUsername(value); - } +
+
+ { + history.goBack(); }} /> - { - if (target instanceof HTMLInputElement) { - const { value } = target; - setPassword(value); - } - }} - /> - {error && } - - -
- {t('register.have_account')} - {t('sign_in.action')} -
- +
+
{t('register.create_account')}
+
); }; diff --git a/packages/ui/src/pages/SignIn/index.module.scss b/packages/ui/src/pages/SignIn/index.module.scss index f7ddf2af9..e315b2c13 100644 --- a/packages/ui/src/pages/SignIn/index.module.scss +++ b/packages/ui/src/pages/SignIn/index.module.scss @@ -3,7 +3,6 @@ .wrapper { position: relative; padding: _.unit(8); - height: 100%; @include _.flex-column; .header { @@ -12,7 +11,7 @@ .createAccount { - position: absolute; - bottom: _.unit(10); + position: fixed; + bottom: _.unit(12); } }