diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js
index ed1fec2..5a18f74 100644
--- a/cypress/.eslintrc.js
+++ b/cypress/.eslintrc.js
@@ -9,5 +9,5 @@ module.exports = {
'cypress/no-pause': 'error',
'@typescript-eslint/no-var-requires': 'off',
},
- env: {'cypress/globals': true},
+ env: { 'cypress/globals': true },
};
diff --git a/package.json b/package.json
index 5577602..080e477 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/App.tsx b/src/App.tsx
index 4b40953..185a097 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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 (
-
+
+
+
);
diff --git a/src/components/AuthSubHeader/AuthSubHeader.tsx b/src/components/AuthSubHeader/AuthSubHeader.tsx
index 2260709..d14640f 100644
--- a/src/components/AuthSubHeader/AuthSubHeader.tsx
+++ b/src/components/AuthSubHeader/AuthSubHeader.tsx
@@ -15,7 +15,7 @@ const AuthSubHeader = ({ authLabel }: AuthSubHeaderProps) => {
);
return (
-
+
', () => {
+ beforeEach(() => {
+ cy.mount(
+
+
+ ,
+ );
+ });
+
+ 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');
+ });
+});
diff --git a/src/components/LoginForm/LoginForm.tsx b/src/components/LoginForm/LoginForm.tsx
new file mode 100644
index 0000000..0a4c685
--- /dev/null
+++ b/src/components/LoginForm/LoginForm.tsx
@@ -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 = {
+ 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({
+ 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) => {
+ return (
+ errors?.[name] && (
+ {errors[name]?.message}
+ )
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ Cadastre-se gratuitamente
+
+
+ );
+};
+
+export default LoginForm;
diff --git a/src/components/LoginForm/index.ts b/src/components/LoginForm/index.ts
new file mode 100644
index 0000000..1195444
--- /dev/null
+++ b/src/components/LoginForm/index.ts
@@ -0,0 +1 @@
+export { default } from './LoginForm';
diff --git a/src/components/LoginForm/schema.ts b/src/components/LoginForm/schema.ts
new file mode 100644
index 0000000..f229313
--- /dev/null
+++ b/src/components/LoginForm/schema.ts
@@ -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;
diff --git a/src/components/LoginForm/styles.ts b/src/components/LoginForm/styles.ts
new file mode 100644
index 0000000..4bd261c
--- /dev/null
+++ b/src/components/LoginForm/styles.ts
@@ -0,0 +1,99 @@
+/* eslint-disable indent */
+import styled from 'styled-components';
+
+type FormContainer = {
+ loading: boolean;
+};
+
+export const FormContainer = styled.div`
+ 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;
+ }
+ }
+`;
diff --git a/src/contexts/user/actions.ts b/src/contexts/user/actions.ts
new file mode 100644
index 0000000..f6415cc
--- /dev/null
+++ b/src/contexts/user/actions.ts
@@ -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,
+ };
+};
diff --git a/src/contexts/user/context.ts b/src/contexts/user/context.ts
new file mode 100644
index 0000000..715ae32
--- /dev/null
+++ b/src/contexts/user/context.ts
@@ -0,0 +1,16 @@
+import { createContext, Dispatch } from 'react';
+
+import { initialState } from './dispatcher';
+import { Action } from './actions';
+
+type UserContext = {
+ user: User | null;
+ userDispatch: Dispatch;
+};
+
+const userContext = createContext({
+ user: initialState,
+ userDispatch: () => {},
+});
+
+export default userContext;
diff --git a/src/contexts/user/dispatcher.ts b/src/contexts/user/dispatcher.ts
new file mode 100644
index 0000000..e57c738
--- /dev/null
+++ b/src/contexts/user/dispatcher.ts
@@ -0,0 +1,21 @@
+import { Action } from './actions';
+
+type UserState = User | null;
+
+export const initialState: UserState = null;
+
+export const userDispatcher = (
+ state: UserState = initialState,
+ action: Action,
+): User | null => {
+ switch (action.type) {
+ case 'SET_USER':
+ return {
+ ...action.payload,
+ };
+ case 'REMOVE_USER':
+ return null;
+ default:
+ return state;
+ }
+};
diff --git a/src/contexts/user/index.ts b/src/contexts/user/index.ts
new file mode 100644
index 0000000..0ba7d3d
--- /dev/null
+++ b/src/contexts/user/index.ts
@@ -0,0 +1,5 @@
+export { default } from './provider';
+
+export * as actions from './actions';
+
+export { default as userContext } from './context';
diff --git a/src/contexts/user/provider.tsx b/src/contexts/user/provider.tsx
new file mode 100644
index 0000000..cec7ec2
--- /dev/null
+++ b/src/contexts/user/provider.tsx
@@ -0,0 +1,34 @@
+import { useReducer, Reducer, useEffect } from 'react';
+
+import userContext from './context';
+
+import { Action } from './actions';
+
+import { userDispatcher, initialState } from './dispatcher';
+
+type Props = {
+ children: React.ReactNode;
+};
+
+const UserProvider = ({ children }: Props) => {
+ const [state, dispatch] = useReducer>(
+ userDispatcher,
+ initialState,
+ );
+
+ useEffect(() => {
+ const user = localStorage.getItem('metavagas-user');
+
+ if (user) {
+ dispatch({ type: 'SET_USER', payload: JSON.parse(user) });
+ }
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default UserProvider;
diff --git a/src/pages/LoginPage/LoginPage.test.tsx b/src/pages/LoginPage/LoginPage.cy.tsx
similarity index 61%
rename from src/pages/LoginPage/LoginPage.test.tsx
rename to src/pages/LoginPage/LoginPage.cy.tsx
index 0ecfa56..c4b21ab 100644
--- a/src/pages/LoginPage/LoginPage.test.tsx
+++ b/src/pages/LoginPage/LoginPage.cy.tsx
@@ -1,12 +1,10 @@
-import { render, screen } from '@testing-library/react';
-
import LoginPage from './LoginPage';
import Sut from '@utils/helpers';
describe('', () => {
beforeEach(() => {
- render(
+ cy.mount(
,
@@ -14,18 +12,22 @@ describe('', () => {
});
it('should be defined', () => {
- screen.getByTestId('login-page');
+ cy.getByCy('login-page');
});
it('should render ', () => {
- screen.getByTestId('auth-sub-header');
+ cy.getByCy('auth-sub-header');
});
it('should render with text "FAÇA SEU LOGIN"', () => {
- screen.getByText('FAÇA SEU LOGIN');
+ cy.getByCy('login-page').contains('FAÇA SEU LOGIN');
});
it('should render ', () => {
- screen.getByTestId('step-card-container');
+ cy.getByCy('step-card-container');
+ });
+
+ it('should render ', () => {
+ cy.getByCy('login-form-container');
});
});
diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/pages/LoginPage/LoginPage.tsx
index 92b7291..7b5191b 100644
--- a/src/pages/LoginPage/LoginPage.tsx
+++ b/src/pages/LoginPage/LoginPage.tsx
@@ -1,5 +1,6 @@
import AuthSubHeader from '@components/AuthSubHeader';
import StepCardContainer from '@components/StepCardContainer';
+import LoginForm from '@components/LoginForm';
import * as S from './styles';
@@ -7,6 +8,7 @@ const LoginPage = () => {
return (
+
);
diff --git a/src/pages/LoginPage/styles.ts b/src/pages/LoginPage/styles.ts
index f87f77a..c3d8c8b 100644
--- a/src/pages/LoginPage/styles.ts
+++ b/src/pages/LoginPage/styles.ts
@@ -1,3 +1,18 @@
import styled from 'styled-components';
-export const LoginPage = styled.div``;
+export const LoginPage = styled.div`
+ position: relative;
+
+ .login-form-container {
+ position: absolute;
+ z-index: 1;
+ top: 60px;
+ left: 788px;
+ right: 136px;
+ width: calc(100% - 924px);
+ }
+
+ .auth-sub-header {
+ padding: 6.25rem 8.4375rem;
+ }
+`;
diff --git a/src/types/User.d.ts b/src/types/User.d.ts
new file mode 100644
index 0000000..dc199b8
--- /dev/null
+++ b/src/types/User.d.ts
@@ -0,0 +1,6 @@
+type User = {
+ name: string;
+ email: string;
+ isActive: boolean;
+ token: string;
+};
diff --git a/src/utils/helpers/Sut.tsx b/src/utils/helpers/Sut.tsx
index 3afd5bb..23652fe 100644
--- a/src/utils/helpers/Sut.tsx
+++ b/src/utils/helpers/Sut.tsx
@@ -3,6 +3,7 @@ import GlobalStyle from '@styles/global';
import { BrowserRouter } from 'react-router-dom';
import FiltersProvider from '@contexts/filters';
+import UserProvider from '@contexts/user';
type Props = {
children: React.ReactNode;
@@ -12,7 +13,9 @@ const Sut = ({ children }: Props) => (
<>
- {children}
+
+ {children}
+
>
);