From c677c3759783e8cd4f007d46d396ed87b651f7e0 Mon Sep 17 00:00:00 2001 From: catarak Date: Fri, 15 Jul 2016 19:05:18 -0400 Subject: [PATCH] get dropzone to render --- client/modules/IDE/actions/files.js | 1 - client/modules/IDE/actions/uploader.js | 51 +++ client/modules/IDE/components/FileUploader.js | 35 ++ client/modules/IDE/components/NewFileModal.js | 4 + client/styles/components/_modal.scss | 12 +- client/styles/main.scss | 1 + client/styles/vendors/_dropzone.scss | 388 ++++++++++++++++++ package.json | 3 + server/controllers/aws.controller.js | 28 ++ server/routes/aws.routes.js | 8 + server/server.js | 2 + 11 files changed, 531 insertions(+), 2 deletions(-) create mode 100644 client/modules/IDE/actions/uploader.js create mode 100644 client/modules/IDE/components/FileUploader.js create mode 100644 client/styles/vendors/_dropzone.scss create mode 100644 server/controllers/aws.controller.js create mode 100644 server/routes/aws.routes.js diff --git a/client/modules/IDE/actions/files.js b/client/modules/IDE/actions/files.js index 1eead2ae..ea8f7078 100644 --- a/client/modules/IDE/actions/files.js +++ b/client/modules/IDE/actions/files.js @@ -11,7 +11,6 @@ export function updateFileContent(name, content) { }; } -// TODO make req to server export function createFile(formProps) { return (dispatch, getState) => { const state = getState(); diff --git a/client/modules/IDE/actions/uploader.js b/client/modules/IDE/actions/uploader.js new file mode 100644 index 00000000..4f62901d --- /dev/null +++ b/client/modules/IDE/actions/uploader.js @@ -0,0 +1,51 @@ +import axios from 'axios'; + +const s3Bucket = 'http://p5js-web-editor-test.s3.amazonaws.com/'; +const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; + +export function dropzoneAcceptCallback(file, done) { + file.postData = []; // eslint-disable-line + axios.post(`${ROOT_URL}/S3/sign`, { + name: file.name, + type: file.type, + size: file.size, + _csrf: document.findElementById('__createPostToken').value + }, + { + withCredentials: true + }) + .then(response => { + file.custom_status = 'ready'; // eslint-disable-line + file.postData = response.data; // eslint-disable-line + file.s3 = response.data.key; // eslint-disable-line + file.previewTemplate.className += ' uploading'; // eslint-disable-line + done(); + }) + .catch(response => { + file.custom_status = 'rejected'; // eslint-disable-line + if (response.data.responseText && response.data.responseText.message) { + done(response.data.responseText.message); + } + done('error preparing the upload'); + }); +} + +export function dropzoneSendingCallback(file, xhr, formData) { + Object.keys(file.postData).forEach(key => { + formData.append(key, file.postData[key]); + }); + formData.append('Content-type', ''); + formData.append('Content-length', ''); + formData.append('acl', 'public-read'); +} + +export function dropzoneCompleteCallback(file) { + let inputHidden = '`; + document.findElementById('createPost').appendChild(inputHidden); +} diff --git a/client/modules/IDE/components/FileUploader.js b/client/modules/IDE/components/FileUploader.js new file mode 100644 index 00000000..19ae737a --- /dev/null +++ b/client/modules/IDE/components/FileUploader.js @@ -0,0 +1,35 @@ +import React from 'react'; +import Dropzone from 'dropzone'; +const s3Bucket = 'http://p5js-web-editor-test.s3.amazonaws.com/'; +import { dropzoneAcceptCallback, + dropzoneSendingCallback, + dropzoneCompleteCallback } from '../actions/uploader'; + +class FileUploader extends React.Component { + componentDidMount() { + Dropzone.options.uploader = { + url: s3Bucket, + method: 'post', + autoProcessQueue: true, + clickable: true, + maxFiles: 1, + parallelUploads: 1, + maxFilesize: 10, // in mb + maxThumbnailFilesize: 8, // 3MB + thumbnailWidth: 150, + thumbnailHeight: 150, + acceptedMimeTypes: 'image/bmp,image/gif,image/jpg,image/jpeg,image/png', + accept: dropzoneAcceptCallback, + sending: dropzoneSendingCallback, + complete: dropzoneCompleteCallback + }; + } + + render() { + return ( +
+ ); + } +} + +export default FileUploader; diff --git a/client/modules/IDE/components/NewFileModal.js b/client/modules/IDE/components/NewFileModal.js index f24ae645..0c299203 100644 --- a/client/modules/IDE/components/NewFileModal.js +++ b/client/modules/IDE/components/NewFileModal.js @@ -7,6 +7,8 @@ import classNames from 'classnames'; import InlineSVG from 'react-inlinesvg'; const exitUrl = require('../../../images/exit.svg'); +import FileUploader from './FileUploader'; + // At some point this will probably be generalized to a generic modal // in which you can insert different content // but for now, let's just make this work @@ -26,6 +28,8 @@ function NewFileModal(props) { +

