diff options
Diffstat (limited to 'FrontEnd')
-rw-r--r-- | FrontEnd/src/index.tsx | 6 | ||||
-rw-r--r-- | FrontEnd/src/locales/en/translation.json | 4 | ||||
-rw-r--r-- | FrontEnd/src/locales/zh/translation.json | 4 | ||||
-rw-r--r-- | FrontEnd/src/views/common/button/Button.tsx | 36 | ||||
-rw-r--r-- | FrontEnd/src/views/common/button/FlatButton.tsx | 31 | ||||
-rw-r--r-- | FrontEnd/src/views/common/button/LoadingButton.tsx | 48 | ||||
-rw-r--r-- | FrontEnd/src/views/common/button/common.ts | 35 | ||||
-rw-r--r-- | FrontEnd/src/views/common/input/InputPanel.tsx | 4 | ||||
-rw-r--r-- | FrontEnd/src/views/register/index.css | 5 | ||||
-rw-r--r-- | FrontEnd/src/views/register/index.tsx | 111 |
10 files changed, 189 insertions, 95 deletions
diff --git a/FrontEnd/src/index.tsx b/FrontEnd/src/index.tsx index ea940004..2affb277 100644 --- a/FrontEnd/src/index.tsx +++ b/FrontEnd/src/index.tsx @@ -17,4 +17,8 @@ import App from "./App"; const container = document.getElementById("app"); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const root = createRoot(container!); -root.render(<App />); +root.render( + <React.StrictMode> + <App /> + </React.StrictMode> +); diff --git a/FrontEnd/src/locales/en/translation.json b/FrontEnd/src/locales/en/translation.json index 5ce4d70e..622ccd18 100644 --- a/FrontEnd/src/locales/en/translation.json +++ b/FrontEnd/src/locales/en/translation.json @@ -25,6 +25,7 @@ "public": "Public To Everyone" }, "register": { + "register": "Register", "username": "Username", "password": "Password", "confirmPassword": "Confirm Password", @@ -33,7 +34,8 @@ "usernameEmpty": "Username can't be empty.", "passwordEmpty": "Password can't be emtpy.", "confirmPasswordWrong": "Password does not match.", - "registerCodeEmpty": "Register code can't be empty." + "registerCodeEmpty": "Register code can't be empty.", + "registerCodeInvalid": "Register code is invalid." } }, "serviceWorker": { diff --git a/FrontEnd/src/locales/zh/translation.json b/FrontEnd/src/locales/zh/translation.json index 721ac81a..8ebc617a 100644 --- a/FrontEnd/src/locales/zh/translation.json +++ b/FrontEnd/src/locales/zh/translation.json @@ -18,6 +18,7 @@ "public": "对所有人公开" }, "register": { + "register": "注册", "username": "用户名", "password": "密码", "confirmPassword": "确认密码", @@ -26,7 +27,8 @@ "usernameEmpty": "用户名不能为空。", "passwordEmpty": "密码不能为空。", "confirmPasswordWrong": "密码不匹配。", - "registerCodeEmpty": "注册码不能为空。" + "registerCodeEmpty": "注册码不能为空。", + "registerCodeInvalid": "注册码无效。" } }, "connectionState": { diff --git a/FrontEnd/src/views/common/button/Button.tsx b/FrontEnd/src/views/common/button/Button.tsx index a39ef8a7..1e4163ff 100644 --- a/FrontEnd/src/views/common/button/Button.tsx +++ b/FrontEnd/src/views/common/button/Button.tsx @@ -1,30 +1,40 @@ import React from "react"; +import classNames from "classnames"; import { useTranslation } from "react-i18next"; -import { calculateProps, CommonButtonProps } from "./common"; +import { convertI18nText, I18nText } from "@/common"; +import { PaletteColorType } from "@/palette"; import "./Button.css"; function _Button( - props: CommonButtonProps & { + props: { + color?: PaletteColorType; + text?: I18nText; outline?: boolean; - customButtonClassName?: string; - }, + } & React.ComponentPropsWithoutRef<"button">, ref: React.ForwardedRef<HTMLButtonElement> -): React.ReactElement | null { +): JSX.Element { const { t } = useTranslation(); - const { customButtonClassName, outline, ...otherProps } = props; + const { color, text, outline, className, children, ...otherProps } = props; - const { newProps, children } = calculateProps( - otherProps, - customButtonClassName ?? "cru-button" + (outline ? " outline" : ""), - t - ); + if (text != null && children != null) { + console.warn("You can't set both text and children props."); + } return ( - <button ref={ref} {...newProps}> - {children} + <button + ref={ref} + className={classNames( + "cru-" + (color ?? "primary"), + "cru-button", + outline && "outline", + className + )} + {...otherProps} + > + {text != null ? convertI18nText(text, t) : children} </button> ); } diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx index 266ea908..a6377708 100644 --- a/FrontEnd/src/views/common/button/FlatButton.tsx +++ b/FrontEnd/src/views/common/button/FlatButton.tsx @@ -1,16 +1,39 @@ import React from "react"; +import { useTranslation } from "react-i18next"; +import classNames from "classnames"; -import { CommonButtonProps } from "./common"; -import Button from "./Button"; +import { convertI18nText, I18nText } from "@/common"; +import { PaletteColorType } from "@/palette"; import "./FlatButton.css"; function _FlatButton( - props: CommonButtonProps, + props: { + color?: PaletteColorType; + text?: I18nText; + } & React.ComponentPropsWithoutRef<"button">, ref: React.ForwardedRef<HTMLButtonElement> ): React.ReactElement | null { + const { t } = useTranslation(); + + const { color, text, className, children, ...otherProps } = props; + + if (text != null && children != null) { + console.warn("You can't set both text and children props."); + } + return ( - <Button ref={ref} customButtonClassName="cru-flat-button" {...props} /> + <button + ref={ref} + className={classNames( + "cru-" + (color ?? "primary"), + "cru-flat-button", + className + )} + {...otherProps} + > + {text != null ? convertI18nText(text, t) : children} + </button> ); } diff --git a/FrontEnd/src/views/common/button/LoadingButton.tsx b/FrontEnd/src/views/common/button/LoadingButton.tsx index a7e34f91..2764f92e 100644 --- a/FrontEnd/src/views/common/button/LoadingButton.tsx +++ b/FrontEnd/src/views/common/button/LoadingButton.tsx @@ -1,28 +1,40 @@ import React from "react"; +import classNames from "classnames"; +import { useTranslation } from "react-i18next"; + +import { convertI18nText, I18nText } from "@/common"; +import { PaletteColorType } from "@/palette"; -import { CommonButtonProps } from "./common"; -import Button from "./Button"; import Spinner from "../Spinner"; -const LoadingButton: React.FC<{ loading?: boolean } & CommonButtonProps> = ({ - loading, - disabled, - color, - ...otherProps -}) => { +function LoadingButton( + props: { + color?: PaletteColorType; + text?: I18nText; + loading?: boolean; + } & React.ComponentPropsWithoutRef<"button"> +): JSX.Element { + const { t } = useTranslation(); + + const { color, text, loading, className, children, ...otherProps } = props; + + if (text != null && children != null) { + console.warn("You can't set both text and children props."); + } + return ( - <Button - color={color} - outline - disabled={disabled || loading} + <button + className={classNames( + "cru-" + (color ?? "primary"), + "cru-button outline", + className + )} {...otherProps} > - {otherProps.children} - {loading ? ( - <Spinner className="cru-align-text-bottom ms-1" color={color} /> - ) : null} - </Button> + {text != null ? convertI18nText(text, t) : children} + {loading && <Spinner />} + </button> ); -}; +} export default LoadingButton; diff --git a/FrontEnd/src/views/common/button/common.ts b/FrontEnd/src/views/common/button/common.ts deleted file mode 100644 index 0d84bae0..00000000 --- a/FrontEnd/src/views/common/button/common.ts +++ /dev/null @@ -1,35 +0,0 @@ -import React from "react"; -import classNames from "classnames"; -import { TFunction } from "i18next"; - -import { convertI18nText, I18nText } from "@/common"; -import { PaletteColorType } from "@/palette"; - -export type CommonButtonProps = { - text?: I18nText; - color?: PaletteColorType; -} & React.ButtonHTMLAttributes<HTMLButtonElement>; - -export function calculateProps( - props: CommonButtonProps, - buttonClassName: string, - t: TFunction -): { - children: React.ReactNode; - newProps: React.ButtonHTMLAttributes<HTMLButtonElement>; -} { - const { text, color, className, children, ...otherProps } = props; - const newProps = { - className: classNames( - buttonClassName, - color != null ? "cru-" + color : "cru-primary", - className - ), - ...otherProps, - }; - - return { - children: text != null ? convertI18nText(text, t) : children, - newProps: newProps, - }; -} diff --git a/FrontEnd/src/views/common/input/InputPanel.tsx b/FrontEnd/src/views/common/input/InputPanel.tsx index 3ac0cb04..c76b8d45 100644 --- a/FrontEnd/src/views/common/input/InputPanel.tsx +++ b/FrontEnd/src/views/common/input/InputPanel.tsx @@ -78,8 +78,8 @@ export type InputPanelError = { export function hasError(e: InputPanelError | null | undefined): boolean { if (e == null) return false; - for (const key in e) { - if (e[key] != null) return true; + for (const key of Object.keys(e)) { + if (e[key as unknown as number] != null) return true; } return false; } diff --git a/FrontEnd/src/views/register/index.css b/FrontEnd/src/views/register/index.css new file mode 100644 index 00000000..c0078b28 --- /dev/null +++ b/FrontEnd/src/views/register/index.css @@ -0,0 +1,5 @@ +.register-page { + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/FrontEnd/src/views/register/index.tsx b/FrontEnd/src/views/register/index.tsx index a051cfaf..d8530fcf 100644 --- a/FrontEnd/src/views/register/index.tsx +++ b/FrontEnd/src/views/register/index.tsx @@ -1,7 +1,41 @@ import React from "react"; -import InputPanel, { InputPanelError } from "../common/input/InputPanel"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; + +import { HttpBadRequestError } from "@/http/common"; +import { getHttpTokenClient } from "@/http/token"; +import { userService, useUser } from "@/services/user"; + +import { LoadingButton } from "../common/button"; +import InputPanel, { + hasError, + InputPanelError, +} from "../common/input/InputPanel"; + +import "./index.css"; + +const validate = (values: string[], dirties: boolean[]): InputPanelError => { + const e: InputPanelError = {}; + if (dirties[0] && values[0].length === 0) { + e[0] = "register.error.usernameEmpty"; + } + if (dirties[1] && values[1].length === 0) { + e[1] = "register.error.passwordEmpty"; + } + if (dirties[2] && values[2] !== values[1]) { + e[2] = "register.error.confirmPasswordWrong"; + } + if (dirties[3] && values[3].length === 0) { + e[3] = "register.error.registerCodeEmpty"; + } + return e; +}; const RegisterPage: React.FC = () => { + const navigate = useNavigate(); + + const { t } = useTranslation(); + const [username, setUsername] = React.useState<string>(""); const [password, setPassword] = React.useState<string>(""); const [confirmPassword, setConfirmPassword] = React.useState<string>(""); @@ -9,27 +43,21 @@ const RegisterPage: React.FC = () => { const [dirty, setDirty] = React.useState<boolean[]>(new Array(4).fill(false)); - const [error, setError] = React.useState<InputPanelError>(); + const [process, setProcess] = React.useState<boolean>(false); - const validate = (): InputPanelError => { - const e: InputPanelError = {}; - if (dirty[0] && username.length === 0) { - e[0] = "register.error.usernameEmpty"; - } - if (dirty[1] && password.length === 0) { - e[1] = "register.error.passwordEmpty"; - } - if (dirty[2] && confirmPassword !== password) { - e[2] = "register.error.confirmPasswordWrong"; - } - if (dirty[3] && registerCode.length === 0) { - e[3] = "register.error.registerCodeEmpty"; + const [inputError, setInputError] = React.useState<InputPanelError>(); + const [resultError, setResultError] = React.useState<string | null>(null); + + const user = useUser(); + + React.useEffect(() => { + if (user != null) { + navigate("/"); } - return e; - }; + }); return ( - <div className="container"> + <div className="container register-page"> <InputPanel scheme={[ { @@ -58,9 +86,52 @@ const RegisterPage: React.FC = () => { newDirty[index] = true; setDirty(newDirty); - setError(validate()); + setInputError(validate(values, newDirty)); + }} + error={inputError} + disable={process} + /> + {resultError && <div className="cru-color-danger">{t(resultError)}</div>} + <LoadingButton + text="register.register" + loading={process} + disabled={hasError(inputError)} + onClick={() => { + const newDirty = dirty.slice().fill(true); + setDirty(newDirty); + const e = validate( + [username, password, confirmPassword, registerCode], + newDirty + ); + if (hasError(e)) { + setInputError(e); + } else { + setProcess(true); + void getHttpTokenClient() + .register({ + username, + password, + registerCode, + }) + .then( + () => { + void userService + .login({ username, password }, true) + .then(() => { + navigate("/"); + }); + }, + (error) => { + if (error instanceof HttpBadRequestError) { + setResultError("register.error.registerCodeInvalid"); + } else { + setResultError("error.network"); + } + setProcess(false); + } + ); + } }} - error={error} /> </div> ); |