├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── components └── file-upload │ ├── file-upload.component.jsx │ └── file-upload.styles.js ├── index.css └── index.js /.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 | .eslintcache 25 | debug.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo contains the code for the [Responsive React File Upload Component With Drag And Drop](https://dev.to/chandrapantachhetri/responsive-react-file-upload-component-with-drag-and-drop-4ef8) article. 2 | 3 | To run the code locally: 4 | 5 | 1. Run ``npm i`` once cloning the repo 6 | 2. Run ``npm start`` 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-file-upload", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.6", 7 | "@testing-library/react": "^11.2.2", 8 | "@testing-library/user-event": "^12.5.0", 9 | "node-sass": "^5.0.0", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-scripts": "4.0.1", 13 | "styled-components": "^5.2.1", 14 | "web-vitals": "^0.2.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chandra-Panta-Chhetri/react-file-upload-tutorial/62418c557a6e5d21e9a74a1b19c34e67e3a9d562/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 22 | 31 | React App 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chandra-Panta-Chhetri/react-file-upload-tutorial/62418c557a6e5d21e9a74a1b19c34e67e3a9d562/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chandra-Panta-Chhetri/react-file-upload-tutorial/62418c557a6e5d21e9a74a1b19c34e67e3a9d562/public/logo512.png -------------------------------------------------------------------------------- /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.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import FileUpload from "./components/file-upload/file-upload.component"; 3 | 4 | function App() { 5 | const [newUserInfo, setNewUserInfo] = useState({ 6 | profileImages: [] 7 | }); 8 | 9 | const updateUploadedFiles = (files) => 10 | setNewUserInfo({ ...newUserInfo, profileImages: files }); 11 | 12 | const handleSubmit = (event) => { 13 | event.preventDefault(); 14 | //logic to create new user... 15 | }; 16 | 17 | return ( 18 |
19 |
20 | 26 | 27 | 28 |
29 | ); 30 | } 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /src/components/file-upload/file-upload.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import { 3 | FileUploadContainer, 4 | FormField, 5 | DragDropText, 6 | UploadFileBtn, 7 | FilePreviewContainer, 8 | ImagePreview, 9 | PreviewContainer, 10 | PreviewList, 11 | FileMetaData, 12 | RemoveFileIcon, 13 | InputLabel 14 | } from "./file-upload.styles"; 15 | 16 | const KILO_BYTES_PER_BYTE = 1000; 17 | const DEFAULT_MAX_FILE_SIZE_IN_BYTES = 500000; 18 | 19 | const convertNestedObjectToArray = (nestedObj) => 20 | Object.keys(nestedObj).map((key) => nestedObj[key]); 21 | 22 | const convertBytesToKB = (bytes) => Math.round(bytes / KILO_BYTES_PER_BYTE); 23 | 24 | const FileUpload = ({ 25 | label, 26 | updateFilesCb, 27 | maxFileSizeInBytes = DEFAULT_MAX_FILE_SIZE_IN_BYTES, 28 | ...otherProps 29 | }) => { 30 | const fileInputField = useRef(null); 31 | const [files, setFiles] = useState({}); 32 | 33 | const handleUploadBtnClick = () => { 34 | fileInputField.current.click(); 35 | }; 36 | 37 | const addNewFiles = (newFiles) => { 38 | for (let file of newFiles) { 39 | if (file.size <= maxFileSizeInBytes) { 40 | if (!otherProps.multiple) { 41 | return { file }; 42 | } 43 | files[file.name] = file; 44 | } 45 | } 46 | return { ...files }; 47 | }; 48 | 49 | const callUpdateFilesCb = (files) => { 50 | const filesAsArray = convertNestedObjectToArray(files); 51 | updateFilesCb(filesAsArray); 52 | }; 53 | 54 | const handleNewFileUpload = (e) => { 55 | const { files: newFiles } = e.target; 56 | if (newFiles.length) { 57 | let updatedFiles = addNewFiles(newFiles); 58 | setFiles(updatedFiles); 59 | callUpdateFilesCb(updatedFiles); 60 | } 61 | }; 62 | 63 | const removeFile = (fileName) => { 64 | delete files[fileName]; 65 | setFiles({ ...files }); 66 | callUpdateFilesCb({ ...files }); 67 | }; 68 | 69 | return ( 70 | <> 71 | 72 | {label} 73 | Drag and drop your files anywhere or 74 | 75 | 76 | Upload {otherProps.multiple ? "files" : "a file"} 77 | 78 | 86 | 87 | 88 | To Upload 89 | 90 | {Object.keys(files).map((fileName, index) => { 91 | let file = files[fileName]; 92 | let isImageFile = file.type.split("/")[0] === "image"; 93 | return ( 94 | 95 |
96 | {isImageFile && ( 97 | 101 | )} 102 | 103 | {file.name} 104 | 111 | 112 |
113 |
114 | ); 115 | })} 116 |
117 |
118 | 119 | ); 120 | }; 121 | 122 | export default FileUpload; 123 | -------------------------------------------------------------------------------- /src/components/file-upload/file-upload.styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const FileUploadContainer = styled.section` 4 | position: relative; 5 | margin: 25px 0 15px; 6 | border: 2px dotted lightgray; 7 | padding: 35px 20px; 8 | border-radius: 6px; 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | background-color: white; 13 | `; 14 | 15 | export const FormField = styled.input` 16 | font-size: 18px; 17 | display: block; 18 | width: 100%; 19 | border: none; 20 | text-transform: none; 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | right: 0; 25 | bottom: 0; 26 | opacity: 0; 27 | 28 | &:focus { 29 | outline: none; 30 | } 31 | `; 32 | 33 | export const InputLabel = styled.label` 34 | top: -21px; 35 | font-size: 13px; 36 | color: black; 37 | left: 0; 38 | position: absolute; 39 | `; 40 | 41 | export const DragDropText = styled.p` 42 | font-weight: bold; 43 | letter-spacing: 2.2px; 44 | margin-top: 0; 45 | text-align: center; 46 | `; 47 | 48 | export const UploadFileBtn = styled.button` 49 | box-sizing: border-box; 50 | appearance: none; 51 | background-color: transparent; 52 | border: 2px solid #3498db; 53 | cursor: pointer; 54 | font-size: 1rem; 55 | line-height: 1; 56 | padding: 1.1em 2.8em; 57 | text-align: center; 58 | text-transform: uppercase; 59 | font-weight: 700; 60 | border-radius: 6px; 61 | color: #3498db; 62 | position: relative; 63 | overflow: hidden; 64 | z-index: 1; 65 | transition: color 250ms ease-in-out; 66 | font-family: "Open Sans", sans-serif; 67 | width: 45%; 68 | display: flex; 69 | align-items: center; 70 | padding-right: 0; 71 | justify-content: center; 72 | 73 | &:after { 74 | content: ""; 75 | position: absolute; 76 | display: block; 77 | top: 0; 78 | left: 50%; 79 | transform: translateX(-50%); 80 | width: 0; 81 | height: 100%; 82 | background: #3498db; 83 | z-index: -1; 84 | transition: width 250ms ease-in-out; 85 | } 86 | 87 | i { 88 | font-size: 22px; 89 | margin-right: 5px; 90 | border-right: 2px solid; 91 | position: absolute; 92 | top: 0; 93 | bottom: 0; 94 | left: 0; 95 | right: 0; 96 | width: 20%; 97 | display: flex; 98 | flex-direction: column; 99 | justify-content: center; 100 | } 101 | 102 | @media only screen and (max-width: 500px) { 103 | width: 70%; 104 | } 105 | 106 | @media only screen and (max-width: 350px) { 107 | width: 100%; 108 | } 109 | 110 | &:hover { 111 | color: #fff; 112 | outline: 0; 113 | background: transparent; 114 | 115 | &:after { 116 | width: 110%; 117 | } 118 | } 119 | 120 | &:focus { 121 | outline: 0; 122 | background: transparent; 123 | } 124 | 125 | &:disabled { 126 | opacity: 0.4; 127 | filter: grayscale(100%); 128 | pointer-events: none; 129 | } 130 | `; 131 | 132 | export const FilePreviewContainer = styled.article` 133 | margin-bottom: 35px; 134 | 135 | span { 136 | font-size: 14px; 137 | } 138 | `; 139 | 140 | export const PreviewList = styled.section` 141 | display: flex; 142 | flex-wrap: wrap; 143 | margin-top: 10px; 144 | 145 | @media only screen and (max-width: 400px) { 146 | flex-direction: column; 147 | } 148 | `; 149 | 150 | export const FileMetaData = styled.div` 151 | display: ${(props) => (props.isImageFile ? "none" : "flex")}; 152 | flex-direction: column; 153 | position: absolute; 154 | top: 0; 155 | left: 0; 156 | right: 0; 157 | bottom: 0; 158 | padding: 10px; 159 | border-radius: 6px; 160 | color: white; 161 | font-weight: bold; 162 | background-color: rgba(5, 5, 5, 0.55); 163 | 164 | aside { 165 | margin-top: auto; 166 | display: flex; 167 | justify-content: space-between; 168 | } 169 | `; 170 | 171 | export const RemoveFileIcon = styled.i` 172 | cursor: pointer; 173 | 174 | &:hover { 175 | transform: scale(1.3); 176 | } 177 | `; 178 | 179 | export const PreviewContainer = styled.section` 180 | padding: 0.25rem; 181 | width: 20%; 182 | height: 120px; 183 | border-radius: 6px; 184 | box-sizing: border-box; 185 | 186 | &:hover { 187 | opacity: 0.55; 188 | 189 | ${FileMetaData} { 190 | display: flex; 191 | } 192 | } 193 | 194 | & > div:first-of-type { 195 | height: 100%; 196 | position: relative; 197 | } 198 | 199 | @media only screen and (max-width: 750px) { 200 | width: 25%; 201 | } 202 | 203 | @media only screen and (max-width: 500px) { 204 | width: 50%; 205 | } 206 | 207 | @media only screen and (max-width: 400px) { 208 | width: 100%; 209 | padding: 0 0 0.4em; 210 | } 211 | `; 212 | 213 | export const ImagePreview = styled.img` 214 | border-radius: 6px; 215 | width: 100%; 216 | height: 100%; 217 | `; 218 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | --------------------------------------------------------------------------------