Skip to content

Commit

Permalink
[#21] Change form error state from map of key/values to just a list d…
Browse files Browse the repository at this point in the history
…ue to uneeded complexity/issues
  • Loading branch information
liamstevens111 committed Feb 20, 2023
1 parent e402bb0 commit 8f0e4e8
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 41 deletions.
6 changes: 4 additions & 2 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{
"login": {
"sign_in": "Sign in",
"sign-in": "Sign in",
"email": "Email",
"password": "Password",
"forgot_password": "Forgot?"
"forgot-password": "Forgot?",
"invalid-email": "Email has invalid format",
"invalid-password": "Password should be at least {{passwordMinLength}}"
}
}
5 changes: 3 additions & 2 deletions src/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ type ButtonProps = {
text: string;
type?: 'button' | 'submit' | 'reset';
className?: string;
disabled?: boolean;
onButtonClick?: () => void;
};

function Button({ text, type, className, onButtonClick }: ButtonProps) {
function Button({ text, type, className, disabled, onButtonClick }: ButtonProps) {
return (
<button className={`${styles.button} ${className}`} type={type} onClick={onButtonClick}>
<button className={`${styles.button} ${className}`} disabled={disabled || false} type={type} onClick={onButtonClick}>
{text}
</button>
);
Expand Down
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PASSWORD_MIN_LENGTH = 5;
7 changes: 4 additions & 3 deletions src/helpers/validators.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { PASSWORD_MIN_LENGTH } from '../constants';

export const isEmailValid = (email: string) => {
const emailRegEx = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;

return emailRegEx.test(email);
};

export const isPasswordValid = (password: string) => {
const passwordRegEx = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;

return passwordRegEx.test(password);
// TODO: Should match backend logic or remove completely
return password.length >= PASSWORD_MIN_LENGTH;
};
85 changes: 51 additions & 34 deletions src/screens/Home/login.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';

import { AxiosError } from 'axios';

import AuthAdapter from 'adapters/authAdapter';
import logo from 'assets/images/logo.svg';
Expand All @@ -8,27 +11,16 @@ import Input from 'components/Input';
import { setToken } from 'helpers/userToken';
import { isEmailValid, isPasswordValid } from 'helpers/validators';

type InputField = 'Email' | 'Password';

type Errors = {
Email?: string;
Password?: string;
};
import { PASSWORD_MIN_LENGTH } from '../../constants';

function LoginScreen() {
const navigate = useNavigate();
const { t } = useTranslation('translation');

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});

const removeError = (field: InputField) => {
const newState: Errors = {
...errors,
};
delete newState[field];
setErrors(newState);
};
const [errors, setErrors] = useState<string[]>([]);
const [formSubmitted, setFormSubmitted] = useState(false);

const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
Expand All @@ -39,31 +31,44 @@ function LoginScreen() {
};

const performLogin = async () => {
const response = await AuthAdapter.login({ email: email, password: password });

const {
attributes: { access_token: accessToken, refresh_token: refreshToken },
} = await response.data;

setToken({ accessToken: accessToken, refreshToken: refreshToken });
try {
const response = await AuthAdapter.login({ email: email, password: password });

const {
attributes: { access_token: accessToken, refresh_token: refreshToken },
} = await response.data;

setToken({ accessToken: accessToken, refreshToken: refreshToken });
navigate('/');
} catch (error) {
let errorMessage = '';

if (error instanceof Error) {
errorMessage = (error as AxiosError).response?.data?.errors[0]?.detail || error.message || 'Internal error';
}
setErrors([errorMessage]);
} finally {
setFormSubmitted(false);
}
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

const formErrors = [];

if (!isEmailValid(email)) {
setErrors({ ...errors, Email: 'Is invalid' });
} else {
removeError('Email');
formErrors.push(t('login.invalid-email'));
}

if (!isPasswordValid(password)) {
setErrors({ ...errors, Password: 'Is invalid' });
} else {
removeError('Password');
formErrors.push(t('login.invalid-password', { passwordMinLength: PASSWORD_MIN_LENGTH }));
}

if (Object.keys(errors).length === 0) {
setErrors(formErrors);

if (formErrors.length === 0) {
setFormSubmitted(true);
performLogin();
}
};
Expand All @@ -72,16 +77,28 @@ function LoginScreen() {
<>
<img className="inline-block" src={logo} alt="logo" />
<p data-test-id="login-header" className="my-8 text-white opacity-50">
{t('login.sign_in')} to Nimble
{t('login.sign-in')} to Nimble
</p>

<div>
{errors.length > 0 &&
errors.map((error) => {
return (
<p className="text-center text-red-700" key={error.toString()}>
{error}
</p>
);
})}
</div>

<form onSubmit={handleSubmit}>
<Input
name="email"
label={t('login.email')}
type="text"
value={email}
className="my-3 block h-14 w-80"
onInputChange={(e) => handleEmailChange(e)}
onInputChange={handleEmailChange}
/>
<div className="relative w-80">
<Input
Expand All @@ -90,14 +107,14 @@ function LoginScreen() {
type="password"
value={password}
className="my-3 block h-14 w-80"
onInputChange={(e) => handlePasswordChange(e)}
onInputChange={handlePasswordChange}
/>
{/* Change to React Router Link when implement #17 */}
<a href="." className="absolute left-60 top-5 my-8 text-white opacity-50">
{t('login.forgot_password')}
{t('login.forgot-password')}
</a>
</div>
<Button text={t('login.sign_in')} className="h-14 w-80" />
<Button text={t('login.sign-in')} className="h-14 w-80" disabled={formSubmitted} />
</form>
</>
);
Expand Down

0 comments on commit 8f0e4e8

Please sign in to comment.