├── .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 | 
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 | 
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 | }
451 | >
452 | Novo Usuário
453 |
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 | 
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 | 
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 | }
26 | >
27 | Novo Usuário
28 |
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 |
--------------------------------------------------------------------------------