diff --git a/.gitignore b/.gitignore index e36333d..3d42112 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ yarn-error.log* # Optional eslint cache .eslintcache + +# não commitar url de environment +src/constants/environment.ts diff --git a/package-lock.json b/package-lock.json index 73ca025..a1e3220 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@capacitor/haptics": "1.1.4", "@capacitor/keyboard": "1.2.2", "@capacitor/status-bar": "1.0.8", + "@hookform/error-message": "^2.0.0", "@ionic/react": "^6.0.0", "@ionic/react-router": "^6.0.0", "@testing-library/jest-dom": "^5.11.9", @@ -28,6 +29,7 @@ "ionicons": "^5.4.0", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-hook-form": "^7.30.0", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "typescript": "^4.1.3", @@ -2369,6 +2371,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@hookform/error-message": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.0.tgz", + "integrity": "sha512-Y90nHzjgL2MP7GFy75kscdvxrCTjtyxGmOLLxX14nd08OXRIh9lMH/y9Kpdo0p1IPowJBiZMHyueg7p+yrqynQ==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -13287,6 +13299,21 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "dev": true }, + "node_modules/react-hook-form": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.30.0.tgz", + "integrity": "sha512-DzjiM6o2vtDGNMB9I4yCqW8J21P314SboNG1O0obROkbg7KVS0I7bMtwSdKyapnCPjHgnxc3L7E5PEdISeEUcQ==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -18244,6 +18271,12 @@ } } }, + "@hookform/error-message": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.0.tgz", + "integrity": "sha512-Y90nHzjgL2MP7GFy75kscdvxrCTjtyxGmOLLxX14nd08OXRIh9lMH/y9Kpdo0p1IPowJBiZMHyueg7p+yrqynQ==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -26381,6 +26414,12 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "dev": true }, + "react-hook-form": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.30.0.tgz", + "integrity": "sha512-DzjiM6o2vtDGNMB9I4yCqW8J21P314SboNG1O0obROkbg7KVS0I7bMtwSdKyapnCPjHgnxc3L7E5PEdISeEUcQ==", + "requires": {} + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/package.json b/package.json index 6962e41..39fd73e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@capacitor/haptics": "1.1.4", "@capacitor/keyboard": "1.2.2", "@capacitor/status-bar": "1.0.8", + "@hookform/error-message": "^2.0.0", "@ionic/react": "^6.0.0", "@ionic/react-router": "^6.0.0", "@testing-library/jest-dom": "^5.11.9", @@ -23,6 +24,7 @@ "ionicons": "^5.4.0", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-hook-form": "^7.30.0", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "typescript": "^4.1.3", diff --git a/src/App.tsx b/src/App.tsx index be70932..1dc24c1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,11 +5,11 @@ import { setupIonicReact } from '@ionic/react'; import { IonReactRouter } from '@ionic/react-router'; -import Cadastro from './pages/Cadastro'; +import Cadastro from './pages/Cadastro/Cadastro'; import MainPages from './pages/MainPages'; // importação das páginas -import Login from './pages/login/Login'; +import Login from './pages/Login'; /* Core CSS required for Ionic components to work properly */ import '@ionic/react/css/core.css'; @@ -29,6 +29,7 @@ import '@ionic/react/css/display.css'; /* Theme variables */ import './theme/variables.css'; +// import Tabs from './components/Tabs'; setupIonicReact(); diff --git a/src/LocalStorage.ts b/src/LocalStorage.ts new file mode 100644 index 0000000..b7a450b --- /dev/null +++ b/src/LocalStorage.ts @@ -0,0 +1,18 @@ +const tokenId = 'token'; +const productDetails = '@productDetails'; + +const LocalStorage = { + getToken: (): string | null => { + return localStorage.getItem(tokenId) + }, + + setToken: (token: string) => { + localStorage.setItem(tokenId, token) + }, + + clearToken: () => { + localStorage.setItem(tokenId, ''); + } +} + +export default LocalStorage \ No newline at end of file diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx new file mode 100644 index 0000000..ceed0f0 --- /dev/null +++ b/src/components/Tabs.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react'; + +import { logInOutline, logInSharp, personOutline, personSharp } from 'ionicons/icons'; + +interface AppTab { + label: string; + iosIcon: string; + mdIcon: string; + // badge: string; +} + +const appPages: AppTab[] = [ + { + label: 'Login', + iosIcon: logInOutline, + mdIcon: logInSharp, + // badge: '', + }, + { + label: 'Cadastro', + iosIcon: personOutline, + mdIcon: personSharp, + // badge: '', + } +] + +const Tabs: React.FC = () => { + return ( + + + {appPages.map((appPage, index) => { + + + + {appPage.label} + + + + })} + + ); +} + +export default Tabs \ No newline at end of file diff --git a/src/config/api.config.ts b/src/config/api.config.ts index c30fbd8..172c67e 100644 --- a/src/config/api.config.ts +++ b/src/config/api.config.ts @@ -1,7 +1,7 @@ import environment from "../environments/environment"; -function getBaseUrl() { - const { hostname } = window.location; +const getBaseUrl = (): string => { + // const { hostname } = window.location; const { url } = environment; let apiUrl = null; diff --git a/src/constants/routes/sessionRoutes.ts b/src/constants/routes/sessionRoutes.ts new file mode 100644 index 0000000..11717b1 --- /dev/null +++ b/src/constants/routes/sessionRoutes.ts @@ -0,0 +1,11 @@ +const sessionRoutesDefault = '/sessions'; +const sessionRoutes = { + create: { + url: `${sessionRoutesDefault}/` + }, + refresh: { + url: `${sessionRoutesDefault}/refresh` + } +} + +export default sessionRoutes; \ No newline at end of file diff --git a/src/pages/Cadastro.css b/src/pages/Cadastro/Cadastro.css similarity index 100% rename from src/pages/Cadastro.css rename to src/pages/Cadastro/Cadastro.css diff --git a/src/pages/Cadastro.tsx b/src/pages/Cadastro/Cadastro.tsx similarity index 98% rename from src/pages/Cadastro.tsx rename to src/pages/Cadastro/Cadastro.tsx index b76dd9f..566d0ad 100644 --- a/src/pages/Cadastro.tsx +++ b/src/pages/Cadastro/Cadastro.tsx @@ -1,11 +1,11 @@ import { IonToast, IonProgressBar, IonItem, IonLabel, IonInput, IonBackButton, IonButton, IonButtons, IonCardTitle, IonCol, IonContent, IonGrid, IonHeader, IonPage, IonRow, IonToolbar } from '@ionic/react'; import { arrowBack, logoFacebook, mail } from 'ionicons/icons'; -import { Action } from '../components/Action'; +import { Action } from '../../components/Action'; import { useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router'; import './Cadastro.css'; -import ModalExample from '../components/Email'; -import * as UsersService from '../services/users' +import ModalExample from '../../components/Email'; +import * as UsersService from '../../services/users' const Cadastro: React.FC = () => { const history = useHistory(); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..cd14fb7 --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,195 @@ +import { + IonContent, + IonHeader, + IonPage, + IonTitle, + IonToolbar +} from "@ionic/react"; +import React, { useState } from "react"; +import { IonGrid, IonRow, IonCol, IonToast } from "@ionic/react"; +import { personCircle } from "ionicons/icons"; +import { useHistory } from "react-router-dom"; +import { + IonItem, + IonLabel, + IonInput, + IonButton, + IonIcon, + IonAlert, +} from "@ionic/react"; + +import * as sessionRoutes from '../services/session'; +import LocalStorage from '../LocalStorage'; +import { Action } from "../components/Action"; + +const Page: React.FC = () => { + const [showToast, setShowToast] = useState(false); + const [messageToast, setMessageToast ] = useState(''); + + const history = useHistory(); + const [email, setEmail] = useState("matheusalb3213@gmail.com"); + const [password, setPassword] = useState("123456"); + const [isError, setIsError] = useState(false); + const [message, setMessage] = useState(""); + + function validateEmail(email: string) { + const re = + /^((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\]))$/; + return re.test(String(email).toLowerCase()); + } + + const validateForm = () => { + if (!email) { + setMessageToast("Por favor, informe o e-mail"); + setShowToast(true); + return false; + } + + if (!validateEmail(email)) { + setMessageToast("E-mail inválido"); + setShowToast(true); + return false; + } + + if (!password) { + setMessageToast("Por favor, digite a sua senha"); + setShowToast(true); + return false; + } + + if(password.length < 7 || password.length > 12) { + setMessageToast("A senha deve conter entre 7 e 12 dígitos"); + setShowToast(true); + return false; + } + + return true; + } + + const handleLogin = async () => { + if (!validateForm()) { + return + } + + const singinForm = { + login: email, + password: password, + }; + + await sessionRoutes.create(singinForm).then(response => { + if (response.status === 'error') { + setMessageToast(response.message); + setShowToast(true); + + return + } + + const { token } = response.token + + LocalStorage.setToken(token); + + history.push({ pathname: '/home' }); + }).catch(error => { + // if (!error.response) return + + // se o backend retornou uma mensagem de erro customizada + // if (error.response.data.message) { + console.dir('Houve um erro: ', { error }) + alert('Houve um erro') + }) + }; + + return ( + + + + Login + + + + + + + + + + + + + Login + + + + + + + setIsError(false)} + cssClass="my-custom-class" + header={"Error!"} + message={message} + buttons={["Dismiss"]} + /> + + + + + + + Email + setEmail(e.detail.value!)} + > + + + + + + + + Senha + setPassword(e.detail.value!)} + > + + + + + + +