OR

+ ); diff --git a/client/styles/components/_modal.scss b/client/styles/components/_modal.scss index 11f79db3..7831eaa7 100644 --- a/client/styles/components/_modal.scss +++ b/client/styles/components/_modal.scss @@ -12,7 +12,7 @@ .modal-content { border: 1px solid $light-modal-border-color; background-color: $light-button-background-color; - height: #{150 / $base-font-size}rem; + height: #{400 / $base-font-size}rem; width: #{400 / $base-font-size}rem; padding: #{20 / $base-font-size}rem; } @@ -33,4 +33,14 @@ .new-file-form__name-input { margin-right: #{10 / $base-font-size}rem; +} + +.modal__divider { + text-align: center; + margin: #{20 / $base-font-size}rem 0; +} + +.uploader { + height: #{200 / $base-font-size}rem; + width: 100%; } \ No newline at end of file diff --git a/client/styles/main.scss b/client/styles/main.scss index 64dc92ab..f93a6f35 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -6,6 +6,7 @@ @import 'vendors/codemirror'; @import 'vendors/lint'; +@import 'vendors/dropzone'; @import 'components/p5-widget-codemirror-theme'; @import 'components/editor'; diff --git a/client/styles/vendors/_dropzone.scss b/client/styles/vendors/_dropzone.scss new file mode 100644 index 00000000..0494d1cc --- /dev/null +++ b/client/styles/vendors/_dropzone.scss @@ -0,0 +1,388 @@ +/* + * The MIT License + * Copyright (c) 2012 Matias Meno + */ +@-webkit-keyframes passing-through { + 0% { + opacity: 0; + -webkit-transform: translateY(40px); + -moz-transform: translateY(40px); + -ms-transform: translateY(40px); + -o-transform: translateY(40px); + transform: translateY(40px); } + 30%, 70% { + opacity: 1; + -webkit-transform: translateY(0px); + -moz-transform: translateY(0px); + -ms-transform: translateY(0px); + -o-transform: translateY(0px); + transform: translateY(0px); } + 100% { + opacity: 0; + -webkit-transform: translateY(-40px); + -moz-transform: translateY(-40px); + -ms-transform: translateY(-40px); + -o-transform: translateY(-40px); + transform: translateY(-40px); } } +@-moz-keyframes passing-through { + 0% { + opacity: 0; + -webkit-transform: translateY(40px); + -moz-transform: translateY(40px); + -ms-transform: translateY(40px); + -o-transform: translateY(40px); + transform: translateY(40px); } + 30%, 70% { + opacity: 1; + -webkit-transform: translateY(0px); + -moz-transform: translateY(0px); + -ms-transform: translateY(0px); + -o-transform: translateY(0px); + transform: translateY(0px); } + 100% { + opacity: 0; + -webkit-transform: translateY(-40px); + -moz-transform: translateY(-40px); + -ms-transform: translateY(-40px); + -o-transform: translateY(-40px); + transform: translateY(-40px); } } +@keyframes passing-through { + 0% { + opacity: 0; + -webkit-transform: translateY(40px); + -moz-transform: translateY(40px); + -ms-transform: translateY(40px); + -o-transform: translateY(40px); + transform: translateY(40px); } + 30%, 70% { + opacity: 1; + -webkit-transform: translateY(0px); + -moz-transform: translateY(0px); + -ms-transform: translateY(0px); + -o-transform: translateY(0px); + transform: translateY(0px); } + 100% { + opacity: 0; + -webkit-transform: translateY(-40px); + -moz-transform: translateY(-40px); + -ms-transform: translateY(-40px); + -o-transform: translateY(-40px); + transform: translateY(-40px); } } +@-webkit-keyframes slide-in { + 0% { + opacity: 0; + -webkit-transform: translateY(40px); + -moz-transform: translateY(40px); + -ms-transform: translateY(40px); + -o-transform: translateY(40px); + transform: translateY(40px); } + 30% { + opacity: 1; + -webkit-transform: translateY(0px); + -moz-transform: translateY(0px); + -ms-transform: translateY(0px); + -o-transform: translateY(0px); + transform: translateY(0px); } } +@-moz-keyframes slide-in { + 0% { + opacity: 0; + -webkit-transform: translateY(40px); + -moz-transform: translateY(40px); + -ms-transform: translateY(40px); + -o-transform: translateY(40px); + transform: translateY(40px); } + 30% { + opacity: 1; + -webkit-transform: translateY(0px); + -moz-transform: translateY(0px); + -ms-transform: translateY(0px); + -o-transform: translateY(0px); + transform: translateY(0px); } } +@keyframes slide-in { + 0% { + opacity: 0; + -webkit-transform: translateY(40px); + -moz-transform: translateY(40px); + -ms-transform: translateY(40px); + -o-transform: translateY(40px); + transform: translateY(40px); } + 30% { + opacity: 1; + -webkit-transform: translateY(0px); + -moz-transform: translateY(0px); + -ms-transform: translateY(0px); + -o-transform: translateY(0px); + transform: translateY(0px); } } +@-webkit-keyframes pulse { + 0% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + -o-transform: scale(1); + transform: scale(1); } + 10% { + -webkit-transform: scale(1.1); + -moz-transform: scale(1.1); + -ms-transform: scale(1.1); + -o-transform: scale(1.1); + transform: scale(1.1); } + 20% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + -o-transform: scale(1); + transform: scale(1); } } +@-moz-keyframes pulse { + 0% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + -o-transform: scale(1); + transform: scale(1); } + 10% { + -webkit-transform: scale(1.1); + -moz-transform: scale(1.1); + -ms-transform: scale(1.1); + -o-transform: scale(1.1); + transform: scale(1.1); } + 20% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + -o-transform: scale(1); + transform: scale(1); } } +@keyframes pulse { + 0% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + -o-transform: scale(1); + transform: scale(1); } + 10% { + -webkit-transform: scale(1.1); + -moz-transform: scale(1.1); + -ms-transform: scale(1.1); + -o-transform: scale(1.1); + transform: scale(1.1); } + 20% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + -o-transform: scale(1); + transform: scale(1); } } +.dropzone, .dropzone * { + box-sizing: border-box; } + +.dropzone { + min-height: 150px; + border: 2px solid rgba(0, 0, 0, 0.3); + background: white; + padding: 20px 20px; } + .dropzone.dz-clickable { + cursor: pointer; } + .dropzone.dz-clickable * { + cursor: default; } + .dropzone.dz-clickable .dz-message, .dropzone.dz-clickable .dz-message * { + cursor: pointer; } + .dropzone.dz-started .dz-message { + display: none; } + .dropzone.dz-drag-hover { + border-style: solid; } + .dropzone.dz-drag-hover .dz-message { + opacity: 0.5; } + .dropzone .dz-message { + text-align: center; + margin: 2em 0; } + .dropzone .dz-preview { + position: relative; + display: inline-block; + vertical-align: top; + margin: 16px; + min-height: 100px; } + .dropzone .dz-preview:hover { + z-index: 1000; } + .dropzone .dz-preview:hover .dz-details { + opacity: 1; } + .dropzone .dz-preview.dz-file-preview .dz-image { + border-radius: 20px; + background: #999; + background: linear-gradient(to bottom, #eee, #ddd); } + .dropzone .dz-preview.dz-file-preview .dz-details { + opacity: 1; } + .dropzone .dz-preview.dz-image-preview { + background: white; } + .dropzone .dz-preview.dz-image-preview .dz-details { + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -ms-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; } + .dropzone .dz-preview .dz-remove { + font-size: 14px; + text-align: center; + display: block; + cursor: pointer; + border: none; } + .dropzone .dz-preview .dz-remove:hover { + text-decoration: underline; } + .dropzone .dz-preview:hover .dz-details { + opacity: 1; } + .dropzone .dz-preview .dz-details { + z-index: 20; + position: absolute; + top: 0; + left: 0; + opacity: 0; + font-size: 13px; + min-width: 100%; + max-width: 100%; + padding: 2em 1em; + text-align: center; + color: rgba(0, 0, 0, 0.9); + line-height: 150%; } + .dropzone .dz-preview .dz-details .dz-size { + margin-bottom: 1em; + font-size: 16px; } + .dropzone .dz-preview .dz-details .dz-filename { + white-space: nowrap; } + .dropzone .dz-preview .dz-details .dz-filename:hover span { + border: 1px solid rgba(200, 200, 200, 0.8); + background-color: rgba(255, 255, 255, 0.8); } + .dropzone .dz-preview .dz-details .dz-filename:not(:hover) { + overflow: hidden; + text-overflow: ellipsis; } + .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span { + border: 1px solid transparent; } + .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span { + background-color: rgba(255, 255, 255, 0.4); + padding: 0 0.4em; + border-radius: 3px; } + .dropzone .dz-preview:hover .dz-image img { + -webkit-transform: scale(1.05, 1.05); + -moz-transform: scale(1.05, 1.05); + -ms-transform: scale(1.05, 1.05); + -o-transform: scale(1.05, 1.05); + transform: scale(1.05, 1.05); + -webkit-filter: blur(8px); + filter: blur(8px); } + .dropzone .dz-preview .dz-image { + border-radius: 20px; + overflow: hidden; + width: 120px; + height: 120px; + position: relative; + display: block; + z-index: 10; } + .dropzone .dz-preview .dz-image img { + display: block; } + .dropzone .dz-preview.dz-success .dz-success-mark { + -webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); + -moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); + -ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); + -o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); + animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); } + .dropzone .dz-preview.dz-error .dz-error-mark { + opacity: 1; + -webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); + -moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); + -ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); + -o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); + animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); } + .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark { + pointer-events: none; + opacity: 0; + z-index: 500; + position: absolute; + display: block; + top: 50%; + left: 50%; + margin-left: -27px; + margin-top: -27px; } + .dropzone .dz-preview .dz-success-mark svg, .dropzone .dz-preview .dz-error-mark svg { + display: block; + width: 54px; + height: 54px; } + .dropzone .dz-preview.dz-processing .dz-progress { + opacity: 1; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; } + .dropzone .dz-preview.dz-complete .dz-progress { + opacity: 0; + -webkit-transition: opacity 0.4s ease-in; + -moz-transition: opacity 0.4s ease-in; + -ms-transition: opacity 0.4s ease-in; + -o-transition: opacity 0.4s ease-in; + transition: opacity 0.4s ease-in; } + .dropzone .dz-preview:not(.dz-processing) .dz-progress { + -webkit-animation: pulse 6s ease infinite; + -moz-animation: pulse 6s ease infinite; + -ms-animation: pulse 6s ease infinite; + -o-animation: pulse 6s ease infinite; + animation: pulse 6s ease infinite; } + .dropzone .dz-preview .dz-progress { + opacity: 1; + z-index: 1000; + pointer-events: none; + position: absolute; + height: 16px; + left: 50%; + top: 50%; + margin-top: -8px; + width: 80px; + margin-left: -40px; + background: rgba(255, 255, 255, 0.9); + -webkit-transform: scale(1); + border-radius: 8px; + overflow: hidden; } + .dropzone .dz-preview .dz-progress .dz-upload { + background: #333; + background: linear-gradient(to bottom, #666, #444); + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 0; + -webkit-transition: width 300ms ease-in-out; + -moz-transition: width 300ms ease-in-out; + -ms-transition: width 300ms ease-in-out; + -o-transition: width 300ms ease-in-out; + transition: width 300ms ease-in-out; } + .dropzone .dz-preview.dz-error .dz-error-message { + display: block; } + .dropzone .dz-preview.dz-error:hover .dz-error-message { + opacity: 1; + pointer-events: auto; } + .dropzone .dz-preview .dz-error-message { + pointer-events: none; + z-index: 1000; + position: absolute; + display: block; + display: none; + opacity: 0; + -webkit-transition: opacity 0.3s ease; + -moz-transition: opacity 0.3s ease; + -ms-transition: opacity 0.3s ease; + -o-transition: opacity 0.3s ease; + transition: opacity 0.3s ease; + border-radius: 8px; + font-size: 13px; + top: 130px; + left: -10px; + width: 140px; + background: #be2626; + background: linear-gradient(to bottom, #be2626, #a92222); + padding: 0.5em 1.2em; + color: white; } + .dropzone .dz-preview .dz-error-message:after { + content: ''; + position: absolute; + top: -6px; + left: 64px; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #be2626; } diff --git a/package.json b/package.json index f92b3565..be925864 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "cookie-parser": "^1.4.1", "csslint": "^0.10.0", "dotenv": "^2.0.0", + "dropzone": "^4.3.0", "escape-string-regexp": "^1.0.5", "eslint-loader": "^1.3.0", "express": "^4.13.4", @@ -77,6 +78,7 @@ "jszip": "^3.0.0", "moment": "^2.14.1", "mongoose": "^4.4.16", + "node-uuid": "^1.4.7", "passport": "^0.3.2", "passport-github": "^1.1.0", "passport-local": "^1.0.0", @@ -88,6 +90,7 @@ "redux": "^3.5.2", "redux-form": "^5.2.5", "redux-thunk": "^2.1.0", + "s3-policy": "^0.2.0", "shortid": "^2.2.6", "srcdoc-polyfill": "^0.2.0", "throttle-debounce": "^1.0.1" diff --git a/server/controllers/aws.controller.js b/server/controllers/aws.controller.js new file mode 100644 index 00000000..69dc81ef --- /dev/null +++ b/server/controllers/aws.controller.js @@ -0,0 +1,28 @@ +import uuid from 'node-uuid'; +import policy from 's3-policy'; + +function getExtension(filename) { + const i = filename.lastIndexOf('.'); + return (i < 0) ? '' : filename.substr(i); +} + +export function signS3(req, res) { + const fileExtension = getExtension(req.body.name), + filename = uuid.v4() + fileExtension, + acl = 'public-read', + p = policy({ + acl: acl, + secret: process.env.AWS_SECRET_KEY, + length: 5000000, // in bytes? + bucket: process.env.S3_BUCKET, + key: filename, + expires: new Date(Date.now() + 60000), + }), + result = { + 'AWSAccessKeyId': process.env.AWS_ACCESS_KEY, + 'key': filename, + 'policy': p.policy, + 'signature': p.signature + }; + return res.json(result); +}; \ No newline at end of file diff --git a/server/routes/aws.routes.js b/server/routes/aws.routes.js new file mode 100644 index 00000000..8f80ca4f --- /dev/null +++ b/server/routes/aws.routes.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import * as AWSController from '../controllers/aws.controller'; + +const router = new Router(); + +router.route('/S3/sign').post(AWSController.signS3); + +export default router; diff --git a/server/server.js b/server/server.js index 3d59d285..22c2c2a5 100644 --- a/server/server.js +++ b/server/server.js @@ -28,6 +28,7 @@ import users from './routes/user.routes'; import sessions from './routes/session.routes'; import projects from './routes/project.routes'; import files from './routes/file.routes'; +import aws from './routes/aws.routes'; import serverRoutes from './routes/server.routes'; // Body parser, cookie parser, sessions, serve public assets @@ -57,6 +58,7 @@ app.use('/api', users); app.use('/api', sessions); app.use('/api', projects); app.use('/api', files); +app.use('/api', aws); // this is supposed to be TEMPORARY -- until i figure out // isomorphic rendering app.use('/', serverRoutes);