├── .gitignore
├── README.md
├── client
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── Buttons.js
│ ├── Footer.js
│ ├── Images.js
│ ├── Spinner.js
│ ├── WakeUp.js
│ ├── config.js
│ ├── index.js
│ └── registerServiceWorker.js
└── yarn.lock
└── server
├── .gitignore
├── config.js
├── package.json
└── server.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /server/node_modules
5 | /client/node_modules
6 |
7 | # testing
8 | /coverage
9 |
10 | # production
11 | /build
12 |
13 | # misc
14 | .DS_Store
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 | /server/.env
20 | client/build/
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 | todos.txt
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Image Upload
2 |
3 | 
4 |
5 | ## Medium post that details this repo
6 | * [Simple Image Upload with React](https://medium.com/p/cc96430eaece)
7 | * [Live demo](https://react-image-upload.surge.sh)
8 |
9 | ## Getting Started
10 |
11 | ```
12 | git clone https://github.com/funador/react-image-upload.git
13 | cd react-image-upload/client
14 | npm i && npm start
15 | // open new terminal
16 | cd react-image-upload/server
17 | npm i && npm run dev
18 | ```
19 |
20 | #### To run this project you need a Cloudinary account.
21 | You can sign up with Cloudinary [here](https://cloudinary.com/users/register/free). Afterwards you will need to plug your keys into a *.env* file that needs to be created as well.
22 |
23 | ```shell
24 | // server/.env
25 | CLOUD_NAME=your_cloud_name
26 | API_KEY=your_cloud_key
27 | API_SECRET=your_cloud_secret
28 | ```
29 |
30 | But that is it for the setup!
31 |
32 | ### Issues
33 |
34 | Something not working? Please [open an issue](https://github.com/funador/react-image-upload/issues)
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-image-upload-client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-free-brands": "^5.0.13",
7 | "@fortawesome/fontawesome-svg-core": "^1.2.0-14",
8 | "@fortawesome/free-solid-svg-icons": "^5.1.0-11",
9 | "@fortawesome/react-fontawesome": "0.1.0-11",
10 | "react": "^16.4.2",
11 | "react-dom": "^16.4.2",
12 | "react-notify-toast": "^0.5.0",
13 | "react-scripts": "1.1.4"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test --env=jsdom",
19 | "eject": "react-scripts eject"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/funador/react-image-upload/76cb396992afefa3395676ba49f231de014df83f/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
15 |
24 | React Image Upload
25 |
26 |
27 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/client/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 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Roboto, sans-serif;
3 | background: #f2f2f2;
4 | color: #757575;
5 | margin: 0px;
6 | }
7 |
8 | .container {
9 | display: flex;
10 | flex-direction: column;
11 | height: 100vh;
12 | justify-content: space-around;
13 | }
14 |
15 | .buttons {
16 | display: flex;
17 | align-items: center;
18 | justify-content: space-around;
19 | flex-wrap: wrap;
20 | height: 75vh
21 | }
22 |
23 | .button {
24 | margin: 0 10vw
25 | }
26 |
27 | .images {
28 | display: flex;
29 | justify-content: center;
30 | }
31 |
32 | .delete {
33 | position: relative;
34 | top: 16px;
35 | right: 15px
36 | }
37 |
38 | footer svg, .button svg, .delete svg {
39 | filter: drop-shadow(3px 3px 3px #999);
40 | transition: 0.3s;
41 | transform: rotate(-0.2deg)
42 | }
43 |
44 | footer svg:hover, .button svg:hover, .delete svg:hover {
45 | filter: drop-shadow(4px 4px 4px #999);
46 | transform: rotate(-0.6deg);
47 | cursor: pointer
48 | }
49 |
50 | .delete svg {
51 | filter: drop-shadow(1px 1px 1px #999)
52 | }
53 |
54 | footer svg {
55 | filter: drop-shadow(1.5px 1px 1px #999)
56 | }
57 |
58 | footer svg:hover, .delete svg:hover {
59 | filter: drop-shadow(1.5px 1.5px 1.5px)
60 | }
61 |
62 | input[type=file] {
63 | visibility: hidden;
64 | position: absolute
65 | }
66 |
67 | img {
68 | max-height: 420px;
69 | max-width: 420px;
70 | padding: 10px;
71 | border: 1px dashed #999;
72 | }
73 |
74 | /* Loading */
75 | .loading-wrapper, .loading {
76 | display: flex;
77 | align-items: center;
78 | justify-content: center;
79 | flex-direction: column;
80 | }
81 |
82 | .loading {
83 | width: 200px;
84 | height: 200px;
85 | }
86 |
87 | .loading .background {
88 | border-radius: 50%;
89 | background: #6762a6;
90 | border: 3px solid #c9c3e6;
91 | box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.25);
92 | width: 200px;
93 | height: 200px;
94 | box-sizing: border-box;
95 | animation: pulse-colors 2s infinite alternate linear;
96 | }
97 |
98 | .loading i {
99 | margin: 25px 5px 5px 55px;
100 | float: left;
101 | font-size: 10em !important;
102 | color: #fff;
103 | animation: pulse-icon 4s infinite alternate linear;
104 | }
105 |
106 | #notification-wrapper {
107 | position: absolute;
108 | }
109 |
110 | /* Footer */
111 | footer {
112 | display: flex;
113 | justify-content: space-between;
114 | width: 96vw;
115 | margin: 0 2vw;
116 | }
117 |
118 | footer .small-button {
119 | border-radius: 50%;
120 | border: 1px solid #fff;
121 | width: 3.2em;
122 | height: 3.2em;
123 | padding: 5px;
124 | transition: .3s;
125 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
126 | text-align: center;
127 | }
128 |
129 | footer .small-button:hover {
130 | background: #767676;
131 | box-shadow: 0px 6px 6px rgba(0, 0, 0, 0.45);
132 | }
133 |
134 | footer .small-button.github {
135 | background: #6e5494;
136 | }
137 |
138 | footer .small-button.medium {
139 | background: #00ab6c;
140 | font-size: 0.85em;
141 | padding: 0.65em;
142 | }
143 |
144 | @keyframes pulse-colors {
145 | from {
146 | background: #c9c3e6;
147 | border: 3px solid #a29ccc;
148 | }
149 | to {
150 | background: #6762a6;
151 | border: 3px solid #c9c3e6;
152 | }
153 | }
154 |
155 | /* Animations */
156 | .fadein {
157 | animation: fadein 2s;
158 | }
159 |
160 | @keyframes fadein {
161 | from {
162 | opacity: 0;
163 | }
164 | to {
165 | opacity: 1;
166 | }
167 | }
168 |
169 | .spinner {
170 | animation: spin 1.2s infinite linear;
171 | }
172 |
173 | .spinner svg {
174 | background: #f2f2f2;
175 | border-radius: 50%
176 | }
177 |
178 | @-webkit-keyframes spin {
179 | 0% {
180 | -webkit-transform: rotate(0deg)
181 | }
182 | 100% {
183 | -webkit-transform: rotate(360deg)
184 | }
185 | }
186 |
187 | .rolling {
188 | position: absolute;
189 | animation: move 6s ease-out;
190 | filter: drop-shadow(4px 4px 4px #999);
191 | }
192 |
193 | @-webkit-keyframes move {
194 | 0% {
195 | left: -5%;
196 | }
197 | 100% {
198 | left: 105%;
199 | }
200 | }
201 |
202 | @keyframes pulse-icon {
203 | from {
204 | text-shadow: 1px 2px 2px rgba(0, 0, 0, 0.25);
205 | }
206 | to {
207 | text-shadow: 2px 4px 4px rgba(0, 0, 0, 0.55);
208 | }
209 | }
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Notifications, { notify } from 'react-notify-toast'
3 | import Spinner from './Spinner'
4 | import Images from './Images'
5 | import Buttons from './Buttons'
6 | import WakeUp from './WakeUp'
7 | import Footer from './Footer'
8 | import { API_URL } from './config'
9 | import './App.css'
10 |
11 | const toastColor = {
12 | background: '#505050',
13 | text: '#fff'
14 | }
15 |
16 | export default class App extends Component {
17 |
18 | state = {
19 | loading: true,
20 | uploading: false,
21 | images: []
22 | }
23 |
24 | componentDidMount() {
25 | fetch(`${API_URL}/wake-up`)
26 | .then(res => {
27 | if (res.ok) {
28 | return this.setState({ loading: false })
29 | }
30 | const msg = 'Something is went wrong with Heroku'
31 | this.toast(msg, 'custom', 2000, toastColor)
32 | })
33 | }
34 |
35 | toast = notify.createShowQueue()
36 |
37 | onChange = e => {
38 | const errs = []
39 | const files = Array.from(e.target.files)
40 |
41 | if (files.length > 3) {
42 | const msg = 'Only 3 images can be uploaded at a time'
43 | return this.toast(msg, 'custom', 2000, toastColor)
44 | }
45 |
46 | const formData = new FormData()
47 | const types = ['image/png', 'image/jpeg', 'image/gif']
48 |
49 | files.forEach((file, i) => {
50 |
51 | if (types.every(type => file.type !== type)) {
52 | errs.push(`'${file.type}' is not a supported format`)
53 | }
54 |
55 | if (file.size > 150000) {
56 | errs.push(`'${file.name}' is too large, please pick a smaller file`)
57 | }
58 |
59 | formData.append(i, file)
60 | })
61 |
62 | if (errs.length) {
63 | return errs.forEach(err => this.toast(err, 'custom', 2000, toastColor))
64 | }
65 |
66 | this.setState({ uploading: true })
67 |
68 | fetch(`${API_URL}/image-upload`, {
69 | method: 'POST',
70 | body: formData
71 | })
72 | .then(res => {
73 | if (!res.ok) {
74 | throw res
75 | }
76 | return res.json()
77 | })
78 | .then(images => {
79 | this.setState({
80 | uploading: false,
81 | images
82 | })
83 | })
84 | .catch(err => {
85 | err.json().then(e => {
86 | this.toast(e.message, 'custom', 2000, toastColor)
87 | this.setState({ uploading: false })
88 | })
89 | })
90 | }
91 |
92 | filter = id => {
93 | return this.state.images.filter(image => image.public_id !== id)
94 | }
95 |
96 | removeImage = id => {
97 | this.setState({ images: this.filter(id) })
98 | }
99 |
100 | onError = id => {
101 | this.toast('Oops, something went wrong', 'custom', 2000, toastColor)
102 | this.setState({ images: this.filter(id) })
103 | }
104 |
105 | render() {
106 | const { loading, uploading, images } = this.state
107 |
108 | const content = () => {
109 | switch(true) {
110 | case loading:
111 | return
112 | case uploading:
113 | return
114 | case images.length > 0:
115 | return
120 | default:
121 | return
122 | }
123 | }
124 |
125 | return (
126 |
127 |
128 |
129 | {content()}
130 |
131 |
132 |
133 | )
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/client/src/Buttons.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import { faImages, faImage } from '@fortawesome/free-solid-svg-icons'
4 |
5 | export default props =>
6 |
--------------------------------------------------------------------------------
/client/src/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import { faMediumM, faGithub } from '@fortawesome/fontawesome-free-brands'
4 |
5 | export default () => (
6 |
22 | )
--------------------------------------------------------------------------------
/client/src/Images.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import { faTimesCircle } from '@fortawesome/free-solid-svg-icons'
4 |
5 | export default props =>
6 | props.images.map((image, i) =>
7 |
8 |
props.removeImage(image.public_id)}
10 | className='delete'
11 | >
12 |
13 |
14 |

props.onError(image.public_id)}
18 | />
19 |
20 | )
--------------------------------------------------------------------------------
/client/src/Spinner.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import { faBowlingBall } from '@fortawesome/free-solid-svg-icons'
4 |
5 | export default () =>
6 |
--------------------------------------------------------------------------------
/client/src/WakeUp.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default () =>
4 |
5 |
Heroku is spinning up, one moment please...
6 |
12 |
--------------------------------------------------------------------------------
/client/src/config.js:
--------------------------------------------------------------------------------
1 | export const API_URL = process.env.NODE_ENV === 'production'
2 | ? 'https://react-image-upload.herokuapp.com'
3 | : 'http://localhost:8080'
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 | import registerServiceWorker from './registerServiceWorker'
5 |
6 | ReactDOM.render(, document.getElementById('root'))
7 | registerServiceWorker()
8 |
--------------------------------------------------------------------------------
/client/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | .env
--------------------------------------------------------------------------------
/server/config.js:
--------------------------------------------------------------------------------
1 | exports.CLIENT_ORIGIN = process.env.NODE_ENV === 'production'
2 | ? 'https://react-image-upload.surge.sh'
3 | : 'http://localhost:3000'
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-image-upload-server",
3 | "version": "0.0.1",
4 | "description": "Image upload example with React",
5 | "main": "server.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "dev": "nodemon server"
9 | },
10 | "author": "jesse heaslip",
11 | "license": "MIT",
12 | "dependencies": {
13 | "cloudinary": "^1.11.0",
14 | "cors": "^2.8.4",
15 | "dotenv": "^6.0.0",
16 | "express": "^4.16.3",
17 | "express-form-data": "^2.0.6"
18 | },
19 | "devDependencies": {
20 | "nodemon": "^1.18.4"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const express = require('express')
3 | const cloudinary = require('cloudinary')
4 | const formData = require('express-form-data')
5 | const cors = require('cors')
6 | const { CLIENT_ORIGIN } = require('./config')
7 |
8 | const app = express()
9 |
10 | cloudinary.config({
11 | cloud_name: process.env.CLOUD_NAME,
12 | api_key: process.env.API_KEY,
13 | api_secret: process.env.API_SECRET
14 | })
15 |
16 | app.use(cors({
17 | origin: CLIENT_ORIGIN
18 | }))
19 |
20 | app.use(formData.parse())
21 |
22 | app.get('/wake-up', (req, res) => res.send('👌'))
23 |
24 | app.post('/image-upload', (req, res) => {
25 |
26 | const values = Object.values(req.files)
27 | const promises = values.map(image => cloudinary.uploader.upload(image.path))
28 |
29 | Promise
30 | .all(promises)
31 | .then(results => res.json(results))
32 | .catch((err) => res.status(400).json(err))
33 | })
34 |
35 | app.listen(process.env.PORT || 8080, () => console.log('👍'))
--------------------------------------------------------------------------------