├── .eslintrc.cjs ├── .gitignore ├── README.md ├── docs ├── hello-world.png ├── mui-data-grid-example.png ├── sample-create.png └── sample-list.png ├── index.html ├── package-lock.json ├── package.json ├── public └── .gitkeep ├── src ├── App.tsx ├── components │ ├── Breadcrumbs.tsx │ ├── DataTable.tsx │ ├── FormTitle.tsx │ └── PageTitle.tsx ├── index.css ├── main.tsx ├── pages │ ├── Dashboard │ │ └── index.tsx │ └── Users │ │ ├── Create.tsx │ │ ├── Edit.tsx │ │ ├── List.tsx │ │ ├── components │ │ ├── Form.tsx │ │ └── Grid.tsx │ │ ├── schemas │ │ └── UserSchema.ts │ │ └── types │ │ └── User.ts ├── routes.tsx ├── services │ └── api.ts ├── theme.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React CRUD // Admin Panel 2 | 3 | Front-end CRUD completo React para sistemas de administração em geral. Ele foi desenvolvido para o conteúdo da [Master Class #014](https://youtube.com/live/mXHkDD9PRM0) da [Dev Samurai](https://devsamurai.com.br). 4 | 5 | ## Como funciona 6 | 7 | Este é um exemplo de como criar um CRUD 'completão' que possuí as principais funções, como: listagem, criação, edição e exclusão de dados. 8 | 9 | Sem falar na estrutura de pastas e arquivos que é bem organizada e escalável. 10 | 11 | O que ideal para apresentar em um portfólio ou para iniciar um projeto. 12 | 13 | ## Como executar 14 | 15 | Clone, e acesse a pasta do projeto e execute o seguinte comando: 16 | 17 | ```sh 18 | git clone git@github.com:DevSamurai/react-crud.git 19 | cd react-crud 20 | npm install 21 | npm run dev 22 | ``` 23 | 24 | ## Passo a passo 25 | 26 | 1. [x] Criar o projeto e instalar o Material UI 27 | 2. [x] Estrutura de base do CRUD 28 | 3. [x] Listagem de usuários 29 | 4. [x] Criação de usuários 30 | 5. [x] Edição de usuários 31 | 6. [x] Leitura de dados na listagem 32 | 33 | ### Passo 1: Criar o projeto e instalar o Material UI 34 | 35 | Para criarmos o projeto iremos utilizar o [Vite](https://vitejs.dev/), que é um bundler extremamente rápido e simples de utilizar. Para instalar o Vite, execute o seguinte comando: 36 | 37 | ```sh 38 | npm create vite@latest react-crud-admin -- --template react-ts 39 | ``` 40 | 41 | Depois do projeto criado, entre na pasta do projeto e instale o [Material UI](https://mui.com/material-ui/getting-started/installation/): 42 | 43 | ```sh 44 | cd react-crud-admin 45 | npm install @mui/material @emotion/react @emotion/styled @fontsource/roboto @mui/icons-material @mui/x-data-grid @mui/x-date-pickers date-fns axios 46 | ``` 47 | 48 | Criar o arquivo [`src/theme.ts`](./src/theme.ts): 49 | 50 | ```ts 51 | import { ptBR as MaterialLocale } from "@mui/material/locale" 52 | import { createTheme } from "@mui/material/styles" 53 | import { ptBR as DataGridLocale } from "@mui/x-data-grid" 54 | 55 | export const theme = createTheme( 56 | { 57 | palette: { 58 | mode: "dark", 59 | }, 60 | }, 61 | DataGridLocale, 62 | MaterialLocale 63 | ) 64 | ``` 65 | 66 | E ajustar o arquivo [`src/main.tsx`](./src/main.tsx): 67 | 68 | ```tsx 69 | import { Box, Container } from "@mui/material" 70 | import CssBaseline from "@mui/material/CssBaseline" 71 | import { ThemeProvider } from "@mui/material/styles" 72 | import { LocalizationProvider } from "@mui/x-date-pickers" 73 | import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns" 74 | import ptBR from "date-fns/locale/pt-BR" 75 | import React from "react" 76 | import ReactDOM from "react-dom/client" 77 | 78 | import { theme } from "./theme.ts" 79 | 80 | import App from "./App.tsx" 81 | 82 | import "@fontsource/roboto/300.css" 83 | import "@fontsource/roboto/400.css" 84 | import "@fontsource/roboto/500.css" 85 | import "@fontsource/roboto/700.css" 86 | 87 | import "./index.css" 88 | 89 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | ) 103 | ``` 104 | 105 | Nisso já não precisaremos mais dos arquivos e podemos excluí-los: 106 | 107 | - `src/App.css` 108 | - `src/index.css` 109 | - `src/assets/logo.svg` 110 | - `src/public/vite.svg` 111 | 112 | Ajustar o `title` e remover `favicon` do [`index.html`](./index.html): 113 | 114 | ```html 115 | 116 | 117 | 118 | 119 | 120 | React CRUD // Admin Panel 121 | 122 | 123 |
124 | 125 | 126 | 127 | ``` 128 | 129 | E para que possamos testar se o Material UI está funcionando, vamos adicionar um componente de botão no arquivo [`src/App.tsx`](./src/App.tsx): 130 | 131 | ```tsx 132 | import { Button } from "@mui/material" 133 | 134 | export default function App() { 135 | return ( 136 |
137 | 138 |
139 | ) 140 | } 141 | ``` 142 | 143 | E pronto! Já temos algo minimamente funcional: 144 | 145 | ![Hello World](./docs/hello-world.png) 146 | 147 | ## Passo 2: Estrutura de base do CRUD 148 | 149 | Agora que já fizemos as instalações iniciais, chegou o momento de criar a estrutura de base do nosso CRUD. 150 | 151 | Sendo assim, inicie instalando o [React Router](https://reactrouter.com/): 152 | 153 | ```sh 154 | npm install react-router-dom 155 | ``` 156 | 157 | Crie o arquivo [`src/routes.tsx`](./src/routes.tsx): 158 | 159 | ```tsx 160 | import { Route, Routes } from "react-router-dom" 161 | 162 | import UserCreate from "./pages/Users/Create" 163 | import UserEdit from "./pages/Users/Edit" 164 | import UserList from "./pages/Users/List" 165 | 166 | export function AppRoutes() { 167 | return ( 168 | 169 | 170 | } /> 171 | } /> 172 | } /> 173 | 174 | 175 | ) 176 | } 177 | ``` 178 | 179 | E ajuste o arquivo [`src/App.tsx`](./src/App.tsx): 180 | 181 | ```tsx 182 | import { BrowserRouter } from "react-router-dom" 183 | 184 | import { AppRoutes } from "./routes" 185 | 186 | export default function App() { 187 | return ( 188 | 189 | 190 | 191 | ) 192 | } 193 | ``` 194 | 195 | E por fim, vamos criar a pasta [`src/pages/Users`](./src/pages/Users/) e dentro dela vamos criar nas próximas sessões uma estrutura organizada de componentes, sendo eles: 196 | 197 | - `pages/Users/List.tsx`: Componente que irá listar os usuários cadastrados. 198 | - `pages/Users/Create.tsx`: Componente que irá criar um novo usuário. 199 | - `pages/Users/Edit.tsx`: Componente que irá editar um usuário existente. 200 | - `pages/Users/components/Form.tsx`: Componente que irá conter o formulário de cadastro e edição de usuários. 201 | - `pages/Users/components/Grid.tsx`: Componente que irá conter a tabela de listagem de usuários. 202 | - `pages/Users/schemas/UserSchema.ts`: Schema de validação do formulário de cadastro e edição de usuários. 203 | - `pages/Users/types/User.ts`: Tipagem do usuário. 204 | 205 | Por hora, para cada componente, vamos criar apenas a estrutura mínima para que o React não deixe de funcionar, por exemplo: 206 | 207 | ```tsx 208 | export default function List() { 209 | return <>List 210 | } 211 | ``` 212 | 213 | E a tipagem de usuário: 214 | 215 | ```ts 216 | export type User = { 217 | id: string 218 | fullName: string 219 | document: string 220 | birthDate: Date 221 | email: string 222 | emailVerified: boolean 223 | mobile: string 224 | zipCode: string 225 | addressName: string 226 | number: string 227 | complement: string 228 | neighborhood: string 229 | city: string 230 | state: string 231 | } 232 | ``` 233 | 234 | ## Passo 3: Listagem de usuários 235 | 236 | Para criar a listagem de usuários, iremos utilizar o componente `DataTable` do [Material UI](https://mui.com/x/react-data-grid/). 237 | 238 | ![MUI Data Grid](./docs/mui-data-grid-example.png) 239 | 240 | Este é um componente que já entrega as principais funcionalidades de uma listagem, como busca, ordenação, paginação, seleção de linhas, etc. 241 | 242 | Ele possui a versão gratuita e a versão paga, sendo que a versão gratuita já atende a maioria dos casos. 243 | 244 | E com isso você não precisa criar 'do zero' muitas dessas coisas e pode focar no que realmente importa, que é a lógica de negócio, além de ter um acabamento visual muito interessante. 245 | 246 | E para criar o componente [`pages/Users/components/Grid.tsx`](./src/pages/Users/components/Grid.tsx) que irá encapsular o `DataTable`, vamos utilizar o seguinte código: 247 | 248 | ```tsx 249 | import DeleteIcon from "@mui/icons-material/Delete" 250 | import EditIcon from "@mui/icons-material/Edit" 251 | import WhatsAppIcon from "@mui/icons-material/WhatsApp" 252 | import { IconButton, Stack } from "@mui/material" 253 | import { 254 | GridColDef, 255 | GridRenderCellParams, 256 | GridValueGetterParams, 257 | } from "@mui/x-data-grid" 258 | import { useNavigate } from "react-router-dom" 259 | 260 | import DataTable from "../../../components/DataTable" 261 | 262 | import { User } from "../types/User" 263 | 264 | export default function Grid() { 265 | const onCall = (params: GridRenderCellParams) => { 266 | // Chamada via WhatsApp 267 | } 268 | 269 | const onEdit = (params: GridRenderCellParams) => { 270 | // Edição de usuário 271 | } 272 | 273 | const onDelete = (params: GridRenderCellParams) => { 274 | // Exclusão de usuário 275 | } 276 | 277 | // Definição das colunas da tabela 278 | const columns: GridColDef[] = [ 279 | { field: "id", headerName: "ID", width: 70 }, 280 | { 281 | field: "firstName", 282 | headerName: "Nome", 283 | valueGetter: (params: GridValueGetterParams) => 284 | `${params.row.fullName.split(" ")?.shift() || ""}`, 285 | }, 286 | { 287 | field: "lastName", 288 | headerName: "Sobrenome", 289 | valueGetter: (params: GridValueGetterParams) => 290 | `${params.row.fullName.split(" ")?.pop() || ""}`, 291 | }, 292 | { field: "document", headerName: "CPF", width: 150 }, 293 | { 294 | field: "age", 295 | headerName: "Idade", 296 | type: "number", 297 | valueGetter: (params: GridValueGetterParams) => 298 | params.row.birthDate && 299 | `${ 300 | new Date().getFullYear() - 301 | new Date(params.row.birthDate).getFullYear() 302 | }`, 303 | }, 304 | { field: "email", headerName: "E-mail", minWidth: 200 }, 305 | { field: "mobile", headerName: "Celular", minWidth: 180 }, 306 | { 307 | field: "actions", 308 | headerName: "Ações", 309 | minWidth: 150, 310 | sortable: false, 311 | renderCell: (params) => ( 312 | 313 | onCall(params)} 317 | > 318 | 319 | 320 | 321 | onEdit(params)}> 322 | 323 | 324 | 325 | onDelete(params)} 329 | > 330 | 331 | 332 | 333 | ), 334 | }, 335 | ] 336 | 337 | // Criação de uma carga dummy de usuários 338 | const users = [ 339 | { 340 | id: '1', 341 | fullName: 'Felipe Fontoura', 342 | document: '986.007.560-30', 343 | birthDate: new Date(1982, 1, 1), 344 | email: 'felipe@teste.com.br', 345 | emailVerified: true, 346 | mobile: '(11) 99999-9999', 347 | zipCode: '00000-000', 348 | addressName: 'Rua Teste', 349 | number: '123', 350 | complement: '', 351 | neighborhood: 'Bairro Teste', 352 | city: 'São Paulo', 353 | state: 'SP', 354 | } 355 | ] 356 | 357 | return 358 | } 359 | ``` 360 | 361 | E então criar os componentes comuns [`components/PageTitle.tsx`](./src/components/PageTitle.tsx) e [`components/Breadcrumbs.tsx`](./src/components/Breadcrumbs.tsx) na sequência: 362 | 363 | ```tsx 364 | import { Typography } from "@mui/material" 365 | 366 | interface PageTitleProps { 367 | title: string 368 | } 369 | 370 | export default function PageTitle({ title }: PageTitleProps) { 371 | return ( 372 | 373 | {title} 374 | 375 | ) 376 | } 377 | ``` 378 | 379 | ```tsx 380 | import { 381 | Link, 382 | Breadcrumbs as MaterialBreadcrumbs, 383 | Typography, 384 | } from "@mui/material" 385 | import { Link as RouterLink } from "react-router-dom" 386 | 387 | interface BreadcrumbProps { 388 | path: { 389 | label: string 390 | to?: string 391 | }[] 392 | } 393 | 394 | export default function Breadcrumbs({ path }: BreadcrumbProps) { 395 | return ( 396 | 397 | 398 | Dashboard 399 | 400 | 401 | {path.map((item, index) => 402 | item.to ? ( 403 | 410 | {item.label} 411 | 412 | ) : ( 413 | 414 | {item.label} 415 | 416 | ) 417 | )} 418 | 419 | ) 420 | } 421 | ``` 422 | 423 | Depois disso, vamos criar o componente/página [`pages/Users/List.tsx`](./src/pages/Users/List.tsx): 424 | 425 | ```tsx 426 | import PersonAddAltIcon from "@mui/icons-material/PersonAddAlt" 427 | import { Box, Button, Paper, Stack } from "@mui/material" 428 | import { Link as RouterLink } from "react-router-dom" 429 | 430 | import Breadcrumbs from "../../components/Breadcrumbs" 431 | import PageTitle from "../../components/PageTitle" 432 | 433 | import Grid from "./components/Grid" 434 | 435 | export default function List() { 436 | return ( 437 | <> 438 | 439 | 440 | 441 | 444 | 445 | 446 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | ) 461 | } 462 | ``` 463 | 464 | Com este componente criado, basta acessar do navegador o endereço que poderemos ver a listagem de usuários com os dados de teste. 465 | 466 | ![Listagem](./docs/sample-list.png) 467 | 468 | ## Passo 4: Criação de usuários 469 | 470 | Para a criação de usuários, iremos utilizar as libs `react-hook-form` e `yup` para validação dos campos, além da lib `react-input-mask` para a criação de máscaras de campos. 471 | 472 | Para instalar a lib `react-hook-form` e `yup`, execute o comando: 473 | 474 | ```sh 475 | npm install react-hook-form @hookform/resolvers yup 476 | ``` 477 | 478 | E a lib `react-input-mask`: 479 | 480 | ```sh 481 | npm install react-input-mask@3.0.0-alpha.2 482 | npm install -D @types/react-input-mask 483 | ``` 484 | 485 | Com as libs instaladas, vamos criar o [`pages/Users/schemas/UserSchema.tsx`](./src/pages/Users/schemas/UserSchema.ts). Este será o schema de validação dos campos do formulário: 486 | 487 | ```tsx 488 | import * as yup from "yup" 489 | 490 | import { User } from "../types/User" 491 | 492 | export const UserSchema = yup 493 | .object({ 494 | fullName: yup.string().required("Este campo é obrigatório"), 495 | document: yup.string().required("Este campo é obrigatório"), 496 | birthDate: yup.date(), 497 | email: yup 498 | .string() 499 | .email("E-mail não reconhecido") 500 | .required("Este campo é obrigatório"), 501 | emailVerified: yup.boolean().default(false), 502 | mobile: yup.string().required("Este campo é obrigatório"), 503 | zipCode: yup 504 | .string() 505 | .required("Este campo é obrigatório") 506 | .transform((value) => value.replace(/[^\d]+/g, "")), 507 | addressName: yup.string().required("Este campo é obrigatório"), 508 | number: yup.string().required("Este campo é obrigatório"), 509 | complement: yup.string(), 510 | neighborhood: yup.string().required("Este campo é obrigatório"), 511 | city: yup.string().required("Este campo é obrigatório"), 512 | state: yup.string().required("Este campo é obrigatório"), 513 | }) 514 | .required() 515 | ``` 516 | 517 | E agora, vamos criar o componente [`src/pages/Users/Form.tsx`](./src/pages/Users/components/Form.tsx): 518 | 519 | ```tsx 520 | import { yupResolver } from "@hookform/resolvers/yup" 521 | import CheckCircleIcon from "@mui/icons-material/CheckCircle" 522 | import InfoIcon from "@mui/icons-material/Info" 523 | import { 524 | Box, 525 | Button, 526 | FormControl, 527 | FormControlLabel, 528 | InputLabel, 529 | MenuItem, 530 | Select, 531 | Stack, 532 | Switch, 533 | TextField, 534 | Tooltip, 535 | } from "@mui/material" 536 | import { DatePicker } from "@mui/x-date-pickers" 537 | import { useState } from "react" 538 | import { Controller, useForm } from "react-hook-form" 539 | import InputMask from "react-input-mask" 540 | import { Link as RouterLink, useNavigate, useParams } from "react-router-dom" 541 | import { useLocalStorage } from "usehooks-ts" 542 | 543 | import FormTitle from "../../../components/FormTitle" 544 | 545 | import { findBrazilianZipCode } from "../../../services/api" 546 | 547 | import { UserSchema } from "../schemas/UserSchema" 548 | 549 | import { User } from "../types/User" 550 | 551 | export default function Form() { 552 | const [users, setUsers] = useLocalStorage("users", []) 553 | const { id } = useParams() 554 | const navigate = useNavigate() 555 | 556 | const { 557 | control, 558 | register, 559 | handleSubmit, 560 | formState: { errors }, 561 | setFocus, 562 | setValue, 563 | } = useForm({ 564 | resolver: yupResolver(UserSchema), 565 | }) 566 | 567 | const [zipCodeFounded, setZipCodeFounded] = useState() 568 | 569 | const onSubmit = (data: User) => { 570 | // registra o usuário 571 | console.log(data) 572 | 573 | navigate("/users/") 574 | } 575 | 576 | // Função que busca o CEP e preenche os campos de endereço 577 | const onZipCodeBlur = async ( 578 | event: React.FocusEvent 579 | ) => { 580 | const { value } = event.target 581 | 582 | if (!value) return 583 | 584 | const address = await findBrazilianZipCode(value) 585 | 586 | if (!address || !address?.addressName) { 587 | setZipCodeFounded(false) 588 | 589 | setValue("addressName", "") 590 | setValue("neighborhood", "") 591 | setValue("city", "") 592 | setValue("state", "") 593 | 594 | setFocus("addressName") 595 | 596 | return 597 | } 598 | 599 | setZipCodeFounded(true) 600 | 601 | setValue("addressName", address.addressName) 602 | setValue("neighborhood", address.neighborhood) 603 | setValue("city", address.city) 604 | setValue("state", address.state) 605 | 606 | setFocus("number") 607 | } 608 | 609 | return ( 610 | 617 | 625 | 626 | 631 | ( 636 | 637 | 638 | 644 | 645 | 646 | )} 647 | /> 648 | 649 | ( 653 | 654 | 655 | 656 | )} 657 | /> 658 | 659 | 660 | 665 | 672 | 673 | ( 678 | 679 | 680 | 686 | 687 | 688 | )} 689 | /> 690 | 691 | 692 | 693 | 694 | 700 | ( 705 | 706 | { 712 | onZipCodeBlur(e) 713 | field.onBlur() 714 | }} 715 | > 716 | 726 | 727 | 728 | )} 729 | /> 730 | {zipCodeFounded === true && } 731 | 732 | 733 | ( 738 | 739 | 746 | 747 | )} 748 | /> 749 | 750 | 755 | 762 | 769 | 770 | 771 | 776 | ( 781 | 782 | 790 | 791 | )} 792 | /> 793 | 794 | ( 799 | 800 | 808 | 809 | )} 810 | /> 811 | 812 | 813 | ( 818 | 819 | Estado 820 | 859 | 860 | )} 861 | /> 862 | 863 | ( 868 | <> 869 | 872 | } 873 | label="Email Pré-verificado" 874 | sx={{ marginBottom: 2 }} 875 | /> 876 | 877 | 878 | 879 | 880 | )} 881 | /> 882 | 883 | 884 | 887 | 890 | 891 | 892 | ) 893 | } 894 | ``` 895 | 896 | Este é um form bem extenso, conta com validações, máscaras, campos dependentes, etc. Mas o que é importante notar é que ele é bem simples de ser lido e entendido. E isso é possível graças ao React Hook Form e o Material UI. 897 | 898 | E como este form, conta com preenchimento automático de CEP, vamos criar a função `findBrazilianZipCode` no arquivo [`services/api.ts`](./src/services/api.ts) que vai nos ajudar com isso: 899 | 900 | ```tsx 901 | import axios from "axios" 902 | 903 | const api = axios.create() 904 | 905 | export async function findBrazilianZipCode(zipCode: string): Promise< 906 | | { 907 | zipCode: string 908 | addressName: string 909 | neighborhood: string 910 | city: string 911 | state: string 912 | } 913 | | undefined 914 | > { 915 | try { 916 | const { data } = await api.get( 917 | `https://viacep.com.br/ws/${zipCode.replace(/\D/g, "")}/json/` 918 | ) 919 | 920 | return { 921 | zipCode: data.cep, 922 | addressName: data.logradouro, 923 | neighborhood: data.bairro, 924 | city: data.localidade, 925 | state: data.uf, 926 | } 927 | } catch (error) { 928 | console.error(error) 929 | return 930 | } 931 | } 932 | ``` 933 | 934 | E por fim a página [`pages/Users/Create.tsx`](./src/pages/Users/Create.tsx): 935 | 936 | ```tsx 937 | import { Paper, Stack } from "@mui/material" 938 | 939 | import Breadcrumbs from "../../components/Breadcrumbs" 940 | import PageTitle from "../../components/PageTitle" 941 | 942 | import Form from "./components/Form" 943 | 944 | export default function Create() { 945 | return ( 946 | <> 947 | 948 | 949 | 952 | 953 | 954 |
955 | 956 | 957 | ) 958 | } 959 | ``` 960 | 961 | Com isso, basta acessar a página de criação de usuários e testar o preenchimento automático de CEP: 962 | 963 | ![Create](./docs/sample-create.png) 964 | 965 | Mas isso não é tudo! Ainda ter que fazer a gravação dos dados, e para isso iremos criar a lib `usehooks-ts` que possui um hook bem interessante para manipulação de dados no `localStorage`: 966 | 967 | ```sh 968 | npm install usehooks-ts 969 | ``` 970 | 971 | E com isso, vamos ajustar o Form para que ele faça a leitura e gravação: 972 | 973 | ```tsx 974 | import { useLocalStorage } from "usehooks-ts" 975 | 976 | // ... 977 | 978 | export default function Form() { 979 | const [users, setUsers] = useLocalStorage("users", []) // monta o hook de leitura e gravação de dados no localStorage 980 | const { id } = useParams() // carrega o parâmetro de id de rota 981 | const navigate = useNavigate() // carrega a função de navegação de rota 982 | 983 | useEffect(() => { 984 | if (!id) return 985 | 986 | // busca o usuário pelo id 987 | const user = users.find((user) => user.id === id) 988 | 989 | if (!user) return 990 | 991 | // se encontrado, preenche o formulário via setValue do React Hook Form 992 | setValue("fullName", user.fullName) 993 | setValue("document", user.document) 994 | setValue("birthDate", new Date(user.birthDate)) 995 | setValue("email", user.email) 996 | setValue("emailVerified", user.emailVerified) 997 | setValue("mobile", user.mobile) 998 | setValue("zipCode", user.zipCode) 999 | setValue("addressName", user.addressName) 1000 | setValue("number", user.number) 1001 | setValue("complement", user.complement) 1002 | setValue("neighborhood", user.neighborhood) 1003 | setValue("city", user.city) 1004 | setValue("state", user.state) 1005 | }, [id, setValue, users]) 1006 | 1007 | const onSubmit = (data: User) => { 1008 | // se não tiver id, cria um novo usuário 1009 | if (!id) { 1010 | setUsers([...users, { ...data, id: `${users.length + 1}` }]) 1011 | } else { 1012 | // se tiver id, atualiza o usuário 1013 | const newUsers = [...users] 1014 | const userIndex = users.findIndex((user) => user.id === id) 1015 | newUsers[userIndex] = { ...data, id } 1016 | 1017 | setUsers(newUsers) 1018 | } 1019 | 1020 | // navega para a página de listagem de usuários 1021 | navigate("/users/") 1022 | } 1023 | ``` 1024 | 1025 | ## Passo 5: Edição de usuários 1026 | 1027 | Com a criação, agora iremos reaproveitar o formulário para criar a página de edição de usuários. Para isso, vamos criar a página [`pages/Users/Edit.tsx`](./src/pages/Users/Edit.tsx): 1028 | 1029 | ```tsx 1030 | import { Paper, Stack } from "@mui/material" 1031 | 1032 | import Breadcrumbs from "../../components/Breadcrumbs" 1033 | import PageTitle from "../../components/PageTitle" 1034 | 1035 | import Form from "./components/Form" 1036 | 1037 | export default function Edit() { 1038 | return ( 1039 | <> 1040 | 1041 | 1042 | 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | ) 1051 | } 1052 | ``` 1053 | 1054 | Como utilizamos o mesmo formulário, e este já está preparado para carregar os dados de um usuário, basta acessar a página de edição de usuários e testar a atualização. 1055 | 1056 | ## Passo 6: Leitura de dados na listagem 1057 | 1058 | Como ainda temos o componente de listagem de dados carregando dados `dummy`, vamos ajustar ele para que leia os dados do `localStorage` e também os botões de ação: 1059 | 1060 | ```tsx 1061 | import DeleteIcon from "@mui/icons-material/Delete" 1062 | import EditIcon from "@mui/icons-material/Edit" 1063 | import WhatsAppIcon from "@mui/icons-material/WhatsApp" 1064 | import { IconButton, Stack } from "@mui/material" 1065 | import { 1066 | GridColDef, 1067 | GridRenderCellParams, 1068 | GridValueGetterParams, 1069 | } from "@mui/x-data-grid" 1070 | import { useNavigate } from "react-router-dom" 1071 | import { useLocalStorage } from "usehooks-ts" 1072 | 1073 | import DataTable from "../../../components/DataTable" 1074 | 1075 | import { User } from "../types/User" 1076 | 1077 | export default function Grid() { 1078 | const [users, setUsers] = useLocalStorage("users", []) // carrega de localStorage 1079 | const navigate = useNavigate() 1080 | 1081 | const onCall = (params: GridRenderCellParams) => { 1082 | // se existe o número de telefone, abre o WhatsApp 1083 | if (!params.row.mobile) return 1084 | 1085 | window.location.href = `https://wa.me/55${params.row.mobile.replace( 1086 | /[^\d]+/g, 1087 | "" 1088 | )}` 1089 | } 1090 | 1091 | const onEdit = (params: GridRenderCellParams) => { 1092 | // se existe o id, navega para a página de edição 1093 | if (!params.row.id) return 1094 | navigate(`/users/${params.row.id}`) 1095 | } 1096 | 1097 | const onDelete = (params: GridRenderCellParams) => { 1098 | // se existe o id, remove o usuário 1099 | if (!params.row.id) return 1100 | setUsers(users.filter((user) => user.id !== params.row.id)) 1101 | } 1102 | 1103 | const columns: GridColDef[] = [ 1104 | { field: "id", headerName: "ID", width: 70 }, 1105 | { 1106 | field: "firstName", 1107 | headerName: "Nome", 1108 | valueGetter: (params: GridValueGetterParams) => 1109 | `${params.row.fullName.split(" ")?.shift() || ""}`, 1110 | }, 1111 | { 1112 | field: "lastName", 1113 | headerName: "Sobrenome", 1114 | valueGetter: (params: GridValueGetterParams) => 1115 | `${params.row.fullName.split(" ")?.pop() || ""}`, 1116 | }, 1117 | { field: "document", headerName: "CPF", width: 150 }, 1118 | { 1119 | field: "age", 1120 | headerName: "Idade", 1121 | type: "number", 1122 | valueGetter: (params: GridValueGetterParams) => 1123 | params.row.birthDate && 1124 | `${ 1125 | new Date().getFullYear() - 1126 | new Date(params.row.birthDate).getFullYear() 1127 | }`, 1128 | }, 1129 | { field: "email", headerName: "E-mail", minWidth: 200 }, 1130 | { field: "mobile", headerName: "Celular", minWidth: 180 }, 1131 | { 1132 | field: "actions", 1133 | headerName: "Ações", 1134 | minWidth: 150, 1135 | sortable: false, 1136 | renderCell: (params) => ( 1137 | 1138 | onCall(params)} 1142 | > 1143 | 1144 | 1145 | 1146 | onEdit(params)}> 1147 | 1148 | 1149 | 1150 | onDelete(params)} 1154 | > 1155 | 1156 | 1157 | 1158 | ), 1159 | }, 1160 | ] 1161 | 1162 | return 1163 | } 1164 | ``` 1165 | 1166 | Com isso, já teremos os dados sendo carregados do `localStorage` e os botões de ação funcionando. 1167 | 1168 | ## Conclusão 1169 | 1170 | Com isso, temos um CRUD completo de usuários, com listagem, criação, edição e remoção de usuários. Além disso, temos um formulário com validação e máscaras, e uma listagem com paginação e ordenação. 1171 | 1172 | Claro, que estamos utilizando os dados de localStorage, mas você pode utilizar qualquer API para carregar os dados, basta ajustar o componente de listagem para carregar os dados da API via Axios, por exemplo. 1173 | 1174 | E caso você queira implementar isso, pode utilizar como base a nossa [Master Class #005 - MÍNIMO pra passar - React e Node.js](https://youtu.be/teJ8e5m71QA) disponível no YouTube. 1175 | 1176 | Espero que tenha gostado 🧡 1177 | 1178 | -- Felipe Fontoura, @DevSamurai 1179 | 1180 | PS: Se você curtiu esse conteúdo, vai curtir também minha newsletter, inscreva-se em 1181 | 1182 | ## Referências 1183 | 1184 | Alguns links e referências que podem te ajudar: 1185 | 1186 | - [Template de Dashboard Profissional](https://minimals.cc/dashboard) 1187 | - [Material UI + React Hook Form](https://levelup.gitconnected.com/reareact-hook-form-with-mui-examples-a3080b71ec45) 1188 | -------------------------------------------------------------------------------- /docs/hello-world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevSamurai/react-crud/09667c3a2001051c5e9463560481779da50e9c90/docs/hello-world.png -------------------------------------------------------------------------------- /docs/mui-data-grid-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevSamurai/react-crud/09667c3a2001051c5e9463560481779da50e9c90/docs/mui-data-grid-example.png -------------------------------------------------------------------------------- /docs/sample-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevSamurai/react-crud/09667c3a2001051c5e9463560481779da50e9c90/docs/sample-create.png -------------------------------------------------------------------------------- /docs/sample-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevSamurai/react-crud/09667c3a2001051c5e9463560481779da50e9c90/docs/sample-list.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React CRUD // Admin Panel 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-crud-admin", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.11.0", 14 | "@emotion/styled": "^11.11.0", 15 | "@fontsource/roboto": "^5.0.2", 16 | "@hookform/resolvers": "^3.1.0", 17 | "@mui/icons-material": "^5.11.16", 18 | "@mui/material": "^5.13.3", 19 | "@mui/x-data-grid": "^6.6.0", 20 | "@mui/x-date-pickers": "^6.6.0", 21 | "axios": "^1.4.0", 22 | "date-fns": "^2.30.0", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-hook-form": "^7.44.3", 26 | "react-input-mask": "^3.0.0-alpha.2", 27 | "react-router-dom": "^6.11.2", 28 | "usehooks-ts": "^2.9.1", 29 | "yup": "^1.2.0" 30 | }, 31 | "devDependencies": { 32 | "@types/react": "^18.0.37", 33 | "@types/react-dom": "^18.0.11", 34 | "@types/react-input-mask": "^3.0.2", 35 | "@typescript-eslint/eslint-plugin": "^5.59.0", 36 | "@typescript-eslint/parser": "^5.59.0", 37 | "@vitejs/plugin-react": "^4.0.0", 38 | "eslint": "^8.38.0", 39 | "eslint-plugin-react-hooks": "^4.6.0", 40 | "eslint-plugin-react-refresh": "^0.3.4", 41 | "typescript": "^5.0.2", 42 | "vite": "^4.3.9" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevSamurai/react-crud/09667c3a2001051c5e9463560481779da50e9c90/public/.gitkeep -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter } from "react-router-dom" 2 | 3 | import { AppRoutes } from "./routes" 4 | 5 | export default function App() { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Link, 3 | Breadcrumbs as MaterialBreadcrumbs, 4 | Typography, 5 | } from "@mui/material" 6 | import { Link as RouterLink } from "react-router-dom" 7 | 8 | interface BreadcrumbProps { 9 | path: { 10 | label: string 11 | to?: string 12 | }[] 13 | } 14 | 15 | export default function Breadcrumbs({ path }: BreadcrumbProps) { 16 | return ( 17 | 18 | 19 | Dashboard 20 | 21 | 22 | {path.map((item, index) => 23 | item.to ? ( 24 | 31 | {item.label} 32 | 33 | ) : ( 34 | 35 | {item.label} 36 | 37 | ) 38 | )} 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/DataTable.tsx: -------------------------------------------------------------------------------- 1 | import { DataGrid, GridColDef, GridValidRowModel } from "@mui/x-data-grid" 2 | 3 | interface DataTableProps { 4 | columns: GridColDef[] 5 | rows: GridValidRowModel[] 6 | } 7 | 8 | export default function DataTable({ columns, rows }: DataTableProps) { 9 | return ( 10 |
11 | 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/FormTitle.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@mui/material" 2 | 3 | interface FormTitleProps { 4 | title: string 5 | } 6 | 7 | export default function FormTitle({ title }: FormTitleProps) { 8 | return ( 9 | 10 | {title} 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/PageTitle.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@mui/material" 2 | 3 | interface PageTitleProps { 4 | title: string 5 | } 6 | 7 | export default function PageTitle({ title }: PageTitleProps) { 8 | return ( 9 | 10 | {title} 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .MuiDataGrid-root, 2 | .MuiDataGrid-cell:focus, 3 | .MuiDataGrid-cell:focus-within, 4 | .MuiDataGrid-columnHeader:focus, 5 | .MuiDataGrid-columnHeader:focus-within { 6 | outline: none !important; 7 | } 8 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Container } from "@mui/material" 2 | import CssBaseline from "@mui/material/CssBaseline" 3 | import { ThemeProvider } from "@mui/material/styles" 4 | import { LocalizationProvider } from "@mui/x-date-pickers" 5 | import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns" 6 | import ptBR from "date-fns/locale/pt-BR" 7 | import React from "react" 8 | import ReactDOM from "react-dom/client" 9 | 10 | import { theme } from "./theme.ts" 11 | 12 | import App from "./App.tsx" 13 | 14 | import "@fontsource/roboto/300.css" 15 | import "@fontsource/roboto/400.css" 16 | import "@fontsource/roboto/500.css" 17 | import "@fontsource/roboto/700.css" 18 | 19 | import "./index.css" 20 | 21 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | -------------------------------------------------------------------------------- /src/pages/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | export default function Dashboard() { 2 | return <>Dashboard 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/Users/Create.tsx: -------------------------------------------------------------------------------- 1 | import { Paper, Stack } from "@mui/material" 2 | 3 | import Breadcrumbs from "../../components/Breadcrumbs" 4 | import PageTitle from "../../components/PageTitle" 5 | 6 | import Form from "./components/Form" 7 | 8 | export default function Create() { 9 | return ( 10 | <> 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/Users/Edit.tsx: -------------------------------------------------------------------------------- 1 | import { Paper, Stack } from "@mui/material" 2 | 3 | import Breadcrumbs from "../../components/Breadcrumbs" 4 | import PageTitle from "../../components/PageTitle" 5 | 6 | import Form from "./components/Form" 7 | 8 | export default function Edit() { 9 | return ( 10 | <> 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/Users/List.tsx: -------------------------------------------------------------------------------- 1 | import PersonAddAltIcon from "@mui/icons-material/PersonAddAlt" 2 | import { Box, Button, Paper, Stack } from "@mui/material" 3 | import { Link as RouterLink } from "react-router-dom" 4 | 5 | import Breadcrumbs from "../../components/Breadcrumbs" 6 | import PageTitle from "../../components/PageTitle" 7 | 8 | import Grid from "./components/Grid" 9 | 10 | export default function List() { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/Users/components/Form.tsx: -------------------------------------------------------------------------------- 1 | import { yupResolver } from "@hookform/resolvers/yup" 2 | import CheckCircleIcon from "@mui/icons-material/CheckCircle" 3 | import InfoIcon from "@mui/icons-material/Info" 4 | import { 5 | Box, 6 | Button, 7 | FormControl, 8 | FormControlLabel, 9 | InputLabel, 10 | MenuItem, 11 | Select, 12 | Stack, 13 | Switch, 14 | TextField, 15 | Tooltip, 16 | } from "@mui/material" 17 | import { DatePicker } from "@mui/x-date-pickers" 18 | import { useEffect, useState } from "react" 19 | import { Controller, useForm } from "react-hook-form" 20 | import InputMask from "react-input-mask" 21 | import { Link as RouterLink, useNavigate, useParams } from "react-router-dom" 22 | import { useLocalStorage } from "usehooks-ts" 23 | 24 | import FormTitle from "../../../components/FormTitle" 25 | 26 | import { findBrazilianZipCode } from "../../../services/api" 27 | 28 | import { UserSchema } from "../schemas/UserSchema" 29 | 30 | import { User } from "../types/User" 31 | 32 | export default function Form() { 33 | const [users, setUsers] = useLocalStorage("users", []) 34 | const { id } = useParams() 35 | const navigate = useNavigate() 36 | 37 | const { 38 | control, 39 | register, 40 | handleSubmit, 41 | formState: { errors }, 42 | setFocus, 43 | setValue, 44 | } = useForm({ 45 | resolver: yupResolver(UserSchema), 46 | }) 47 | 48 | const [zipCodeFounded, setZipCodeFounded] = useState() 49 | 50 | useEffect(() => { 51 | if (!id) return 52 | 53 | const user = users.find((user) => user.id === id) 54 | 55 | if (!user) return 56 | 57 | setValue("fullName", user.fullName) 58 | setValue("document", user.document) 59 | setValue("birthDate", new Date(user.birthDate)) 60 | setValue("email", user.email) 61 | setValue("emailVerified", user.emailVerified) 62 | setValue("mobile", user.mobile) 63 | setValue("zipCode", user.zipCode) 64 | setValue("addressName", user.addressName) 65 | setValue("number", user.number) 66 | setValue("complement", user.complement) 67 | setValue("neighborhood", user.neighborhood) 68 | setValue("city", user.city) 69 | setValue("state", user.state) 70 | }, [id, setValue, users]) 71 | 72 | const onSubmit = (data: User) => { 73 | if (!id) { 74 | setUsers([...users, { ...data, id: `${users.length + 1}` }]) 75 | } else { 76 | const newUsers = [...users] 77 | const userIndex = users.findIndex((user) => user.id === id) 78 | newUsers[userIndex] = { ...data, id } 79 | 80 | setUsers(newUsers) 81 | } 82 | 83 | navigate("/users/") 84 | } 85 | 86 | const onZipCodeBlur = async ( 87 | event: React.FocusEvent 88 | ) => { 89 | const { value } = event.target 90 | 91 | if (!value) return 92 | 93 | const address = await findBrazilianZipCode(value) 94 | 95 | if (!address || !address?.addressName) { 96 | setZipCodeFounded(false) 97 | 98 | setValue("addressName", "") 99 | setValue("neighborhood", "") 100 | setValue("city", "") 101 | setValue("state", "") 102 | 103 | setFocus("addressName") 104 | 105 | return 106 | } 107 | 108 | setZipCodeFounded(true) 109 | 110 | setValue("addressName", address.addressName) 111 | setValue("neighborhood", address.neighborhood) 112 | setValue("city", address.city) 113 | setValue("state", address.state) 114 | 115 | setFocus("number") 116 | } 117 | 118 | return ( 119 | 126 | 134 | 135 | 140 | ( 145 | 146 | 147 | 153 | 154 | 155 | )} 156 | /> 157 | 158 | ( 162 | 163 | 164 | 165 | )} 166 | /> 167 | 168 | 169 | 174 | 181 | 182 | ( 187 | 188 | 189 | 195 | 196 | 197 | )} 198 | /> 199 | 200 | 201 | 202 | 203 | 209 | ( 214 | 215 | { 221 | onZipCodeBlur(e) 222 | field.onBlur() 223 | }} 224 | > 225 | 235 | 236 | 237 | )} 238 | /> 239 | {zipCodeFounded === true && } 240 | 241 | 242 | ( 247 | 248 | 255 | 256 | )} 257 | /> 258 | 259 | 264 | 271 | 278 | 279 | 280 | 285 | ( 290 | 291 | 299 | 300 | )} 301 | /> 302 | 303 | ( 308 | 309 | 317 | 318 | )} 319 | /> 320 | 321 | 322 | ( 327 | 328 | Estado 329 | 368 | 369 | )} 370 | /> 371 | 372 | ( 377 | <> 378 | 381 | } 382 | label="Email Pré-verificado" 383 | sx={{ marginBottom: 2 }} 384 | /> 385 | 386 | 387 | 388 | 389 | )} 390 | /> 391 | 392 | 393 | 396 | 399 | 400 | 401 | ) 402 | } 403 | -------------------------------------------------------------------------------- /src/pages/Users/components/Grid.tsx: -------------------------------------------------------------------------------- 1 | import DeleteIcon from "@mui/icons-material/Delete" 2 | import EditIcon from "@mui/icons-material/Edit" 3 | import WhatsAppIcon from "@mui/icons-material/WhatsApp" 4 | import { IconButton, Stack } from "@mui/material" 5 | import { 6 | GridColDef, 7 | GridRenderCellParams, 8 | GridValueGetterParams, 9 | } from "@mui/x-data-grid" 10 | import { useNavigate } from "react-router-dom" 11 | import { useLocalStorage } from "usehooks-ts" 12 | 13 | import DataTable from "../../../components/DataTable" 14 | 15 | import { User } from "../types/User" 16 | 17 | export default function Grid() { 18 | const [users, setUsers] = useLocalStorage("users", []) 19 | const navigate = useNavigate() 20 | 21 | const onCall = (params: GridRenderCellParams) => { 22 | if (!params.row.mobile) return 23 | 24 | window.location.href = `https://wa.me/55${params.row.mobile.replace( 25 | /[^\d]+/g, 26 | "" 27 | )}` 28 | } 29 | 30 | const onEdit = (params: GridRenderCellParams) => { 31 | if (!params.row.id) return 32 | navigate(`/users/${params.row.id}`) 33 | } 34 | 35 | const onDelete = (params: GridRenderCellParams) => { 36 | if (!params.row.id) return 37 | setUsers(users.filter((user) => user.id !== params.row.id)) 38 | } 39 | 40 | const columns: GridColDef[] = [ 41 | { field: "id", headerName: "ID", width: 70 }, 42 | { 43 | field: "firstName", 44 | headerName: "Nome", 45 | valueGetter: (params: GridValueGetterParams) => 46 | `${params.row.fullName.split(" ")?.shift() || ""}`, 47 | }, 48 | { 49 | field: "lastName", 50 | headerName: "Sobrenome", 51 | valueGetter: (params: GridValueGetterParams) => 52 | `${params.row.fullName.split(" ")?.pop() || ""}`, 53 | }, 54 | { field: "document", headerName: "CPF", width: 150 }, 55 | { 56 | field: "age", 57 | headerName: "Idade", 58 | type: "number", 59 | valueGetter: (params: GridValueGetterParams) => 60 | params.row.birthDate && 61 | `${ 62 | new Date().getFullYear() - 63 | new Date(params.row.birthDate).getFullYear() 64 | }`, 65 | }, 66 | { field: "email", headerName: "E-mail", minWidth: 200 }, 67 | { field: "mobile", headerName: "Celular", minWidth: 180 }, 68 | { 69 | field: "actions", 70 | headerName: "Ações", 71 | minWidth: 150, 72 | sortable: false, 73 | renderCell: (params) => ( 74 | 75 | onCall(params)} 79 | > 80 | 81 | 82 | 83 | onEdit(params)}> 84 | 85 | 86 | 87 | onDelete(params)} 91 | > 92 | 93 | 94 | 95 | ), 96 | }, 97 | ] 98 | 99 | return 100 | } 101 | -------------------------------------------------------------------------------- /src/pages/Users/schemas/UserSchema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from "yup" 2 | 3 | import { User } from "../types/User" 4 | 5 | export const UserSchema = yup 6 | .object({ 7 | fullName: yup.string().required("Este campo é obrigatório"), 8 | document: yup.string().required("Este campo é obrigatório"), 9 | birthDate: yup.date(), 10 | email: yup 11 | .string() 12 | .email("E-mail não reconhecido") 13 | .required("Este campo é obrigatório"), 14 | emailVerified: yup.boolean().default(false), 15 | mobile: yup.string().required("Este campo é obrigatório"), 16 | zipCode: yup 17 | .string() 18 | .required("Este campo é obrigatório") 19 | .transform((value) => value.replace(/[^\d]+/g, "")), 20 | addressName: yup.string().required("Este campo é obrigatório"), 21 | number: yup.string().required("Este campo é obrigatório"), 22 | complement: yup.string(), 23 | neighborhood: yup.string().required("Este campo é obrigatório"), 24 | city: yup.string().required("Este campo é obrigatório"), 25 | state: yup.string().required("Este campo é obrigatório"), 26 | }) 27 | .required() 28 | -------------------------------------------------------------------------------- /src/pages/Users/types/User.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string 3 | fullName: string 4 | document: string 5 | birthDate: Date 6 | email: string 7 | emailVerified: boolean 8 | mobile: string 9 | zipCode: string 10 | addressName: string 11 | number: string 12 | complement: string 13 | neighborhood: string 14 | city: string 15 | state: string 16 | } 17 | -------------------------------------------------------------------------------- /src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from "react-router-dom" 2 | 3 | import Dashboard from "./pages/Dashboard" 4 | import UserCreate from "./pages/Users/Create" 5 | import UserEdit from "./pages/Users/Edit" 6 | import UserList from "./pages/Users/List" 7 | 8 | // import NotFound from "./pages/NotFound" 9 | 10 | export function AppRoutes() { 11 | return ( 12 | 13 | } /> 14 | 15 | } /> 16 | } /> 17 | } /> 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | const api = axios.create() 4 | 5 | export async function findBrazilianZipCode(zipCode: string): Promise< 6 | | { 7 | zipCode: string 8 | addressName: string 9 | neighborhood: string 10 | city: string 11 | state: string 12 | } 13 | | undefined 14 | > { 15 | try { 16 | const { data } = await api.get( 17 | `https://viacep.com.br/ws/${zipCode.replace(/\D/g, "")}/json/` 18 | ) 19 | 20 | return { 21 | zipCode: data.cep, 22 | addressName: data.logradouro, 23 | neighborhood: data.bairro, 24 | city: data.localidade, 25 | state: data.uf, 26 | } 27 | } catch (error) { 28 | console.error(error) 29 | return 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { ptBR as MaterialLocale } from "@mui/material/locale" 2 | import { createTheme } from "@mui/material/styles" 3 | import { ptBR as DataGridLocale } from "@mui/x-data-grid" 4 | 5 | export const theme = createTheme( 6 | { 7 | palette: { 8 | mode: "dark", 9 | }, 10 | }, 11 | DataGridLocale, 12 | MaterialLocale 13 | ) 14 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------