├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── README-install.md ├── README.md ├── mock ├── README.md └── database.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── index.tsx ├── pages │ ├── cidades │ │ ├── DetalheDeCidades.tsx │ │ └── ListagemDeCidades.tsx │ ├── dashboard │ │ └── Dashboard.tsx │ ├── index.ts │ └── pessoas │ │ ├── DetalheDePessoas.tsx │ │ ├── ListagemDePessoas.tsx │ │ └── components │ │ └── AutoCompleteCidade.tsx ├── react-app-env.d.ts ├── routes │ └── index.tsx └── shared │ ├── components │ ├── ferramentas-da-listagem │ │ └── FerramentasDaListagem.tsx │ ├── ferramentas-de-detalhe │ │ └── FerramentasDeDetalhe.tsx │ ├── index.ts │ ├── login │ │ └── Login.tsx │ └── menu-lateral │ │ └── MenuLateral.tsx │ ├── contexts │ ├── AuthContext.tsx │ ├── DrawerContext.tsx │ ├── ThemeContext.tsx │ └── index.ts │ ├── environment │ └── index.ts │ ├── forms │ ├── IVFormErrors.ts │ ├── TraducoesYup.ts │ ├── VForm.ts │ ├── VNumericFormat.tsx │ ├── VPatternFormat.tsx │ ├── VScope.ts │ ├── VSelect.tsx │ ├── VSwitch.tsx │ ├── VTextField.tsx │ ├── index.ts │ └── useVForm.ts │ ├── hooks │ ├── UseDebounce.ts │ └── index.ts │ ├── layouts │ ├── LayoutBaseDePagina.tsx │ └── index.ts │ ├── services │ └── api │ │ ├── auth │ │ └── AuthService.ts │ │ ├── axios-config │ │ ├── index.ts │ │ └── interceptors │ │ │ ├── ErrorInterceptor.ts │ │ │ ├── ResponseInterceptor.ts │ │ │ └── index.ts │ │ ├── cidades │ │ └── CidadesService.ts │ │ └── pessoas │ │ └── PessoasService.ts │ └── themes │ ├── Dark.ts │ ├── Light.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": 13, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "react", 21 | "@typescript-eslint" 22 | ], 23 | "rules": { 24 | "indent": [ 25 | "error", 26 | 2 27 | ], 28 | "linebreak-style": [ 29 | "error", 30 | "unix" 31 | ], 32 | "quotes": [ 33 | "error", 34 | "single" 35 | ], 36 | "semi": [ 37 | "error", 38 | "always" 39 | ], 40 | "react/prop-types": "off", 41 | "react/react-in-jsx-scope": "off" 42 | } 43 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "opcoes" 4 | ] 5 | } -------------------------------------------------------------------------------- /README-install.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Cadastros 3 | -------------------------------------------------------------------------------- /mock/README.md: -------------------------------------------------------------------------------- 1 | # Como usar 2 | 3 | 1. Abra uma nova aba no terminal 4 | 2. Execute o seguinte comando: 5 | ``` 6 | yarn run json-server -w -p 3333 ./mock/database.json 7 | ``` 8 | ou 9 | ``` 10 | npm run json-server -w -p 3333 ./mock/database.json 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /mock/database.json: -------------------------------------------------------------------------------- 1 | { 2 | "pessoas": [ 3 | { 4 | "id": 3, 5 | "nomeCompleto": "Carlos Silva", 6 | "email": "carlos@gmail.com", 7 | "cidadeId": 1 8 | }, 9 | { 10 | "id": 5, 11 | "nomeCompleto": "Alfredo Pereira", 12 | "email": "alfredo@gmail.com", 13 | "cidadeId": 1 14 | }, 15 | { 16 | "id": 6, 17 | "nomeCompleto": "Juca Silva", 18 | "email": "Juca@gmail.com", 19 | "cidadeId": 1 20 | }, 21 | { 22 | "id": 7, 23 | "nomeCompleto": "Maria Silva", 24 | "email": "maria@gmail.com", 25 | "cidadeId": 3 26 | }, 27 | { 28 | "id": 8, 29 | "cidadeId": 1, 30 | "email": "carlos@gmail.com", 31 | "nomeCompleto": "Carlos Silva" 32 | }, 33 | { 34 | "id": 9, 35 | "cidadeId": 1, 36 | "email": "pedro@gmail.com", 37 | "nomeCompleto": "Pedro Silva" 38 | }, 39 | { 40 | "id": 10, 41 | "cidadeId": 1, 42 | "email": "alfredo@gmail.com", 43 | "nomeCompleto": "Alfredo Silva" 44 | }, 45 | { 46 | "id": 11, 47 | "cidadeId": 1, 48 | "email": "alfredo@gmail.com", 49 | "nomeCompleto": "Alfredo Silva" 50 | }, 51 | { 52 | "id": 12, 53 | "cidadeId": 1, 54 | "email": "Juca@gmail.com", 55 | "nomeCompleto": "Juca Silva" 56 | }, 57 | { 58 | "id": 13, 59 | "cidadeId": 1, 60 | "email": "maria@gmail.com", 61 | "nomeCompleto": "Maria Silva" 62 | }, 63 | { 64 | "id": 14, 65 | "cidadeId": 1, 66 | "email": "carlos@gmail.com", 67 | "nomeCompleto": "Carlos Silva" 68 | }, 69 | { 70 | "id": 154, 71 | "cidadeId": 1, 72 | "email": "pedro@gmail.com", 73 | "nomeCompleto": "Pedro Silva" 74 | }, 75 | { 76 | "id": 16, 77 | "cidadeId": 1, 78 | "email": "alfredo@gmail.com", 79 | "nomeCompleto": "Alfredo Silva" 80 | }, 81 | { 82 | "nomeCompleto": "Ana Silva", 83 | "email": "ana@gmail.com", 84 | "cidadeId": "1", 85 | "id": 156 86 | }, 87 | { 88 | "nomeCompleto": "Geronimo", 89 | "email": "geronimo@gmail.com", 90 | "cidadeId": 5, 91 | "id": 157 92 | } 93 | ], 94 | "cidades": [ 95 | { 96 | "nome": "Porto Alegre", 97 | "id": 1 98 | }, 99 | { 100 | "nome": "Novo Hamburgo", 101 | "id": 2 102 | }, 103 | { 104 | "nome": "Taquara", 105 | "id": 3 106 | }, 107 | { 108 | "nome": "Palmitinho", 109 | "id": 4 110 | }, 111 | { 112 | "nome": "Caxias", 113 | "id": 5 114 | }, 115 | { 116 | "nome": "Sapucaia do Sul", 117 | "id": 6 118 | }, 119 | { 120 | "nome": "Tramandaí", 121 | "id": 7 122 | } 123 | ], 124 | "auth": { 125 | "accessToken": "aaaaaaaaaa.bbbbbbbbbb.cccccccc" 126 | } 127 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youtube-curso-react-materialui-typescript", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "BROWSER=none react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test", 9 | "mock": "json-server -w -p 3333 ./mock/database.json", 10 | "eject": "react-scripts eject" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.7.1", 14 | "@emotion/styled": "^11.6.0", 15 | "@mui/icons-material": "^5.2.5", 16 | "@mui/material": "^5.2.6", 17 | "@unform/core": "^2.1.6", 18 | "@unform/web": "^2.1.6", 19 | "axios": "^0.26.0", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-number-format": "^5.0.0-beta.4", 23 | "react-router-dom": "6", 24 | "react-scripts": "5.0.0", 25 | "yup": "^0.32.11" 26 | }, 27 | "devDependencies": { 28 | "@testing-library/jest-dom": "^5.16.1", 29 | "@testing-library/react": "^12.1.2", 30 | "@testing-library/user-event": "^13.5.0", 31 | "@types/jest": "^27.0.3", 32 | "@types/node": "^16.11.17", 33 | "@types/react": "^18.0.13", 34 | "@types/react-dom": "^18.0.5", 35 | "@typescript-eslint/eslint-plugin": "^5.8.1", 36 | "@typescript-eslint/parser": "^5.8.1", 37 | "eslint": "^8.5.0", 38 | "eslint-plugin-react": "^7.28.0", 39 | "json-server": "^0.17.0", 40 | "typescript": "^4.5.4", 41 | "web-vitals": "^2.1.2" 42 | }, 43 | "eslintConfig": { 44 | "extends": [ 45 | "react-app", 46 | "react-app/jest" 47 | ] 48 | }, 49 | "browserslist": { 50 | "production": [ 51 | ">0.2%", 52 | "not dead", 53 | "not op_mini all" 54 | ], 55 | "development": [ 56 | "last 1 chrome version", 57 | "last 1 firefox version", 58 | "last 1 safari version" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvsouza/youtube-curso-react-materialui-typescript/4458b121f3a8678e84abcd8d059fe3d61cb01dc4/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Cadastros 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvsouza/youtube-curso-react-materialui-typescript/4458b121f3a8678e84abcd8d059fe3d61cb01dc4/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvsouza/youtube-curso-react-materialui-typescript/4458b121f3a8678e84abcd8d059fe3d61cb01dc4/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Cadastros", 3 | "name": "Cadastros básicos", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter } from 'react-router-dom'; 2 | 3 | import './shared/forms/TraducoesYup'; 4 | 5 | import { AppThemeProvider, AuthProvider, DrawerProvider } from './shared/contexts'; 6 | import { Login, MenuLateral } from './shared/components'; 7 | import { AppRoutes } from './routes'; 8 | 9 | 10 | export const App = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { App } from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /src/pages/cidades/DetalheDeCidades.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Box, Grid, LinearProgress, Paper, Typography } from '@mui/material'; 3 | import { useNavigate, useParams } from 'react-router-dom'; 4 | import * as yup from 'yup'; 5 | 6 | import { CidadesService } from '../../shared/services/api/cidades/CidadesService'; 7 | import { VTextField, VForm, useVForm, IVFormErrors } from '../../shared/forms'; 8 | import { FerramentasDeDetalhe } from '../../shared/components'; 9 | import { LayoutBaseDePagina } from '../../shared/layouts'; 10 | 11 | 12 | interface IFormData { 13 | nome: string; 14 | } 15 | const formValidationSchema: yup.SchemaOf = yup.object().shape({ 16 | nome: yup.string().required().min(3), 17 | }); 18 | 19 | export const DetalheDeCidades: React.FC = () => { 20 | const { formRef, save, saveAndClose, isSaveAndClose } = useVForm(); 21 | const { id = 'nova' } = useParams<'id'>(); 22 | const navigate = useNavigate(); 23 | 24 | 25 | const [isLoading, setIsLoading] = useState(false); 26 | const [nome, setNome] = useState(''); 27 | 28 | useEffect(() => { 29 | if (id !== 'nova') { 30 | setIsLoading(true); 31 | 32 | CidadesService.getById(Number(id)) 33 | .then((result) => { 34 | setIsLoading(false); 35 | 36 | if (result instanceof Error) { 37 | alert(result.message); 38 | navigate('/cidades'); 39 | } else { 40 | setNome(result.nome); 41 | formRef.current?.setData(result); 42 | } 43 | }); 44 | } else { 45 | formRef.current?.setData({ 46 | nome: '', 47 | }); 48 | } 49 | }, [id]); 50 | 51 | 52 | const handleSave = (dados: IFormData) => { 53 | formValidationSchema. 54 | validate(dados, { abortEarly: false }) 55 | .then((dadosValidados) => { 56 | setIsLoading(true); 57 | 58 | if (id === 'nova') { 59 | CidadesService 60 | .create(dadosValidados) 61 | .then((result) => { 62 | setIsLoading(false); 63 | 64 | if (result instanceof Error) { 65 | alert(result.message); 66 | } else { 67 | if (isSaveAndClose()) { 68 | navigate('/cidades'); 69 | } else { 70 | navigate(`/cidades/detalhe/${result}`); 71 | } 72 | } 73 | }); 74 | } else { 75 | CidadesService 76 | .updateById(Number(id), { id: Number(id), ...dadosValidados }) 77 | .then((result) => { 78 | setIsLoading(false); 79 | 80 | if (result instanceof Error) { 81 | alert(result.message); 82 | } else { 83 | if (isSaveAndClose()) { 84 | navigate('/cidades'); 85 | } 86 | } 87 | }); 88 | } 89 | }) 90 | .catch((errors: yup.ValidationError) => { 91 | const validationErrors: IVFormErrors = {}; 92 | 93 | errors.inner.forEach(error => { 94 | if (!error.path) return; 95 | 96 | validationErrors[error.path] = error.message; 97 | }); 98 | 99 | formRef.current?.setErrors(validationErrors); 100 | }); 101 | }; 102 | 103 | const handleDelete = (id: number) => { 104 | if (confirm('Realmente deseja apagar?')) { 105 | CidadesService.deleteById(id) 106 | .then(result => { 107 | if (result instanceof Error) { 108 | alert(result.message); 109 | } else { 110 | alert('Registro apagado com sucesso!'); 111 | navigate('/cidades'); 112 | } 113 | }); 114 | } 115 | }; 116 | 117 | 118 | return ( 119 | navigate('/cidades')} 131 | aoClicarEmApagar={() => handleDelete(Number(id))} 132 | aoClicarEmNovo={() => navigate('/cidades/detalhe/nova')} 133 | /> 134 | } 135 | > 136 | 137 | 138 | 139 | 140 | 141 | {isLoading && ( 142 | 143 | 144 | 145 | )} 146 | 147 | 148 | Geral 149 | 150 | 151 | 152 | 153 | setNome(e.target.value)} 159 | /> 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | ); 169 | }; 170 | -------------------------------------------------------------------------------- /src/pages/cidades/ListagemDeCidades.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react'; 2 | import { Icon, IconButton, LinearProgress, Pagination, Paper, Table, TableBody, TableCell, TableContainer, TableFooter, TableHead, TableRow } from '@mui/material'; 3 | import { useNavigate, useSearchParams } from 'react-router-dom'; 4 | 5 | import { IListagemCidade, CidadesService, } from '../../shared/services/api/cidades/CidadesService'; 6 | import { FerramentasDaListagem } from '../../shared/components'; 7 | import { LayoutBaseDePagina } from '../../shared/layouts'; 8 | import { Environment } from '../../shared/environment'; 9 | import { useDebounce } from '../../shared/hooks'; 10 | 11 | 12 | export const ListagemDeCidades: React.FC = () => { 13 | const [searchParams, setSearchParams] = useSearchParams(); 14 | const { debounce } = useDebounce(); 15 | const navigate = useNavigate(); 16 | 17 | const [rows, setRows] = useState([]); 18 | const [isLoading, setIsLoading] = useState(true); 19 | const [totalCount, setTotalCount] = useState(0); 20 | 21 | 22 | const busca = useMemo(() => { 23 | return searchParams.get('busca') || ''; 24 | }, [searchParams]); 25 | 26 | const pagina = useMemo(() => { 27 | return Number(searchParams.get('pagina') || '1'); 28 | }, [searchParams]); 29 | 30 | 31 | useEffect(() => { 32 | setIsLoading(true); 33 | 34 | debounce(() => { 35 | CidadesService.getAll(pagina, busca) 36 | .then((result) => { 37 | setIsLoading(false); 38 | 39 | if (result instanceof Error) { 40 | alert(result.message); 41 | } else { 42 | console.log(result); 43 | 44 | setTotalCount(result.totalCount); 45 | setRows(result.data); 46 | } 47 | }); 48 | }); 49 | }, [busca, pagina]); 50 | 51 | const handleDelete = (id: number) => { 52 | if (confirm('Realmente deseja apagar?')) { 53 | CidadesService.deleteById(id) 54 | .then(result => { 55 | if (result instanceof Error) { 56 | alert(result.message); 57 | } else { 58 | setRows(oldRows => [ 59 | ...oldRows.filter(oldRow => oldRow.id !== id), 60 | ]); 61 | alert('Registro apagado com sucesso!'); 62 | } 63 | }); 64 | } 65 | }; 66 | 67 | 68 | return ( 69 | navigate('/cidades/detalhe/nova')} 77 | aoMudarTextoDeBusca={texto => setSearchParams({ busca: texto, pagina: '1' }, { replace: true })} 78 | /> 79 | } 80 | > 81 | 82 | 83 | 84 | 85 | Ações 86 | Nome 87 | 88 | 89 | 90 | {rows.map(row => ( 91 | 92 | 93 | handleDelete(row.id)}> 94 | delete 95 | 96 | navigate(`/cidades/detalhe/${row.id}`)}> 97 | edit 98 | 99 | 100 | {row.nome} 101 | 102 | ))} 103 | 104 | 105 | {totalCount === 0 && !isLoading && ( 106 | 107 | )} 108 | 109 | 110 | {isLoading && ( 111 | 112 | 113 | 114 | 115 | 116 | )} 117 | {(totalCount > 0 && totalCount > Environment.LIMITE_DE_LINHAS) && ( 118 | 119 | 120 | setSearchParams({ busca, pagina: newPage.toString() }, { replace: true })} 124 | /> 125 | 126 | 127 | )} 128 | 129 |
{Environment.LISTAGEM_VAZIA}
130 |
131 |
132 | ); 133 | }; 134 | -------------------------------------------------------------------------------- /src/pages/dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Box, Card, CardContent, Grid, Typography } from '@mui/material'; 3 | 4 | import { CidadesService } from '../../shared/services/api/cidades/CidadesService'; 5 | import { PessoasService } from '../../shared/services/api/pessoas/PessoasService'; 6 | import { FerramentasDaListagem } from '../../shared/components'; 7 | import { LayoutBaseDePagina } from '../../shared/layouts'; 8 | 9 | 10 | export const Dashboard = () => { 11 | const [isLoadingCidades, setIsLoadingCidades] = useState(true); 12 | const [isLoadingPessoas, setIsLoadingPessoas] = useState(true); 13 | const [totalCountCidades, setTotalCountCidades] = useState(0); 14 | const [totalCountPessoas, setTotalCountPessoas] = useState(0); 15 | 16 | useEffect(() => { 17 | setIsLoadingCidades(true); 18 | setIsLoadingPessoas(true); 19 | 20 | CidadesService.getAll(1) 21 | .then((result) => { 22 | setIsLoadingCidades(false); 23 | 24 | if (result instanceof Error) { 25 | alert(result.message); 26 | } else { 27 | setTotalCountCidades(result.totalCount); 28 | } 29 | }); 30 | PessoasService.getAll(1) 31 | .then((result) => { 32 | setIsLoadingPessoas(false); 33 | 34 | if (result instanceof Error) { 35 | alert(result.message); 36 | } else { 37 | setTotalCountPessoas(result.totalCount); 38 | } 39 | }); 40 | }, []); 41 | 42 | 43 | return ( 44 | } 47 | > 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Total de pessoas 58 | 59 | 60 | 61 | {!isLoadingPessoas && ( 62 | 63 | {totalCountPessoas} 64 | 65 | )} 66 | {isLoadingPessoas && ( 67 | 68 | Carregando... 69 | 70 | )} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | Total de cidades 82 | 83 | 84 | 85 | {!isLoadingCidades && ( 86 | 87 | {totalCountCidades} 88 | 89 | )} 90 | {isLoadingCidades && ( 91 | 92 | Carregando... 93 | 94 | )} 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pessoas/ListagemDePessoas'; 2 | export * from './pessoas/DetalheDePessoas'; 3 | export * from './cidades/ListagemDeCidades'; 4 | export * from './cidades/DetalheDeCidades'; 5 | export * from './dashboard/Dashboard'; 6 | -------------------------------------------------------------------------------- /src/pages/pessoas/DetalheDePessoas.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Box, Grid, LinearProgress, Paper, Typography } from '@mui/material'; 3 | import { useNavigate, useParams } from 'react-router-dom'; 4 | import * as yup from 'yup'; 5 | 6 | import { PessoasService } from '../../shared/services/api/pessoas/PessoasService'; 7 | import { VTextField, VForm, useVForm, IVFormErrors } from '../../shared/forms'; 8 | import { AutoCompleteCidade } from './components/AutoCompleteCidade'; 9 | import { FerramentasDeDetalhe } from '../../shared/components'; 10 | import { LayoutBaseDePagina } from '../../shared/layouts'; 11 | 12 | 13 | interface IFormData { 14 | email: string; 15 | cidadeId: number; 16 | nomeCompleto: string; 17 | } 18 | const formValidationSchema: yup.SchemaOf = yup.object().shape({ 19 | cidadeId: yup.number().required(), 20 | email: yup.string().required().email(), 21 | nomeCompleto: yup.string().required().min(3), 22 | }); 23 | 24 | export const DetalheDePessoas: React.FC = () => { 25 | const { formRef, save, saveAndClose, isSaveAndClose } = useVForm(); 26 | const { id = 'nova' } = useParams<'id'>(); 27 | const navigate = useNavigate(); 28 | 29 | 30 | const [isLoading, setIsLoading] = useState(false); 31 | const [nome, setNome] = useState(''); 32 | 33 | useEffect(() => { 34 | if (id !== 'nova') { 35 | setIsLoading(true); 36 | 37 | PessoasService.getById(Number(id)) 38 | .then((result) => { 39 | setIsLoading(false); 40 | 41 | if (result instanceof Error) { 42 | alert(result.message); 43 | navigate('/pessoas'); 44 | } else { 45 | setNome(result.nomeCompleto); 46 | formRef.current?.setData(result); 47 | } 48 | }); 49 | } else { 50 | formRef.current?.setData({ 51 | email: '', 52 | nomeCompleto: '', 53 | cidadeId: undefined, 54 | }); 55 | } 56 | }, [id]); 57 | 58 | 59 | const handleSave = (dados: IFormData) => { 60 | 61 | formValidationSchema. 62 | validate(dados, { abortEarly: false }) 63 | .then((dadosValidados) => { 64 | setIsLoading(true); 65 | 66 | if (id === 'nova') { 67 | PessoasService 68 | .create(dadosValidados) 69 | .then((result) => { 70 | setIsLoading(false); 71 | 72 | if (result instanceof Error) { 73 | alert(result.message); 74 | } else { 75 | if (isSaveAndClose()) { 76 | navigate('/pessoas'); 77 | } else { 78 | navigate(`/pessoas/detalhe/${result}`); 79 | } 80 | } 81 | }); 82 | } else { 83 | PessoasService 84 | .updateById(Number(id), { id: Number(id), ...dadosValidados }) 85 | .then((result) => { 86 | setIsLoading(false); 87 | 88 | if (result instanceof Error) { 89 | alert(result.message); 90 | } else { 91 | if (isSaveAndClose()) { 92 | navigate('/pessoas'); 93 | } 94 | } 95 | }); 96 | } 97 | }) 98 | .catch((errors: yup.ValidationError) => { 99 | const validationErrors: IVFormErrors = {}; 100 | 101 | errors.inner.forEach(error => { 102 | if (!error.path) return; 103 | 104 | validationErrors[error.path] = error.message; 105 | }); 106 | 107 | formRef.current?.setErrors(validationErrors); 108 | }); 109 | }; 110 | 111 | const handleDelete = (id: number) => { 112 | if (confirm('Realmente deseja apagar?')) { 113 | PessoasService.deleteById(id) 114 | .then(result => { 115 | if (result instanceof Error) { 116 | alert(result.message); 117 | } else { 118 | alert('Registro apagado com sucesso!'); 119 | navigate('/pessoas'); 120 | } 121 | }); 122 | } 123 | }; 124 | 125 | 126 | return ( 127 | navigate('/pessoas')} 139 | aoClicarEmApagar={() => handleDelete(Number(id))} 140 | aoClicarEmNovo={() => navigate('/pessoas/detalhe/nova')} 141 | /> 142 | } 143 | > 144 | 145 | 146 | 147 | 148 | 149 | {isLoading && ( 150 | 151 | 152 | 153 | )} 154 | 155 | 156 | Geral 157 | 158 | 159 | 160 | 161 | setNome(e.target.value)} 167 | /> 168 | 169 | 170 | 171 | 172 | 173 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | ); 194 | }; 195 | -------------------------------------------------------------------------------- /src/pages/pessoas/ListagemDePessoas.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react'; 2 | import { Icon, IconButton, LinearProgress, Pagination, Paper, Table, TableBody, TableCell, TableContainer, TableFooter, TableHead, TableRow } from '@mui/material'; 3 | import { useNavigate, useSearchParams } from 'react-router-dom'; 4 | 5 | import { IListagemPessoa, PessoasService, } from '../../shared/services/api/pessoas/PessoasService'; 6 | import { FerramentasDaListagem } from '../../shared/components'; 7 | import { LayoutBaseDePagina } from '../../shared/layouts'; 8 | import { useDebounce } from '../../shared/hooks'; 9 | import { Environment } from '../../shared/environment'; 10 | 11 | 12 | export const ListagemDePessoas: React.FC = () => { 13 | const [searchParams, setSearchParams] = useSearchParams(); 14 | const { debounce } = useDebounce(); 15 | const navigate = useNavigate(); 16 | 17 | const [rows, setRows] = useState([]); 18 | const [isLoading, setIsLoading] = useState(true); 19 | const [totalCount, setTotalCount] = useState(0); 20 | 21 | 22 | const busca = useMemo(() => { 23 | return searchParams.get('busca') || ''; 24 | }, [searchParams]); 25 | 26 | const pagina = useMemo(() => { 27 | return Number(searchParams.get('pagina') || '1'); 28 | }, [searchParams]); 29 | 30 | 31 | useEffect(() => { 32 | setIsLoading(true); 33 | 34 | debounce(() => { 35 | PessoasService.getAll(pagina, busca) 36 | .then((result) => { 37 | setIsLoading(false); 38 | 39 | if (result instanceof Error) { 40 | alert(result.message); 41 | } else { 42 | console.log(result); 43 | 44 | setTotalCount(result.totalCount); 45 | setRows(result.data); 46 | } 47 | }); 48 | }); 49 | }, [busca, pagina]); 50 | 51 | const handleDelete = (id: number) => { 52 | if (confirm('Realmente deseja apagar?')) { 53 | PessoasService.deleteById(id) 54 | .then(result => { 55 | if (result instanceof Error) { 56 | alert(result.message); 57 | } else { 58 | setRows(oldRows => [ 59 | ...oldRows.filter(oldRow => oldRow.id !== id), 60 | ]); 61 | alert('Registro apagado com sucesso!'); 62 | } 63 | }); 64 | } 65 | }; 66 | 67 | 68 | return ( 69 | navigate('/pessoas/detalhe/nova')} 77 | aoMudarTextoDeBusca={texto => setSearchParams({ busca: texto, pagina: '1' }, { replace: true })} 78 | /> 79 | } 80 | > 81 | 82 | 83 | 84 | 85 | Ações 86 | Nome completo 87 | Email 88 | 89 | 90 | 91 | {rows.map(row => ( 92 | 93 | 94 | handleDelete(row.id)}> 95 | delete 96 | 97 | navigate(`/pessoas/detalhe/${row.id}`)}> 98 | edit 99 | 100 | 101 | {row.nomeCompleto} 102 | {row.email} 103 | 104 | ))} 105 | 106 | 107 | {totalCount === 0 && !isLoading && ( 108 | 109 | )} 110 | 111 | 112 | {isLoading && ( 113 | 114 | 115 | 116 | 117 | 118 | )} 119 | {(totalCount > 0 && totalCount > Environment.LIMITE_DE_LINHAS) && ( 120 | 121 | 122 | setSearchParams({ busca, pagina: newPage.toString() }, { replace: true })} 126 | /> 127 | 128 | 129 | )} 130 | 131 |
{Environment.LISTAGEM_VAZIA}
132 |
133 |
134 | ); 135 | }; 136 | -------------------------------------------------------------------------------- /src/pages/pessoas/components/AutoCompleteCidade.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react'; 2 | import { Autocomplete, CircularProgress, TextField } from '@mui/material'; 3 | 4 | import { CidadesService } from '../../../shared/services/api/cidades/CidadesService'; 5 | import { useDebounce } from '../../../shared/hooks'; 6 | import { useField } from '@unform/core'; 7 | 8 | 9 | type TAutoCompleteOption = { 10 | id: number; 11 | label: string; 12 | } 13 | 14 | interface IAutoCompleteCidadeProps { 15 | isExternalLoading?: boolean; 16 | } 17 | export const AutoCompleteCidade: React.FC = ({ isExternalLoading = false }) => { 18 | const { fieldName, registerField, defaultValue, error, clearError } = useField('cidadeId'); 19 | const { debounce } = useDebounce(); 20 | 21 | const [selectedId, setSelectedId] = useState(defaultValue); 22 | 23 | const [opcoes, setOpcoes] = useState([]); 24 | const [isLoading, setIsLoading] = useState(false); 25 | const [busca, setBusca] = useState(''); 26 | 27 | useEffect(() => { 28 | registerField({ 29 | name: fieldName, 30 | getValue: () => selectedId, 31 | setValue: (_, newSelectedId) => setSelectedId(newSelectedId), 32 | }); 33 | }, [registerField, fieldName, selectedId]); 34 | 35 | useEffect(() => { 36 | setIsLoading(true); 37 | 38 | debounce(() => { 39 | CidadesService.getAll(1, busca, selectedId?.toString()) 40 | .then((result) => { 41 | setIsLoading(false); 42 | 43 | if (result instanceof Error) { 44 | // alert(result.message); 45 | } else { 46 | console.log(result); 47 | 48 | setOpcoes(result.data.map(cidade => ({ id: cidade.id, label: cidade.nome }))); 49 | } 50 | }); 51 | }); 52 | }, [busca, selectedId]); 53 | 54 | const autoCompleteSelectedOption = useMemo(() => { 55 | if (!selectedId) return null; 56 | 57 | const selectedOption = opcoes.find(opcao => opcao.id === selectedId); 58 | if (!selectedOption) return null; 59 | 60 | return selectedOption; 61 | }, [selectedId, opcoes]); 62 | 63 | 64 | return ( 65 | setBusca(newValue)} 78 | onChange={(_, newValue) => { setSelectedId(newValue?.id); setBusca(''); clearError(); }} 79 | popupIcon={(isExternalLoading || isLoading) ? : undefined} 80 | renderInput={(params) => ( 81 | 88 | )} 89 | /> 90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { Routes, Route, Navigate } from 'react-router-dom'; 3 | 4 | import { useDrawerContext } from '../shared/contexts'; 5 | import { 6 | Dashboard, 7 | DetalheDePessoas, 8 | ListagemDePessoas, 9 | DetalheDeCidades, 10 | ListagemDeCidades, 11 | } from '../pages'; 12 | 13 | export const AppRoutes = () => { 14 | const { setDrawerOptions } = useDrawerContext(); 15 | 16 | useEffect(() => { 17 | setDrawerOptions([ 18 | { 19 | icon: 'home', 20 | path: '/pagina-inicial', 21 | label: 'Página inicial', 22 | }, 23 | { 24 | icon: 'location_city', 25 | path: '/cidades', 26 | label: 'Cidades', 27 | }, 28 | { 29 | icon: 'people', 30 | path: '/pessoas', 31 | label: 'Pessoas', 32 | }, 33 | ]); 34 | }, []); 35 | 36 | return ( 37 | 38 | } /> 39 | 40 | } /> 41 | } /> 42 | 43 | } /> 44 | } /> 45 | 46 | } /> 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/shared/components/ferramentas-da-listagem/FerramentasDaListagem.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Icon, Paper, TextField, useTheme } from '@mui/material'; 2 | 3 | import { Environment } from '../../environment'; 4 | 5 | 6 | interface IFerramentasDaListagemProps { 7 | textoDaBusca?: string; 8 | mostrarInputBusca?: boolean; 9 | aoMudarTextoDeBusca?: (novoTexto: string) => void; 10 | textoBotaoNovo?: string; 11 | mostrarBotaoNovo?: boolean; 12 | aoClicarEmNovo?: () => void; 13 | } 14 | export const FerramentasDaListagem: React.FC = ({ 15 | textoDaBusca = '', 16 | aoMudarTextoDeBusca, 17 | mostrarInputBusca = false, 18 | aoClicarEmNovo, 19 | textoBotaoNovo = 'Novo', 20 | mostrarBotaoNovo = true, 21 | }) => { 22 | const theme = useTheme(); 23 | 24 | return ( 25 | 35 | {mostrarInputBusca && ( 36 | aoMudarTextoDeBusca?.(e.target.value)} 41 | /> 42 | )} 43 | 44 | 45 | {mostrarBotaoNovo && ( 46 | 53 | )} 54 | 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/shared/components/ferramentas-de-detalhe/FerramentasDeDetalhe.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Divider, Icon, Paper, Skeleton, Theme, Typography, useMediaQuery, useTheme } from '@mui/material'; 2 | 3 | 4 | interface IFerramentasDeDetalheProps { 5 | textoBotaoNovo?: string; 6 | 7 | mostrarBotaoNovo?: boolean; 8 | mostrarBotaoVoltar?: boolean; 9 | mostrarBotaoApagar?: boolean; 10 | mostrarBotaoSalvar?: boolean; 11 | mostrarBotaoSalvarEFechar?: boolean; 12 | 13 | mostrarBotaoNovoCarregando?: boolean; 14 | mostrarBotaoVoltarCarregando?: boolean; 15 | mostrarBotaoApagarCarregando?: boolean; 16 | mostrarBotaoSalvarCarregando?: boolean; 17 | mostrarBotaoSalvarEFecharCarregando?: boolean; 18 | 19 | aoClicarEmNovo?: () => void; 20 | aoClicarEmVoltar?: () => void; 21 | aoClicarEmApagar?: () => void; 22 | aoClicarEmSalvar?: () => void; 23 | aoClicarEmSalvarEFechar?: () => void; 24 | } 25 | export const FerramentasDeDetalhe: React.FC = ({ 26 | textoBotaoNovo = 'Novo', 27 | 28 | mostrarBotaoNovo = true, 29 | mostrarBotaoVoltar = true, 30 | mostrarBotaoApagar = true, 31 | mostrarBotaoSalvar = true, 32 | mostrarBotaoSalvarEFechar = false, 33 | 34 | mostrarBotaoNovoCarregando = false, 35 | mostrarBotaoVoltarCarregando = false, 36 | mostrarBotaoApagarCarregando = false, 37 | mostrarBotaoSalvarCarregando = false, 38 | mostrarBotaoSalvarEFecharCarregando = false, 39 | 40 | aoClicarEmNovo, 41 | aoClicarEmVoltar, 42 | aoClicarEmApagar, 43 | aoClicarEmSalvar, 44 | aoClicarEmSalvarEFechar, 45 | }) => { 46 | const smDown = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm')); 47 | const mdDown = useMediaQuery((theme: Theme) => theme.breakpoints.down('md')); 48 | const theme = useTheme(); 49 | 50 | return ( 51 | 61 | {(mostrarBotaoSalvar && !mostrarBotaoSalvarCarregando) && ( 62 | 73 | )} 74 | 75 | {mostrarBotaoSalvarCarregando && ( 76 | 77 | )} 78 | 79 | {(mostrarBotaoSalvarEFechar && !mostrarBotaoSalvarEFecharCarregando && !smDown && !mdDown) && ( 80 | 91 | )} 92 | 93 | {(mostrarBotaoSalvarEFecharCarregando && !smDown && !mdDown) && ( 94 | 95 | )} 96 | 97 | {(mostrarBotaoApagar && !mostrarBotaoApagarCarregando) && ( 98 | 109 | )} 110 | 111 | {mostrarBotaoApagarCarregando && ( 112 | 113 | )} 114 | 115 | {(mostrarBotaoNovo && !mostrarBotaoNovoCarregando && !smDown) && ( 116 | 127 | )} 128 | 129 | {(mostrarBotaoNovoCarregando && !smDown) && ( 130 | 131 | )} 132 | 133 | { 134 | ( 135 | mostrarBotaoVoltar && 136 | (mostrarBotaoNovo || mostrarBotaoApagar || mostrarBotaoSalvar || mostrarBotaoSalvarEFechar) 137 | ) && ( 138 | 139 | ) 140 | } 141 | 142 | {(mostrarBotaoVoltar && !mostrarBotaoVoltarCarregando) && ( 143 | 154 | )} 155 | 156 | {mostrarBotaoVoltarCarregando && ( 157 | 158 | )} 159 | 160 | ); 161 | }; 162 | -------------------------------------------------------------------------------- /src/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ferramentas-da-listagem/FerramentasDaListagem'; 2 | export * from './ferramentas-de-detalhe/FerramentasDeDetalhe'; 3 | export * from './menu-lateral/MenuLateral'; 4 | export * from './login/Login'; 5 | -------------------------------------------------------------------------------- /src/shared/components/login/Login.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Box, Button, Card, CardActions, CardContent, CircularProgress, TextField, Typography } from '@mui/material'; 3 | import * as yup from 'yup'; 4 | 5 | import { useAuthContext } from '../../contexts'; 6 | 7 | 8 | const loginSchema = yup.object().shape({ 9 | email: yup.string().email().required(), 10 | password: yup.string().required().min(5), 11 | }); 12 | 13 | interface ILoginProps { 14 | children: React.ReactNode; 15 | } 16 | export const Login: React.FC = ({ children }) => { 17 | const { isAuthenticated, login } = useAuthContext(); 18 | 19 | const [isLoading, setIsLoading] = useState(false); 20 | 21 | const [passwordError, setPasswordError] = useState(''); 22 | const [emailError, setEmailError] = useState(''); 23 | const [password, setPassword] = useState(''); 24 | const [email, setEmail] = useState(''); 25 | 26 | 27 | const handleSubmit = () => { 28 | setIsLoading(true); 29 | 30 | loginSchema 31 | .validate({ email, password }, { abortEarly: false }) 32 | .then(dadosValidados => { 33 | login(dadosValidados.email, dadosValidados.password) 34 | .then(() => { 35 | setIsLoading(false); 36 | }); 37 | }) 38 | .catch((errors: yup.ValidationError) => { 39 | setIsLoading(false); 40 | 41 | errors.inner.forEach(error => { 42 | if (error.path === 'email') { 43 | setEmailError(error.message); 44 | } else if (error.path === 'password') { 45 | setPasswordError(error.message); 46 | } 47 | }); 48 | }); 49 | }; 50 | 51 | 52 | if (isAuthenticated) return ( 53 | <>{children} 54 | ); 55 | 56 | return ( 57 | 58 | 59 | 60 | 61 | 62 | Identifique-se 63 | 64 | setEmailError('')} 73 | onChange={e => setEmail(e.target.value)} 74 | /> 75 | 76 | setPasswordError('')} 85 | onChange={e => setPassword(e.target.value)} 86 | /> 87 | 88 | 89 | 90 | 91 | 92 | 100 | 101 | 102 | 103 | 104 | 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/shared/components/menu-lateral/MenuLateral.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Divider, Drawer, Icon, List, ListItemButton, ListItemIcon, ListItemText, useMediaQuery, useTheme } from '@mui/material'; 2 | import { useMatch, useNavigate, useResolvedPath } from 'react-router-dom'; 3 | import { Box } from '@mui/system'; 4 | 5 | import { useAppThemeContext, useAuthContext, useDrawerContext } from '../../contexts'; 6 | 7 | interface IListItemLinkProps { 8 | to: string; 9 | icon: string; 10 | label: string; 11 | onClick: (() => void) | undefined; 12 | } 13 | const ListItemLink: React.FC = ({ to, icon, label, onClick }) => { 14 | const navigate = useNavigate(); 15 | 16 | const resolvedPath = useResolvedPath(to); 17 | const match = useMatch({ path: resolvedPath.pathname, end: false }); 18 | 19 | 20 | const handleClick = () => { 21 | navigate(to); 22 | onClick?.(); 23 | }; 24 | 25 | return ( 26 | 27 | 28 | {icon} 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | interface IMenuLateralProps { 36 | children: React.ReactNode; 37 | } 38 | export const MenuLateral: React.FC = ({ children }) => { 39 | const theme = useTheme(); 40 | const smDown = useMediaQuery(theme.breakpoints.down('sm')); 41 | 42 | const { isDrawerOpen, drawerOptions, toggleDrawerOpen } = useDrawerContext(); 43 | const { toggleTheme } = useAppThemeContext(); 44 | const { logout } = useAuthContext(); 45 | 46 | return ( 47 | <> 48 | 49 | 50 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {drawerOptions.map(drawerOption => ( 63 | 70 | ))} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | dark_mode 79 | 80 | 81 | 82 | 83 | 84 | logout 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | {children} 95 | 96 | 97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /src/shared/contexts/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; 2 | 3 | import { AuthService } from '../services/api/auth/AuthService'; 4 | 5 | 6 | interface IAuthContextData { 7 | logout: () => void; 8 | isAuthenticated: boolean; 9 | login: (email: string, password: string) => Promise; 10 | } 11 | 12 | const AuthContext = createContext({} as IAuthContextData); 13 | 14 | const LOCAL_STORAGE_KEY__ACCESS_TOKEN = 'APP_ACCESS_TOKEN'; 15 | 16 | interface IAuthProviderProps { 17 | children: React.ReactNode; 18 | } 19 | export const AuthProvider: React.FC = ({ children }) => { 20 | const [accessToken, setAccessToken] = useState(); 21 | 22 | useEffect(() => { 23 | const accessToken = localStorage.getItem(LOCAL_STORAGE_KEY__ACCESS_TOKEN); 24 | 25 | if (accessToken) { 26 | setAccessToken(JSON.parse(accessToken)); 27 | } else { 28 | setAccessToken(undefined); 29 | } 30 | }, []); 31 | 32 | 33 | const handleLogin = useCallback(async (email: string, password: string) => { 34 | const result = await AuthService.auth(email, password); 35 | if (result instanceof Error) { 36 | return result.message; 37 | } else { 38 | localStorage.setItem(LOCAL_STORAGE_KEY__ACCESS_TOKEN, JSON.stringify(result.accessToken)); 39 | setAccessToken(result.accessToken); 40 | } 41 | }, []); 42 | 43 | const handleLogout = useCallback(() => { 44 | localStorage.removeItem(LOCAL_STORAGE_KEY__ACCESS_TOKEN); 45 | setAccessToken(undefined); 46 | }, []); 47 | 48 | const isAuthenticated = useMemo(() => !!accessToken, [accessToken]); 49 | 50 | 51 | return ( 52 | 53 | {children} 54 | 55 | ); 56 | }; 57 | 58 | export const useAuthContext = () => useContext(AuthContext); 59 | -------------------------------------------------------------------------------- /src/shared/contexts/DrawerContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useCallback, useContext, useState } from 'react'; 2 | 3 | interface IDrawerOption { 4 | icon: string; 5 | path: string; 6 | label: string; 7 | } 8 | 9 | interface IDrawerContextData { 10 | isDrawerOpen: boolean; 11 | toggleDrawerOpen: () => void; 12 | drawerOptions: IDrawerOption[]; 13 | setDrawerOptions: (newDrawerOptions: IDrawerOption[]) => void; 14 | } 15 | 16 | const DrawerContext = createContext({} as IDrawerContextData); 17 | 18 | export const useDrawerContext = () => { 19 | return useContext(DrawerContext); 20 | }; 21 | 22 | interface IDrawerProviderProps { 23 | children: React.ReactNode 24 | } 25 | export const DrawerProvider: React.FC = ({ children }) => { 26 | const [drawerOptions, setDrawerOptions] = useState([]); 27 | const [isDrawerOpen, setIsDrawerOpen] = useState(false); 28 | 29 | const toggleDrawerOpen = useCallback(() => { 30 | setIsDrawerOpen(oldDrawerOpen => !oldDrawerOpen); 31 | }, []); 32 | 33 | const handleSetDrawerOptions = useCallback((newDrawerOptions: IDrawerOption[]) => { 34 | setDrawerOptions(newDrawerOptions); 35 | }, []); 36 | 37 | return ( 38 | 39 | {children} 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/shared/contexts/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useCallback, useContext, useMemo, useState } from 'react'; 2 | import { ThemeProvider } from '@mui/material'; 3 | import { Box } from '@mui/system'; 4 | 5 | import { DarkTheme, LightTheme } from './../themes'; 6 | 7 | interface IThemeContextData { 8 | themeName: 'light' | 'dark'; 9 | toggleTheme: () => void; 10 | } 11 | 12 | const ThemeContext = createContext({} as IThemeContextData); 13 | 14 | export const useAppThemeContext = () => { 15 | return useContext(ThemeContext); 16 | }; 17 | 18 | interface IAppThemeProviderProps { 19 | children: React.ReactNode 20 | } 21 | export const AppThemeProvider: React.FC = ({ children }) => { 22 | const [themeName, setThemeName] = useState<'light' | 'dark'>('light'); 23 | 24 | const toggleTheme = useCallback(() => { 25 | setThemeName(oldThemeName => oldThemeName === 'light' ? 'dark' : 'light'); 26 | }, []); 27 | 28 | const theme = useMemo(() => { 29 | if (themeName === 'light') return LightTheme; 30 | 31 | return DarkTheme; 32 | }, [themeName]); 33 | 34 | 35 | return ( 36 | 37 | 38 | 39 | {children} 40 | 41 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/shared/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DrawerContext'; 2 | export * from './ThemeContext'; 3 | export * from './AuthContext'; 4 | -------------------------------------------------------------------------------- /src/shared/environment/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export const Environment = { 3 | /** 4 | * Define a quantidade de linhas a ser carregada por padrão nas listagens 5 | */ 6 | LIMITE_DE_LINHAS: 5, 7 | /** 8 | * Placeholder exibido nas inputs 9 | */ 10 | INPUT_DE_BUSCA: 'Pesquisar...', 11 | /** 12 | * Texto exibido quando nenhum registro é encontrado em uma listagem 13 | */ 14 | LISTAGEM_VAZIA: 'Nenhum registro encontrado.', 15 | /** 16 | * Url base de consultado dos dados dessa aplicação 17 | */ 18 | URL_BASE: 'http://localhost:3333', 19 | }; 20 | -------------------------------------------------------------------------------- /src/shared/forms/IVFormErrors.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IVFormErrors { 3 | [key: string]: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/shared/forms/TraducoesYup.ts: -------------------------------------------------------------------------------- 1 | import { setLocale } from 'yup'; 2 | 3 | setLocale({ 4 | mixed: { 5 | default: 'Campo não é válido', 6 | required: 'O campo é obrigatório', 7 | }, 8 | string: { 9 | email: () => 'O campo precisa conter um email válido', 10 | max: ({ max }) => `O campo pode ter no máximo ${max} caracteres`, 11 | min: ({ min }) => `O campo precisa ter pelo menos ${min} caracteres`, 12 | length: ({ length }) => `O campo precisa ter exatamente ${length} caracteres`, 13 | }, 14 | date: { 15 | max: ({ max }) => `A data deve ser menor que ${max}`, 16 | min: ({ min }) => `A data deve ser maior que ${min}`, 17 | }, 18 | number: { 19 | integer: () => 'O campo precisa ter um valor inteiro', 20 | negative: () => 'O campo precisa ter um valor negativo', 21 | positive: () => 'O campo precisa ter um valor positivo', 22 | moreThan: ({ more }) => `O campo precisa ter um valor maior que ${more}`, 23 | lessThan: ({ less }) => `O campo precisa ter um valor menor que ${less}`, 24 | min: ({ min }) => `O campo precisa ter um valor com mais de ${min} caracteres`, 25 | max: ({ max }) => `O campo precisa ter um valor com menos de ${max} caracteres`, 26 | }, 27 | boolean: {}, 28 | object: {}, 29 | array: {}, 30 | }); 31 | -------------------------------------------------------------------------------- /src/shared/forms/VForm.ts: -------------------------------------------------------------------------------- 1 | export { Form as VForm } from '@unform/web'; 2 | -------------------------------------------------------------------------------- /src/shared/forms/VNumericFormat.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { TextField, TextFieldProps } from '@mui/material'; 3 | import { NumericFormatProps, NumericFormat } from 'react-number-format'; 4 | import { useField } from '@unform/core'; 5 | 6 | 7 | type TVTextFieldProps = Omit & Omit & { 8 | name: string; 9 | 10 | onValueChange?: (value: string) => void; 11 | } 12 | /** 13 | * - Para resgatar o valor numérico no correto use o `onValueChange` 14 | * - Para eventos normais use o `onChange` 15 | * 16 | * Para como customizar a formatação verifique a documentação original do `react-number-format` [nesse link](https://www.npmjs.com/package/react-number-format) ou [nesse link](https://s-yadav.github.io/react-number-format/docs/intro/) 17 | */ 18 | export const VNumericFormat: React.FC = ({ name, onValueChange, ...rest }) => { 19 | const { fieldName, defaultValue, registerField, error } = useField(name); 20 | const [value, setValue] = useState(defaultValue); 21 | 22 | 23 | useEffect(() => { 24 | registerField({ 25 | name: fieldName, 26 | getValue: () => value, 27 | setValue: (_, value) => setValue(value), 28 | }); 29 | }, [fieldName, value, registerField]); 30 | 31 | 32 | const handleChange = (value: string) => { 33 | setValue(value); 34 | onValueChange && onValueChange(value); 35 | }; 36 | 37 | 38 | return ( 39 | handleChange(value)} 47 | /> 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/shared/forms/VPatternFormat.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { TextField, TextFieldProps } from '@mui/material'; 3 | import { PatternFormatProps, PatternFormat } from 'react-number-format'; 4 | import { useField } from '@unform/core'; 5 | 6 | 7 | type TVTextFieldProps = Omit & Omit & { 8 | name: string; 9 | 10 | onValueChange?: (value: string) => void; 11 | } 12 | /** 13 | * - Para resgatar o valor numérico no correto use o `onValueChange` 14 | * - Para eventos normais use o `onChange` 15 | * 16 | * Para como customizar a formatação verifique a documentação original do `react-number-format` [nesse link](https://www.npmjs.com/package/react-number-format) ou [nesse link](https://s-yadav.github.io/react-number-format/docs/intro/) 17 | */ 18 | export const VPatternFormat: React.FC = ({ name, onValueChange, ...rest }) => { 19 | const { fieldName, defaultValue, registerField, error } = useField(name); 20 | const [value, setValue] = useState(defaultValue); 21 | 22 | 23 | useEffect(() => { 24 | registerField({ 25 | name: fieldName, 26 | getValue: () => value, 27 | setValue: (_, value) => setValue(value), 28 | }); 29 | }, [fieldName, value, registerField]); 30 | 31 | 32 | const handleChange = (value: string) => { 33 | setValue(value); 34 | onValueChange && onValueChange(value); 35 | }; 36 | 37 | 38 | return ( 39 | handleChange(value)} 47 | /> 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/shared/forms/VScope.ts: -------------------------------------------------------------------------------- 1 | export { Scope as VScope } from '@unform/core'; 2 | -------------------------------------------------------------------------------- /src/shared/forms/VSelect.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { TextField, TextFieldProps } from '@mui/material'; 3 | import { useField } from '@unform/core'; 4 | 5 | 6 | type TVSelectProps = TextFieldProps & { 7 | name: string; 8 | } 9 | export const VSelect: React.FC = ({ name, ...rest }) => { 10 | const { fieldName, registerField, defaultValue, error, clearError } = useField(name); 11 | 12 | const [value, setValue] = useState(defaultValue || ''); 13 | 14 | 15 | useEffect(() => { 16 | registerField({ 17 | name: fieldName, 18 | getValue: () => value, 19 | setValue: (_, newValue) => setValue(newValue), 20 | }); 21 | }, [registerField, fieldName, value]); 22 | 23 | 24 | return ( 25 | { error && clearError(); rest.onKeyDown?.(e); }} 35 | onChange={e => { setValue(e.target.value); rest.onChange?.(e); }} 36 | /> 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/shared/forms/VSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Switch, SwitchProps } from '@mui/material'; 3 | import { useField } from '@unform/core'; 4 | 5 | 6 | type TVSwitchProps = SwitchProps & { 7 | name: string; 8 | } 9 | export const VSwitch: React.FC = ({ name, ...rest }) => { 10 | const { fieldName, registerField, defaultValue, error, clearError } = useField(name); 11 | 12 | const [value, setValue] = useState(defaultValue || false); 13 | 14 | 15 | useEffect(() => { 16 | registerField({ 17 | name: fieldName, 18 | getValue: () => value, 19 | setValue: (_, newValue) => setValue(newValue), 20 | }); 21 | }, [registerField, fieldName, value]); 22 | 23 | 24 | return ( 25 | { setValue(checked); rest.onChange?.(e, checked); error && clearError(); }} 32 | /> 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/shared/forms/VTextField.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { TextField, TextFieldProps } from '@mui/material'; 3 | import { useField } from '@unform/core'; 4 | 5 | 6 | type TVTextFieldProps = TextFieldProps & { 7 | name: string; 8 | } 9 | export const VTextField: React.FC = ({ name, ...rest }) => { 10 | const { fieldName, registerField, defaultValue, error, clearError } = useField(name); 11 | 12 | const [value, setValue] = useState(defaultValue || ''); 13 | 14 | 15 | useEffect(() => { 16 | registerField({ 17 | name: fieldName, 18 | getValue: () => value, 19 | setValue: (_, newValue) => setValue(newValue), 20 | }); 21 | }, [registerField, fieldName, value]); 22 | 23 | 24 | return ( 25 | { setValue(e.target.value); rest.onChange?.(e); }} 34 | onKeyDown={(e) => { error && clearError(); rest.onKeyDown?.(e); }} 35 | /> 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/shared/forms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './VNumericFormat'; 2 | export * from './VPatternFormat'; 3 | export * from './IVFormErrors'; 4 | export * from './VTextField'; 5 | export * from './useVForm'; 6 | export * from './VSelect'; 7 | export * from './VScope'; 8 | export * from './VForm'; 9 | -------------------------------------------------------------------------------- /src/shared/forms/useVForm.ts: -------------------------------------------------------------------------------- 1 | import { FormHandles } from '@unform/core'; 2 | import { useCallback, useRef } from 'react'; 3 | 4 | 5 | export const useVForm = () => { 6 | const formRef = useRef(null); 7 | 8 | const isSavingAndClose = useRef(false); 9 | const isSavingAndNew = useRef(false); 10 | 11 | 12 | const handleSave = useCallback(() => { 13 | isSavingAndClose.current = false; 14 | isSavingAndNew.current = false; 15 | formRef.current?.submitForm(); 16 | }, []); 17 | 18 | const handleSaveAndNew = useCallback(() => { 19 | isSavingAndClose.current = false; 20 | isSavingAndNew.current = true; 21 | formRef.current?.submitForm(); 22 | }, []); 23 | 24 | const handleSaveAndClose = useCallback(() => { 25 | isSavingAndClose.current = true; 26 | isSavingAndNew.current = false; 27 | formRef.current?.submitForm(); 28 | }, []); 29 | 30 | 31 | const handleIsSaveAndNew = useCallback(() => { 32 | return isSavingAndNew.current; 33 | }, []); 34 | 35 | const handleIsSaveAndClose = useCallback(() => { 36 | return isSavingAndClose.current; 37 | }, []); 38 | 39 | 40 | return { 41 | formRef, 42 | 43 | save: handleSave, 44 | saveAndNew: handleSaveAndNew, 45 | saveAndClose: handleSaveAndClose, 46 | 47 | isSaveAndNew: handleIsSaveAndNew, 48 | isSaveAndClose: handleIsSaveAndClose, 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /src/shared/hooks/UseDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react'; 2 | 3 | 4 | export const useDebounce = (delay = 300, notDelayInFirstTime = true) => { 5 | const isFirstTime = useRef(notDelayInFirstTime); 6 | const debouncing = useRef(); 7 | 8 | 9 | const debounce = useCallback((func: () => void) => { 10 | if (isFirstTime.current) { 11 | isFirstTime.current = false; 12 | func(); 13 | } else { 14 | if (debouncing.current) { 15 | clearTimeout(debouncing.current); 16 | } 17 | debouncing.current = setTimeout(() => func(), delay); 18 | } 19 | }, [delay]); 20 | 21 | return { debounce }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/shared/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UseDebounce'; 2 | -------------------------------------------------------------------------------- /src/shared/layouts/LayoutBaseDePagina.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { Icon, IconButton, Theme, Typography, useMediaQuery, useTheme } from '@mui/material'; 3 | import { Box } from '@mui/system'; 4 | 5 | import { useDrawerContext } from '../contexts'; 6 | 7 | 8 | interface ILayoutBaseDePaginaProps { 9 | titulo: string; 10 | children: ReactNode; 11 | barraDeFerramentas?: ReactNode; 12 | } 13 | export const LayoutBaseDePagina: React.FC = ({ children, titulo, barraDeFerramentas }) => { 14 | const smDown = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm')); 15 | const mdDown = useMediaQuery((theme: Theme) => theme.breakpoints.down('md')); 16 | const theme = useTheme(); 17 | 18 | const { toggleDrawerOpen } = useDrawerContext(); 19 | 20 | return ( 21 | 22 | 23 | {smDown && ( 24 | 25 | menu 26 | 27 | )} 28 | 29 | 35 | {titulo} 36 | 37 | 38 | 39 | {barraDeFerramentas && ( 40 | 41 | {barraDeFerramentas} 42 | 43 | )} 44 | 45 | 46 | {children} 47 | 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/shared/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LayoutBaseDePagina'; 2 | -------------------------------------------------------------------------------- /src/shared/services/api/auth/AuthService.ts: -------------------------------------------------------------------------------- 1 | import { Api } from '../axios-config'; 2 | 3 | 4 | interface IAuth { 5 | accessToken: string; 6 | } 7 | 8 | const auth = async (email: string, password: string): Promise => { 9 | try { 10 | const { data } = await Api.get('/auth', { data: { email, password } }); 11 | 12 | if (data) { 13 | return data; 14 | } 15 | 16 | return new Error('Erro no login.'); 17 | } catch (error) { 18 | console.error(error); 19 | return new Error((error as { message: string }).message || 'Erro no login.'); 20 | } 21 | }; 22 | 23 | export const AuthService = { 24 | auth, 25 | }; 26 | -------------------------------------------------------------------------------- /src/shared/services/api/axios-config/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { responseInterceptor, errorInterceptor } from './interceptors'; 4 | import { Environment } from '../../../environment'; 5 | 6 | 7 | const Api = axios.create({ 8 | baseURL: Environment.URL_BASE 9 | }); 10 | 11 | Api.interceptors.response.use( 12 | (response) => responseInterceptor(response), 13 | (error) => errorInterceptor(error), 14 | ); 15 | 16 | export { Api }; 17 | -------------------------------------------------------------------------------- /src/shared/services/api/axios-config/interceptors/ErrorInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios'; 2 | 3 | 4 | export const errorInterceptor = (error: AxiosError) => { 5 | 6 | if (error.message === 'Network Error') { 7 | return Promise.reject(new Error('Erro de conexão.')); 8 | } 9 | 10 | if (error.response?.status === 401) { 11 | // Do something 12 | } 13 | 14 | return Promise.reject(error); 15 | }; 16 | -------------------------------------------------------------------------------- /src/shared/services/api/axios-config/interceptors/ResponseInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | 3 | 4 | export const responseInterceptor = (response: AxiosResponse) => { 5 | return response; 6 | }; 7 | -------------------------------------------------------------------------------- /src/shared/services/api/axios-config/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ResponseInterceptor'; 2 | export * from './ErrorInterceptor'; 3 | -------------------------------------------------------------------------------- /src/shared/services/api/cidades/CidadesService.ts: -------------------------------------------------------------------------------- 1 | import { Environment } from '../../../environment'; 2 | import { Api } from '../axios-config'; 3 | 4 | 5 | export interface IListagemCidade { 6 | id: number; 7 | nome: string; 8 | } 9 | 10 | export interface IDetalheCidade { 11 | id: number; 12 | nome: string; 13 | } 14 | 15 | type TCidadesComTotalCount = { 16 | data: IListagemCidade[]; 17 | totalCount: number; 18 | } 19 | 20 | const getAll = async (page = 1, filter = '', id = ''): Promise => { 21 | try { 22 | const urlRelativa = `/cidades?_page=${page}&_limit=${Environment.LIMITE_DE_LINHAS}&nome_like=${filter}&id_like=${id}`; 23 | 24 | const { data, headers } = await Api.get(urlRelativa); 25 | 26 | if (data) { 27 | return { 28 | data, 29 | totalCount: Number(headers['x-total-count'] || Environment.LIMITE_DE_LINHAS), 30 | }; 31 | } 32 | 33 | return new Error('Erro ao listar os registros.'); 34 | } catch (error) { 35 | console.error(error); 36 | return new Error((error as { message: string }).message || 'Erro ao listar os registros.'); 37 | } 38 | }; 39 | 40 | const getById = async (id: number): Promise => { 41 | try { 42 | const { data } = await Api.get(`/cidades/${id}`); 43 | 44 | if (data) { 45 | return data; 46 | } 47 | 48 | return new Error('Erro ao consultar o registro.'); 49 | } catch (error) { 50 | console.error(error); 51 | return new Error((error as { message: string }).message || 'Erro ao consultar o registro.'); 52 | } 53 | }; 54 | 55 | const create = async (dados: Omit): Promise => { 56 | try { 57 | const { data } = await Api.post('/cidades', dados); 58 | 59 | if (data) { 60 | return data.id; 61 | } 62 | 63 | return new Error('Erro ao criar o registro.'); 64 | } catch (error) { 65 | console.error(error); 66 | return new Error((error as { message: string }).message || 'Erro ao criar o registro.'); 67 | } 68 | }; 69 | 70 | const updateById = async (id: number, dados: IDetalheCidade): Promise => { 71 | try { 72 | await Api.put(`/cidades/${id}`, dados); 73 | } catch (error) { 74 | console.error(error); 75 | return new Error((error as { message: string }).message || 'Erro ao atualizar o registro.'); 76 | } 77 | }; 78 | 79 | const deleteById = async (id: number): Promise => { 80 | try { 81 | await Api.delete(`/cidades/${id}`); 82 | } catch (error) { 83 | console.error(error); 84 | return new Error((error as { message: string }).message || 'Erro ao apagar o registro.'); 85 | } 86 | }; 87 | 88 | 89 | export const CidadesService = { 90 | getAll, 91 | create, 92 | getById, 93 | updateById, 94 | deleteById, 95 | }; 96 | -------------------------------------------------------------------------------- /src/shared/services/api/pessoas/PessoasService.ts: -------------------------------------------------------------------------------- 1 | import { Environment } from '../../../environment'; 2 | import { Api } from '../axios-config'; 3 | 4 | 5 | export interface IListagemPessoa { 6 | id: number; 7 | email: string; 8 | cidadeId: number; 9 | nomeCompleto: string; 10 | } 11 | 12 | export interface IDetalhePessoa { 13 | id: number; 14 | email: string; 15 | cidadeId: number; 16 | nomeCompleto: string; 17 | } 18 | 19 | type TPessoasComTotalCount = { 20 | data: IListagemPessoa[]; 21 | totalCount: number; 22 | } 23 | 24 | const getAll = async (page = 1, filter = ''): Promise => { 25 | try { 26 | const urlRelativa = `/pessoas?_page=${page}&_limit=${Environment.LIMITE_DE_LINHAS}&nomeCompleto_like=${filter}`; 27 | 28 | const { data, headers } = await Api.get(urlRelativa); 29 | 30 | if (data) { 31 | return { 32 | data, 33 | totalCount: Number(headers['x-total-count'] || Environment.LIMITE_DE_LINHAS), 34 | }; 35 | } 36 | 37 | return new Error('Erro ao listar os registros.'); 38 | } catch (error) { 39 | console.error(error); 40 | return new Error((error as { message: string }).message || 'Erro ao listar os registros.'); 41 | } 42 | }; 43 | 44 | const getById = async (id: number): Promise => { 45 | try { 46 | const { data } = await Api.get(`/pessoas/${id}`); 47 | 48 | if (data) { 49 | return data; 50 | } 51 | 52 | return new Error('Erro ao consultar o registro.'); 53 | } catch (error) { 54 | console.error(error); 55 | return new Error((error as { message: string }).message || 'Erro ao consultar o registro.'); 56 | } 57 | }; 58 | 59 | const create = async (dados: Omit): Promise => { 60 | try { 61 | const { data } = await Api.post('/pessoas', dados); 62 | 63 | if (data) { 64 | return data.id; 65 | } 66 | 67 | return new Error('Erro ao criar o registro.'); 68 | } catch (error) { 69 | console.error(error); 70 | return new Error((error as { message: string }).message || 'Erro ao criar o registro.'); 71 | } 72 | }; 73 | 74 | const updateById = async (id: number, dados: IDetalhePessoa): Promise => { 75 | try { 76 | await Api.put(`/pessoas/${id}`, dados); 77 | } catch (error) { 78 | console.error(error); 79 | return new Error((error as { message: string }).message || 'Erro ao atualizar o registro.'); 80 | } 81 | }; 82 | 83 | const deleteById = async (id: number): Promise => { 84 | try { 85 | await Api.delete(`/pessoas/${id}`); 86 | } catch (error) { 87 | console.error(error); 88 | return new Error((error as { message: string }).message || 'Erro ao apagar o registro.'); 89 | } 90 | }; 91 | 92 | 93 | export const PessoasService = { 94 | getAll, 95 | create, 96 | getById, 97 | updateById, 98 | deleteById, 99 | }; 100 | -------------------------------------------------------------------------------- /src/shared/themes/Dark.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material'; 2 | import { cyan, yellow } from '@mui/material/colors'; 3 | 4 | export const DarkTheme = createTheme({ 5 | palette: { 6 | mode: 'dark', 7 | primary: { 8 | main: yellow[700], 9 | dark: yellow[800], 10 | light: yellow[500], 11 | contrastText: '#ffffff', 12 | }, 13 | secondary: { 14 | main: cyan[500], 15 | dark: cyan[400], 16 | light: cyan[300], 17 | contrastText: '#ffffff', 18 | }, 19 | background: { 20 | paper: '#303134', 21 | default: '#202124', 22 | }, 23 | }, 24 | typography: { 25 | allVariants: { 26 | color: 'white', 27 | } 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /src/shared/themes/Light.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material'; 2 | import { cyan, yellow } from '@mui/material/colors'; 3 | 4 | export const LightTheme = createTheme({ 5 | palette: { 6 | primary: { 7 | main: yellow[700], 8 | dark: yellow[800], 9 | light: yellow[500], 10 | contrastText: '#ffffff', 11 | }, 12 | secondary: { 13 | main: cyan[500], 14 | dark: cyan[400], 15 | light: cyan[300], 16 | contrastText: '#ffffff', 17 | }, 18 | background: { 19 | paper: '#ffffff', 20 | default: '#f7f6f3', 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/shared/themes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Light'; 2 | export * from './Dark'; 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------