├── .env.example ├── .github └── dropbox-clone-banner.png ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public └── index.html ├── src ├── App.js ├── index.css ├── index.js ├── pages │ ├── Folder │ │ └── index.js │ └── Folders │ │ └── index.js ├── routes.js └── services │ └── api.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=http://localhost:3000 -------------------------------------------------------------------------------- /.github/dropbox-clone-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emkis/dropbox-clone/e13709cc181e2104089c5889abbc0d50c5cb68c9/.github/dropbox-clone-banner.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 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nicolas Jardim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![an easy way to store, share and access files from anywhere](https://github.com/emkis/dropbox-clone/blob/master/.github/dropbox-clone-banner.png?raw=true) 2 | 3 | 4 | # :page_with_curl: about 5 | this is a beautiful web app project that you can use for **store, share and acess files any time from anywhere** 6 | 7 | i made this project for fun and also to challenge myself in learn more about reactjs 8 | 9 | **[you can see live app here](https://dropbox-clone.herokuapp.com)** 10 | 11 | # :pushpin: features 12 | 13 | ### realtime 14 | everything that happens is **always in sync**, if you add something, if someone change something, this app stays updated always 15 | 16 | ### storage & share files 17 | you can upload any files you want, share with someone instantly, or you can send the folders link to a friend 18 | 19 | ### organize your files 20 | you can also create folders for organizing your files better 21 | 22 | # :hammer: how it was built 23 | this project was developed with the following technologies: 24 | 25 | - [ReactJS](https://github.com/facebook/react/) 26 | - CSS only 27 | - [Socket.IO](https://github.com/socketio/socket.io-client) 28 | - [Axios](https://github.com/axios/axios) 29 | 30 | # :speech_balloon: rest api 31 | all data is being consumed from an rest api written in node, you can check the [repo here](https://github.com/emkis/dropbox-clone-api) 32 | 33 | # :information_source: note 34 | every file you store here is deleted about every 30 minutes, so dont worry 😉 35 | 36 | the api that storages all files is hosted on [heroku](https://www.heroku.com/), and heroku always erase everything when the app is restarted, thats why it happen 37 | 38 | but if you use another host service for the api you can use this app without this prolem 39 | 40 | # :electric_plug: how to use 41 | to clone this repository and run this app, you'll need **[git](https://git-scm.com)** and **[node.js](https://nodejs.org/en/)** installed on your computer. 42 | 43 | i highly recommend **[yarn](https://yarnpkg.com/)** for handling node packages faster, but you can use npm if you want, no problem. 44 | 45 | **from your command line *(using npm)*:** 46 | 47 | ```bash 48 | # clone this repository 49 | $ git clone https://github.com/emkis/dropbox-clone.git 50 | 51 | # go into the repository 52 | $ cd dropbox-clone 53 | 54 | # create a .env file based on the example and define the api url 55 | # you can use the api that is used in production: https://dropbox-clone-back.herokuapp.com 56 | $ cp .env.example .env 57 | 58 | # install dependencies 59 | $ npm install 60 | 61 | # run the app in development mode 62 | $ npm run start 63 | ``` 64 | 65 | --- 66 | 67 | :v: **[say hello](https://www.linkedin.com/in/nicolas-jardim/)** to me on linkedin or send me and **[email](mailto:nicolasemkis@gmail.com)** :mailbox: 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dropbox-clone", 3 | "repository": "https://github.com/emkis/dropbox-clone.git", 4 | "author": "emkis ", 5 | "license": "MIT", 6 | "version": "1.1.0", 7 | "dependencies": { 8 | "axios": "^0.21.1", 9 | "date-fns": "^2.16.1", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-dropzone": "^11.2.4", 13 | "react-icons": "^4.1.0", 14 | "react-router-dom": "^5.2.0", 15 | "react-scripts": "4.0.1", 16 | "socket.io-client": "^2.4.0" 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 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all", 32 | "not ie > 0" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Dropbox Clone 12 | 13 | 14 | 15 | 16 |
17 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import './index.css' 4 | import Routes from './routes' 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 | 10 | ) 11 | } 12 | } 13 | 14 | export default App 15 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Lato:400,700&display=swap"); 2 | 3 | html { 4 | font-size: 62.5%; 5 | } 6 | 7 | body { 8 | font-size: 1.6rem; 9 | color: #333; 10 | font-family: "Lato", sans-serif; 11 | min-height: 100vh; 12 | margin: 0 1.5rem; 13 | } 14 | 15 | *, 16 | *::after, 17 | *::before { 18 | margin: 0; 19 | padding: 0; 20 | line-height: 1.25; 21 | outline: none; 22 | box-sizing: border-box; 23 | } 24 | 25 | a { 26 | text-decoration: none; 27 | } 28 | 29 | svg { 30 | stroke: #333333; 31 | } 32 | 33 | button { 34 | transition: transform ease 100ms; 35 | line-height: normal; 36 | } 37 | 38 | button[disabled], 39 | button:disabled { 40 | cursor: not-allowed !important; 41 | } 42 | 43 | button:hover, 44 | button:focus { 45 | transform: scale(1.05); 46 | cursor: pointer; 47 | } 48 | 49 | .btn-icon { 50 | border-radius: 8px; 51 | padding: 10px; 52 | } 53 | 54 | .btn-icon:hover, 55 | .btn-icon:focus { 56 | background-color: rgba(95, 99, 104, 0.1); 57 | } 58 | 59 | .overlay { 60 | position: fixed; 61 | top: 0; 62 | left: 0; 63 | width: 100%; 64 | height: 100vh; 65 | 66 | display: flex; 67 | justify-content: center; 68 | align-items: center; 69 | 70 | padding: 0 15px; 71 | background: rgba(0,0,0,.55); 72 | z-index: 10; 73 | } 74 | 75 | .loading { 76 | font-size: 4.5rem; 77 | font-weight: 700; 78 | color: #000; 79 | height: 100vh; 80 | display: flex; 81 | justify-content: center; 82 | align-items: center; 83 | flex: 1; 84 | } 85 | 86 | .loading svg, 87 | .rotate { 88 | animation: rotate 2s linear infinite; 89 | } 90 | 91 | @keyframes rotate { 92 | from { 93 | transform: rotate(0deg); 94 | } 95 | 96 | to { 97 | transform: rotate(360deg); 98 | } 99 | } 100 | 101 | .folder__container { 102 | max-width: 600px; 103 | margin: 0 auto; 104 | padding: 5rem 0 8rem; 105 | } 106 | 107 | .folder__container header h1 { 108 | font-family: "Lato", sans-serif; 109 | font-size: 4.3rem; 110 | line-height: 1; 111 | } 112 | 113 | .folder__container li { 114 | position: relative; 115 | list-style: none; 116 | display: flex; 117 | padding: 1.4rem 2rem; 118 | margin-bottom: 1.2rem; 119 | border: 1px solid #ddd; 120 | border-radius: 8px; 121 | justify-content: space-between; 122 | align-items: center; 123 | transition: background 200ms ease-out; 124 | flex-wrap: wrap; 125 | word-break: break-word; 126 | } 127 | 128 | .folder__container li:hover, 129 | .folder__container li:focus, 130 | .folder__container li:focus-within { 131 | background: rgba(95, 99, 104, 0.1); 132 | } 133 | 134 | .file__info { 135 | color: #333; 136 | display: flex; 137 | justify-content: center; 138 | align-items: center; 139 | text-decoration: none; 140 | } 141 | 142 | .file__info:focus, 143 | .file__info:hover { 144 | text-decoration: underline; 145 | } 146 | 147 | .file__info::before { 148 | position: absolute; 149 | content: ""; 150 | top: 0; 151 | left: 0; 152 | width: 100%; 153 | height: 100%; 154 | } 155 | 156 | .file__info svg { 157 | color: #5f6368; 158 | min-width: 40px; 159 | margin-right: 1rem; 160 | } 161 | 162 | .folder__container .updated-at { 163 | color: #5f6368; 164 | font-size: 1.4rem; 165 | } 166 | 167 | .box__upload { 168 | padding: 2rem 4rem; 169 | margin: 2rem 0; 170 | min-height: 11rem; 171 | 172 | display: flex; 173 | justify-content: center; 174 | align-items: center; 175 | 176 | border-radius: 8px; 177 | border-width: 2px; 178 | border-color: #5f6368; 179 | border-style: dashed; 180 | 181 | transition: background 200ms ease-out; 182 | cursor: pointer; 183 | } 184 | 185 | .box__upload:hover { 186 | background: rgba(95, 99, 104, 0.1); 187 | } 188 | 189 | .box__upload strong { 190 | color: #5f6368; 191 | } 192 | 193 | .folder__container header { 194 | margin-bottom: 2rem; 195 | } 196 | 197 | header { 198 | display: flex; 199 | align-items: center; 200 | justify-content: space-between; 201 | } 202 | 203 | header div { 204 | text-align: right; 205 | margin-bottom: -8px; 206 | margin-right: -8px; 207 | } 208 | 209 | header .btn { 210 | border: 0; 211 | background: none; 212 | } 213 | 214 | header .btn + .btn { 215 | margin-left: 0.5rem; 216 | } 217 | 218 | .folders { 219 | display: flex; 220 | flex-wrap: wrap; 221 | margin: 0 -0.7rem; 222 | } 223 | 224 | .folder__container .folders li { 225 | position: relative; 226 | margin: 0.7rem; 227 | min-height: 15rem; 228 | flex: 1 1 18rem; 229 | } 230 | 231 | .folders li .wrapper-_link { 232 | text-align: center; 233 | width: 100%; 234 | height: 100%; 235 | display: flex; 236 | flex-direction: column; 237 | align-items: center; 238 | justify-content: center; 239 | } 240 | 241 | .folders li .wrapper-_link::before { 242 | position: absolute; 243 | content: ""; 244 | top: 0; 245 | left: 0; 246 | width: 100%; 247 | height: 100%; 248 | } 249 | 250 | .folders li .file__info { 251 | order: 2; 252 | text-align: center; 253 | margin-top: 1.5rem; 254 | } 255 | 256 | .folders svg { 257 | color: #333; 258 | } 259 | 260 | .folders__empty-message { 261 | margin-top: 20px; 262 | } 263 | 264 | .folders__empty-message > strong { 265 | cursor: pointer; 266 | } 267 | 268 | .main__box, 269 | .message__container { 270 | max-width: 45rem; 271 | width: 100%; 272 | 273 | display: flex; 274 | justify-content: center; 275 | align-items: stretch; 276 | flex-direction: column; 277 | 278 | background: #fff; 279 | border: 2px solid #ddd; 280 | border-radius: 8px; 281 | padding: 6.5rem 4.5rem; 282 | } 283 | 284 | .main__box h1 { 285 | font-family: "Lato", sans-serif; 286 | font-size: 4rem; 287 | font-weight: 700; 288 | margin-bottom: 2.5rem; 289 | } 290 | 291 | .main__box input { 292 | border: 1px solid #333; 293 | padding: 1.5rem 2rem; 294 | font-size: 1.8rem; 295 | background: rgba(95, 99, 104, 0.1); 296 | border-radius: 8px; 297 | margin-bottom: 1.3rem; 298 | outline: none; 299 | height: 5rem; 300 | } 301 | 302 | .main__box button { 303 | border: 0; 304 | padding: 1.5rem 2.5rem; 305 | font-size: 1.8rem; 306 | font-weight: 700; 307 | background: #000; 308 | border-radius: 8px; 309 | color: #fff; 310 | cursor: pointer; 311 | height: 5rem; 312 | } 313 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /src/pages/Folder/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from "react" 2 | import { formatDistance, parseISO } from "date-fns" 3 | import Dropzone from "react-dropzone" 4 | import socket from "socket.io-client" 5 | 6 | import { MdInsertDriveFile } from "react-icons/md" 7 | import { FiTrash2, FiAlertCircle } from "react-icons/fi" 8 | import { FaCircleNotch } from "react-icons/fa" 9 | 10 | import api from "../../services/api" 11 | 12 | class Folder extends Component { 13 | state = { 14 | folder: {}, 15 | loading: true, 16 | deleteClickCounter: 0, 17 | deletingFolder: false 18 | } 19 | 20 | buttonRef = createRef() 21 | 22 | async componentDidMount() { 23 | this.subscribeToNewFiles() 24 | 25 | const folderId = this.props.match.params.id 26 | 27 | try { 28 | const response = await api.get(`folder/${folderId}`) 29 | 30 | if(!response.data) throw Error() 31 | 32 | this.setState({ 33 | folder: response.data, 34 | loading: false 35 | }) 36 | } catch (error) { 37 | this.props.history.push('/') 38 | } 39 | } 40 | 41 | componentDidUpdate() { 42 | if (this.state.deleteClickCounter > 0) { 43 | document.addEventListener("mousedown", this.handleClickOutside) 44 | } 45 | } 46 | 47 | handleClickOutside = event => { 48 | if (this.buttonRef.current.contains(event.target)) return 49 | 50 | this.setState({ deleteClickCounter: 0 }) 51 | document.removeEventListener("mousedown", this.handleClickOutside) 52 | } 53 | 54 | subscribeToNewFiles = () => { 55 | const folderId = this.props.match.params.id 56 | const io = socket(process.env.REACT_APP_API_URL) 57 | 58 | io.emit("connectionRoom", folderId) 59 | 60 | io.on("@file/CREATED", data => { 61 | this.setState({ 62 | folder: { 63 | ...this.state.folder, 64 | files: [data, ...this.state.folder.files] 65 | } 66 | }) 67 | }) 68 | 69 | io.on("@file/CHANGED", data => { 70 | const modifiedFile = this.state.folder.files.map(file => { 71 | if (file.id === data.id) { 72 | file.url = data.url 73 | file.updatedAt = data.updatedAt 74 | return file 75 | } 76 | return file 77 | }) 78 | 79 | this.setState({ 80 | folder: { ...this.state.folder, files: [...modifiedFile] } 81 | }) 82 | }) 83 | } 84 | 85 | handleUpload = files => { 86 | files.forEach(file => { 87 | const data = new FormData() 88 | const folderId = this.props.match.params.id 89 | 90 | data.append("file", file) 91 | api.post(`folder/${folderId}/files`, data) 92 | }) 93 | } 94 | 95 | handleDeleteFolder = async () => { 96 | this.setState({ 97 | deleteClickCounter: this.state.deleteClickCounter += 1 98 | }) 99 | 100 | if (this.state.deleteClickCounter > 1) { 101 | document.removeEventListener("mousedown", this.handleClickOutside) 102 | this.setState({ deletingFolder: true, deleteClickCounter: 0 }) 103 | 104 | try { 105 | const folderId = this.props.match.params.id 106 | 107 | await api.delete(`folder/${folderId}`) 108 | this.props.history.push("/") 109 | } catch (error) { 110 | this.setState({ deletingFolder: false }) 111 | } 112 | } 113 | } 114 | 115 | render() { 116 | const { title, files } = this.state.folder 117 | const { loading, deletingFolder, deleteClickCounter } = this.state 118 | 119 | if (loading) { 120 | return ( 121 |
122 | 123 |
124 | ) 125 | } 126 | 127 | return ( 128 |
129 |
130 |

{title}

131 |
132 | 144 |
145 |
146 | 147 | 148 | {({ getRootProps, getInputProps }) => ( 149 |
150 | 151 | drag a file or click here 152 |
153 | )} 154 |
155 | 156 |
    157 | {files ? ( 158 | files.map(file => ( 159 |
  • 160 | 167 | 168 | {file.title} 169 | 170 | 171 | 172 | {formatDistance(parseISO(file.updatedAt), new Date(), { 173 | addSuffix: true, 174 | includeSeconds: true 175 | })} 176 | 177 |
  • 178 | )) 179 | ) : ( 180 |

    this folder is empty

    181 | )} 182 |
183 |
184 | ) 185 | } 186 | } 187 | 188 | export default Folder 189 | -------------------------------------------------------------------------------- /src/pages/Folders/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from "react" 2 | import { MdFolder } from "react-icons/md" 3 | import { FiDownload, FiFolderPlus } from "react-icons/fi" 4 | import { FaCircleNotch } from "react-icons/fa" 5 | 6 | import socket from "socket.io-client" 7 | import { Link } from "react-router-dom" 8 | 9 | import api from "../../services/api" 10 | 11 | class Folders extends Component { 12 | state = { 13 | folders: [], 14 | loading: true, 15 | downloadingFiles: false, 16 | creatingFolder: false, 17 | iWantAFolder: false, 18 | newFolderName: "" 19 | } 20 | 21 | async componentDidMount() { 22 | this.subscribeToNewFolders() 23 | 24 | const response = await api.get(`/folders`) 25 | const data = await response.data 26 | 27 | this.setState({ 28 | folders: data, 29 | loading: false 30 | }) 31 | } 32 | 33 | componentDidUpdate() { 34 | this.textInput.current && this.focusTextInput() 35 | 36 | if (this.state.iWantAFolder) { 37 | document.addEventListener("mousedown", this.handleClickOutside) 38 | window.addEventListener('scroll', this.noScroll); 39 | } 40 | } 41 | 42 | textInput = createRef() 43 | focusTextInput = () => this.textInput.current.focus() 44 | 45 | containerRef = createRef() 46 | 47 | handleClickOutside = event => { 48 | if (this.containerRef.current.contains(event.target)) return 49 | 50 | this.setState({ 51 | iWantAFolder: false, 52 | newFolderName: "", 53 | creatingFolder: false 54 | }) 55 | document.removeEventListener("mousedown", this.handleClickOutside) 56 | window.removeEventListener('scroll', this.noScroll); 57 | } 58 | 59 | noScroll = (x = 0, y = 0) => window.scrollTo(x, y) 60 | 61 | subscribeToNewFolders = () => { 62 | const io = socket(process.env.REACT_APP_API_URL) 63 | 64 | io.on("@folder/CREATED", data => { 65 | this.setState({ 66 | folders: [...this.state.folders, data] 67 | }) 68 | }) 69 | 70 | io.on('@folder/REMOVED', removedFolder => { 71 | this.setState({ 72 | folders: [ 73 | ...this.state.folders.filter(folder => 74 | folder._id !== removedFolder._id && folder 75 | )] 76 | }) 77 | }) 78 | } 79 | 80 | handleDownload = async () => { 81 | this.setState({ downloadingFiles: true }) 82 | 83 | try { 84 | const response = await api.get("/download", { 85 | responseType: "blob", 86 | timeout: 30000 87 | }) 88 | 89 | const url = window.URL.createObjectURL(new Blob([response.data])) 90 | const link = document.createElement("a") 91 | link.href = url 92 | link.setAttribute("download", "all_files.zip") 93 | document.body.appendChild(link) 94 | link.click() 95 | link.remove() 96 | } catch (error) {} 97 | 98 | this.setState({ downloadingFiles: false }) 99 | } 100 | 101 | handleNewFolder = () => { 102 | this.setState({ 103 | iWantAFolder: true 104 | }) 105 | } 106 | 107 | createNewFolder = async e => { 108 | e.preventDefault() 109 | if (!this.state.newFolderName.trim()) return 110 | 111 | this.setState({ creatingFolder: true }) 112 | 113 | try { 114 | await api.post("folder", { 115 | title: this.state.newFolderName 116 | }) 117 | 118 | this.setState({ iWantAFolder: false, creatingFolder: false, newFolderName: "" }) 119 | document.removeEventListener("mousedown", this.handleClickOutside) 120 | } catch (error) { 121 | this.setState({ creatingFolder: false }) 122 | } 123 | } 124 | 125 | render() { 126 | const { 127 | folders, 128 | loading, 129 | iWantAFolder, 130 | creatingFolder, 131 | downloadingFiles 132 | } = this.state 133 | 134 | if (loading) { 135 | return ( 136 |
137 | 138 |
139 | ) 140 | } 141 | 142 | return ( 143 |
144 | {iWantAFolder && ( 145 |
146 |
147 |

type the folder name

148 | this.setState({ newFolderName: e.target.value })} 153 | /> 154 | 165 |
166 |
167 | )} 168 | 169 |
170 |

current folders

171 |
172 | 179 | 191 |
192 |
193 | 194 |
    195 | {folders.length ? ( 196 | folders.map(folder => ( 197 |
  • 198 | 202 | 203 | {folder.title} 204 | 205 |
  • 206 | )) 207 | ) : ( 208 |

    209 | there's no folders here, you can create one 210 | clicking here. 211 |

    212 | )} 213 |
214 |
215 | ) 216 | } 217 | } 218 | 219 | export default Folders 220 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 3 | 4 | import Folder from "./pages/Folder"; 5 | import Folders from "./pages/Folders"; 6 | 7 | const Routes = () => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | export default Routes; 18 | -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const api = axios.create({ 4 | baseURL: process.env.REACT_APP_API_URL 5 | }); 6 | 7 | export default api; 8 | --------------------------------------------------------------------------------