Skip to content

Commit

Permalink
Web: Better registration form (#775)
Browse files Browse the repository at this point in the history
  • Loading branch information
postrowinski committed May 8, 2023
1 parent 1156677 commit e0a3d53
Show file tree
Hide file tree
Showing 14 changed files with 587 additions and 307 deletions.
3 changes: 2 additions & 1 deletion mwdb/web/src/commons/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./useCheckCapabilities";
export { useNavRedirect } from "./useNavRedirect";
export { useCheckCapabilities } from "./useCheckCapabilities";
22 changes: 22 additions & 0 deletions mwdb/web/src/commons/hooks/useNavRedirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useCallback } from "react";
import { useNavigate, useLocation } from "react-router-dom";

export function useNavRedirect() {
const navigate = useNavigate();
const location = useLocation();

function redirectTo(url) {
navigate(url);
}

const goBackToPrevLocation = useCallback(() => {
const locationState = location.state || {};
const prevLocation = locationState.prevLocation || "/";
navigate(prevLocation);
}, [location]);

return {
goBackToPrevLocation,
redirectTo,
};
}
15 changes: 15 additions & 0 deletions mwdb/web/src/commons/ui/FormError.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from "react";
import { isNil } from "lodash";

export default function FormError(props) {
const { errorField } = props;
if (isNil(errorField)) {
return <></>;
}

return (
<div className="invalid-feedback" style={{ display: "block" }}>
{errorField.message}
</div>
);
}
11 changes: 11 additions & 0 deletions mwdb/web/src/commons/ui/Label.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";

export default function Label(props) {
const { label, required, htmlFor } = props;

return (
<label className={required ? "required" : ""} htmlFor={htmlFor}>
{label}
</label>
);
}
21 changes: 21 additions & 0 deletions mwdb/web/src/commons/ui/LoadingSpinner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { useMemo } from "react";

export default function LoadingSpinner(props) {
//types is one of: [primary, secondary, success, danger, warning, info, light, dark]
const { loading, type } = props;
if (!loading) {
return <></>;
}

const className = useMemo(() => {
return `spinner-border ${
type ? `text-${type}` : ""
} mr-2 spinner-border-sm`.trim();
}, [type]);

return (
<span className={className} role="status">
<span className="sr-only">Loading...</span>
</span>
);
}
3 changes: 3 additions & 0 deletions mwdb/web/src/commons/ui/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export { default as SortedList } from "./SortedList";
export { default as View, useViewAlert } from "./View";
export { default as ActionCopyToClipboard } from "./ActionCopyToClipboard";
export { RequiresAuth, RequiresCapability } from "./RequiresAuth";
export { default as FormError } from "./FormError";
export { default as Label } from "./Label";
export { default as LoadingSpinner } from "./LoadingSpinner";

