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);