├── .prettierrc ├── .vscode └── settings.json ├── src ├── assets │ ├── demo.gif │ ├── app1-banner.png │ ├── photo.svg │ └── folder-success.json ├── services │ └── api.js ├── index.js ├── components │ ├── Animation │ │ └── index.js │ ├── FileList │ │ ├── styles.js │ │ └── index.js │ └── Upload │ │ ├── index.js │ │ └── styles.js ├── styles │ ├── themes │ │ └── light.js │ └── global.js ├── styles.js ├── App.js └── providers │ └── UploadProvider.js ├── .editorconfig ├── debug.log ├── .gitignore ├── .eslintrc.js ├── public └── index.html ├── package.json └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Dropzone", 4 | "filesize" 5 | ] 6 | } -------------------------------------------------------------------------------- /src/assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juan-patrick/reactjs-upload-image/HEAD/src/assets/demo.gif -------------------------------------------------------------------------------- /src/assets/app1-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juan-patrick/reactjs-upload-image/HEAD/src/assets/app1-banner.png -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ baseURL: 'http://localhost:3333' }); 4 | 5 | export default api; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /debug.log: -------------------------------------------------------------------------------- 1 | [1110/082235.769:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 2 | [1110/121756.439:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import UploadProvider from './providers/UploadProvider'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /.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/components/Animation/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Lottie from 'react-lottie'; 4 | 5 | function Animation({ loop, autoplay, size, data }) { 6 | const animationSetting = { 7 | loop: !!loop, 8 | autoplay: !!autoplay, 9 | animationData: data.default, 10 | rendererSettings: { 11 | preserveAspectRatio: 'xMidYMid slice', 12 | }, 13 | }; 14 | 15 | return ; 16 | } 17 | 18 | export default Animation; 19 | -------------------------------------------------------------------------------- /src/styles/themes/light.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'light', 3 | 4 | colors: { 5 | primary: '#7d2ae8', 6 | primaryTransparent: 'rgba(125, 42, 232, 0.75)', 7 | backgroundPrimaryTransparent: 'rgba(125, 42, 232, 0.05)', 8 | background: '#fff', 9 | backgroundSecondary: '#ddd', 10 | success: '#4caf50', 11 | successTransparent: 'rgba(76, 175, 79, 0.05)', 12 | error: '#f44336', 13 | errorTransparent: 'rgba(244, 67, 54, 0.05)', 14 | text: '#222', 15 | subtext: 'rgba(0,0,0, 0.45)', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/styles.js: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import styled from 'styled-components'; 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | height: 100%; 9 | `; 10 | 11 | export const Content = styled(motion.main)` 12 | display: flex; 13 | flex-direction: column; 14 | 15 | gap: 20px; 16 | width: 100%; 17 | 18 | max-width: 500px; 19 | 20 | height: auto; 21 | max-height: 600px; 22 | 23 | transition: height 2 ease; 24 | 25 | margin: 30px; 26 | padding: 20px; 27 | 28 | border-radius: 4px; 29 | background: ${(props) => props.theme.colors.background}; 30 | `; 31 | 32 | export const Header = styled.header` 33 | text-align: center; 34 | 35 | & p { 36 | margin-top: 3px; 37 | font-size: 0.9em; 38 | color: ${(props) => props.theme.colors.subtext}; 39 | font-weight: 600; 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: [ 7 | 'plugin:react/recommended', 8 | 'airbnb', 9 | 'prettier', 10 | 'prettier/react', 11 | ], 12 | globals: { 13 | Atomics: 'readonly', 14 | SharedArrayBuffer: 'readonly', 15 | }, 16 | parser: 'babel-eslint', 17 | parserOptions: { 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | ecmaVersion: 2018, 22 | sourceType: 'module', 23 | }, 24 | plugins: ['react', 'prettier'], 25 | rules: { 26 | 'prettier/prettier': 'error', 27 | 'react/jsx-filename-extension': [ 28 | 'warn', 29 | { extensions: ['.jsx', 'js'] }, 30 | ], 31 | 'import/prefer-default-export': 'off', 32 | 'no-underscore-dangle': 'off', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import GlobalStyle from './styles/global'; 4 | import { ThemeProvider } from 'styled-components'; 5 | import light from './styles/themes/light'; 6 | 7 | import { Container, Header, Content } from './styles'; 8 | import Upload from './components/Upload'; 9 | import FileList from './components/FileList'; 10 | 11 | import { UploadContext } from './providers/UploadProvider'; 12 | 13 | function App() { 14 | const { uploadedFiles } = useContext(UploadContext); 15 | 16 | return ( 17 | 18 | 19 | 20 |
21 |

