-
-
-
- Unleash
-
-
- Committed to creating new ways of developing
-
-
-
-
- }
- />
-
-
+
);
};
diff --git a/frontend/src/component/user/Login/Login.styles.js b/frontend/src/component/user/Login/Login.styles.js
index 8db086628f..39068eb550 100644
--- a/frontend/src/component/user/Login/Login.styles.js
+++ b/frontend/src/component/user/Login/Login.styles.js
@@ -24,7 +24,7 @@ export const useStyles = makeStyles(theme => ({
color: theme.palette.login.main,
},
title: {
- fontSize: '1.5rem',
+ fontSize: theme.fontSizes.mainHeader,
marginBottom: '0.5rem',
display: 'flex',
alignItems: 'center',
diff --git a/frontend/src/component/user/NewUser/NewUser.styles.ts b/frontend/src/component/user/NewUser/NewUser.styles.ts
new file mode 100644
index 0000000000..9b9f1d8c56
--- /dev/null
+++ b/frontend/src/component/user/NewUser/NewUser.styles.ts
@@ -0,0 +1,29 @@
+import { makeStyles } from '@material-ui/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ display: 'flex',
+ },
+ roleContainer: {
+ marginTop: '2rem',
+ },
+ innerContainer: {
+ width: '60%',
+ minHeight: '100vh',
+ padding: '4rem 3rem',
+ },
+ buttonContainer: {
+ display: 'flex',
+ marginTop: '1rem',
+ },
+ primaryBtn: {
+ marginRight: '8px',
+ },
+ subtitle: {
+ marginBottom: '0.5rem',
+ fontSize: '1.1rem',
+ },
+ emailField: {
+ minWidth: '300px',
+ },
+}));
diff --git a/frontend/src/component/user/NewUser/NewUser.tsx b/frontend/src/component/user/NewUser/NewUser.tsx
new file mode 100644
index 0000000000..8b2596c340
--- /dev/null
+++ b/frontend/src/component/user/NewUser/NewUser.tsx
@@ -0,0 +1,94 @@
+import useLoading from '../../../hooks/useLoading';
+import { TextField, Typography } from '@material-ui/core';
+
+import StandaloneBanner from '../StandaloneBanner/StandaloneBanner';
+import ResetPasswordDetails from '../common/ResetPasswordDetails/ResetPasswordDetails';
+
+import { useStyles } from './NewUser.styles';
+import { useCommonStyles } from '../../../common.styles';
+import useResetPassword from '../../../hooks/useResetPassword';
+import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
+import ConditionallyRender from '../../common/ConditionallyRender';
+import InvalidToken from '../common/InvalidToken/InvalidToken';
+
+const NewUser = () => {
+ const {
+ token,
+ data,
+ loading,
+ setLoading,
+ invalidToken,
+ } = useResetPassword();
+ const ref = useLoading(loading);
+ const commonStyles = useCommonStyles();
+ const styles = useStyles();
+
+ return (
+
+
+
+ You have been invited by {data?.createdBy}
+
+ }
+ />
+
+ }
+ >
+ }
+ elseShow={
+
+
+ Your username is
+
+
+
+
+ In Unleash your role is:{' '}
+ {data?.role?.name}
+
+
+ {data?.role?.description}
+
+
+
+ Set a password for your account.
+
+
+
+ }
+ />
+
+
+ );
+};
+
+export default NewUser;
diff --git a/frontend/src/component/user/ResetPassword/ResetPassword.styles.ts b/frontend/src/component/user/ResetPassword/ResetPassword.styles.ts
new file mode 100644
index 0000000000..11c61655fd
--- /dev/null
+++ b/frontend/src/component/user/ResetPassword/ResetPassword.styles.ts
@@ -0,0 +1,13 @@
+import { makeStyles } from '@material-ui/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ display: 'flex',
+ },
+ innerContainer: { width: '40%', minHeight: '100vh' },
+ title: {
+ fontWeight: 'bold',
+ fontSize: '1.2rem',
+ marginBottom: '1rem',
+ },
+}));
diff --git a/frontend/src/component/user/ResetPassword/ResetPassword.tsx b/frontend/src/component/user/ResetPassword/ResetPassword.tsx
new file mode 100644
index 0000000000..2e2ed35950
--- /dev/null
+++ b/frontend/src/component/user/ResetPassword/ResetPassword.tsx
@@ -0,0 +1,43 @@
+import useLoading from '../../../hooks/useLoading';
+
+import ResetPasswordDetails from '../common/ResetPasswordDetails/ResetPasswordDetails';
+
+import { useStyles } from './ResetPassword.styles';
+import { Typography } from '@material-ui/core';
+import ConditionallyRender from '../../common/ConditionallyRender';
+import InvalidToken from '../common/InvalidToken/InvalidToken';
+import useResetPassword from '../../../hooks/useResetPassword';
+import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
+
+const ResetPassword = () => {
+ const styles = useStyles();
+ const { token, loading, setLoading, invalidToken } = useResetPassword();
+ const ref = useLoading(loading);
+
+ return (
+
+
+ }
+ elseShow={
+
+
+ Reset password
+
+
+ }
+ />
+
+
+ );
+};
+
+export default ResetPassword;
diff --git a/frontend/src/component/user/StandaloneBanner/StandaloneBanner.styles.ts b/frontend/src/component/user/StandaloneBanner/StandaloneBanner.styles.ts
new file mode 100644
index 0000000000..ec44c01a72
--- /dev/null
+++ b/frontend/src/component/user/StandaloneBanner/StandaloneBanner.styles.ts
@@ -0,0 +1,49 @@
+import { makeStyles } from '@material-ui/styles';
+
+export const useStyles = makeStyles(theme => ({
+ title: {
+ color: '#fff',
+ fontSize: '1.2rem',
+ fontWeight: 'bold',
+ marginBottom: '1rem',
+ },
+ container: {
+ padding: '4rem 2rem',
+ color: '#fff',
+ position: 'relative',
+ },
+ switchesContainer: {
+ position: 'fixed',
+ bottom: '40px',
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ switchIcon: {
+ height: '180px',
+ },
+ bottomStar: {
+ position: 'absolute',
+ bottom: '-54px',
+ left: '100px',
+ },
+ bottomRightStar: {
+ position: 'absolute',
+ bottom: '-100px',
+ left: '200px',
+ },
+ midRightStar: {
+ position: 'absolute',
+ bottom: '-80px',
+ left: '300px',
+ },
+ midLeftStar: {
+ position: 'absolute',
+ top: '10px',
+ left: '150px',
+ },
+ midLeftStarTwo: {
+ position: 'absolute',
+ top: '25px',
+ left: '350px',
+ },
+}));
diff --git a/frontend/src/component/user/StandaloneBanner/StandaloneBanner.tsx b/frontend/src/component/user/StandaloneBanner/StandaloneBanner.tsx
new file mode 100644
index 0000000000..23a8bed3d4
--- /dev/null
+++ b/frontend/src/component/user/StandaloneBanner/StandaloneBanner.tsx
@@ -0,0 +1,55 @@
+import { FC } from 'react';
+
+import { Typography, useTheme } from '@material-ui/core';
+import Gradient from '../../common/Gradient/Gradient';
+import { ReactComponent as StarIcon } from '../../../icons/star.svg';
+import { ReactComponent as RightToggleIcon } from '../../../icons/toggleRight.svg';
+import { ReactComponent as LeftToggleIcon } from '../../../icons/toggleLeft.svg';
+
+import { useStyles } from './StandaloneBanner.styles';
+import ConditionallyRender from '../../common/ConditionallyRender';
+
+interface IStandaloneBannerProps {
+ showStars?: boolean;
+ title: string;
+}
+
+const StandaloneBanner: FC
= ({
+ showStars = false,
+ title,
+ children,
+}) => {
+ const theme = useTheme();
+ const styles = useStyles();
+ return (
+
+
+
+ {title}
+
+ {children}
+
+
+
+
+
+
+
+ >
+ }
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+export default StandaloneBanner;
diff --git a/frontend/src/component/user/common/InvalidToken/InvalidToken.tsx b/frontend/src/component/user/common/InvalidToken/InvalidToken.tsx
new file mode 100644
index 0000000000..712a30c0af
--- /dev/null
+++ b/frontend/src/component/user/common/InvalidToken/InvalidToken.tsx
@@ -0,0 +1,29 @@
+import { Button, Typography } from '@material-ui/core';
+import { Link } from 'react-router-dom';
+import { useCommonStyles } from '../../../../common.styles';
+
+const InvalidToken = () => {
+ const commonStyles = useCommonStyles();
+ return (
+
+
+ Invalid token
+
+
+ Your token has either been used to reset your password, or it
+ has expired. Please request a new reset password URL in order to
+ reset your password.
+
+
+
+ );
+};
+
+export default InvalidToken;
diff --git a/frontend/src/component/user/common/ResetPasswordDetails/ResetPasswordDetails.tsx b/frontend/src/component/user/common/ResetPasswordDetails/ResetPasswordDetails.tsx
new file mode 100644
index 0000000000..aee5a111da
--- /dev/null
+++ b/frontend/src/component/user/common/ResetPasswordDetails/ResetPasswordDetails.tsx
@@ -0,0 +1,22 @@
+import { FC, Dispatch, SetStateAction } from 'react';
+import ResetPasswordForm from '../ResetPasswordForm/ResetPasswordForm';
+
+interface IResetPasswordDetails {
+ token: string;
+ setLoading: Dispatch>;
+}
+
+const ResetPasswordDetails: FC = ({
+ children,
+ token,
+ setLoading,
+}) => {
+ return (
+
+ {children}
+
+
+ );
+};
+
+export default ResetPasswordDetails;
diff --git a/frontend/src/component/user/common/ResetPasswordError/ResetPasswordError.tsx b/frontend/src/component/user/common/ResetPasswordError/ResetPasswordError.tsx
new file mode 100644
index 0000000000..876498f242
--- /dev/null
+++ b/frontend/src/component/user/common/ResetPasswordError/ResetPasswordError.tsx
@@ -0,0 +1,14 @@
+import { Alert, AlertTitle } from '@material-ui/lab';
+
+const ResetPasswordError = () => {
+ return (
+
+ Unable to reset password
+ Something went wrong when attempting to update your password. This
+ could be due to unstable internet connectivity. If retrying the
+ request does not work, please try again later.
+
+ );
+};
+
+export default ResetPasswordError;
diff --git a/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts
new file mode 100644
index 0000000000..736bbd3290
--- /dev/null
+++ b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts
@@ -0,0 +1,43 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ border: '1px solid #f1f1f1',
+ borderRadius: '3px',
+ right: '100px',
+ color: '#44606e',
+ },
+ headerContainer: { display: 'flex', padding: '0.5rem' },
+ divider: {
+ backgroundColor: theme.palette.borders?.main,
+ height: '1px',
+ width: '100%',
+ },
+ checkContainer: {
+ width: '95px',
+ margin: '0 0.25rem',
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ statusBarContainer: {
+ display: 'flex',
+ padding: '0.5rem',
+ },
+ statusBar: {
+ width: '50px',
+ borderRadius: '3px',
+ backgroundColor: 'red',
+ height: '6px',
+ },
+ title: {
+ marginBottom: '0',
+ display: 'flex',
+ alignItems: 'center',
+ },
+ statusBarSuccess: {
+ backgroundColor: theme.palette.primary.main,
+ },
+ helpIcon: {
+ height: '17.5px',
+ },
+}));
diff --git a/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.tsx b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.tsx
new file mode 100644
index 0000000000..82fb61fab7
--- /dev/null
+++ b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.tsx
@@ -0,0 +1,184 @@
+import { Tooltip, Typography } from '@material-ui/core';
+import classnames from 'classnames';
+import { Dispatch, SetStateAction, useEffect, useState } from 'react';
+import { BAD_REQUEST, OK } from '../../../../../constants/statusCodes';
+import { useStyles } from './PasswordChecker.styles';
+import HelpIcon from '@material-ui/icons/Help';
+import { useCallback } from 'react';
+
+interface IPasswordCheckerProps {
+ password: string;
+ callback: Dispatch>;
+}
+
+interface IErrorResponse {
+ details: IErrorDetails[];
+}
+
+interface IErrorDetails {
+ message: string;
+ validationErrors: string[];
+}
+
+const LENGTH_ERROR = 'The password must be at least 10 characters long.';
+const NUMBER_ERROR = 'The password must contain at least one number.';
+const SYMBOL_ERROR =
+ 'The password must contain at least one special character.';
+const UPPERCASE_ERROR =
+ 'The password must contain at least one uppercase letter';
+const LOWERCASE_ERROR =
+ 'The password must contain at least one lowercase letter.';
+
+const PasswordChecker = ({ password, callback }: IPasswordCheckerProps) => {
+ const styles = useStyles();
+ const [casingError, setCasingError] = useState(true);
+ const [numberError, setNumberError] = useState(true);
+ const [symbolError, setSymbolError] = useState(true);
+ const [lengthError, setLengthError] = useState(true);
+
+ const makeValidatePassReq = useCallback(() => {
+ return fetch('auth/reset/validate-password', {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ method: 'POST',
+ body: JSON.stringify({ password }),
+ });
+ }, [password]);
+
+ const checkPassword = useCallback(async () => {
+ try {
+ const res = await makeValidatePassReq();
+ if (res.status === BAD_REQUEST) {
+ const data = await res.json();
+ handleErrorResponse(data);
+ callback(false);
+ }
+
+ if (res.status === OK) {
+ clearErrors();
+ callback(true);
+ }
+ } catch (e) {
+ // ResetPasswordForm handles errors related to submitting the form.
+ console.log(e);
+ }
+ }, [makeValidatePassReq, callback]);
+
+ useEffect(() => {
+ checkPassword();
+ }, [password, checkPassword]);
+
+ const clearErrors = () => {
+ setCasingError(false);
+ setNumberError(false);
+ setSymbolError(false);
+ setLengthError(false);
+ };
+
+ const handleErrorResponse = (data: IErrorResponse) => {
+ const errors = data.details[0].validationErrors;
+
+ if (errors.includes(NUMBER_ERROR)) {
+ setNumberError(true);
+ } else {
+ setNumberError(false);
+ }
+
+ if (errors.includes(SYMBOL_ERROR)) {
+ setSymbolError(true);
+ } else {
+ setSymbolError(false);
+ }
+
+ if (errors.includes(LENGTH_ERROR)) {
+ setLengthError(true);
+ } else {
+ setLengthError(false);
+ }
+
+ if (
+ errors.includes(LOWERCASE_ERROR) ||
+ errors.includes(UPPERCASE_ERROR)
+ ) {
+ setCasingError(true);
+ } else {
+ setCasingError(false);
+ }
+ };
+
+ const lengthStatusBarClasses = classnames(styles.statusBar, {
+ [styles.statusBarSuccess]: !lengthError,
+ });
+
+ const numberStatusBarClasses = classnames(styles.statusBar, {
+ [styles.statusBarSuccess]: !numberError,
+ });
+
+ const symbolStatusBarClasses = classnames(styles.statusBar, {
+ [styles.statusBarSuccess]: !symbolError,
+ });
+
+ const casingStatusBarClasses = classnames(styles.statusBar, {
+ [styles.statusBarSuccess]: !casingError,
+ });
+
+ return (
+ <>
+
+
+ Please set a strong password
+
+
+
+
+
+
+
+ Length
+
+
+
+
+ Casing
+
+
+
+
+ Number
+
+
+
+
+ Symbol
+
+
+
+
+
+
+ >
+ );
+};
+
+export default PasswordChecker;
diff --git a/frontend/src/component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher.styles.ts b/frontend/src/component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher.styles.ts
new file mode 100644
index 0000000000..0b112d46dd
--- /dev/null
+++ b/frontend/src/component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher.styles.ts
@@ -0,0 +1,22 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ matcherContainer: {
+ position: 'relative',
+ },
+ matcherIcon: {
+ marginRight: '5px',
+ },
+ matcher: {
+ position: 'absolute',
+ bottom: '-8px',
+ display: 'flex',
+ alignItems: 'center',
+ },
+ matcherError: {
+ color: theme.palette.error.main,
+ },
+ matcherSuccess: {
+ color: theme.palette.primary.main,
+ },
+}));
diff --git a/frontend/src/component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher.tsx b/frontend/src/component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher.tsx
new file mode 100644
index 0000000000..82f0de09a8
--- /dev/null
+++ b/frontend/src/component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher.tsx
@@ -0,0 +1,55 @@
+import { Typography } from '@material-ui/core';
+import ConditionallyRender from '../../../../common/ConditionallyRender';
+import classnames from 'classnames';
+import CheckIcon from '@material-ui/icons/Check';
+
+import { useStyles } from './PasswordMatcher.styles';
+
+interface IPasswordMatcherProps {
+ started: boolean;
+ matchingPasswords: boolean;
+}
+
+const PasswordMatcher = ({
+ started,
+ matchingPasswords,
+}: IPasswordMatcherProps) => {
+ const styles = useStyles();
+ return (
+
+
+ {' '}
+ Passwords match
+
+ }
+ elseShow={
+
+ Passwords do not match
+
+ }
+ />
+ }
+ />
+
+ );
+};
+
+export default PasswordMatcher;
diff --git a/frontend/src/component/user/common/ResetPasswordForm/ResetPasswordForm.styles.ts b/frontend/src/component/user/common/ResetPasswordForm/ResetPasswordForm.styles.ts
new file mode 100644
index 0000000000..919ddf58c8
--- /dev/null
+++ b/frontend/src/component/user/common/ResetPasswordForm/ResetPasswordForm.styles.ts
@@ -0,0 +1,12 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ display: 'flex',
+ flexDirection: 'column',
+ maxWidth: '300px',
+ },
+ button: {
+ width: '150px',
+ },
+}));
diff --git a/frontend/src/component/user/common/ResetPasswordForm/ResetPasswordForm.tsx b/frontend/src/component/user/common/ResetPasswordForm/ResetPasswordForm.tsx
new file mode 100644
index 0000000000..474e313fdc
--- /dev/null
+++ b/frontend/src/component/user/common/ResetPasswordForm/ResetPasswordForm.tsx
@@ -0,0 +1,144 @@
+import { Button, TextField } from '@material-ui/core';
+import classnames from 'classnames';
+import {
+ SyntheticEvent,
+ useEffect,
+ useState,
+ Dispatch,
+ SetStateAction,
+} from 'react';
+import { useHistory } from 'react-router';
+import { useCommonStyles } from '../../../../common.styles';
+import { OK } from '../../../../constants/statusCodes';
+import ConditionallyRender from '../../../common/ConditionallyRender';
+import ResetPasswordError from '../ResetPasswordError/ResetPasswordError';
+import PasswordChecker from './PasswordChecker/PasswordChecker';
+import PasswordMatcher from './PasswordMatcher/PasswordMatcher';
+import { useStyles } from './ResetPasswordForm.styles';
+import { useCallback } from 'react';
+
+interface IResetPasswordProps {
+ token: string;
+ setLoading: Dispatch>;
+}
+
+const ResetPasswordForm = ({ token, setLoading }: IResetPasswordProps) => {
+ const styles = useStyles();
+ const commonStyles = useCommonStyles();
+ const [apiError, setApiError] = useState(false);
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [matchingPasswords, setMatchingPasswords] = useState(false);
+ const [validOwaspPassword, setValidOwaspPassword] = useState(false);
+ const history = useHistory();
+
+ const submittable = matchingPasswords && validOwaspPassword;
+
+ const setValidOwaspPasswordMemo = useCallback(setValidOwaspPassword, [
+ setValidOwaspPassword,
+ ]);
+
+ useEffect(() => {
+ if (password === confirmPassword) {
+ setMatchingPasswords(true);
+ } else {
+ setMatchingPasswords(false);
+ }
+ }, [password, confirmPassword]);
+
+ const makeResetPasswordReq = () => {
+ return fetch('auth/reset/password', {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ method: 'POST',
+ body: JSON.stringify({
+ token,
+ password,
+ }),
+ });
+ };
+
+ const submitResetPassword = async () => {
+ setLoading(true);
+
+ try {
+ const res = await makeResetPasswordReq();
+ setLoading(false);
+ if (res.status === OK) {
+ history.push('login?reset=true');
+ setApiError(false);
+ } else {
+ setApiError(true);
+ }
+ } catch (e) {
+ setApiError(true);
+ setLoading(false);
+ }
+ };
+
+ const handleSubmit = (e: SyntheticEvent) => {
+ e.preventDefault();
+
+ if (submittable) {
+ submitResetPassword();
+ }
+ };
+
+ const started = Boolean(password && confirmPassword);
+
+ return (
+ <>
+ }
+ />
+
+ >
+ );
+};
+
+export default ResetPasswordForm;
diff --git a/frontend/src/component/user/common/ResetPasswordSuccess/ResetPasswordSuccess.tsx b/frontend/src/component/user/common/ResetPasswordSuccess/ResetPasswordSuccess.tsx
new file mode 100644
index 0000000000..0202b8c8c0
--- /dev/null
+++ b/frontend/src/component/user/common/ResetPasswordSuccess/ResetPasswordSuccess.tsx
@@ -0,0 +1,12 @@
+import { Alert, AlertTitle } from '@material-ui/lab';
+
+const ResetPasswordSuccess = () => {
+ return (
+
+ Success
+ You successfully reset your password.
+
+ );
+};
+
+export default ResetPasswordSuccess;
diff --git a/frontend/src/component/user/common/StandaloneLayout/StandaloneLayout.styles.ts b/frontend/src/component/user/common/StandaloneLayout/StandaloneLayout.styles.ts
new file mode 100644
index 0000000000..78e96e5304
--- /dev/null
+++ b/frontend/src/component/user/common/StandaloneLayout/StandaloneLayout.styles.ts
@@ -0,0 +1,26 @@
+import { makeStyles } from '@material-ui/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ display: 'flex',
+ },
+ leftContainer: { width: '40%', minHeight: '100vh' },
+ rightContainer: { width: '60%', minHeight: '100vh', position: 'relative' },
+ menu: {
+ position: 'absolute',
+ right: '20px',
+ top: '20px',
+ '& a': {
+ textDecoration: 'none',
+ color: '#000',
+ },
+ },
+ title: {
+ fontWeight: 'bold',
+ fontSize: '1.2rem',
+ marginBottom: '1rem',
+ },
+ innerRightContainer: {
+ padding: '4rem 3rem',
+ },
+}));
diff --git a/frontend/src/component/user/common/StandaloneLayout/StandaloneLayout.tsx b/frontend/src/component/user/common/StandaloneLayout/StandaloneLayout.tsx
new file mode 100644
index 0000000000..14938b8300
--- /dev/null
+++ b/frontend/src/component/user/common/StandaloneLayout/StandaloneLayout.tsx
@@ -0,0 +1,53 @@
+import { FC } from 'react';
+import StandaloneBanner from '../../StandaloneBanner/StandaloneBanner';
+
+import { Typography } from '@material-ui/core';
+
+import { useStyles } from './StandaloneLayout.styles';
+import ConditionallyRender from '../../../common/ConditionallyRender';
+import { Link } from 'react-router-dom';
+
+interface IStandaloneLayout {
+ BannerComponent?: JSX.Element;
+ showMenu?: boolean;
+}
+
+const StandaloneLayout: FC = ({
+ children,
+ showMenu = true,
+ BannerComponent,
+}) => {
+ const styles = useStyles();
+
+ let banner = (
+
+
+ Committed to creating new ways of developing.
+
+
+ );
+
+ if (BannerComponent) {
+ banner = BannerComponent;
+ }
+
+ return (
+
+
{banner}
+
+
+ Login
+
+ }
+ />
+
+
{children}
+
+
+ );
+};
+
+export default StandaloneLayout;
diff --git a/frontend/src/component/user/show-user-component.jsx b/frontend/src/component/user/show-user-component.jsx
index 0f9872f9f4..223fafafcc 100644
--- a/frontend/src/component/user/show-user-component.jsx
+++ b/frontend/src/component/user/show-user-component.jsx
@@ -26,6 +26,7 @@ export default class ShowUserComponent extends React.Component {
componentDidMount() {
this.props.fetchUser();
+
// find default locale and add it in choices if not present
const locale = navigator.language || navigator.userLanguage;
let found = this.possibleLocales.find(l => l.value === locale);
diff --git a/frontend/src/constants/statusCodes.ts b/frontend/src/constants/statusCodes.ts
new file mode 100644
index 0000000000..aeb843466f
--- /dev/null
+++ b/frontend/src/constants/statusCodes.ts
@@ -0,0 +1,3 @@
+export const BAD_REQUEST = 400;
+export const OK = 200;
+export const NOT_FOUND = 404;
diff --git a/frontend/src/hooks/useLoading.ts b/frontend/src/hooks/useLoading.ts
new file mode 100644
index 0000000000..ba76b95753
--- /dev/null
+++ b/frontend/src/hooks/useLoading.ts
@@ -0,0 +1,24 @@
+import { useEffect, createRef } from 'react';
+
+type refElement = HTMLDivElement;
+
+const useLoading = (loading: boolean) => {
+ const ref = createRef