Skip to content

Commit

Permalink
Convert Login, Signup to function components (#2777)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Nov 14, 2023
1 parent 51f9db2 commit 8947af6
Show file tree
Hide file tree
Showing 19 changed files with 807 additions and 912 deletions.
2 changes: 1 addition & 1 deletion src/components/Buttons/LoadingButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, { ReactElement } from "react";
import { themeColors } from "types/theme";

interface LoadingProps {
buttonProps?: ButtonProps;
buttonProps?: ButtonProps & { "data-testid"?: string };
children?: React.ReactNode;
disabled?: boolean;
loading?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/components/InvalidLink/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function InvalidLink(props: InvalidLinkProps): ReactElement {
<Button
id={`${idAffix}-signUp`}
onClick={() => {
navigate(Path.SignUp);
navigate(Path.Signup);
}}
>
{t("login.signUp")}
Expand Down
2 changes: 1 addition & 1 deletion src/components/LandingPage/LandingButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function SignUpButton(props: SignUpButtonProps): ReactElement {

return (
<LandingButton
onClick={() => navigate(Path.SignUp)}
onClick={() => navigate(Path.Signup)}
textId="login.signUp"
buttonId={`${props.buttonIdPrefix ?? idAffix}-signUp`}
filled
Expand Down
28 changes: 28 additions & 0 deletions src/components/Login/Captcha.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ReCaptcha from "@matt-block/react-recaptcha-v2";
import { Fragment, ReactElement } from "react";

import { RuntimeConfig } from "types/runtimeConfig";

interface CaptchaProps {
onExpire: () => void;
onSuccess: () => void;
}

export default function Captcha(props: CaptchaProps): ReactElement {
return RuntimeConfig.getInstance().captchaRequired() ? (
<div className="form-group" style={{ margin: "5px" }}>
<ReCaptcha
onError={() =>
console.error("Something went wrong; check your connection.")
}
onExpire={props.onExpire}
onSuccess={props.onSuccess}
siteKey={RuntimeConfig.getInstance().captchaSiteKey()}
size="normal"
theme="light"
/>
</div>
) : (
<Fragment />
);
}
210 changes: 210 additions & 0 deletions src/components/Login/Login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { Help } from "@mui/icons-material";
import {
Button,
Card,
CardContent,
Grid,
Link,
TextField,
TextFieldProps,
Typography,
} from "@mui/material";
import {
ChangeEvent,
FormEvent,
ReactElement,
useEffect,
useState,
} from "react";
import { useTranslation } from "react-i18next";

import { BannerType } from "api/models";
import { getBannerText } from "backend";
import router from "browserRouter";
import { LoadingButton } from "components/Buttons";
import Captcha from "components/Login/Captcha";
import { asyncLogIn } 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 theme from "types/theme";
import { openUserGuide } from "utilities/pathUtilities";

export enum LoginId {
ButtonLogIn = "login-log-in-button",
ButtonSignUp = "login-sign-up-button",
ButtonUserGuide = "login-user-guide-button",
FieldPassword = "login-password-field",
FieldUsername = "login-username-field",
Form = "login-form",
}

/** The Login page (also doubles as a Logout page) */
export default function Login(): ReactElement {
const dispatch = useAppDispatch();

const status = useAppSelector(
(state: StoreState) => state.loginState.loginStatus
);

const [banner, setBanner] = useState("");
const [isVerified, setIsVerified] = useState(
!RuntimeConfig.getInstance().captchaRequired()
);
const [password, setPassword] = useState("");
const [passwordError, setPasswordError] = useState(false);
const [username, setUsername] = useState("");
const [usernameError, setUsernameError] = useState(false);

const { t } = useTranslation();

useEffect(() => {
dispatch(reset());
getBannerText(BannerType.Login).then(setBanner);
}, [dispatch]);

const handleUpdatePassword = (
e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
): void => setPassword(e.target.value);

const handleUpdateUsername = (
e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
): void => setUsername(e.target.value);

const logIn = (e: FormEvent): void => {
e.preventDefault();
const p = password.trim();
const u = username.trim();
setPasswordError(!p);
setUsernameError(!u);
if (p && u) {
dispatch(asyncLogIn(u, p));
}
};

const defaultTextFieldProps: TextFieldProps = {
inputProps: { maxLength: 100 },
margin: "normal",
required: true,
style: { width: "100%" },
variant: "outlined",
};

return (
<Grid container justifyContent="center">
<Card style={{ width: 450 }}>
<form id={LoginId.Form} onSubmit={logIn}>
<CardContent>
{/* Title */}
<Typography variant="h5" align="center" gutterBottom>
{t("login.title")}
</Typography>

{/* Username field */}
<TextField
{...defaultTextFieldProps}
autoComplete="username"
autoFocus
error={usernameError}
helperText={usernameError ? t("login.required") : undefined}
id={LoginId.FieldUsername}
label={t("login.username")}
onChange={handleUpdateUsername}
value={username}
/>

{/* Password field */}
<TextField
{...defaultTextFieldProps}
autoComplete="current-password"
error={passwordError}
helperText={passwordError ? t("login.required") : undefined}
id={LoginId.FieldPassword}
label={t("login.password")}
onChange={handleUpdatePassword}
type="password"
value={password}
/>

{/* "Forgot password?" link to reset password */}
{RuntimeConfig.getInstance().emailServicesEnabled() && (
<Typography>
<Link
href={"#"}
onClick={() => router.navigate(Path.PwRequest)}
underline="hover"
variant="subtitle2"
>
{t("login.forgotPassword")}
</Link>
</Typography>
)}

{/* "Failed to log in" */}
{status === LoginStatus.Failure && (
<Typography
style={{ color: "red", marginBottom: 24, marginTop: 24 }}
variant="body2"
>
{t("login.failed")}
</Typography>
)}

<Captcha
onExpire={() => setIsVerified(false)}
onSuccess={() => setIsVerified(true)}
/>

{/* User Guide, Sign Up, and Log In buttons */}
<Grid container justifyContent="flex-end" spacing={2}>
<Grid item xs={4} sm={6}>
<Button id={LoginId.ButtonUserGuide} onClick={openUserGuide}>
<Help />
</Button>
</Grid>

<Grid item xs={4} sm={3}>
<Button
id={LoginId.ButtonSignUp}
onClick={() => router.navigate(Path.Signup)}
variant="outlined"
>
{t("login.signUp")}
</Button>
</Grid>

<Grid item xs={4} sm={3}>
<LoadingButton
buttonProps={{
color: "primary",
id: LoginId.ButtonLogIn,
type: "submit",
}}
disabled={!isVerified}
loading={status === LoginStatus.InProgress}
>
{t("login.login")}
</LoadingButton>
</Grid>
</Grid>

{/* Login announcement banner */}
{!!banner && (
<Typography
style={{
marginTop: theme.spacing(2),
padding: theme.spacing(1),
}}
>
{banner}
</Typography>
)}
</CardContent>
</form>
</Card>
</Grid>
);
}
Loading

0 comments on commit 8947af6

Please sign in to comment.