+ Clicando no botão de "Login", você concorda com a nossa{" "} + política de termos e serviços +

+ + Login + +

+ +

+
+
+
+ + setShowToast(false)} + message={messageToast} + duration={2500} + /> +
+
+ ); +}; + +export default Page; diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx deleted file mode 100644 index ee3adcd..0000000 --- a/src/pages/login/Login.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { - IonContent, - IonHeader, - IonPage, - IonTitle, - IonToolbar, - IonButtons, -} from "@ionic/react"; -import React, { useState } from "react"; -import axios from "axios"; -import { IonGrid, IonRow, IonCol } from "@ionic/react"; -import { personCircle } from "ionicons/icons"; -import { useHistory } from "react-router-dom"; -import { - IonItem, - IonLabel, - IonInput, - IonButton, - IonIcon, - IonAlert, -} from "@ionic/react"; - -function validateEmail(email: string) { - const re = - /^((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\]))$/; - return re.test(String(email).toLowerCase()); -} - -const Page: React.FC = () => { - const history = useHistory(); - const [email, setEmail] = useState("matheusalb3213@gmail.com"); - const [password, setPassword] = useState("1234"); - const [isError, setIsError] = useState(false); - const [message, setMessage] = useState(""); - - const handleLogin = () => { - // validação de inputs - if (!email) { - setMessage("Por favor, informe um e-mail válido"); - setIsError(true); - return; - } - - if (validateEmail(email) === false) { - setMessage("E-mail inválido"); - setIsError(true); - return; - } - - if (!password || password.length < 6) { - setMessage("Por favor, digite a sua senha"); - setIsError(true); - return; - } - - const loginData = { - email: email, - password: password, - }; - - const api = axios.create({ - baseURL: `https://625dc16c4c36c7535779792c.mockapi.io/api/v1`, - }); - - api - // .post("/login", loginData) - .get("/users/2") - .then((res) => { - // login bem-sucedido - history.push("/dashboard/" + email); - }) - .catch((error) => { - setMessage("Falha na autenticação! Por favor, crie uma conta"); - setIsError(true); - }); - }; - - return ( - - - - Login - - - - - - - - - - - - - Login - - - - - - - setIsError(false)} - cssClass="my-custom-class" - header={"Error!"} - message={message} - buttons={["Dismiss"]} - /> - - - - - - - Email - setEmail(e.detail.value!)} - > - - - - - - - - Senha - setPassword(e.detail.value!)} - > - - - - - - -

- Clicando no botão de "LOGIN", você concorda com a nossa{" "} - política de termos e serviços -

- - Login - -

- Ainda não possui uma conta? Cadastre-se aqui! -

-
-
-
-
-
- ); -}; - -export default Page; diff --git a/src/services/session.ts b/src/services/session.ts new file mode 100644 index 0000000..004ab03 --- /dev/null +++ b/src/services/session.ts @@ -0,0 +1,33 @@ +import instance from '../services/api'; +import sessionRoutes from '../constants/routes/sessionRoutes'; +import LocalStorage from '../LocalStorage'; +import { AxiosRequestHeaders } from 'axios'; + +let token: string | null; +let header: AxiosRequestHeaders; + +interface createData { + login: string, + password: string, +} + +function updateHeader() { + token = LocalStorage.getToken(); + header = { + Accept: 'application/json', + 'Content-Type': 'application/json', + "Authorization": 'Bearer ' + token + } +} + +export async function create(data: createData) { + const response = await instance.post(sessionRoutes.create.url, data); + return response.data; +} + +export async function refresh() { + updateHeader(); + + let response = await instance.post(sessionRoutes.refresh.url, { token }, { headers: header }); + return response.data; +} \ No newline at end of file