Skip to content

Commit

Permalink
Merge pull request #63 from cesarhenrq/qa
Browse files Browse the repository at this point in the history
1114 feat  login form
  • Loading branch information
cesarhenrq authored Oct 9, 2023
2 parents a7054c4 + 0fd6685 commit 4062f5c
Show file tree
Hide file tree
Showing 19 changed files with 464 additions and 13 deletions.
2 changes: 1 addition & 1 deletion cypress/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ module.exports = {
'cypress/no-pause': 'error',
'@typescript-eslint/no-var-requires': 'off',
},
env: {'cypress/globals': true},
env: { 'cypress/globals': true },
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"cy:run-e2e": "server-test 3000 'cypress run --e2e --browser chrome'",
"cy:open-ct": "cypress open --component --browser chrome",
"cy:run-ct": "cypress run --component --browser chrome",
"cy:run-ct-fast": "yarn cy:run-ct --config video=false screenshot=false"
"cy:run-ct-fast": "yarn cy:run-ct --config video=false screenshot=false",
"ci-pipeline": "yarn validate:ci && yarn test && yarn cy:run-ct-fast && yarn cy:run-e2e"
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
Expand Down
5 changes: 4 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import GlobalStyle from '@styles/global';
import Router from '@routes/Router';

import FiltersProvider from '@contexts/filters';
import UserProvider from '@contexts/user';

