├── .github
├── .gitkeep
├── exemplo.webm
└── poster.png
├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── manifest.json
└── robots.txt
├── src
├── App.test.tsx
├── App.tsx
├── components
│ ├── FileList
│ │ ├── index.tsx
│ │ └── styles.ts
│ └── Upload
│ │ ├── index.tsx
│ │ └── styles.ts
├── context
│ └── files.tsx
├── index.tsx
├── react-app-env.d.ts
├── services
│ └── api.ts
├── setupTests.ts
├── styles.ts
└── styles
│ └── global.ts
├── tsconfig.json
└── yarn.lock
/.github/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-content/blog-upload-reactjs-frontend/ba36ae37081545b1968ce6d1a1d219078508a846/.github/.gitkeep
--------------------------------------------------------------------------------
/.github/exemplo.webm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-content/blog-upload-reactjs-frontend/ba36ae37081545b1968ce6d1a1d219078508a846/.github/exemplo.webm
--------------------------------------------------------------------------------
/.github/poster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-content/blog-upload-reactjs-frontend/ba36ae37081545b1968ce6d1a1d219078508a846/.github/poster.png
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Upload de arquivos
2 |
3 | > Front End com React Hooks e Context API
4 |
5 | ### Como executar
6 |
7 | Primeiro execute o Back End:
8 |
9 | - [Leia esse post](https://blog.rocketseat.com.br/upload-de-imagens-no-s3-da-aws-com-node-js/)
10 |
11 | - Execute o Back End [Link do Projeto](https://github.com/rocketseat-content/youtube-upload-nodejs-reactjs-backend)
12 |
13 | > Depois de subir o servidor na sua máquina, bora para o Front End:
14 |
15 | - Baixe o projeto
16 |
17 | ```
18 | git clone git@github.com:rocketseat-content/blog-upload-reactjs-frontend.git
19 | ```
20 |
21 | - Execute o Front End:
22 |
23 | ```
24 | yarn install
25 | ```
26 |
27 | ```
28 | yarn start
29 | ```
30 |
31 | Só enviar os arquivos para o servidor! ;)
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "upload-frontend-react-hooks",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "@types/jest": "^24.0.0",
10 | "@types/lodash": "^4.14.161",
11 | "@types/node": "^12.0.0",
12 | "@types/react": "^16.9.0",
13 | "@types/react-dom": "^16.9.0",
14 | "@types/react-dropzone": "^5.1.0",
15 | "@types/styled-components": "^5.1.3",
16 | "@types/uuid": "^8.3.0",
17 | "axios": "^0.20.0",
18 | "filesize": "^6.1.0",
19 | "react": "^16.13.1",
20 | "react-circular-progressbar": "^2.0.3",
21 | "react-dom": "^16.13.1",
22 | "react-dropzone": "^11.1.0",
23 | "react-icons": "^3.11.0",
24 | "react-scripts": "3.4.3",
25 | "styled-components": "^5.2.0",
26 | "typescript": "~3.7.2",
27 | "uuid": "^8.3.0"
28 | },
29 | "scripts": {
30 | "start": "react-scripts start",
31 | "build": "react-scripts build",
32 | "test": "react-scripts test",
33 | "eject": "react-scripts eject"
34 | },
35 | "eslintConfig": {
36 | "extends": "react-app"
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-content/blog-upload-reactjs-frontend/ba36ae37081545b1968ce6d1a1d219078508a846/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Upload de Arquivos
10 |
11 |
12 |
13 |
14 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import GlobalStyle from "./styles/global";
4 | import { Container, Content } from "./styles";
5 |
6 | import Upload from "./components/Upload";
7 | import FileList from "./components/FileList";
8 |
9 | import { FileProvider } from "./context/files";
10 |
11 | const App: React.FC = () => (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
23 | export default App;
24 |
--------------------------------------------------------------------------------
/src/components/FileList/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { CircularProgressbar } from "react-circular-progressbar";
3 | import { MdCheckCircle, MdError, MdLink, MdMoodBad } from "react-icons/md";
4 | import { useFiles } from "../../context/files";
5 | import { IFile } from "../../context/files";
6 |
7 | import { Container, FileInfo, Preview } from "./styles";
8 |
9 | const FileList = () => {
10 | const { uploadedFiles: files, deleteFile } = useFiles();
11 |
12 | if (!files.length)
13 | return (
14 |
15 |
20 |
21 | );
22 |
23 | return (
24 |
25 | {files.map((uploadedFile: IFile) => (
26 |
27 |
28 |
29 |
30 | {uploadedFile.name}
31 |
32 | {uploadedFile.readableSize}{" "}
33 | {!!uploadedFile.url && (
34 |
37 | )}
38 |
39 |
40 |
41 |
42 |
43 | {!uploadedFile.uploaded && !uploadedFile.error && (
44 |
53 | )}
54 |
55 | {uploadedFile.url && (
56 |
61 |
62 |
63 | )}
64 |
65 | {uploadedFile.uploaded && (
66 |
67 | )}
68 | {uploadedFile.error &&
}
69 |
70 |
71 | ))}
72 |
73 | );
74 | };
75 |
76 | export default FileList;
77 |
--------------------------------------------------------------------------------
/src/components/FileList/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Container = styled.ul`
4 | margin-top: 20px;
5 |
6 | li {
7 | display: flex;
8 | justify-content: space-between;
9 | align-items: center;
10 | color: #444;
11 |
12 | & + li {
13 | margin-top: 15px;
14 | }
15 | }
16 | `;
17 |
18 | export const FileInfo = styled.div`
19 | display: flex;
20 | align-items: center;
21 |
22 | div {
23 | display: flex;
24 | flex-direction: column;
25 |
26 | span {
27 | font-size: 12px;
28 | color: #999;
29 | margin-top: 5px;
30 |
31 | button {
32 | border: 0;
33 | background: transparent;
34 | color: #e57878;
35 | margin-left: 5px;
36 | cursor: pointer;
37 | }
38 | }
39 | }
40 | `;
41 |
42 | interface PreviewProps {
43 | src?: string;
44 | }
45 |
46 | export const Preview = styled.div`
47 | width: 36px;
48 | height: 36px;
49 | border-radius: 5px;
50 | background-image: url(${(props) => props.src});
51 | background-repeat: no-repeat;
52 | background-size: cover;
53 | background-position: 50% 50%;
54 | margin-right: 10px;
55 | `;
56 |
--------------------------------------------------------------------------------
/src/components/Upload/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import { useDropzone } from "react-dropzone";
3 | import { useFiles } from "../../context/files";
4 |
5 | import { DropContainer, UploadMessage } from "./styles";
6 |
7 | function Upload() {
8 | const { handleUpload } = useFiles();
9 |
10 | const onDrop = useCallback(
11 | (files) => {
12 | handleUpload(files);
13 | },
14 | [handleUpload]
15 | );
16 |
17 | const {
18 | getRootProps,
19 | getInputProps,
20 | isDragActive,
21 | isDragReject,
22 | } = useDropzone({
23 | accept: ["image/jpeg", "image/pjpeg", "image/png", "image/gif"],
24 | onDrop,
25 | });
26 |
27 | const renderDragMessage = useCallback(() => {
28 | if (!isDragActive) {
29 | return Arraste imagens aqui...;
30 | }
31 |
32 | if (isDragReject) {
33 | return (
34 |
35 | Tipo de arquivo não suportado
36 |
37 | );
38 | }
39 |
40 | return Solte as imagens aqui;
41 | }, [isDragActive, isDragReject]);
42 |
43 | return (
44 |
45 |
46 | {renderDragMessage()}
47 |
48 | );
49 | }
50 |
51 | export default Upload;
52 |
--------------------------------------------------------------------------------
/src/components/Upload/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components";
2 |
3 | const dragActive = css`
4 | border-color: #78e5d5;
5 | `;
6 |
7 | const dragReject = css`
8 | border-color: #e57878;
9 | `;
10 |
11 | type IDropContainer = {
12 | isDragActive?: boolean;
13 | isDragReject?: boolean;
14 | };
15 |
16 | export const DropContainer = styled.div`
17 | border: 1px dashed #ddd;
18 | border-radius: 4px;
19 | cursor: pointer;
20 |
21 | transition: height 0.2s ease;
22 |
23 | ${(props: any) => props.isDragActive && dragActive};
24 | ${(props: any) => props.isDragReject && dragReject};
25 | `;
26 |
27 | const messageColors = {
28 | default: "#999",
29 | error: "#e57878",
30 | success: "#78e5d5",
31 | };
32 |
33 | interface ITypeMessageColor {
34 | type?: "default" | "error" | "success";
35 | }
36 |
37 | export const UploadMessage = styled.p`
38 | display: flex;
39 | color: ${(props) => messageColors[props.type || "default"]};
40 | justify-content: center;
41 | align-items: center;
42 | padding: 15px 0;
43 | `;
44 |
--------------------------------------------------------------------------------
/src/context/files.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useState,
4 | useEffect,
5 | useCallback,
6 | useContext,
7 | } from "react";
8 | import { v4 as uuidv4 } from "uuid";
9 | import filesize from "filesize";
10 |
11 | import api from "../services/api";
12 |
13 | export interface IPost {
14 | _id: string;
15 | name: string;
16 | size: number;
17 | key: string;
18 | url: string;
19 | }
20 |
21 | export interface IFile {
22 | id: string;
23 | name: string;
24 | readableSize: string;
25 | uploaded?: boolean;
26 | preview: string;
27 | file: File | null;
28 | progress?: number;
29 | error?: boolean;
30 | url: string;
31 | }
32 |
33 | interface IFileContextData {
34 | uploadedFiles: IFile[];
35 | deleteFile(id: string): void;
36 | handleUpload(file: any): void;
37 | }
38 |
39 | const FileContext = createContext({} as IFileContextData);
40 |
41 | const FileProvider: React.FC = ({ children }) => {
42 | const [uploadedFiles, setUploadedFiles] = useState([]);
43 |
44 | useEffect(() => {
45 | api.get("posts").then((response) => {
46 | const postFormatted: IFile[] = response.data.map((post) => {
47 | return {
48 | ...post,
49 | id: post._id,
50 | preview: post.url,
51 | readableSize: filesize(post.size),
52 | file: null,
53 | error: false,
54 | uploaded: true,
55 | };
56 | });
57 |
58 | setUploadedFiles(postFormatted);
59 | });
60 | }, []);
61 |
62 | useEffect(() => {
63 | return () => {
64 | uploadedFiles.forEach((file) => URL.revokeObjectURL(file.preview));
65 | };
66 | });
67 |
68 | const updateFile = useCallback((id, data) => {
69 | setUploadedFiles((state) =>
70 | state.map((file) => (file.id === id ? { ...file, ...data } : file))
71 | );
72 | }, []);
73 |
74 | const processUpload = useCallback(
75 | (uploadedFile: IFile) => {
76 | const data = new FormData();
77 | if (uploadedFile.file) {
78 | data.append("file", uploadedFile.file, uploadedFile.name);
79 | }
80 |
81 | api
82 | .post("posts", data, {
83 | onUploadProgress: (progressEvent) => {
84 | let progress: number = Math.round(
85 | (progressEvent.loaded * 100) / progressEvent.total
86 | );
87 |
88 | console.log(
89 | `A imagem ${uploadedFile.name} está ${progress}% carregada... `
90 | );
91 |
92 | updateFile(uploadedFile.id, { progress });
93 | },
94 | })
95 | .then((response) => {
96 | console.log(
97 | `A imagem ${uploadedFile.name} já foi enviada para o servidor!`
98 | );
99 |
100 | updateFile(uploadedFile.id, {
101 | uploaded: true,
102 | id: response.data._id,
103 | url: response.data.url,
104 | });
105 | })
106 | .catch((err) => {
107 | console.error(
108 | `Houve um problema para fazer upload da imagem ${uploadedFile.name} no servidor AWS`
109 | );
110 | console.log(err);
111 |
112 | updateFile(uploadedFile.id, {
113 | error: true,
114 | });
115 | });
116 | },
117 | [updateFile]
118 | );
119 |
120 | const handleUpload = useCallback(
121 | (files: File[]) => {
122 | const newUploadedFiles: IFile[] = files.map((file: File) => ({
123 | file,
124 | id: uuidv4(),
125 | name: file.name,
126 | readableSize: filesize(file.size),
127 | preview: URL.createObjectURL(file),
128 | progress: 0,
129 | uploaded: false,
130 | error: false,
131 | url: "",
132 | }));
133 |
134 | // concat é mais performático que ...spread
135 | // https://www.malgol.com/how-to-merge-two-arrays-in-javascript/
136 | setUploadedFiles((state) => state.concat(newUploadedFiles));
137 | newUploadedFiles.forEach(processUpload);
138 | },
139 | [processUpload]
140 | );
141 |
142 | const deleteFile = useCallback((id: string) => {
143 | api.delete(`posts/${id}`);
144 | setUploadedFiles((state) => state.filter((file) => file.id !== id));
145 | }, []);
146 |
147 | return (
148 |
149 | {children}
150 |
151 | );
152 | };
153 |
154 | function useFiles(): IFileContextData {
155 | const context = useContext(FileContext);
156 |
157 | if (!context) {
158 | throw new Error("useFiles must be used within FileProvider");
159 | }
160 |
161 | return context;
162 | }
163 |
164 | export { FileProvider, useFiles };
165 |
--------------------------------------------------------------------------------
/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/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:3000",
5 | });
6 |
7 | export default api;
8 |
--------------------------------------------------------------------------------
/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/extend-expect';
6 |
--------------------------------------------------------------------------------
/src/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Container = styled.div`
4 | height: 100%;
5 | display: flex;
6 | justify-content: center;
7 | align-items: center;
8 | `;
9 |
10 | export const Content = styled.div`
11 | width: 100%;
12 | max-width: 400px;
13 | margin: 30px;
14 | background: #fff;
15 | border-radius: 4px;
16 | padding: 20px;
17 | `;
18 |
--------------------------------------------------------------------------------
/src/styles/global.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components";
2 |
3 | import "react-circular-progressbar/dist/styles.css";
4 |
5 | export default createGlobalStyle`
6 | * {
7 | margin: 0;
8 | padding: 0;
9 | outline: 0;
10 | box-sizing: border-box;
11 | }
12 |
13 | body {
14 | font-family: Arial, Helvetica, sans-serif;
15 | font-size: 14px;
16 | background: #7159c1;
17 | text-rendering: optimizeLegibility;
18 | -webkit-font-smoothing: antialiased;
19 | }
20 |
21 | html, body, #root {
22 | height: 100%;
23 | }
24 | `;
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react"
17 | },
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------