diff --git a/react-app/.eslintrc.json b/react-app/.eslintrc.json new file mode 100644 index 0000000..26969f0 --- /dev/null +++ b/react-app/.eslintrc.json @@ -0,0 +1,22 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "plugin:react/recommended", + "standard", + "prettier/@typescript-eslint", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": ["react", "@typescript-eslint"], + "rules": {} +} diff --git a/react-app/.gitignore b/react-app/.gitignore new file mode 100644 index 0000000..d451ff1 --- /dev/null +++ b/react-app/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/react-app/.prettierrc b/react-app/.prettierrc new file mode 100644 index 0000000..fa51da2 --- /dev/null +++ b/react-app/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true +} diff --git a/react-app/__tests__/components/Button.test.tsx b/react-app/__tests__/components/Button.test.tsx new file mode 100644 index 0000000..2ac32ed --- /dev/null +++ b/react-app/__tests__/components/Button.test.tsx @@ -0,0 +1,18 @@ +import * as renderer from 'react-test-renderer' +import { Button } from '../../src/components/Button' + +describe(' +`; diff --git a/react-app/__tests__/components/__snapshots__/Card.test.tsx.snap b/react-app/__tests__/components/__snapshots__/Card.test.tsx.snap new file mode 100644 index 0000000..f4be0d8 --- /dev/null +++ b/react-app/__tests__/components/__snapshots__/Card.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` component should renders correctly 1`] = ` +
+`; diff --git a/react-app/__tests__/components/__snapshots__/Input.test.tsx.snap b/react-app/__tests__/components/__snapshots__/Input.test.tsx.snap new file mode 100644 index 0000000..68c7cb2 --- /dev/null +++ b/react-app/__tests__/components/__snapshots__/Input.test.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` component should renders correctly 1`] = ` +
+ + + Email + +
+`; diff --git a/react-app/__tests__/components/__snapshots__/SnackBar.test.tsx.snap b/react-app/__tests__/components/__snapshots__/SnackBar.test.tsx.snap new file mode 100644 index 0000000..0df0a01 --- /dev/null +++ b/react-app/__tests__/components/__snapshots__/SnackBar.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` component should renders correctly 1`] = ` +
+ Test +
+`; diff --git a/react-app/__tests__/components/__snapshots__/Title.test.tsx.snap b/react-app/__tests__/components/__snapshots__/Title.test.tsx.snap new file mode 100644 index 0000000..c491fb2 --- /dev/null +++ b/react-app/__tests__/components/__snapshots__/Title.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` component should renders correctly 1`] = ` +Object { + "children": ArrayContaining [ + "Test", + ], + "props": Object { + "className": "title", + }, + "type": "h1", +} +`; diff --git a/react-app/index.html b/react-app/index.html new file mode 100644 index 0000000..dc68c18 --- /dev/null +++ b/react-app/index.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <link rel="icon" type="image/svg+xml" href="/src/favicon.ico" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link rel="preconnect" href="https://fonts.gstatic.com" /> + <link + href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap" + rel="stylesheet" + /> + <title>MasterTech App + + +
+ + + diff --git a/react-app/jest.setup.ts b/react-app/jest.setup.ts new file mode 100644 index 0000000..c44951a --- /dev/null +++ b/react-app/jest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom' diff --git a/react-app/package.json b/react-app/package.json new file mode 100644 index 0000000..ba0c2ce --- /dev/null +++ b/react-app/package.json @@ -0,0 +1,61 @@ +{ + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "serve": "vite preview", + "test": "jest", + "test:watch": "jest --watch" + }, + "jest": { + "transform": { + "^.+\\.tsx?$": "ts-jest" + }, + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json", + "node" + ], + "moduleNameMapper": { + "\\.(css|scss)$": "identity-obj-proxy" + } + }, + "dependencies": { + "axios": "^0.21.1", + "react": "^17.0.0", + "react-dom": "^17.0.0", + "react-router-dom": "^5.2.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^5.13.0", + "@testing-library/react": "^11.2.7", + "@types/jest": "^26.0.23", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "@types/react-router-dom": "^5.1.7", + "@types/react-test-renderer": "^17.0.1", + "@typescript-eslint/eslint-plugin": "^4.26.1", + "@typescript-eslint/parser": "^4.26.1", + "@vitejs/plugin-react-refresh": "^1.3.1", + "eslint": "^7.28.0", + "eslint-config-prettier": "^8.3.0", + "eslint-config-standard": "^16.0.3", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^3.4.0", + "eslint-plugin-promise": "^5.1.0", + "eslint-plugin-react": "^7.24.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^27.0.4", + "prettier": "^2.3.1", + "react-test-renderer": "^17.0.2", + "sass": "^1.34.1", + "ts-jest": "^27.0.3", + "typescript": "^4.3.2", + "vite": "^2.3.7" + } +} diff --git a/react-app/src/App.tsx b/react-app/src/App.tsx new file mode 100644 index 0000000..70ce023 --- /dev/null +++ b/react-app/src/App.tsx @@ -0,0 +1,18 @@ +import { FC } from 'react' +import { Router } from 'react-router-dom' +import { history } from './helpers/history' +import { Routes } from './routes' +import { AuthProvider } from './context/AuthContext' +import { SnackBarProvider } from './context/SnackBarContext' + +export const App: FC = () => { + return ( + + + + + + + + ) +} diff --git a/react-app/src/assets/404.svg b/react-app/src/assets/404.svg new file mode 100644 index 0000000..c4e2583 --- /dev/null +++ b/react-app/src/assets/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-app/src/components/Button/index.scss b/react-app/src/components/Button/index.scss new file mode 100644 index 0000000..019356d --- /dev/null +++ b/react-app/src/components/Button/index.scss @@ -0,0 +1,41 @@ +@use "../../themes/variables.scss" as theme; + +.button { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + padding: 10px; + background-color: theme.$color-yellow-primary; + box-shadow: theme.$shadow; + color: black; + @extend .text-lg; + font-weight: theme.$font-bold; + width: 100%; + max-width: 100px; + margin: theme.$margin-default; + cursor: pointer; + transition: transform 0.25s ease, box-shadow 0.25s ease, + background-color 0.25s ease; +} + +.button:hover { + transform: translate3d(0px, -1px, 0px); + box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08); +} + +.donut-spinner { + border: 4px solid white; + border-left-color: black; + border-radius: 50%; + width: 20px; + height: 20px; + animation: donut-spin 1.2s linear infinite; +} + +@keyframes donut-spin { + to { + transform: rotate(1turn); + } +} diff --git a/react-app/src/components/Button/index.tsx b/react-app/src/components/Button/index.tsx new file mode 100644 index 0000000..a9a6fec --- /dev/null +++ b/react-app/src/components/Button/index.tsx @@ -0,0 +1,20 @@ +import { ButtonHTMLAttributes, FC } from 'react' +import './index.scss' + +type ButtonProps = ButtonHTMLAttributes & { + title: string + isLoading: boolean +} + +export const Button: FC = ({ + children, + title, + isLoading, + ...props +}) => { + return ( + + ) +} diff --git a/react-app/src/components/Card/index.tsx b/react-app/src/components/Card/index.tsx new file mode 100644 index 0000000..59c81b3 --- /dev/null +++ b/react-app/src/components/Card/index.tsx @@ -0,0 +1,6 @@ +import { FC } from 'react' +import './styles.scss' + +export const Card: FC = ({ children }) => { + return
{children}
+} diff --git a/react-app/src/components/Card/styles.scss b/react-app/src/components/Card/styles.scss new file mode 100644 index 0000000..5c12866 --- /dev/null +++ b/react-app/src/components/Card/styles.scss @@ -0,0 +1,14 @@ +@use "../../themes/variables.scss" as theme; + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-around; + width: 85vw; + max-width: 600px; + height: auto; + background-color: #fff; + box-shadow: theme.$shadow; + padding: 15px; +} diff --git a/react-app/src/components/Input/index.tsx b/react-app/src/components/Input/index.tsx new file mode 100644 index 0000000..ededd85 --- /dev/null +++ b/react-app/src/components/Input/index.tsx @@ -0,0 +1,22 @@ +import { FC, forwardRef, HTMLProps, InputHTMLAttributes } from 'react' +import './styles.scss' + +type InputProps = HTMLProps & { + title: string +} + +export const Input = forwardRef( + ({ title, ...props }, ref) => { + return ( +
+ + {title} +
+ ) + } +) diff --git a/react-app/src/components/Input/styles.scss b/react-app/src/components/Input/styles.scss new file mode 100644 index 0000000..059fd9b --- /dev/null +++ b/react-app/src/components/Input/styles.scss @@ -0,0 +1,41 @@ +@use "../../themes/variables.scss" as theme; + +.wrapper { + position: relative; + width: 100%; + margin: theme.$margin-default; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.floating-input { + width: 80%; + background: none; + padding: 10px; + border: none; + border-bottom: 2px solid theme.$border-color; + transition: box-shadow 0.25s; +} + +.floating-input:focus { + box-shadow: theme.$shadow-input; + outline: none; +} + +.floating-label { + left: 9%; + top: -20px; + display: block; + visibility: hidden; + position: absolute; + opacity: 0; + transition: visibility 0.25s, opacity 0.5s linear; + color: theme.$color-darkest-gray; +} + +.floating-input:focus ~ .floating-label { + visibility: visible; + opacity: 1; +} diff --git a/react-app/src/components/Snackbar/index.tsx b/react-app/src/components/Snackbar/index.tsx new file mode 100644 index 0000000..339270b --- /dev/null +++ b/react-app/src/components/Snackbar/index.tsx @@ -0,0 +1,10 @@ +import { FC } from 'react' +import './styles.scss' + +type SnackBarProps = { + title: string +} + +export const SnackBar: FC = ({ title }) => { + return
{title}
+} diff --git a/react-app/src/components/Snackbar/styles.scss b/react-app/src/components/Snackbar/styles.scss new file mode 100644 index 0000000..27119d1 --- /dev/null +++ b/react-app/src/components/Snackbar/styles.scss @@ -0,0 +1,28 @@ +@use "../../themes/variables.scss" as theme; + +.snackbar { + visibility: visible; + min-width: 250px; + margin-left: -125px; + background-color: theme.$color-darkest-gray; + color: #fff; + text-align: center; + border-radius: 2px; + padding: 16px; + position: fixed; + z-index: 1; + left: 50%; + bottom: 30px; + animation: fadein 1.25s infinite alternate; +} + +@keyframes fadein { + from { + bottom: 0; + opacity: 0; + } + to { + bottom: 50px; + opacity: 1; + } +} diff --git a/react-app/src/components/Title/index.tsx b/react-app/src/components/Title/index.tsx new file mode 100644 index 0000000..5b9ae80 --- /dev/null +++ b/react-app/src/components/Title/index.tsx @@ -0,0 +1,6 @@ +import { FC } from 'react' +import './styles.scss' + +export const Title: FC = ({ children }) => { + return

{children}

+} diff --git a/react-app/src/components/Title/styles.scss b/react-app/src/components/Title/styles.scss new file mode 100644 index 0000000..950d080 --- /dev/null +++ b/react-app/src/components/Title/styles.scss @@ -0,0 +1,8 @@ +@use "../../themes/variables.scss" as theme; + +.title { + color: black; + @extend .text-3xl; + font-weight: theme.$font-bold; + margin: theme.$margin-default; +} diff --git a/react-app/src/config/index.ts b/react-app/src/config/index.ts new file mode 100644 index 0000000..2760978 --- /dev/null +++ b/react-app/src/config/index.ts @@ -0,0 +1,3 @@ +export default { + baseURL: 'http://jrwee.mocklab.io/user', +} diff --git a/react-app/src/context/AuthContext.tsx b/react-app/src/context/AuthContext.tsx new file mode 100644 index 0000000..917b116 --- /dev/null +++ b/react-app/src/context/AuthContext.tsx @@ -0,0 +1,35 @@ +import { createContext, FC } from 'react' +import { useAuth } from '../hooks/useAuth' + +type FormDataProps = { + email: string + password: string +} + +type AuthContextProps = { + isAuth: boolean + isLoading: boolean + handleLogin: (formData: FormDataProps) => Promise + handleLogout: () => void +} + +const AuthContext = createContext({} as AuthContextProps) + +const AuthProvider: FC = ({ children }) => { + const { isAuth, isLoading, handleLogin, handleLogout } = useAuth() + + return ( + + {children} + + ) +} + +export { AuthContext, AuthProvider } diff --git a/react-app/src/context/SnackBarContext.tsx b/react-app/src/context/SnackBarContext.tsx new file mode 100644 index 0000000..65c9522 --- /dev/null +++ b/react-app/src/context/SnackBarContext.tsx @@ -0,0 +1,45 @@ +import { + createContext, + FC, + useCallback, + useEffect, + useMemo, + useState, +} from 'react' +import { SnackBar } from '../components/Snackbar' + +export const SnackBarContext = createContext({ + addAlert: (content: any) => {}, +}) + +const AUTO_DISMISS = 2500 + +export const SnackBarProvider: FC = ({ children }) => { + const [alerts, setAlerts] = useState([]) + const activeAlertIds = alerts.join(',') + useEffect(() => { + if (activeAlertIds.length > 0) { + const timer = setTimeout( + () => setAlerts((alerts) => alerts.slice(0, alerts.length - 1)), + AUTO_DISMISS + ) + return () => clearTimeout(timer) + } + }, [activeAlertIds]) + + const addAlert = useCallback( + (content) => setAlerts((alerts) => [content, ...alerts]), + [] + ) + + const value = useMemo(() => ({ addAlert }), [addAlert]) + + return ( + + {children} + {alerts.map((alert) => ( + + ))} + + ) +} diff --git a/react-app/src/favicon.ico b/react-app/src/favicon.ico new file mode 100644 index 0000000..166b20e Binary files /dev/null and b/react-app/src/favicon.ico differ diff --git a/react-app/src/helpers/apiHelper.ts b/react-app/src/helpers/apiHelper.ts new file mode 100644 index 0000000..e9a1c78 --- /dev/null +++ b/react-app/src/helpers/apiHelper.ts @@ -0,0 +1,27 @@ +import axios, { AxiosRequestConfig } from 'axios' +import { api } from '../services/api' + +type ApiHelperProps = { + url: string + options: AxiosRequestConfig +} + +export const apiHelper = async ({ url, options }: ApiHelperProps) => { + try { + const { data } = await api(url, options) + return { data, error: null } + } catch (error) { + const statusCode = error?.response?.status + const errorDetail = error?.response?.data?.statusText + + if (statusCode && !errorDetail) { + return { data: null, error: { statusCode, message: error.message } } + } + + if (statusCode && errorDetail) { + return { data: null, error: { statusCode, message: errorDetail } } + } + + return { data: null, error: error } + } +} diff --git a/react-app/src/helpers/history.ts b/react-app/src/helpers/history.ts new file mode 100644 index 0000000..8d6bfaf --- /dev/null +++ b/react-app/src/helpers/history.ts @@ -0,0 +1,2 @@ +import { createBrowserHistory } from 'history' +export const history = createBrowserHistory() diff --git a/react-app/src/hooks/useAuth.ts b/react-app/src/hooks/useAuth.ts new file mode 100644 index 0000000..d6cb778 --- /dev/null +++ b/react-app/src/hooks/useAuth.ts @@ -0,0 +1,98 @@ +import { FC, useEffect, useState } from 'react' +import config from '../config' +import { apiHelper } from '../helpers/apiHelper' +import { history } from '../helpers/history' +import { isValidUrl } from '../utils/isValidUrl' +import { useLocalStorage } from './useLocalStorage' + +/* + Por "segurança", faço uma verificação se no localstorage existe uma chave chamada + token, que nada mais é a url da imagem do user, + apenas pra não ficar algo e estar autorizado apenas trocando o valor do boolean "isAuth" +*/ + +type FormDataProps = { + email: string + password: string +} + +type ResponseData = { + birthday: string + email: string + gender: string + id: number + name: string + state: string + avatar: string +} + +export const useAuth = () => { + const [isAuth, setIsAuth] = useLocalStorage('isAuth', false) + const [isLoading, setIsLoading] = useState(false) + const baseUrl = config.baseURL + + useEffect(() => { + setIsLoading(true) + const fakeToken = localStorage.getItem('token') + + if (fakeToken && isValidUrl(JSON.parse(fakeToken))) { + setIsAuth(true) + return history.push('/dashboard') + } + + setIsAuth(false) + setIsLoading(false) + }, []) + + const handleLogin = async (formData: FormDataProps) => { + setIsLoading(true) + const { email, password } = formData + const { data, error } = await apiHelper({ + url: `${baseUrl}/login`, + options: { + method: 'POST', + data: { + email, + password, + }, + }, + }) + + if (error) { + setIsLoading(false) + console.log(error) + + if (error.statusCode) { + return alert( + `Erro com status : ${error.statusCode} - Usuário não encontrado!` + ) + } + + return setIsLoading(false) + } + + const { avatar } = data + localStorage.setItem('token', JSON.stringify(avatar)) + + setIsAuth(true) + setIsLoading(false) + return history.push('/dashboard', { + user: data, + }) + } + + const handleLogout = () => { + setIsLoading(true) + setIsAuth(false) + localStorage.clear() + history.push('/login') + setIsLoading(false) + } + + return { + handleLogin, + handleLogout, + isAuth, + isLoading, + } +} diff --git a/react-app/src/hooks/useLocalStorage.ts b/react-app/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000..31e9649 --- /dev/null +++ b/react-app/src/hooks/useLocalStorage.ts @@ -0,0 +1,24 @@ +import { useState } from 'react' + +export const useLocalStorage = (key: string, initialValue: any) => { + const [storedValue, setStoredValue] = useState(() => { + try { + const item = localStorage.getItem(key) + return item ? JSON.parse(item) : initialValue + } catch (error) { + console.log(error) + return initialValue + } + }) + const setValue = (value: any) => { + try { + const valueToStore = + value instanceof Function ? value(storedValue) : value + setStoredValue(valueToStore) + localStorage.setItem(key, JSON.stringify(valueToStore)) + } catch (error) { + console.log(error) + } + } + return [storedValue, setValue] +} diff --git a/react-app/src/hooks/useSnackBars.ts b/react-app/src/hooks/useSnackBars.ts new file mode 100644 index 0000000..517515a --- /dev/null +++ b/react-app/src/hooks/useSnackBars.ts @@ -0,0 +1,7 @@ +import { useContext } from 'react' + +import { SnackBarContext } from '../context/SnackBarContext' + +const useSnackBars = () => useContext(SnackBarContext) + +export { useSnackBars } diff --git a/react-app/src/index.scss b/react-app/src/index.scss new file mode 100644 index 0000000..a626e29 --- /dev/null +++ b/react-app/src/index.scss @@ -0,0 +1,18 @@ +@use "./themes/variables.scss" as theme; + +body { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + width: 100vw; + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: theme.$font-name; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + scroll-behavior: smooth; + background-color: theme.$color-lightest-gray; +} diff --git a/react-app/src/main.tsx b/react-app/src/main.tsx new file mode 100644 index 0000000..8489daa --- /dev/null +++ b/react-app/src/main.tsx @@ -0,0 +1,12 @@ +import { StrictMode } from 'react' +import ReactDOM from 'react-dom' +import { App } from './App' +import './reset.scss' +import './index.scss' + +ReactDOM.render( + + + , + document.getElementById('root') +) diff --git a/react-app/src/pages/Dashboard/index.tsx b/react-app/src/pages/Dashboard/index.tsx new file mode 100644 index 0000000..92da8f4 --- /dev/null +++ b/react-app/src/pages/Dashboard/index.tsx @@ -0,0 +1,50 @@ +import { FC, useContext } from 'react' +import { RouteComponentProps } from 'react-router' +import { Button } from '../../components/Button' +import { Card } from '../../components/Card' +import { Title } from '../../components/Title' +import { AuthContext } from '../../context/AuthContext' +import './styles.scss' + +type UserProps = { + birthday: string + email: string + gender: string + id: number + name: string + state: string + avatar: string +} + +type DashboardProps = RouteComponentProps<{}, any, { user: UserProps }> + +export const Dashboard: FC = ({ location }) => { + const { handleLogout } = useContext(AuthContext) + const user = location.state?.user + + return ( + + {!user && ( +

Sua sessão expirou, logue novamente!

+ )} +
+
+ {user?.name} +

Email: {user?.email}

+

Estado: {user?.state}

+
+
+ User avatar +
+
+