export { Tag, TagList, getStyleForTag } from "./Tag";
export {
Expand Down
4 changes: 2 additions & 2 deletions mwdb/web/src/components/UserLogin.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export default function UserLogin() {
>
<Extension ident="userLoginNote" />
<div className="form-group">
<label>Login</label>
<label className="required">Login</label>
<input
type="text"
name="login"
Expand All @@ -115,7 +115,7 @@ export default function UserLogin() {
/>
</div>
<div className="form-group">
<label>Password</label>
<label className="required">Password</label>
<input
type="password"
name="password"
Expand Down
224 changes: 141 additions & 83 deletions mwdb/web/src/components/UserPasswordRecover.jsx
Original file line number Diff line number Diff line change
@@ -1,109 +1,167 @@
import React, { useState, useContext } from "react";
import React, { useState, useContext, useRef } from "react";
import ReCAPTCHA from "react-google-recaptcha";
import { toast } from "react-toastify";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as Yup from "yup";

import { api } from "@mwdb-web/commons/api";
import { ConfigContext } from "@mwdb-web/commons/config";
import { View, getErrorMessage } from "@mwdb-web/commons/ui";
import {
View,
getErrorMessage,
Label,
FormError,
LoadingSpinner,
} from "@mwdb-web/commons/ui";
import { useNavRedirect } from "@mwdb-web/commons/hooks";

export default function UserPasswordRecover() {
const initialState = {
login: "",
email: "",
};
const formFields = {
login: "login",
email: "email",
recaptcha: "recaptcha",
};

const config = useContext(ConfigContext);
const [fieldState, setFieldState] = useState(initialState);
const [success, setSuccess] = useState(false);
const [recaptcha, setRecaptcha] = useState(null);
const validationSchema = Yup.object().shape({
[formFields.login]: Yup.string().required("Login is required"),
[formFields.email]: Yup.string()
.required("Email is required")
.matches(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g, "Value is not email"),
[formFields.recaptcha]: Yup.string()
.nullable()
.required("Recaptcha is required"),
});

const handleInputChange = (event) => {
const value = event.target.value;
const name = event.target.name;
const formOptions = {
resolver: yupResolver(validationSchema),
mode: "onSubmit",
reValidateMode: "onSubmit",
shouldFocusError: true,
};

export default function UserPasswordRecover() {
const config = useContext(ConfigContext);
const { redirectTo } = useNavRedirect();
const {
register,
handleSubmit,
formState: { errors },
reset,
setValue,
} = useForm(formOptions);

setFieldState((fieldState) => ({
...fieldState,
[name]: value,
}));
};
const [loading, setLoading] = useState(false);
const captchaRef = useRef(null);

async function recoverPassword() {
async function recoverPassword(values) {
try {
setLoading(true);
await api.authRecoverPassword(
fieldState.login,
fieldState.email,
recaptcha
values.login,
values.email,
values.recaptcha
);
setSuccess(true);

toast("Password reset link has been sent to the e-mail address", {
type: "success",
});
setLoading(false);
redirectTo("/login");
} catch (error) {
toast(getErrorMessage(error), {
type: "error",
});
setFieldState(initialState);
setLoading(false);
reset();
} finally {
captchaRef.current?.reset();
setValue(formFields.recaptcha, null);
}
}

const onCaptchaChange = (value) => {
setRecaptcha(value);
};

const handleSubmit = (event) => {
event.preventDefault();
recoverPassword();
};

return (
<View>
<h2>Recover password</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Login</label>
<input
type="text"
name="login"
value={fieldState.login}
onChange={handleInputChange}
className="form-control"
disabled={success}
required
/>
</div>
<div className="form-group">
<label>Email</label>
<input
type="email"
name="email"
value={fieldState.email}
onChange={handleInputChange}
className="form-control"
disabled={success}
required
/>
</div>
<div>
<label>
Please enter the information above to recover your
password.
</label>
</div>
{config.config["recaptcha_site_key"] ? (
<ReCAPTCHA
sitekey={config.config["recaptcha_site_key"]}
onChange={onCaptchaChange}
/>
) : (
[]
)}
<input
type="submit"
value="Submit"
className="btn btn-primary"
disabled={success}
/>
</form>
</View>
<div className="user-password-recover">
<div className="user-password-recover__background" />
<div className="user-password-recover__container">
<View>
<h2 className="text-center">Recover password</h2>
<form onSubmit={handleSubmit(recoverPassword)}>
<div className="form-group">
<Label
label="Login"
required
htmlFor={formFields.login}
/>
<input
{...register(formFields.login)}
id={formFields.login}
className={`form-control ${
errors.login ? "is-invalid" : ""
}`}
/>
<FormError errorField={errors.login} />
</div>
<div className="form-group">
<Label
label="Email"
required
htmlFor={formFields.email}
/>
<input
{...register(formFields.email)}
id={formFields.email}
className={`form-control ${
errors.email ? "is-invalid" : ""
}`}
/>
<FormError errorField={errors.email} />
</div>
<div>
<p>
Please enter the information above to recover
your password.
</p>
</div>
{config.config["recaptcha_site_key"] && (
<>
<ReCAPTCHA
ref={captchaRef}
style={{
display: "flex",
justifyContent: "center",
marginBottom: 12,
}}
sitekey={
config.config["recaptcha_site_key"]
}
onChange={(val) =>
setValue(formFields.recaptcha, val)
}
/>
<div className="text-center">
<FormError errorField={errors.recaptcha} />
</div>
</>
)}
<div className="d-flex justify-content-between">
<button
className="btn btn-outline-primary btn-lg"
onClick={() => redirectTo("/login")}
>
Back
</button>
<button
type="submit"
className="btn btn-primary btn-lg"
disabled={loading}
>
<LoadingSpinner loading={loading} />
Submit
</button>
</div>
</form>
</View>
</div>
</div>
);
}

0 comments on commit e0a3d53

Please sign in to comment.