From b71f77c4f38eee3c2ee1250e189476ddbc74b8aa Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 2 Nov 2023 16:26:03 -0400 Subject: [PATCH] Convert Signup to function component --- src/components/Login/Login.tsx | 4 +- src/components/Login/Redux/LoginActions.ts | 6 +- src/components/Login/Signup.tsx | 290 +++++++++++++++++++++ 3 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 src/components/Login/Signup.tsx diff --git a/src/components/Login/Login.tsx b/src/components/Login/Login.tsx index 388723f0bc..3812623c5d 100644 --- a/src/components/Login/Login.tsx +++ b/src/components/Login/Login.tsx @@ -73,7 +73,7 @@ export default function Login(): ReactElement { e: ChangeEvent ): void => setUsername(e.target.value); - const login = (e: FormEvent): void => { + const logIn = (e: FormEvent): void => { e.preventDefault(); const p = password.trim(); const u = username.trim(); @@ -87,7 +87,7 @@ export default function Login(): ReactElement { return ( -
+ {/* Title */} diff --git a/src/components/Login/Redux/LoginActions.ts b/src/components/Login/Redux/LoginActions.ts index 883c8edea2..2d795b2c7d 100644 --- a/src/components/Login/Redux/LoginActions.ts +++ b/src/components/Login/Redux/LoginActions.ts @@ -66,7 +66,8 @@ export function asyncSignUp( name: string, username: string, email: string, - password: string + password: string, + onSuccess?: () => void ) { return async (dispatch: StoreStateDispatch) => { dispatch(signupAttempt(username)); @@ -77,6 +78,9 @@ export function asyncSignUp( .addUser(user) .then(() => { dispatch(signupSuccess()); + if (onSuccess) { + onSuccess(); + } setTimeout(() => { dispatch(asyncLogIn(username, password)); }, 1000); diff --git a/src/components/Login/Signup.tsx b/src/components/Login/Signup.tsx new file mode 100644 index 0000000000..1a65333ed6 --- /dev/null +++ b/src/components/Login/Signup.tsx @@ -0,0 +1,290 @@ +import { + Button, + Card, + CardContent, + Grid, + TextField, + Typography, +} from "@mui/material"; +import { ChangeEvent, ReactElement, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import router from "browserRouter"; +import { LoadingDoneButton } from "components/Buttons"; +import Captcha from "components/Login/Captcha"; +import { asyncSignUp } from "components/Login/Redux/LoginActions"; +import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; +import { reset } from "rootActions"; +import { StoreState } from "types"; +import { useAppDispatch, useAppSelector } from "types/hooks"; +import { Path } from "types/path"; +import { RuntimeConfig } from "types/runtimeConfig"; +import { + meetsPasswordRequirements, + meetsUsernameRequirements, +} from "utilities/utilities"; + +enum SignupField { + Email, + Name, + Password1, + Password2, + Username, +} + +type SignupError = Record; +type SignupText = Record; + +const defaultSignupError: SignupError = { + [SignupField.Email]: false, + [SignupField.Name]: false, + [SignupField.Password1]: false, + [SignupField.Password2]: false, + [SignupField.Username]: false, +}; +const defaultSignupText: SignupText = { + [SignupField.Email]: "", + [SignupField.Name]: "", + [SignupField.Password1]: "", + [SignupField.Password2]: "", + [SignupField.Username]: "", +}; + +export enum SignupIds { + ButtonLogIn = "signup-log-in-button", + ButtonSignUp = "signup-sign-up-button", + ButtonUserGuide = "signup-user-guide-button", + FieldEmail = "signup-email-field", + FieldName = "signup-name-field", + FieldPassword1 = "signup-password1-field", + FieldPassword2 = "signup-password2-field", + FieldUsername = "signup-username-field", + Form = "signup-form", +} + +// Chrome silently converts non-ASCII characters in a Textfield of type="email". +// Use punycode.toUnicode() to convert them from punycode back to Unicode. +// eslint-disable-next-line @typescript-eslint/no-var-requires +const punycode = require("punycode/"); + +interface SignupProps { + returnToEmailInvite?: () => void; +} + +/** The signup page */ +export default function Signup(props: SignupProps): ReactElement { + const dispatch = useAppDispatch(); + + const { error, signupStatus } = useAppSelector( + (state: StoreState) => state.loginState + ); + + const [fieldError, setFieldError] = useState(defaultSignupError); + const [fieldText, setFieldText] = useState(defaultSignupText); + const [isVerified, setIsVerified] = useState( + !RuntimeConfig.getInstance().captchaRequired() + ); + + const { t } = useTranslation(); + + useEffect(() => { + const search = window.location.search; + const email = new URLSearchParams(search).get("email"); + if (email) { + setFieldText((prev) => ({ ...prev, [SignupField.Email]: email })); + } + dispatch(reset()); + }, [dispatch]); + + const errorField = (field: SignupField): void => { + setFieldText({ ...fieldText, [field]: true }); + }; + + const checkUsername = (): void => { + if (!meetsUsernameRequirements(fieldText[SignupField.Username])) { + errorField(SignupField.Username); + } + }; + + const updateField = ( + e: ChangeEvent, + field: SignupField + ): void => { + const partialRecord = { [field]: e.target.value }; + setFieldText((prev) => ({ ...prev, ...partialRecord })); + }; + + const signUp = async (e: React.FormEvent): Promise => { + e.preventDefault(); + const name = fieldText[SignupField.Name].trim(); + const username = fieldText[SignupField.Username].trim(); + const email = punycode.toUnicode(fieldText[SignupField.Email].trim()); + const password1 = fieldText[SignupField.Password1].trim(); + const password2 = fieldText[SignupField.Password2].trim(); + + // Error checking. + const err: SignupError = { + [SignupField.Name]: !name, + [SignupField.Email]: !email, + [SignupField.Password1]: !meetsPasswordRequirements(password1), + [SignupField.Password2]: password1 !== password2!, + [SignupField.Username]: !meetsUsernameRequirements(username), + }; + if (Object.values(err).some((e) => e)) { + setFieldError(err); + } else { + await dispatch( + asyncSignUp(name, username, email, password1, props.returnToEmailInvite) + ); + } + }; + + return ( + + + signUp(e)}> + + {/* Title */} + + {t("signup.signUpNew")} + + + {/* Name field */} + updateField(e, SignupField.Name)} + error={fieldError[SignupField.Name]} + helperText={ + fieldError[SignupField.Name] ? t("signup.required") : undefined + } + variant="outlined" + style={{ width: "100%" }} + margin="normal" + inputProps={{ maxLength: 100 }} + /> + + {/* Username field */} + updateField(e, SignupField.Username)} + onBlur={() => checkUsername()} + error={fieldError[SignupField.Username]} + helperText={t("signup.usernameRequirements")} + variant="outlined" + style={{ width: "100%" }} + margin="normal" + inputProps={{ maxLength: 100 }} + /> + + {/* email field */} + updateField(e, SignupField.Email)} + error={fieldError[SignupField.Email]} + variant="outlined" + style={{ width: "100%" }} + margin="normal" + inputProps={{ maxLength: 100 }} + /> + + {/* Password field */} + updateField(e, SignupField.Password1)} + error={fieldError[SignupField.Password1]} + helperText={t("signup.passwordRequirements")} + variant="outlined" + style={{ width: "100%" }} + margin="normal" + inputProps={{ maxLength: 100 }} + /> + + {/* Confirm Password field */} + updateField(e, SignupField.Password2)} + error={fieldError[SignupField.Password2]} + helperText={ + fieldError[SignupField.Password1] + ? t("signup.confirmPasswordError") + : undefined + } + variant="outlined" + style={{ width: "100%" }} + margin="normal" + inputProps={{ maxLength: 100 }} + /> + + {/* "Failed to sign up" */} + {!!error && ( + + {t(error)} + + )} + + setIsVerified(false)} + onSuccess={() => setIsVerified(true)} + /> + + {/* Sign Up and Log In buttons */} + + + + + + + {t("signup.signUp")} + + + + + + + + ); +}