├── .vscode
└── settings.json
├── src
├── react-app-env.d.ts
├── services
│ └── api.ts
├── App.tsx
├── index.tsx
├── setupTests.ts
├── routes.tsx
├── App.css
├── components
│ └── Dropzone
│ │ ├── styles.css
│ │ └── index.tsx
├── pages
│ ├── Home
│ │ ├── index.tsx
│ │ └── styles.css
│ └── CreateLocation
│ │ ├── styles.css
│ │ └── index.tsx
└── assets
│ └── logo.svg
├── .gitignore
├── tsconfig.json
├── public
└── index.html
├── package.json
└── README.md
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "git.ignoreLimitWarning": true
3 | }
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/services/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const api = axios.create({
4 | baseURL: 'http://localhost:3333'
5 | });
6 |
7 | export default api;
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './App.css';
3 |
4 | import Routes from './routes';
5 |
6 | function App() {
7 | return (
8 | <>
9 |
10 | >
11 | );
12 | }
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | ReactDOM.render(
6 |
7 |
8 | ,
9 | document.getElementById('root')
10 | );
11 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, BrowserRouter } from 'react-router-dom';
3 |
4 | import Home from './pages/Home';
5 | import CreateLocation from './pages/CreateLocation';
6 |
7 | const Routes = () => {
8 | return (
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | export default Routes;
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary-color: #79ab7f;
3 | --title-color: #79ab7f;
4 | --text-color: #034AAD;
5 | }
6 |
7 | * {
8 | margin: 0;
9 | padding: 0;
10 | box-sizing: border-box;
11 | }
12 |
13 | body {
14 | background: #fff5e7;
15 | -webkit-font-smoothing: antialiased;
16 | color: var(--text-color);
17 | }
18 |
19 | body, input, button {
20 | font-family: Roboto, Arial, Helvetica, sans-serif;
21 | }
22 |
23 | h1, h2, h3, h4, h5, h6 {
24 | color: var(--title-color);
25 | font-family: Ubuntu;
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Dropzone/styles.css:
--------------------------------------------------------------------------------
1 | .dropzone {
2 | height: 300px;
3 | background: #e1faec;
4 | border-radius: 10px;
5 |
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | margin-top: 48px;
10 | outline: 0;
11 | }
12 |
13 | .dropzone img {
14 | width: 100%;
15 | height: 100%;
16 | object-fit: cover;
17 | }
18 |
19 | .dropzone p {
20 | width: calc(100% - 60px);
21 | height: calc(100% - 60px);
22 | border-radius: 10px;
23 | border: 1px dashed #79ab7f;
24 |
25 | display: flex;
26 | flex-direction: column;
27 | justify-content: center;
28 | align-items: center;
29 | color: #333;
30 | }
31 |
32 | .dropzone p svg {
33 | color: #79ab7f;
34 | width: 24px;
35 | height: 24px;
36 | margin-bottom: 8px;
37 | }
38 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 | Coleta Seletiva
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/pages/Home/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FiLogIn } from 'react-icons/fi';
3 | import { Link } from 'react-router-dom';
4 |
5 | import './styles.css';
6 | import logo from '../../assets/logo.svg';
7 |
8 | const Home: React.FC = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Coleta Seletiva e reciclagem em geral.
18 | Reciclagem de materiais diversos, tais como, papel, plástico, metal, pilhas e baterias, etc.
19 |
20 |
21 |
22 |
23 |
24 | Cadastrar novo local de coleta
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | export default Home;
--------------------------------------------------------------------------------
/src/components/Dropzone/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback, useState} from 'react';
2 | import { FiUpload } from 'react-icons/fi';
3 | import {useDropzone} from 'react-dropzone';
4 | import './styles.css';
5 |
6 | interface Props {
7 | onFileUploaded: (file: File) => void;
8 | }
9 |
10 | const Dropzone: React.FC = ({ onFileUploaded }) => {
11 | const [selectedFileUrl, setSelectedFileUrl] = useState('');
12 |
13 | const onDrop = useCallback(acceptedFiles => {
14 | const file = acceptedFiles[0];
15 |
16 | const fileUrl = URL.createObjectURL(file);
17 |
18 | setSelectedFileUrl(fileUrl);
19 |
20 | onFileUploaded(file);
21 | }, [onFileUploaded]);
22 |
23 | const {getRootProps, getInputProps} = useDropzone({
24 | onDrop,
25 | accept: 'image/*'
26 | })
27 |
28 | return (
29 | {}}>
30 |
31 | { selectedFileUrl
32 | ?

33 | : (
34 |
35 |
36 | Imagem do Estabelecimento
37 |
38 | )
39 | }
40 |
41 | )
42 | }
43 |
44 | export default Dropzone;
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-curso-reactjs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "@types/jest": "^26.0.15",
10 | "@types/node": "^12.0.0",
11 | "@types/react-dom": "^16.9.8",
12 | "axios": "^0.21.0",
13 | "leaflet": "^1.7.1",
14 | "react": "^17.0.1",
15 | "react-dom": "^17.0.1",
16 | "react-dropzone": "^11.0.1",
17 | "react-icons": "^3.11.0",
18 | "react-leaflet": "^2.8.0",
19 | "react-router-dom": "^5.2.0",
20 | "react-scripts": "4.0.0",
21 | "typescript": "^4.0.3",
22 | "web-vitals": "^0.2.4"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test",
28 | "eject": "react-scripts eject"
29 | },
30 | "eslintConfig": {
31 | "extends": [
32 | "react-app",
33 | "react-app/jest"
34 | ]
35 | },
36 | "browserslist": {
37 | "production": [
38 | ">0.2%",
39 | "not dead",
40 | "not op_mini all"
41 | ],
42 | "development": [
43 | "last 1 chrome version",
44 | "last 1 firefox version",
45 | "last 1 safari version"
46 | ]
47 | },
48 | "devDependencies": {
49 | "@types/react": "^16.9.56",
50 | "@types/react-leaflet": "^2.5.2",
51 | "@types/react-router-dom": "^5.1.6"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/pages/Home/styles.css:
--------------------------------------------------------------------------------
1 | #page-home {
2 | height: 100vh;
3 |
4 | background: url('../../assets/background.svg') no-repeat;
5 | }
6 |
7 | #page-home .content {
8 | width: 100%;
9 | height: 100%;
10 | max-width: 1100px;
11 | margin: 0 auto;
12 | padding: 0 30px;
13 |
14 | display: flex;
15 | flex-direction: column;
16 | align-items: flex-start;
17 | }
18 |
19 | #page-home .content header {
20 | margin: 48px 0 0;
21 | }
22 |
23 | #page-home .content main {
24 | flex: 1;
25 | max-width: 660px;
26 |
27 | display: flex;
28 | flex-direction: column;
29 | justify-content: center;
30 | }
31 |
32 | #page-home .content main h1 {
33 | font-size: 64px;
34 | color: #333;
35 | }
36 |
37 | #page-home .content main p {
38 | font-size: 24px;
39 | margin-top: 24px;
40 | line-height: 38px;
41 | }
42 |
43 | #page-home .content main a {
44 | width: 100%;
45 | max-width: 360px;
46 | height: 72px;
47 | background: #79ab7f;
48 | border-radius: 8px;
49 | text-decoration: none;
50 |
51 | display: flex;
52 | align-items: center;
53 | overflow: hidden;
54 |
55 | margin-top: 40px;
56 | }
57 |
58 | #page-home .content main a span {
59 | display: block;
60 | background: rgba(0, 0, 0, 0.08);
61 | width: 72px;
62 | height: 72px;
63 |
64 | display: flex;
65 | align-items: center;
66 | justify-content: center;
67 | transition: background-color 0.2s;
68 | }
69 |
70 | #page-home .content main a span svg {
71 | color: #FFF;
72 | width: 20px;
73 | height: 20px;
74 | }
75 |
76 | #page-home .content main a strong {
77 | flex: 1;
78 | text-align: center;
79 | color: #FFF;
80 | }
81 |
82 | #page-home .content main a:hover {
83 | background: #79ab7f;
84 | }
85 |
86 | @media(max-width: 900px) {
87 | #page-home .content {
88 | align-items: center;
89 | text-align: center;
90 | }
91 |
92 | #page-home .content header {
93 | margin: 48px auto 0;
94 | }
95 |
96 | #page-home .content main {
97 | align-items: center;
98 | }
99 |
100 | #page-home .content main h1 {
101 | font-size: 42px;
102 | }
103 |
104 | #page-home .content main p {
105 | font-size: 24px;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Informação sobre tecnologia, dicas, tutoriais, mini-cursos e muito mais.
8 |
9 |
10 | ## Mini Curso Gratuito - Criação de SPA com ReactJS e Typescript
11 |
12 | Seja bem-vindo a este mini curso onde iremos conhecer o ReactJS e entender o processo de criação de SPA's - Single Page Application.
13 |
14 | Mini curso disponível no YouTube. Segue abaixo o link da playlist.
15 |
16 | [Playlist no YouTube deste Mini Curso de ReactJS e Typescript](https://www.youtube.com/watch?v=KhuHgLdwIsg&list=PLE0DHiXlN_qpm0nMlvcVxG_O580IXmeRU)
17 |
18 | ## Objetivo
19 |
20 | O objetivo do mini curso é dar o ponta pé inicial nos conceitos e recursos do ReactJS, focado no aprendizado daqueles que ainda não conhecem essa biblioteca ou framework, iniciantes em desenvolvimento de aplicações web com foco no front-end.
21 |
22 | A idéia é implementar uma aplicação front-end com ReactJS e Typescript para consumir a API que foi desenvolvida durante o mini curso gratuito de API Node.js com Typescript, também disponível no meu canal do Youtube.
23 |
24 | Mesmo que o seu foco seja somente front-end, recomendo que acompanhe também o mini curso de back-end, pois vários conceitos que estão naquele curso não serão repetidos aqui, como por exemplo, Node.js e NPM, API Restful, Requisições HTTP, Typescript, além de outros.
25 |
26 | Playlist Mini Curso API Restful com Node.js e Typescript: https://www.youtube.com/watch?v=M-pNDHC25Vg&list=PLE0DHiXlN_qp251xWxdb_stPj98y1auhc
27 |
28 | Repositório no Github da API: https://github.com/aluiziodeveloper/mini-curso-gratuito-node-typescript
29 |
30 | ## Rodando a aplicação no seu PC
31 |
32 | Faça um clone deste repositório e instale no seu ambiente de desenvolvimento usando o seguinte comando no seu terminal (escolha um diretório apropriado):
33 |
34 | ```
35 | git clone https://github.com/aluiziodeveloper/mini-curso-reactjs.git
36 | ```
37 |
38 | Após clonar o conteúdo do repositório, acesse o diretório criado e efetue a instalação das dependências:
39 |
40 | ```
41 | cd mini-curso-reactjs
42 |
43 | npm install
44 | ```
45 |
46 | Após essa instalação execute a aplicação com o comando `npm start`. Acesse a url `http://localhost:3000` no browser.
47 |
48 | ## Redes Sociais
49 |
50 | [Site Aluizio Developer](https://aluiziodeveloper.com.br)
51 |
52 | [YouTube](https://www.youtube.com/jorgealuizio)
53 |
54 | [Servidor no Discord](https://discord.gg/3J87BMz5fD)
55 |
56 | [LinkedIn](https://www.linkedin.com/in/jorgealuizio/)
57 |
--------------------------------------------------------------------------------
/src/pages/CreateLocation/styles.css:
--------------------------------------------------------------------------------
1 | #page-create-location {
2 | width: 100%;
3 | max-width: 1100px;
4 |
5 | margin: 0 auto;
6 | }
7 |
8 | #page-create-location header {
9 | margin-top: 48px;
10 |
11 | display: flex;
12 | justify-content: space-between;
13 | align-items: center;
14 | }
15 |
16 | #page-create-location header a {
17 | color: var(--title-color);
18 | font-weight: bold;
19 | text-decoration: none;
20 |
21 | display: flex;
22 | align-items: center;
23 | }
24 |
25 | #page-create-location header a svg {
26 | margin-right: 16px;
27 | color: var(--primary-color);
28 | }
29 |
30 | #page-create-location form {
31 | margin: 80px auto;
32 | padding: 64px;
33 | max-width: 730px;
34 | background: #FFF;
35 | border-radius: 8px;
36 |
37 | display: flex;
38 | flex-direction: column;
39 | }
40 |
41 | #page-create-location form h1 {
42 | font-size: 36px;
43 | }
44 |
45 | #page-create-location form fieldset {
46 | margin-top: 64px;
47 | min-inline-size: auto;
48 | border: 0;
49 | }
50 |
51 | #page-create-location form legend {
52 | width: 100%;
53 | display: flex;
54 | justify-content: space-between;
55 | align-items: center;
56 | margin-bottom: 40px;
57 | }
58 |
59 | #page-create-location form legend h2 {
60 | font-size: 24px;
61 | }
62 |
63 | #page-create-location form legend span {
64 | font-size: 14px;
65 | font-weight: normal;
66 | color: #034AAD;
67 | }
68 |
69 | #page-create-location form .field-group {
70 | flex: 1;
71 | display: flex;
72 | }
73 |
74 | #page-create-location form .field {
75 | flex: 1;
76 |
77 | display: flex;
78 | flex-direction: column;
79 | margin-bottom: 24px;
80 | }
81 |
82 | #page-create-location form .field input[type=text],
83 | #page-create-location form .field input[type=email],
84 | #page-create-location form .field input[type=number] {
85 | flex: 1;
86 | background: #fff5e7;
87 | border-radius: 8px;
88 | border: 0;
89 | padding: 16px 24px;
90 | font-size: 16px;
91 | color: #333;
92 | }
93 |
94 | #page-create-location form .field select {
95 | -webkit-appearance: none;
96 | -moz-appearance: none;
97 | appearance: none;
98 | flex: 1;
99 | background: #fff5e7;
100 | border-radius: 8px;
101 | border: 0;
102 | padding: 16px 24px;
103 | font-size: 16px;
104 | color: #333;
105 | }
106 |
107 | #page-create-location form .field input::placeholder {
108 | color: #A0A0B2;
109 | }
110 |
111 | #page-create-location form .field label {
112 | font-size: 14px;
113 | margin-bottom: 8px;
114 | }
115 |
116 | #page-create-location form .field :disabled {
117 | cursor: not-allowed;
118 | }
119 |
120 | #page-create-location form .field-group .field + .field {
121 | margin-left: 24px;
122 | }
123 |
124 | #page-create-location form .field-group input + input {
125 | margin-left: 24px;
126 | }
127 |
128 | #page-create-location form .field-check {
129 | flex-direction: row;
130 | align-items: center;
131 | }
132 |
133 | #page-create-location form .field-check input[type=checkbox] {
134 | background: #fff5e7;
135 | }
136 |
137 | #page-create-location form .field-check label {
138 | margin: 0 0 0 8px;
139 | }
140 |
141 | #page-create-location form .leaflet-container {
142 | width: 100%;
143 | height: 350px;
144 | border-radius: 8px;
145 | margin-bottom: 24px;
146 | }
147 |
148 | #page-create-location form button {
149 | width: 260px;
150 | height: 56px;
151 | background: var(--primary-color);
152 | border-radius: 8px;
153 | color: #FFF;
154 | font-weight: bold;
155 | font-size: 16px;
156 | border: 0;
157 | align-self: flex-end;
158 | margin-top: 40px;
159 | transition: background-color 0.2s;
160 | cursor: locationer;
161 | }
162 |
163 | #page-create-location form button:hover {
164 | background: #79ab7f;
165 | }
166 |
167 | .items-grid {
168 | display: grid;
169 | grid-template-columns: repeat(3, 1fr);
170 | gap: 16px;
171 | list-style: none;
172 | }
173 |
174 | .items-grid li {
175 | background: #fff5e7;
176 | border: 2px solid #fff5e7;
177 | height: 180px;
178 | border-radius: 8px;
179 | padding: 32px 24px 16px;
180 |
181 | display: flex;
182 | flex-direction: column;
183 | justify-content: space-between;
184 | align-items: center;
185 |
186 | text-align: center;
187 |
188 | cursor: locationer;
189 | }
190 |
191 | .items-grid li span {
192 | flex: 1;
193 | margin-top: 12px;
194 |
195 | display: flex;
196 | align-items: center;
197 | color: var(--title-color)
198 | }
199 |
200 | .items-grid li.selected {
201 | background: #F6DF8C;
202 | border: 2px solid #79ab7f;
203 | }
204 |
--------------------------------------------------------------------------------
/src/pages/CreateLocation/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent, FormEvent, useCallback, useEffect, useState } from 'react';
2 | import { Link, useHistory } from 'react-router-dom';
3 | import {FiArrowLeft} from 'react-icons/fi';
4 | import { Map, TileLayer, Marker} from 'react-leaflet';
5 | import { LeafletMouseEvent } from 'leaflet';
6 | import api from '../../services/api';
7 | import Dropzone from '../../components/Dropzone';
8 | import logo from '../../assets/logo.svg';
9 | import './styles.css';
10 |
11 | interface Item {
12 | id: number;
13 | title: string;
14 | image_url: string;
15 | }
16 |
17 | const CreateLocation: React.FC = () => {
18 | const [items, setItems] = useState- ([]);
19 |
20 | const [selectedMapPosition, setSelectedMapPosition] = useState<[number, number]>([0, 0]);
21 |
22 | const [formData, setFormData] = useState({
23 | name: '',
24 | email: '',
25 | whatsapp: '',
26 | city: '',
27 | uf: '',
28 | });
29 |
30 | const [selectedItems, setSelectedItems] = useState([]);
31 |
32 | const [selectedFile, setSelectedFile] = useState();
33 |
34 | const history = useHistory();
35 |
36 | useEffect(() => {
37 | api.get('items').then(response => {
38 | setItems(response.data);
39 | });
40 | }, []);
41 |
42 | const handleMapClick = useCallback((event: LeafletMouseEvent): void => {
43 | //console.log(event);
44 | setSelectedMapPosition([
45 | event.latlng.lat,
46 | event.latlng.lng,
47 | ]);
48 | }, []);
49 |
50 | const handleInputChange = useCallback((event: ChangeEvent) => {
51 | //console.log(event.target.name, event.target.value);
52 | const { name, value } = event.target;
53 | setFormData({...formData, [name]: value });
54 | }, [formData]);
55 |
56 | const handleSelectItem = useCallback((id: number) => {
57 | //setSelectedItems([...selectedItems, id]);
58 | const alreadySelected = selectedItems.findIndex(item => item === id);
59 |
60 | if (alreadySelected >= 0) {
61 | const filteredItems = selectedItems.filter(item => item !== id);
62 | setSelectedItems(filteredItems);
63 | } else {
64 | setSelectedItems([...selectedItems, id]);
65 | }
66 | }, [selectedItems]);
67 |
68 | const handleSubmit = useCallback(async (event: FormEvent) => {
69 | event.preventDefault();
70 |
71 | const {city, email, name, uf, whatsapp} = formData;
72 | const [latitude, longitude] = selectedMapPosition;
73 | const items = selectedItems;
74 |
75 | const data = new FormData();
76 |
77 | data.append('name', name);
78 | data.append('email', email);
79 | data.append('whatsapp', whatsapp);
80 | data.append('uf', uf);
81 | data.append('city', city);
82 | data.append('latitude', String(latitude));
83 | data.append('longitude', String(longitude));
84 | data.append('items', items.join(','));
85 | if (selectedFile) {
86 | data.append('image', selectedFile);
87 | }
88 |
89 | await api.post('locations', data);
90 |
91 | alert('Estabelecimento cadastrado com sucesso!');
92 |
93 | history.push('/');
94 | }, [formData, selectedItems, selectedMapPosition, history, selectedFile]);
95 |
96 | return (
97 |
98 |
99 |
100 |
101 |
102 |
103 | Voltar para home
104 |
105 |
106 |
107 |
204 |
205 |
206 | );
207 | }
208 |
209 | export default CreateLocation;
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
71 |
--------------------------------------------------------------------------------