Upload de Imagens

22 |

Imagens do tipo .png ou .jpg

23 |
24 | 25 | {!!uploadedFiles.length && } 26 |
27 |
28 | 29 |
30 | ); 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | Upload Image 12 | 13 | 14 | 15 | 16 |
17 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/styles/global.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | export default createGlobalStyle` 4 | *{ 5 | margin: 0; 6 | padding:0; 7 | box-sizing: border-box; 8 | outline: 0; 9 | } 10 | 11 | body{ 12 | font-size: 14px; 13 | font-family: 'Roboto', sans-serif; 14 | text-rendering: optimizeLegibility; 15 | -webkit-font-smoothing: antialiased; 16 | background: ${(props) => props.theme.colors.primary}; 17 | color: ${(props) => props.theme.colors.text}; 18 | } 19 | 20 | html, body, #root{ 21 | height: 100%; 22 | } 23 | 24 | h1, 25 | h2, 26 | h3, 27 | h4, 28 | h5, 29 | h6 { 30 | font-family: 'Roboto', sans-serif; 31 | } 32 | 33 | h1{ 34 | font-size: 2em; 35 | } 36 | 37 | a{ 38 | color: ${(props) => props.theme.colors.text}; 39 | text-decoration: none; 40 | transition: ease-in-out 0.2s; 41 | } 42 | 43 | a:hover{ 44 | color: ${(props) => props.theme.colors.primary}; 45 | } 46 | 47 | span { 48 | color: ${(props) => props.theme.colors.primary}; 49 | } 50 | `; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.21.0", 7 | "filesize": "^6.1.0", 8 | "framer-motion": "^2.9.4", 9 | "lodash": "^4.17.20", 10 | "react": "^16.13.1", 11 | "react-circular-progressbar": "^2.0.3", 12 | "react-dom": "^16.13.1", 13 | "react-dropzone": "^11.2.3", 14 | "react-icons": "^3.11.0", 15 | "react-lottie": "^1.2.3", 16 | "react-scripts": "3.4.1" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "files.associations": { 28 | "*.js": "javascriptreact" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "styled-components": "^5.2.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/FileList/styles.js: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import styled from 'styled-components'; 3 | 4 | export const Container = styled(motion.section)` 5 | display: flex; 6 | flex-direction: column; 7 | min-height: 50px; 8 | width: 100%; 9 | 10 | & h4 { 11 | font-size: 1.2em; 12 | color: ${(props) => props.theme.colors.subtext}; 13 | margin-bottom: 10px; 14 | } 15 | `; 16 | 17 | export const List = styled(motion.ul)` 18 | display: flex; 19 | flex-direction: column; 20 | 21 | width: 100%; 22 | list-style: none; 23 | 24 | padding: 0 20px 0 0; 25 | 26 | max-height: 200px; 27 | overflow-x: auto; 28 | `; 29 | 30 | export const ListItem = styled(motion.li)` 31 | display: flex; 32 | justify-content: space-between; 33 | align-items: center; 34 | 35 | & + li { 36 | margin-top: 10px; 37 | } 38 | `; 39 | 40 | export const Preview = styled.img` 41 | width: 50px; 42 | height: 50px; 43 | border-radius: 4px; 44 | object-fit: cover; 45 | `; 46 | 47 | export const FileInfo = styled.div` 48 | display: flex; 49 | width: 100%; 50 | 51 | & div { 52 | display: flex; 53 | flex-direction: column; 54 | justify-content: center; 55 | margin: 0 0 0 10px; 56 | } 57 | 58 | & span { 59 | margin-top: 5px; 60 | color: ${(props) => props.theme.colors.subtext}; 61 | font-weight: 600; 62 | font-size: 0.85em; 63 | 64 | button { 65 | margin-left: 5px; 66 | border: none; 67 | background: transparent; 68 | font-size: 1em; 69 | color: ${(props) => props.theme.colors.error}; 70 | } 71 | 72 | button:hover { 73 | color: #da3d31; 74 | cursor: pointer; 75 | } 76 | } 77 | `; 78 | -------------------------------------------------------------------------------- /src/components/Upload/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import Dropzone from 'react-dropzone'; 4 | 5 | import { DropContainer, UploadMessage } from './styles'; 6 | 7 | import { FaRegSadCry } from 'react-icons/fa'; 8 | 9 | import photoIcon from '../../assets/photo.svg'; 10 | 11 | import * as folderSuccess from '../../assets/folder-success.json'; 12 | 13 | import Animation from '../Animation'; 14 | import { UploadContext } from '../../providers/UploadProvider'; 15 | 16 | function Upload() { 17 | const { handleUpload } = useContext(UploadContext); 18 | 19 | const DragMessage = (isDragActive, isDragReject) => { 20 | if (!isDragActive) { 21 | return 'Arraste suas imagens aqui'; 22 | } 23 | 24 | if (isDragReject) { 25 | return 'Ohh não! Isto não é uma imagem'; 26 | } 27 | 28 | return 'Solte suas imagens'; 29 | }; 30 | return ( 31 | 32 | {({ getRootProps, getInputProps, isDragActive, isDragReject }) => ( 33 | 38 | 39 | {!isDragReject && !isDragActive ? ( 40 | icon 41 | ) : !isDragReject && isDragActive ? ( 42 | 48 | ) : ( 49 | 50 | )} 51 | 52 | {DragMessage(isDragActive, isDragReject)} 53 | 54 | 55 | )} 56 | 57 | ); 58 | } 59 | 60 | export default Upload; 61 | -------------------------------------------------------------------------------- /src/components/Upload/styles.js: -------------------------------------------------------------------------------- 1 | import styled, { css, keyframes } from 'styled-components'; 2 | 3 | const errorAnimation = keyframes` 4 | 0%, 5 | 50% { 6 | transform: rotate(-3deg); 7 | } 8 | 9 | 25%, 10 | 75% { 11 | transform: rotate(3deg); 12 | } 13 | 14 | 100% { 15 | transform: rotate(0deg); 16 | } 17 | `; 18 | 19 | const dragActive = css` 20 | color: ${(props) => props.theme.colors.success}; 21 | border-color: ${(props) => props.theme.colors.success}; 22 | background: ${(props) => props.theme.colors.successTransparent}; 23 | `; 24 | 25 | const dragReject = css` 26 | color: ${(props) => props.theme.colors.error}; 27 | border-color: ${(props) => props.theme.colors.error}; 28 | background: ${(props) => props.theme.colors.errorTransparent}; 29 | animation: ${errorAnimation} 0.3s linear; 30 | `; 31 | 32 | export const DropContainer = styled.div.attrs({ className: 'dropzone' })` 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: center; 36 | align-items: center; 37 | height: 150px; 38 | width: 100%; 39 | padding: 15px 30px; 40 | 41 | cursor: pointer; 42 | 43 | border-radius: 4px; 44 | 45 | border: 3px dashed ${(props) => props.theme.colors.primaryTransparent}; 46 | color: ${(props) => props.theme.colors.primaryTransparent}; 47 | 48 | &:hover { 49 | background: ${(props) => props.theme.colors.backgroundPrimaryTransparent}; 50 | } 51 | 52 | ${(props) => props.isDragActive && dragActive}; 53 | ${(props) => props.isDragReject && dragReject}; 54 | 55 | & img { 56 | width: 64px; 57 | height: 64px; 58 | margin: 0 0 10px 0; 59 | } 60 | 61 | & svg { 62 | margin: 0 0 10px 0; 63 | } 64 | `; 65 | 66 | export const UploadMessage = styled.p` 67 | display: flex; 68 | justify-content: center; 69 | align-items: center; 70 | text-align: center; 71 | margin: 0.5em 0 0 0; 72 | font-weight: 700; 73 | letter-spacing: 0.04em; 74 | `; 75 | -------------------------------------------------------------------------------- /src/components/FileList/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | 3 | import { CircularProgressbar } from 'react-circular-progressbar'; 4 | 5 | import { Container, FileInfo, Preview, List, ListItem } from './styles'; 6 | 7 | import { ThemeContext } from 'styled-components'; 8 | 9 | import { MdCheckCircle, MdError, MdLink } from 'react-icons/md'; 10 | import { UploadContext } from '../../providers/UploadProvider'; 11 | 12 | function FileList() { 13 | const themeContext = useContext(ThemeContext); 14 | const { uploadedFiles, handleDelete } = useContext(UploadContext); 15 | const [deleteFileAnimation, setDeleteFileAnimation] = useState(''); 16 | 17 | return ( 18 | 19 |

Imagens carregadas

20 | 21 | {uploadedFiles.map((file) => ( 22 | 31 | 32 | 33 |
34 | {file.name} 35 | 36 | {file.readableSize} 37 | {!!file.url && ( 38 | 46 | )} 47 | 48 |
49 |
50 |
51 | {!file.uploaded && !file.error && ( 52 | 66 | )} 67 |
68 | 69 | {file.url && ( 70 | 71 | 72 | 73 | )} 74 | {file.uploaded && } 75 | {file.error && } 76 |
77 | ))} 78 |
79 |
80 | ); 81 | } 82 | 83 | export default FileList; 84 | -------------------------------------------------------------------------------- /src/providers/UploadProvider.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useEffect } from 'react'; 2 | 3 | import { uniqueId } from 'lodash'; 4 | 5 | import filesize from 'filesize'; 6 | 7 | import api from '../services/api'; 8 | 9 | export const UploadContext = createContext({}); 10 | 11 | export default function UploadProvider({ children }) { 12 | const [uploadedFiles, setUploadedFiles] = useState([]); 13 | const [filesToUpload, setFilesToUpload] = useState([]); 14 | 15 | const [uploading, setUploading] = useState(false); 16 | 17 | useEffect(() => { 18 | async function loadFiles() { 19 | await api 20 | .get('/posts') 21 | .then(({ data }) => { 22 | setUploadedFiles( 23 | data.map((file) => ({ 24 | id: file._id, 25 | name: file.name, 26 | readableSize: filesize(file.size), 27 | preview: file.url, 28 | uploaded: true, 29 | url: file.url, 30 | })) 31 | ); 32 | }) 33 | .catch(() => {}); 34 | } 35 | 36 | loadFiles(); 37 | }, []); 38 | 39 | useEffect(() => { 40 | function updateFile(id, data) { 41 | setFilesToUpload( 42 | filesToUpload.map((uploadedFile) => { 43 | return id === uploadedFile.id 44 | ? { ...uploadedFile, ...data } 45 | : uploadedFile; 46 | }) 47 | ); 48 | 49 | setUploadedFiles((state) => [ 50 | ...state.map((uploadedFile) => { 51 | return id === uploadedFile.id 52 | ? { ...uploadedFile, ...data } 53 | : uploadedFile; 54 | }), 55 | ]); 56 | } 57 | 58 | function processUpload(uploadedFile) { 59 | const data = new FormData(); 60 | 61 | data.append('file', uploadedFile.file, uploadedFile.name); 62 | 63 | api 64 | .post('/posts', data, { 65 | onUploadProgress: (e) => { 66 | const progress = parseInt(Math.round((e.loaded * 100) / e.total)); 67 | 68 | updateFile(uploadedFile.id, { 69 | progress, 70 | }); 71 | }, 72 | }) 73 | .then(({ data }) => { 74 | updateFile(uploadedFile.id, { 75 | uploaded: true, 76 | id: data._id, 77 | url: data.url, 78 | }); 79 | }) 80 | .catch(() => { 81 | updateFile(uploadedFile.id, { 82 | error: true, 83 | }); 84 | }); 85 | } 86 | 87 | if (uploading) { 88 | filesToUpload.forEach(processUpload); 89 | setUploading(false); 90 | } 91 | }, [uploading, filesToUpload]); 92 | 93 | function handleUpload(files) { 94 | const newFiles = files.map((file) => ({ 95 | file, 96 | id: uniqueId(), 97 | name: file.name, 98 | readableSize: filesize(file.size), 99 | preview: URL.createObjectURL(file), 100 | progress: 0, 101 | uploaded: false, 102 | error: false, 103 | url: null, 104 | })); 105 | 106 | setFilesToUpload(newFiles); 107 | 108 | setUploadedFiles([...newFiles, ...uploadedFiles]); 109 | 110 | setUploading(true); 111 | } 112 | 113 | async function handleDelete(fileId) { 114 | await api.delete(`/posts/${fileId}`); 115 | 116 | setUploadedFiles(uploadedFiles.filter((file) => file.id !== fileId)); 117 | } 118 | 119 | return ( 120 | 123 | {children} 124 | 125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /src/assets/photo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 12 | 15 | 16 | 17 | 20 | 21 | 26 | 28 | 30 | 31 | 34 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/assets/folder-success.json: -------------------------------------------------------------------------------- 1 | {"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":24,"w":64,"h":64,"nm":"Folder_download","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Rotasi","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[93]},{"t":20,"s":[90]}],"ix":10},"p":{"a":0,"k":[8.453333333333326,51.306666666666615,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[5.333333333333329,5.333333333333329,100],"ix":6}},"ao":0,"ip":0,"op":1798,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Panah","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[-362,-441.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.5,-59.5],[1,280]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":5,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[34]},{"t":20,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.146158988803,0.48555800494,0.776471007104,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 2","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-120,42],[0,-79.5],[120,41]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.146158988803,0.48555800494,0.776471007104,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":0,"s":[0,0],"to":[0,0],"ti":[0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":5,"s":[0,0],"to":[0,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":10,"s":[0,0],"to":[0,16.333],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":15,"s":[0,98],"to":[0,0],"ti":[0,16.333]},{"t":20,"s":[0,0]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1798,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Folder","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[-362.5,-441.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-450,-360],[-450,-199.75],[-449.5,361],[450,361],[450,-200],[0,-199],[0,-359]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2980392156862745,0.6862745098039216,0.3137254901960784,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.403921568627451,0.6784313725490196,0.41568627450980394,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":40,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false}],"ip":0,"op":1798,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

React.js Image Upload

6 | 7 | 8 |

9 | Aplicação React com animações utilizando Framer Motion e drag & drop usando React-Dropzone. 10 | 11 | 12 | ## Sobre 13 | 14 | Essa aplicação é baseada no [vídeo](https://www.youtube.com/watch?v=G5UZmvkLWSQ) da [Rocketseat](https://github.com/Rocketseat) que por sua vez é a representa o front-end de um sistema de armazenamento de imagens localmente ou utilizando o serviço da [AWS S3](https://aws.amazon.com/pt/s3/). Essa é a minha versão da aplicação da Rocketseat e traz bibliotecas de animações como [Framer Motion](https://github.com/framer/motion) e [Lottie](https://github.com/airbnb/lottie-web) para melhorar a experiência do usuário. 15 | 16 | ![alt](src/assets/app1-banner.png) 17 | 18 | Sendo assim o back-end já foi desenvolvido por mim também e está disponível [aqui](https://github.com/juan-patrick/nodejs-image-upload). Por sua vez se trata de uma API Rest que faz integração com a AWS e o LocalStorage do servidor usando o Multer. 19 | 20 | 21 | 22 | ## ✅ Instalação 23 | 24 | ### Pré-requisitos 25 | 26 | - [Nodejs](https://nodejs.org) para rodar o servidor back-end. 27 | - NPM ou [Yarn](yarnpkg.com) (recomendo ✌) para gerencia de pacotes 28 | - [MongoDB](https://www.mongodb.com/) para armazenamento de informações das imagens que serão armazenadas. Pode ser uma conexão local ou uma conexão em Cloud como o [MongoDB Atlas](https://www.mongodb.com/cloud/atlas). 29 | - (Opcional) Para upload de imagem no S3 da AWS será necessário ter uma [conta AWS](https://aws.amazon.com/pt/account/) ter algum bucket criado. Saiba mais sobre a [criação de bucket's](https://docs.aws.amazon.com/pt_br/AmazonS3/latest/user-guide/create-bucket.html). 30 | 31 | Esse projeto depende de um back-end para rodar, então você precisa instalá-lo primeiro. 32 | 33 | ### Instalação do back-end 34 | Recomendado criar uma pasta para guardar o back-end e o front-end. 35 | 1. Realizar o clone do back-end usando o terminal: `git clone https://github.com/juan-patrick/nodejs-image-upload.git` 36 | 2. Acessar a pasta do back-end: `cd nodejs-image-upload`. 37 | 3. Instalar as dependências do projeto: `npm i` ou `yarn` 38 | 4. Criar as variáveis no arquivo `.env` 39 | 40 | ### Configurando e entendendo o `.env` 41 | 42 | As variáveis de ambiente são essenciais para essa aplicação, pois nela está armazenados dados sensíveis que não podem ficar disponíveis para qualquer pessoa. No caso dessa aplicação, algumas credenciais são de vital importância para o funcionamento da aplicação. 43 | 44 | - **APP_URL** é essencial, pois será usado pelo Multer e MongoDB para gravar o local das imagens caso seja armazenado localmente e refere-se ao endereço da sua aplicação, se estiver rodando local, será provavelmente http://localhost:3333 - `APP_URL=` 45 | - **STORAGE_TYPE** recebe apenas um dos dois valores, sendo eles `local` ou `s3` que será responsável por definir qual será o tipo de armazenamento, ser estiver `local` as imagens gravadas e as referências das imagens serão gravadas exclusivamente no local storage(`./tmp/uploads`): `STORAGE_TYPE=`. 46 | - **MONGO_URL** é a URL de conexão com o MongoDB, nessa URL é necessário passar uma URL com usuário e senha do banco e com uma base de dados já criada para o projeto, o [Mongoose](https://mongoosejs.com/) ser responsabilizará por criar de maneira automática as `collections` necessárias, nesse caso apenas uma chamada `posts`: `MONGO_URL=`. 47 | - Para não ficar muito grande, as outras configurações são referente a AWS, sendo `BUCKET-NAME` a variável que recebe o nome do bucket onde as imagens serão guardadas e as outras variáveis com inicio AWS se referem ao usuário da AWS. 48 | 49 | **Nota**: Lembre-se de configurar de maneira correta as permissões para o usuário da AWS que terá acesso ao bucket da S3. 50 | 51 | ### Rodando o back-end 52 | 53 | Após instalar todas as dependências, configurar as variáveis de ambiente, agora, resta apenas iniciar a aplicação usando o `npm dev` ou `yarn dev`. 54 | 55 | ## Instalação do front-end 56 | O processo de instalação do front-end é bem parecido com o back-end. 57 | 1. Realizar o clone do front-end usando o terminal: `git clone https://github.com/juan-patrick/reactjs-image-upload.git` 58 | 2. Acessar a pasta do front-end: `cd reactjs-image-upload` 59 | 3. Instalar as dependências com `npm i` ou `yarn`. 60 | 4. Iniciar o servidor ReactJS com `npm start` ou `yarn start`. 61 | 5. Acesse a aplicação através a URL sugerida no seu terminal(normalmente http://localhost:3000) 62 | 63 | **Nota**: Caso ocorra algum erro as imagens não carreguem, verifique se a URL no arquivo api.js (src/services/api.js) está a mesma do seu back-end, as vezes pode mudar a porta ou algo do tipo. 64 | 65 | 66 | ## Finalizando! 67 | Com tudo isso rodando, basta entrar testar a aplicação e entender como tudo isso se integrou em um só lugar, animações, hook's, AWS, local storage, carregamento dinâmico e outras coisinhas que fazem a diferença para o usuário final. 68 | 69 | 70 | 71 | > *A melhor aplicação é aquela que foi pensada para agradar ao usuário!* 72 | 73 | 74 | ### Créditos 75 |


76 | 77 | - Agradecimentos ao [Gustavo Bedasi](https://github.com/GusBedasi/) por ter ajudado em alguns problemas de manipulação de 'state' dentro do front-end. 78 | - Agradecimento a todos os programadores que criaram bibliotecas incríveis que facilitam e elevam a experiencia de programar para um outro patamar. 79 | - Icons made by Adib Sulthon from www.flaticon.com 80 | --------------------------------------------------------------------------------