export default function App() {
return (
<div className="App">
<GlobalStyle />
<FiltersProvider>
<Router />
<UserProvider>
<Router />
</UserProvider>
</FiltersProvider>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/AuthSubHeader/AuthSubHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const AuthSubHeader = ({ authLabel }: AuthSubHeaderProps) => {
);

return (
<S.AuthSubHeader data-cy="auth-sub-header">
<S.AuthSubHeader data-cy="auth-sub-header" className="auth-sub-header">
<Text label={authLabel} fontSize="medium" fontColor="white" />
<Text
label={largerText}
Expand Down
53 changes: 53 additions & 0 deletions src/components/LoginForm/LoginForm.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import LoginForm from './LoginForm';

import Sut from '@utils/helpers';

describe('<LoginForm />', () => {
beforeEach(() => {
cy.mount(
<Sut>
<LoginForm />
</Sut>,
);
});

it('should be defined', () => {
cy.getByCy('login-form-container');
});

it('should render text "Faça seu login"', () => {
cy.getByCy('login-form-container').contains('Faça seu login');
});

it('should render label "Seu e-mail"', () => {
cy.getByCy('login-form-container').contains('Seu e-mail');
});

it('should render input email', () => {
cy.getByCy('email-input');
});

it('should render label "Senha"', () => {
cy.getByCy('login-form-container').contains('Senha');
});

it('should render input password', () => {
cy.getByCy('password-input');
});

it('should render eye icon', () => {
cy.getByCy('eye-icon').should('have.length', 1);
});

it('should render button "Entrar"', () => {
cy.getByCy('login-form-container').contains('Entrar');
});

it('should render text "Não é cadastrado?"', () => {
cy.getByCy('login-form-container').contains('Não é cadastrado?');
});

it('should render link "Cadastre-se gratuitamente"', () => {
cy.getByCy('login-form-container').contains('Cadastre-se gratuitamente');
});
});
159 changes: 159 additions & 0 deletions src/components/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { useState, useContext } from 'react';

import { Link, useNavigate } from 'react-router-dom';

import { useForm, DefaultValues } from 'react-hook-form';

import { yupResolver } from '@hookform/resolvers/yup';
import { schema, LoginFormValues } from './schema';

import { AxiosError } from 'axios';

import Text from '@components/Text';
import Button from '@components/Button';

import useResource from '@hooks/useResource';

import { userContext, actions } from '@contexts/user';

import * as S from './styles';

import eye from '@assets/eye-line.png';

type Response = {
token: string;
data: {
name: string;
email: string;
isActive: boolean;
};
};

const defaultValues: DefaultValues<LoginFormValues> = {
email: '',
password: '',
};

const LoginForm = () => {
const [showPassword, setShowPassword] = useState(false);
const [notification, setNotification] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);

const { userDispatch } = useContext(userContext);

const navigate = useNavigate();

const [, userService] = useResource('login');

const {
handleSubmit,
register,
formState: { errors },
} = useForm<LoginFormValues>({
defaultValues,
resolver: yupResolver(schema),
mode: 'onBlur',
});

const onSubmit = async (data: LoginFormValues) => {
setLoading(true);

try {
const response = (await userService.post(data)) as Response;

userDispatch(
actions.setUser({ token: response.token, ...response.data }),
);

setError(false);
setNotification('Login realizado com sucesso!');
setTimeout(() => {
setNotification('');
navigate('/');
}, 1000);
console.log(response);
} catch (error) {
if (error instanceof AxiosError) {
setError(true);
setNotification(error.response?.data.message);

setTimeout(() => {
setNotification('');
}, 3000);
}
}

setLoading(false);
};

const handleError = (name: keyof Partial<LoginFormValues>) => {
return (
errors?.[name] && (
<span className="error-message">{errors[name]?.message}</span>
)
);
};

return (
<S.FormContainer
data-cy="login-form-container"
className="login-form-container"
loading={loading}
>
<div className="title-container">
<Text
label="Faça seu login"
fontSize="xmedium"
fontWeight="700"
fontColor="purple-dark-secondary"
/>
</div>

<form data-cy="login-form" onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Seu e-mail</label>
<div className="email-input-wrapper">
<input
placeholder="Digite seu e-mail"
id="email"
{...register('email')}
data-cy="email-input"
/>
{handleError('email')}
</div>

<label htmlFor="password">Senha</label>
<div className="password-wrapper">
<input
placeholder="Digite sua senha"
type={showPassword ? 'text' : 'password'}
id="password"
{...register('password')}
data-cy="password-input"
/>
{handleError('password')}
<img
src={eye}
alt="eye"
onClick={() => setShowPassword(!showPassword)}
data-cy="eye-icon"
/>
</div>

{notification && (
<span className={`notification ${error ? 'error' : 'success'}`}>
{notification}
</span>
)}

<Button label="Entrar" type="submit" />
</form>
<div className="text-and-link">
<Text label="Não é cadastrado?" />
<Link to="/register">Cadastre-se gratuitamente</Link>
</div>
</S.FormContainer>
);
};

export default LoginForm;
1 change: 1 addition & 0 deletions src/components/LoginForm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './LoginForm';
11 changes: 11 additions & 0 deletions src/components/LoginForm/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as yup from 'yup';

export const schema = yup.object().shape({
email: yup
.string()
.email('O e-mail é obrigatório')
.required('E-mail inválido'),
password: yup.string().required('A senha é obrigatória'),
});

export type LoginFormValues = yup.InferType<typeof schema>;
99 changes: 99 additions & 0 deletions src/components/LoginForm/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* eslint-disable indent */
import styled from 'styled-components';

type FormContainer = {
loading: boolean;
};

export const FormContainer = styled.div<FormContainer>`
background-color: var(--white);
padding: 2.125rem 2rem 2.5rem;
box-shadow: 0rem 1rem 2rem rgba(207.7, 207.7, 207.7, 0.2);
border-radius: 1rem;
.notification {
text-align: center;
}
.error {
color: red;
}
.success {
color: green;
}
.error-message {
color: red;
}
.title-container {
margin-bottom: 1.5rem;
.text {
justify-content: flex-start;
}
}
.text-and-link {
display: flex;
justify-content: center;
align-items: center;
.text {
margin-right: 0.3rem;
}
}
.email-input-wrapper {
margin-bottom: 1rem;
}
button {
margin-top: 1rem;
margin-bottom: 1rem;
background-color: ${props =>
props.loading ? 'var(--gray-light)' : 'var(--yellow)'};
cursor: ${props => (props.loading ? 'not-allowed' : 'pointer')};
.text {
font-weight: 500;
}
}
form {
width: 100%;
display: flex;
flex-direction: column;
label {
color: var(--purple-dark);
margin-bottom: 0.5rem;
font-weight: 500;
}
.password-wrapper {
position: relative;
margin-bottom: 1.5rem;
img {
position: absolute;
right: 1rem;
top: 1rem;
cursor: pointer;
}
}
input {
border-radius: 0.5rem;
width: 100%;
border: 1px solid var(--gray-light);
padding: 1rem 0.8125rem;
}
img {
width: 24px;
height: 24px;
}
}
`;
20 changes: 20 additions & 0 deletions src/contexts/user/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export type Action = {
type: string;
payload: User;
};

export const setUser = (user: User) => {
localStorage.setItem('metavagas-user', JSON.stringify(user));
return {
type: 'SET_USER',
payload: user,
};
};

export const removeUser = () => {
localStorage.removeItem('metavagas-user');
return {
type: 'REMOVE_USER',
payload: null,
};
};
Loading

0 comments on commit 4062f5c

Please sign in to comment.