From bc69995fb1c2c474cd31956071baa2db34b73d41 Mon Sep 17 00:00:00 2001 From: catarak Date: Tue, 12 Jul 2016 17:38:24 -0400 Subject: [PATCH 01/11] super ugly, but added jslinting --- client/modules/IDE/components/Editor.js | 8 ++- client/styles/main.scss | 1 + client/styles/vendors/_lint.scss | 73 +++++++++++++++++++++++++ package.json | 1 + 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 client/styles/vendors/_lint.scss diff --git a/client/modules/IDE/components/Editor.js b/client/modules/IDE/components/Editor.js index 70ee4030..9be67dff 100644 --- a/client/modules/IDE/components/Editor.js +++ b/client/modules/IDE/components/Editor.js @@ -2,6 +2,10 @@ import React, { PropTypes } from 'react'; import CodeMirror from 'codemirror'; import 'codemirror/mode/javascript/javascript'; import 'codemirror/addon/selection/active-line'; +import 'codemirror/addon/lint/lint'; +import 'codemirror/addon/lint/javascript-lint'; +import { JSHINT } from 'jshint'; +window.JSHINT = JSHINT; class Editor extends React.Component { @@ -12,7 +16,9 @@ class Editor extends React.Component { lineNumbers: true, styleActiveLine: true, mode: 'javascript', - lineWrapping: true + lineWrapping: true, + gutters: ['CodeMirror-lint-markers'], + lint: true }); this._cm.on('change', () => { // eslint-disable-line // this.props.updateFileContent('sketch.js', this._cm.getValue()); diff --git a/client/styles/main.scss b/client/styles/main.scss index c16cff9d..4a20ba94 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -5,6 +5,7 @@ @import 'base/base'; @import 'vendors/codemirror'; +@import 'vendors/lint'; @import 'components/p5-widget-codemirror-theme'; @import 'components/editor'; diff --git a/client/styles/vendors/_lint.scss b/client/styles/vendors/_lint.scss new file mode 100644 index 00000000..414a9a0e --- /dev/null +++ b/client/styles/vendors/_lint.scss @@ -0,0 +1,73 @@ +/* The lint marker gutter */ +.CodeMirror-lint-markers { + width: 16px; +} + +.CodeMirror-lint-tooltip { + background-color: infobackground; + border: 1px solid black; + border-radius: 4px 4px 4px 4px; + color: infotext; + font-family: monospace; + font-size: 10pt; + overflow: hidden; + padding: 2px 5px; + position: fixed; + white-space: pre; + white-space: pre-wrap; + z-index: 100; + max-width: 600px; + opacity: 0; + transition: opacity .4s; + -moz-transition: opacity .4s; + -webkit-transition: opacity .4s; + -o-transition: opacity .4s; + -ms-transition: opacity .4s; +} + +.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { + background-position: left bottom; + background-repeat: repeat-x; +} + +.CodeMirror-lint-mark-error { + background-image: + url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==") + ; +} + +.CodeMirror-lint-mark-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); +} + +.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { + background-position: center center; + background-repeat: no-repeat; + cursor: pointer; + display: inline-block; + height: 16px; + width: 16px; + vertical-align: middle; + position: relative; +} + +.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { + padding-left: 18px; + background-position: top left; + background-repeat: no-repeat; +} + +.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); +} + +.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); +} + +.CodeMirror-lint-marker-multiple { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); + background-repeat: no-repeat; + background-position: right bottom; + width: 100%; height: 100%; +} diff --git a/package.json b/package.json index dca229de..1623aa62 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "eslint-loader": "^1.3.0", "express": "^4.13.4", "express-session": "^1.13.0", + "jshint": "^2.9.2", "moment": "^2.14.1", "mongoose": "^4.4.16", "passport": "^0.3.2", From cbdf68cbd07f4b22d4748f8a39e99ef3e546507d Mon Sep 17 00:00:00 2001 From: catarak Date: Tue, 12 Jul 2016 19:11:07 -0400 Subject: [PATCH 02/11] add styling for error/warning, not for tooltip --- client/styles/components/_editor.scss | 29 +++++++++++++++++++ .../_p5-widget-codemirror-theme.scss | 1 + 2 files changed, 30 insertions(+) diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss index 5209e517..d75fbfbc 100644 --- a/client/styles/components/_editor.scss +++ b/client/styles/components/_editor.scss @@ -19,3 +19,32 @@ .CodeMirror-line { padding-left: #{5 / $base-font-size}rem; } + +.CodeMirror-gutter-wrapper { + right: 100%; + top: 0; + bottom: 0; +} + + +.CodeMirror-lint-marker-warning, .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-multiple { + background-image: none; + width: 70px; + position: absolute; + height: 100%; +} + +.CodeMirror-lint-marker-warning { + background-color: rgb(255, 190, 5); +} + +.CodeMirror-lint-marker-error { + background-color: rgb(255, 95, 82); +} + +.CodeMirror-gutter-elt:not(.CodeMirror-linenumber) { + opacity: 0.3; + width: 70px !important; + height: 100%; + // background-color: rgb(255, 95, 82); +} diff --git a/client/styles/components/_p5-widget-codemirror-theme.scss b/client/styles/components/_p5-widget-codemirror-theme.scss index 1c7fab3e..fe3be0b1 100644 --- a/client/styles/components/_p5-widget-codemirror-theme.scss +++ b/client/styles/components/_p5-widget-codemirror-theme.scss @@ -27,6 +27,7 @@ .cm-s-p5-widget span.cm-error { color: #f00; } .cm-s-p5-widget .CodeMirror-activeline-background { background-color: #e8f2ff; } +// .cm-s-p5-widget .CodeMirror-activeline-gutter { background-color: #e8f2ff; } .cm-s-p5-widget .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; } /* These styles don't seem to be set by CodeMirror's javascript mode. */ From 30992ac2de854de698a68ec2f7ba93b9cb9ccaa0 Mon Sep 17 00:00:00 2001 From: catarak Date: Tue, 12 Jul 2016 19:24:57 -0400 Subject: [PATCH 03/11] fix tooltip styling --- client/styles/components/_editor.scss | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss index d75fbfbc..0964019c 100644 --- a/client/styles/components/_editor.scss +++ b/client/styles/components/_editor.scss @@ -26,7 +26,6 @@ bottom: 0; } - .CodeMirror-lint-marker-warning, .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-multiple { background-image: none; width: 70px; @@ -34,6 +33,11 @@ height: 100%; } +.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { + background-image: none; + padding-left: inherit; +} + .CodeMirror-lint-marker-warning { background-color: rgb(255, 190, 5); } @@ -48,3 +52,11 @@ height: 100%; // background-color: rgb(255, 95, 82); } + +.CodeMirror-lint-tooltip { + font-family: Montserrat, sans-serif; + border-radius: 2px; + border: 1px solid #B9D0E1; + background-color: $light-button-background-color; + +} From 70588cd4876d1868172402f2baf88b35d52c4ca0 Mon Sep 17 00:00:00 2001 From: catarak Date: Wed, 13 Jul 2016 16:13:28 -0400 Subject: [PATCH 04/11] add new file popup, not tested with redux --- client/constants.js | 3 + client/modules/IDE/actions/files.js | 8 +++ client/modules/IDE/actions/ide.js | 12 ++++ client/modules/IDE/components/NewFileForm.js | 28 ++++++++ client/modules/IDE/components/NewFileModal.js | 68 +++++++++++++++++++ client/modules/IDE/components/Sidebar.js | 9 +++ client/modules/IDE/pages/IDEView.js | 13 +++- client/modules/IDE/reducers/files.js | 2 + client/modules/IDE/reducers/ide.js | 7 +- client/styles/abstracts/_variables.scss | 1 + client/styles/base/_base.scss | 3 +- client/styles/components/_editor.scss | 2 +- client/styles/components/_modal.scss | 36 ++++++++++ client/styles/components/_sidebar.scss | 17 +++++ client/styles/main.scss | 1 + 15 files changed, 205 insertions(+), 5 deletions(-) create mode 100644 client/modules/IDE/components/NewFileForm.js create mode 100644 client/modules/IDE/components/NewFileModal.js create mode 100644 client/styles/components/_modal.scss diff --git a/client/constants.js b/client/constants.js index 967df39b..6f5833fd 100644 --- a/client/constants.js +++ b/client/constants.js @@ -30,6 +30,9 @@ export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECTS = 'SET_PROJECTS'; export const SET_SELECTED_FILE = 'SET_SELECTED_FILE'; +export const SHOW_MODAL = 'SHOW_MODAL'; +export const HIDE_MODAL = 'HIDE_MODAL'; +export const CREATE_FILE = 'CREATE_FILE'; // eventually, handle errors more specifically and better export const ERROR = 'ERROR'; diff --git a/client/modules/IDE/actions/files.js b/client/modules/IDE/actions/files.js index f7ebd8f3..8bc0c6c3 100644 --- a/client/modules/IDE/actions/files.js +++ b/client/modules/IDE/actions/files.js @@ -7,3 +7,11 @@ export function updateFileContent(name, content) { content }; } + +// TODO make req to server +export function createFile(name) { + return { + type: ActionTypes.CREATE_FILE, + name + }; +} diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index 0bcbc414..70363669 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -24,3 +24,15 @@ export function setSelectedFile(fileId) { selectedFile: fileId }; } + +export function newFile() { + return { + type: ActionTypes.SHOW_MODAL + }; +} + +export function closeNewFileModal() { + return { + type: ActionTypes.HIDE_MODAL + }; +} diff --git a/client/modules/IDE/components/NewFileForm.js b/client/modules/IDE/components/NewFileForm.js new file mode 100644 index 00000000..c46ca9e5 --- /dev/null +++ b/client/modules/IDE/components/NewFileForm.js @@ -0,0 +1,28 @@ +import React, { PropTypes } from 'react'; + +function NewFileForm(props) { + const { fields: { name }, handleSubmit } = props; + return ( +
+ + + +
+ ); +} + +NewFileForm.propTypes = { + fields: PropTypes.shape({ + name: PropTypes.string.isRequired + }).isRequired, + handleSubmit: PropTypes.func.isRequired, + createFile: PropTypes.func.isRequired +}; + +export default NewFileForm; diff --git a/client/modules/IDE/components/NewFileModal.js b/client/modules/IDE/components/NewFileModal.js new file mode 100644 index 00000000..1072348b --- /dev/null +++ b/client/modules/IDE/components/NewFileModal.js @@ -0,0 +1,68 @@ +import React, { PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { reduxForm } from 'redux-form'; +import NewFileForm from './NewFileForm'; +import * as FileActions from '../actions/files'; +import classNames from 'classnames'; +import InlineSVG from 'react-inlinesvg'; +const exitUrl = require('../../../images/exit.svg'); + +// 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 +function NewFileModal(props) { + const modalClass = classNames({ + modal: true, + 'modal--hidden': !props.isVisible + }); + + return ( +
+
+
+

Add File

+ +
+ +
+
+ ); +} + +NewFileModal.propTypes = { + isVisible: PropTypes.bool.isRequired, + closeModal: PropTypes.func.isRequired +}; + +function mapStateToProps(state) { + return { + file: state.files + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators(FileActions, dispatch); +} + +function validate(formProps) { + const errors = {}; + + if (!formProps.name) { + errors.name = 'Please enter a name'; + } + + // if (formProps.name.match(/(.+\.js$|.+\.css$)/)) { + // errors.name = 'File must be of type JavaScript or CSS.'; + // } + + return errors; +} + + +export default reduxForm({ + form: 'new-file', + fields: ['name'], + validate +}, mapStateToProps, mapDispatchToProps)(NewFileModal); diff --git a/client/modules/IDE/components/Sidebar.js b/client/modules/IDE/components/Sidebar.js index cb8c276f..ffeea432 100644 --- a/client/modules/IDE/components/Sidebar.js +++ b/client/modules/IDE/components/Sidebar.js @@ -4,6 +4,15 @@ import classNames from 'classnames'; function Sidebar(props) { return (
+
+

Sketch Files

+ + + + +
    {props.files.map(file => { let itemClass = classNames({ diff --git a/client/modules/IDE/pages/IDEView.js b/client/modules/IDE/pages/IDEView.js index c7308fa9..edc9043b 100644 --- a/client/modules/IDE/pages/IDEView.js +++ b/client/modules/IDE/pages/IDEView.js @@ -4,6 +4,7 @@ import Sidebar from '../components/Sidebar'; import PreviewFrame from '../components/PreviewFrame'; import Toolbar from '../components/Toolbar'; import Preferences from '../components/Preferences'; +import NewFileModal from '../components/NewFileModal'; import Nav from '../../../components/Nav'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; @@ -58,6 +59,7 @@ class IDEView extends React.Component { files={this.props.files} selectedFile={this.props.selectedFile} setSelectedFile={this.props.setSelectedFile} + newFile={this.props.newFile} /> + ); } @@ -92,7 +98,8 @@ IDEView.propTypes = { createProject: PropTypes.func.isRequired, saveProject: PropTypes.func.isRequired, ide: PropTypes.shape({ - isPlaying: PropTypes.bool.isRequired + isPlaying: PropTypes.bool.isRequired, + modalIsVisible: PropTypes.bool.isRequired }).isRequired, startSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, @@ -125,7 +132,9 @@ IDEView.propTypes = { setSelectedFile: PropTypes.func.isRequired, htmlFile: PropTypes.object.isRequired, jsFiles: PropTypes.array.isRequired, - cssFiles: PropTypes.array.isRequired + cssFiles: PropTypes.array.isRequired, + newFile: PropTypes.func.isRequired, + closeNewFileModal: PropTypes.func.isRequired }; function mapStateToProps(state) { diff --git a/client/modules/IDE/reducers/files.js b/client/modules/IDE/reducers/files.js index db53dbc3..6bc35e1b 100644 --- a/client/modules/IDE/reducers/files.js +++ b/client/modules/IDE/reducers/files.js @@ -64,6 +64,8 @@ const files = (state = initialState, action) => { return [...action.files]; case ActionTypes.SET_PROJECT: return [...action.files]; + case ActionTypes.NEW_FILE: + return [{ name: action.name, content: '' }, ...state.files]; default: return state; } diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index 9c4c5b88..8b283ca0 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -2,7 +2,8 @@ import * as ActionTypes from '../../../constants'; const initialState = { isPlaying: false, - selectedFile: '1' + selectedFile: '1', + modalIsVisible: false }; const ide = (state = initialState, action) => { @@ -17,6 +18,10 @@ const ide = (state = initialState, action) => { case ActionTypes.SET_PROJECT: case ActionTypes.NEW_PROJECT: return Object.assign({}, state, { selectedFile: action.selectedFile }); + case ActionTypes.SHOW_MODAL: + return Object.assign({}, state, { modalIsVisible: true }); + case ActionTypes.HIDE_MODAL: + return Object.assign({}, state, { modalIsVisible: false }); default: return state; } diff --git a/client/styles/abstracts/_variables.scss b/client/styles/abstracts/_variables.scss index a41a2097..aea3f2fb 100644 --- a/client/styles/abstracts/_variables.scss +++ b/client/styles/abstracts/_variables.scss @@ -19,6 +19,7 @@ $light-button-background-active-color: #f10046; $light-button-hover-color: $white; $light-button-active-color: $white; $light-modal-button-background-color: #e6e6e6; +$light-modal-border-color: #B9D0E1; $light-icon-color: #8b8b8b; $light-icon-hover-color: $light-primary-text-color; diff --git a/client/styles/base/_base.scss b/client/styles/base/_base.scss index fda0b662..4f5358bb 100644 --- a/client/styles/base/_base.scss +++ b/client/styles/base/_base.scss @@ -31,8 +31,9 @@ input, button { input { padding: #{5 / $base-font-size}rem; - border-radius: 2px; + // border-radius: 2px; border: 1px solid $input-border-color; + padding: #{10 / $base-font-size}rem; } input[type="submit"] { diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss index 0964019c..c1efcc43 100644 --- a/client/styles/components/_editor.scss +++ b/client/styles/components/_editor.scss @@ -56,7 +56,7 @@ .CodeMirror-lint-tooltip { font-family: Montserrat, sans-serif; border-radius: 2px; - border: 1px solid #B9D0E1; + border: 1px solid $light-modal-border-color; background-color: $light-button-background-color; } diff --git a/client/styles/components/_modal.scss b/client/styles/components/_modal.scss new file mode 100644 index 00000000..22745606 --- /dev/null +++ b/client/styles/components/_modal.scss @@ -0,0 +1,36 @@ +.modal { + position: absolute; + top: #{66 / $base-font-size}rem; + right: #{400 / $base-font-size}rem;; + z-index: 100; +} + +.modal--hidden { + display: none; +} + +.modal-content { + border: 1px solid $light-modal-border-color; + background-color: $light-button-background-color; + height: #{150 / $base-font-size}rem; + width: #{400 / $base-font-size}rem; + padding: #{20 / $base-font-size}rem; +} + +.modal__exit-button { + @extend %icon; +} + +.modal__header { + display: flex; + justify-content: space-between; + margin-bottom: #{20 / $base-font-size}rem; +} + +.new-file-form__name-label { + display: none; +} + +.new-file-form__name-input { + margin-right: #{10 / $base-font-size}rem; +} \ No newline at end of file diff --git a/client/styles/components/_sidebar.scss b/client/styles/components/_sidebar.scss index 38883386..b5f3d8e2 100644 --- a/client/styles/components/_sidebar.scss +++ b/client/styles/components/_sidebar.scss @@ -1,8 +1,25 @@ +.sidebar__header { + padding: #{10 / $base-font-size}rem #{6 / $base-font-size}rem; + display: flex; + justify-content: space-between; + border-top: 1px solid $ide-border-color; +} + +.sidebar__title { + font-size: #{12 / $base-font-size}rem; + display: inline-block; +} + +.sidebar__add { + cursor: pointer; +} + .sidebar__file-list { border-top: 1px solid $ide-border-color; } .sidebar__file-item { + font-size: #{12 / $base-font-size}rem; padding: #{8 / $base-font-size}rem #{20 / $base-font-size}rem; color: $light-inactive-text-color; cursor: pointer; diff --git a/client/styles/main.scss b/client/styles/main.scss index 4a20ba94..64dc92ab 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -16,6 +16,7 @@ @import 'components/login'; @import 'components/sketch-list'; @import 'components/sidebar'; +@import 'components/modal'; @import 'layout/ide'; @import 'layout/sketch-list'; From 4d6e4857ba427d3256b55f22c1a652147c6989f5 Mon Sep 17 00:00:00 2001 From: catarak Date: Wed, 13 Jul 2016 18:53:56 -0400 Subject: [PATCH 05/11] add files, server side, only css and js files --- client/modules/IDE/actions/files.js | 41 +++++++++++++++++-- client/modules/IDE/components/NewFileModal.js | 6 +-- client/modules/IDE/reducers/files.js | 4 +- client/styles/components/_modal.scss | 2 +- server/controllers/file.controller.js | 19 +++++++++ server/routes/file.routes.js | 8 ++++ server/server.js | 2 + 7 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 server/controllers/file.controller.js create mode 100644 server/routes/file.routes.js diff --git a/client/modules/IDE/actions/files.js b/client/modules/IDE/actions/files.js index 8bc0c6c3..1eead2ae 100644 --- a/client/modules/IDE/actions/files.js +++ b/client/modules/IDE/actions/files.js @@ -1,4 +1,7 @@ import * as ActionTypes from '../../../constants'; +import axios from 'axios'; + +const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; export function updateFileContent(name, content) { return { @@ -9,9 +12,39 @@ export function updateFileContent(name, content) { } // TODO make req to server -export function createFile(name) { - return { - type: ActionTypes.CREATE_FILE, - name +export function createFile(formProps) { + return (dispatch, getState) => { + const state = getState(); + if (state.project.id) { + const postParams = { + name: formProps.name + }; + axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true }) + .then(response => { + dispatch({ + type: ActionTypes.CREATE_FILE, + ...response.data + }); + }) + .catch(response => dispatch({ + type: ActionTypes.ERROR, + error: response.data + })); + } else { + let maxFileId = 0; + state.files.forEach(file => { + if (parseInt(file.id, 10) > maxFileId) { + maxFileId = parseInt(file.id, 10); + } + }); + dispatch({ + type: ActionTypes.CREATE_FILE, + name: formProps.name, + id: `${maxFileId + 1}` + }); + dispatch({ + type: ActionTypes.HIDE_MODAL + }); + } }; } diff --git a/client/modules/IDE/components/NewFileModal.js b/client/modules/IDE/components/NewFileModal.js index 1072348b..f24ae645 100644 --- a/client/modules/IDE/components/NewFileModal.js +++ b/client/modules/IDE/components/NewFileModal.js @@ -51,12 +51,10 @@ function validate(formProps) { if (!formProps.name) { errors.name = 'Please enter a name'; + } else if (!formProps.name.match(/(.+\.js$|.+\.css$)/)) { + errors.name = 'File must be of type JavaScript or CSS.'; } - // if (formProps.name.match(/(.+\.js$|.+\.css$)/)) { - // errors.name = 'File must be of type JavaScript or CSS.'; - // } - return errors; } diff --git a/client/modules/IDE/reducers/files.js b/client/modules/IDE/reducers/files.js index 6bc35e1b..2b32bc04 100644 --- a/client/modules/IDE/reducers/files.js +++ b/client/modules/IDE/reducers/files.js @@ -64,8 +64,8 @@ const files = (state = initialState, action) => { return [...action.files]; case ActionTypes.SET_PROJECT: return [...action.files]; - case ActionTypes.NEW_FILE: - return [{ name: action.name, content: '' }, ...state.files]; + case ActionTypes.CREATE_FILE: + return [...state, { name: action.name, id: action.id, content: '' }]; default: return state; } diff --git a/client/styles/components/_modal.scss b/client/styles/components/_modal.scss index 22745606..11f79db3 100644 --- a/client/styles/components/_modal.scss +++ b/client/styles/components/_modal.scss @@ -1,7 +1,7 @@ .modal { position: absolute; top: #{66 / $base-font-size}rem; - right: #{400 / $base-font-size}rem;; + right: #{400 / $base-font-size}rem; z-index: 100; } diff --git a/server/controllers/file.controller.js b/server/controllers/file.controller.js new file mode 100644 index 00000000..73058b07 --- /dev/null +++ b/server/controllers/file.controller.js @@ -0,0 +1,19 @@ +import Project from '../models/Project' + +// Bug -> timestamps don't get created, but it seems like this will +// be fixed in mongoose soon +// https://github.com/Automattic/mongoose/issues/4049 +export function createFile(req, res) { + Project.findByIdAndUpdate(req.params.project_id, + { + $push: { + 'files': req.body + } + }, + { + new: true + }, (err, updatedProject) => { + if (err) { return res.json({ success: false }); } + return res.json(updatedProject.files[updatedProject.files.length - 1]); + }); +} \ No newline at end of file diff --git a/server/routes/file.routes.js b/server/routes/file.routes.js new file mode 100644 index 00000000..c7035ac6 --- /dev/null +++ b/server/routes/file.routes.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import * as FileController from '../controllers/file.controller'; + +const router = new Router(); + +router.route('/projects/:project_id/files').post(FileController.createFile); + +export default router; \ No newline at end of file diff --git a/server/server.js b/server/server.js index 99879244..3d59d285 100644 --- a/server/server.js +++ b/server/server.js @@ -27,6 +27,7 @@ import serverConfig from './config'; 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 serverRoutes from './routes/server.routes'; // Body parser, cookie parser, sessions, serve public assets @@ -55,6 +56,7 @@ app.use(passport.session()); app.use('/api', users); app.use('/api', sessions); app.use('/api', projects); +app.use('/api', files); // this is supposed to be TEMPORARY -- until i figure out // isomorphic rendering app.use('/', serverRoutes); From c29f5aee6880236430e4eaab65791697cc12b3a2 Mon Sep 17 00:00:00 2001 From: catarak Date: Wed, 13 Jul 2016 19:52:50 -0400 Subject: [PATCH 06/11] add syntax highlighting for different file types, and linting for html and css --- client/modules/IDE/components/Editor.js | 17 +++++++++++++++++ package.json | 2 ++ 2 files changed, 19 insertions(+) diff --git a/client/modules/IDE/components/Editor.js b/client/modules/IDE/components/Editor.js index 9be67dff..ccb6d52e 100644 --- a/client/modules/IDE/components/Editor.js +++ b/client/modules/IDE/components/Editor.js @@ -1,11 +1,19 @@ import React, { PropTypes } from 'react'; import CodeMirror from 'codemirror'; import 'codemirror/mode/javascript/javascript'; +import 'codemirror/mode/css/css'; +import 'codemirror/mode/htmlmixed/htmlmixed'; import 'codemirror/addon/selection/active-line'; import 'codemirror/addon/lint/lint'; import 'codemirror/addon/lint/javascript-lint'; +import 'codemirror/addon/lint/css-lint'; +import 'codemirror/addon/lint/html-lint'; import { JSHINT } from 'jshint'; window.JSHINT = JSHINT; +import { CSSLint } from 'csslint'; +window.CSSLint = CSSLint; +import { HTMLHint } from 'htmlhint'; +window.HTMLHint = HTMLHint; class Editor extends React.Component { @@ -43,6 +51,15 @@ class Editor extends React.Component { if (this.props.isTabIndent !== prevProps.isTabIndent) { this._cm.setOption('indentWithTabs', this.props.isTabIndent); } + if (this.props.file.name !== prevProps.name) { + if (this.props.file.name.match(/.+\.js$/)) { + this._cm.setOption('mode', 'javascript'); + } else if (this.props.file.name.match(/.+\.css$/)) { + this._cm.setOption('mode', 'css'); + } else if (this.props.file.name.match(/.+\.html$/)) { + this._cm.setOption('mode', 'htmlmixed'); + } + } } componentWillUnmount() { diff --git a/package.json b/package.json index 1623aa62..23b2dd8d 100644 --- a/package.json +++ b/package.json @@ -65,11 +65,13 @@ "codemirror": "^5.14.2", "connect-mongo": "^1.2.0", "cookie-parser": "^1.4.1", + "csslint": "^0.10.0", "dotenv": "^2.0.0", "escape-string-regexp": "^1.0.5", "eslint-loader": "^1.3.0", "express": "^4.13.4", "express-session": "^1.13.0", + "htmlhint": "^0.9.13", "jshint": "^2.9.2", "moment": "^2.14.1", "mongoose": "^4.4.16", From d9a11cea4c09e1aab498c88d22a39a6cfe1dbad2 Mon Sep 17 00:00:00 2001 From: catarak Date: Wed, 13 Jul 2016 21:50:59 -0400 Subject: [PATCH 07/11] add debounce to editor input --- client/modules/IDE/components/Editor.js | 12 +++++++++--- package.json | 3 ++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/client/modules/IDE/components/Editor.js b/client/modules/IDE/components/Editor.js index ccb6d52e..1e79bd21 100644 --- a/client/modules/IDE/components/Editor.js +++ b/client/modules/IDE/components/Editor.js @@ -15,6 +15,8 @@ window.CSSLint = CSSLint; import { HTMLHint } from 'htmlhint'; window.HTMLHint = HTMLHint; +import { debounce } from 'throttle-debounce'; + class Editor extends React.Component { componentDidMount() { @@ -28,10 +30,14 @@ class Editor extends React.Component { gutters: ['CodeMirror-lint-markers'], lint: true }); - this._cm.on('change', () => { // eslint-disable-line - // this.props.updateFileContent('sketch.js', this._cm.getValue()); + this._cm.on('change', debounce(200, () => { this.props.updateFileContent(this.props.file.name, this._cm.getValue()); - }); + })); + // this._cm.on('change', () => { // eslint-disable-line + // // this.props.updateFileContent('sketch.js', this._cm.getValue()); + // throttle(1000, () => console.log('debounce is working!')); + // this.props.updateFileContent(this.props.file.name, this._cm.getValue()); + // }); this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; this._cm.setOption('indentWithTabs', this.props.isTabIndent); this._cm.setOption('tabSize', this.props.indentationAmount); diff --git a/package.json b/package.json index 23b2dd8d..d55afffc 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "redux-form": "^5.2.5", "redux-thunk": "^2.1.0", "shortid": "^2.2.6", - "srcdoc-polyfill": "^0.2.0" + "srcdoc-polyfill": "^0.2.0", + "throttle-debounce": "^1.0.1" } } From accf8e25040425fb0ae412537b517bc17340cbd5 Mon Sep 17 00:00:00 2001 From: catarak Date: Thu, 14 Jul 2016 12:47:54 -0400 Subject: [PATCH 08/11] add expand/contract sidebar --- client/constants.js | 3 ++ client/images/left-arrow.svg | 12 ++++++ client/images/plus-icon.svg | 16 ++++++++ client/images/right-arrow.svg | 12 ++++++ client/modules/IDE/actions/ide.js | 12 ++++++ client/modules/IDE/components/Sidebar.js | 36 +++++++++++++---- client/modules/IDE/pages/IDEView.js | 10 ++++- client/modules/IDE/reducers/ide.js | 7 +++- client/styles/abstracts/_placeholders.scss | 1 + client/styles/components/_sidebar.scss | 46 +++++++++++++++++++++- client/styles/layout/_ide.scss | 5 ++- 11 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 client/images/left-arrow.svg create mode 100644 client/images/plus-icon.svg create mode 100644 client/images/right-arrow.svg diff --git a/client/constants.js b/client/constants.js index 6f5833fd..ed64f2b0 100644 --- a/client/constants.js +++ b/client/constants.js @@ -34,5 +34,8 @@ export const SHOW_MODAL = 'SHOW_MODAL'; export const HIDE_MODAL = 'HIDE_MODAL'; export const CREATE_FILE = 'CREATE_FILE'; +export const EXPAND_SIDEBAR = 'EXPAND_SIDEBAR'; +export const COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR'; + // eventually, handle errors more specifically and better export const ERROR = 'ERROR'; diff --git a/client/images/left-arrow.svg b/client/images/left-arrow.svg new file mode 100644 index 00000000..22f17dcb --- /dev/null +++ b/client/images/left-arrow.svg @@ -0,0 +1,12 @@ + + + + arrow shape copy + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/client/images/plus-icon.svg b/client/images/plus-icon.svg new file mode 100644 index 00000000..0b9347f2 --- /dev/null +++ b/client/images/plus-icon.svg @@ -0,0 +1,16 @@ + + + + close shape + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/right-arrow.svg b/client/images/right-arrow.svg new file mode 100644 index 00000000..6d62c369 --- /dev/null +++ b/client/images/right-arrow.svg @@ -0,0 +1,12 @@ + + + + arrow shape copy + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index 70363669..a0b48434 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -36,3 +36,15 @@ export function closeNewFileModal() { type: ActionTypes.HIDE_MODAL }; } + +export function expandSidebar() { + return { + type: ActionTypes.EXPAND_SIDEBAR + }; +} + +export function collapseSidebar() { + return { + type: ActionTypes.COLLAPSE_SIDEBAR + }; +} diff --git a/client/modules/IDE/components/Sidebar.js b/client/modules/IDE/components/Sidebar.js index ffeea432..f1ccf179 100644 --- a/client/modules/IDE/components/Sidebar.js +++ b/client/modules/IDE/components/Sidebar.js @@ -1,17 +1,39 @@ import React, { PropTypes } from 'react'; import classNames from 'classnames'; +import InlineSVG from 'react-inlinesvg'; +const rightArrowUrl = require('../../../images/right-arrow.svg'); +const leftArrowUrl = require('../../../images/left-arrow.svg'); function Sidebar(props) { + const sidebarClass = classNames({ + sidebar: true, + 'sidebar--contracted': !props.isExpanded + }); + return ( -
    +

    Sketch Files

    - - + - +
      {props.files.map(file => { diff --git a/client/modules/IDE/pages/IDEView.js b/client/modules/IDE/pages/IDEView.js index edc9043b..7a94a16d 100644 --- a/client/modules/IDE/pages/IDEView.js +++ b/client/modules/IDE/pages/IDEView.js @@ -60,6 +60,9 @@ class IDEView extends React.Component { selectedFile={this.props.selectedFile} setSelectedFile={this.props.setSelectedFile} newFile={this.props.newFile} + isExpanded={this.props.ide.sidebarIsExpanded} + expandSidebar={this.props.expandSidebar} + collapseSidebar={this.props.collapseSidebar} /> { @@ -22,6 +23,10 @@ const ide = (state = initialState, action) => { return Object.assign({}, state, { modalIsVisible: true }); case ActionTypes.HIDE_MODAL: return Object.assign({}, state, { modalIsVisible: false }); + case ActionTypes.COLLAPSE_SIDEBAR: + return Object.assign({}, state, { sidebarIsExpanded: false }); + case ActionTypes.EXPAND_SIDEBAR: + return Object.assign({}, state, { sidebarIsExpanded: true }); default: return state; } diff --git a/client/styles/abstracts/_placeholders.scss b/client/styles/abstracts/_placeholders.scss index 4b89cf7c..f27caed9 100644 --- a/client/styles/abstracts/_placeholders.scss +++ b/client/styles/abstracts/_placeholders.scss @@ -41,6 +41,7 @@ &:hover { color: $light-icon-hover-color; & g { + opacity: 1; fill: $light-icon-hover-color; } } diff --git a/client/styles/components/_sidebar.scss b/client/styles/components/_sidebar.scss index b5f3d8e2..7919678f 100644 --- a/client/styles/components/_sidebar.scss +++ b/client/styles/components/_sidebar.scss @@ -3,23 +3,37 @@ display: flex; justify-content: space-between; border-top: 1px solid $ide-border-color; + align-items: center; + height: #{47 / $base-font-size}rem; } .sidebar__title { - font-size: #{12 / $base-font-size}rem; + font-size: #{16 / $base-font-size}rem; display: inline-block; + .sidebar--contracted & { + display: none; + } } .sidebar__add { cursor: pointer; + height: #{26 / $base-font-size}rem; + margin-right: #{16 / $base-font-size}rem; + font-size: #{24 / $base-font-size}rem; + .sidebar--contracted & { + display: none; + } } .sidebar__file-list { border-top: 1px solid $ide-border-color; + .sidebar--contracted & { + display: none; + } } .sidebar__file-item { - font-size: #{12 / $base-font-size}rem; + font-size: #{16 / $base-font-size}rem; padding: #{8 / $base-font-size}rem #{20 / $base-font-size}rem; color: $light-inactive-text-color; cursor: pointer; @@ -27,3 +41,31 @@ background-color: $ide-border-color; } } + +.sidebar__contract { + @extend %icon; + height: #{14 / $base-font-size}rem; + & svg { + height: #{14 / $base-font-size}rem; + } + .sidebar--contracted & { + display: none; + } +} + +.sidebar__expand { + @extend %icon; + height: #{14 / $base-font-size}rem; + & svg { + height: #{14 / $base-font-size}rem; + } + display: none; + .sidebar--contracted & { + display: inline-block; + } +} + +.sidebar__icons { + display: flex; + align-items: center; +} diff --git a/client/styles/layout/_ide.scss b/client/styles/layout/_ide.scss index 7f78e871..a37c636d 100644 --- a/client/styles/layout/_ide.scss +++ b/client/styles/layout/_ide.scss @@ -20,5 +20,8 @@ } .sidebar { - width: #{140 / $base-font-size}rem; + width: #{180 / $base-font-size}rem; + &.sidebar--contracted { + width: #{20 / $base-font-size}rem; + } } From c3486af0311c0b41fc9007b607ca175b356803f4 Mon Sep 17 00:00:00 2001 From: catarak Date: Fri, 15 Jul 2016 11:54:47 -0400 Subject: [PATCH 09/11] add author name to sketches --- client/modules/IDE/actions/project.js | 7 +++++- client/modules/IDE/components/Toolbar.js | 10 ++++++++- client/modules/IDE/pages/IDEView.js | 6 +++++- client/modules/IDE/reducers/project.js | 10 ++++----- client/styles/components/_toolbar.scss | 4 ++++ server/controllers/project.controller.js | 27 ++++++++++++++++-------- 6 files changed, 47 insertions(+), 17 deletions(-) diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index 8f820c30..913d979e 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -8,12 +8,14 @@ export function getProject(id) { return (dispatch) => { axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true }) .then(response => { + console.log(response.data); browserHistory.push(`/projects/${id}`); dispatch({ type: ActionTypes.SET_PROJECT, project: response.data, files: response.data.files, - selectedFile: response.data.selectedFile + selectedFile: response.data.selectedFile, + owner: response.data.user }); }) .catch(response => dispatch({ @@ -61,6 +63,7 @@ export function saveProject() { type: ActionTypes.NEW_PROJECT, name: response.data.name, id: response.data.id, + owner: response.data.user, selectedFile: response.data.selectedFile, files: response.data.files }); @@ -78,11 +81,13 @@ export function createProject() { return (dispatch) => { axios.post(`${ROOT_URL}/projects`, {}, { withCredentials: true }) .then(response => { + console.log(response.data); browserHistory.push(`/projects/${response.data.id}`); dispatch({ type: ActionTypes.NEW_PROJECT, name: response.data.name, id: response.data.id, + owner: response.data.user, selectedFile: response.data.selectedFile, files: response.data.files }); diff --git a/client/modules/IDE/components/Toolbar.js b/client/modules/IDE/components/Toolbar.js index fd8d3289..1d4a66c7 100644 --- a/client/modules/IDE/components/Toolbar.js +++ b/client/modules/IDE/components/Toolbar.js @@ -40,6 +40,13 @@ function Toolbar(props) { > {props.projectName} + {(() => { // eslint-disable-line + if (props.owner) { + return ( +

      by {props.owner.username}

      + ); + } + })()}
    • @@ -42,6 +52,7 @@ function Nav(props) { Nav.propTypes = { createProject: PropTypes.func.isRequired, saveProject: PropTypes.func.isRequired, + exportProjectAsZip: PropTypes.func.isRequired, user: PropTypes.shape({ authenticated: PropTypes.bool.isRequired, username: PropTypes.string diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index 913d979e..a26d2e2f 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -1,6 +1,8 @@ import * as ActionTypes from '../../../constants'; import { browserHistory } from 'react-router'; import axios from 'axios'; +import JSZip from 'jszip'; +import { saveAs } from 'file-saver'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; @@ -98,3 +100,19 @@ export function createProject() { })); }; } + +export function exportProjectAsZip() { + return (dispatch, getState) => { + console.log('exporting project!'); + const state = getState(); + const zip = new JSZip(); + state.files.forEach(file => { + zip.file(file.name, file.content); + }); + + zip.generateAsync({ type: 'blob' }).then((content) => { + saveAs(content, `${state.project.name}.zip`); + }); + }; +} + diff --git a/client/modules/IDE/components/Toolbar.js b/client/modules/IDE/components/Toolbar.js index 1d4a66c7..4d3d00bd 100644 --- a/client/modules/IDE/components/Toolbar.js +++ b/client/modules/IDE/components/Toolbar.js @@ -63,7 +63,9 @@ Toolbar.propTypes = { setProjectName: PropTypes.func.isRequired, projectName: PropTypes.string.isRequired, openPreferences: PropTypes.func.isRequired, - owner: PropTypes.string.isRequired + owner: PropTypes.shape({ + username: PropTypes.string + }) }; export default Toolbar; diff --git a/client/modules/IDE/pages/IDEView.js b/client/modules/IDE/pages/IDEView.js index d186d375..ec3842a6 100644 --- a/client/modules/IDE/pages/IDEView.js +++ b/client/modules/IDE/pages/IDEView.js @@ -29,6 +29,7 @@ class IDEView extends React.Component { user={this.props.user} createProject={this.props.createProject} saveProject={this.props.saveProject} + exportProjectAsZip={this.props.exportProjectAsZip} /> Date: Fri, 15 Jul 2016 13:36:33 -0400 Subject: [PATCH 11/11] add cloning of projects --- client/components/Nav.js | 3 ++- client/modules/IDE/actions/project.js | 24 +++++++++++++++++++++++- client/modules/IDE/pages/IDEView.js | 4 +++- server/controllers/project.controller.js | 4 ++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/client/components/Nav.js b/client/components/Nav.js index 26068f49..a9b015df 100644 --- a/client/components/Nav.js +++ b/client/components/Nav.js @@ -33,7 +33,7 @@ function Nav(props) { Export (zip)
    • -
    • +
    • Clone @@ -53,6 +53,7 @@ Nav.propTypes = { createProject: PropTypes.func.isRequired, saveProject: PropTypes.func.isRequired, exportProjectAsZip: PropTypes.func.isRequired, + cloneProject: PropTypes.func.isRequired, user: PropTypes.shape({ authenticated: PropTypes.bool.isRequired, username: PropTypes.string diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index a26d2e2f..804963b8 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -83,7 +83,6 @@ export function createProject() { return (dispatch) => { axios.post(`${ROOT_URL}/projects`, {}, { withCredentials: true }) .then(response => { - console.log(response.data); browserHistory.push(`/projects/${response.data.id}`); dispatch({ type: ActionTypes.NEW_PROJECT, @@ -116,3 +115,26 @@ export function exportProjectAsZip() { }; } +export function cloneProject() { + return (dispatch, getState) => { + const state = getState(); + const formParams = Object.assign({}, { name: state.project.name }, { files: state.files }); + axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) + .then(response => { + browserHistory.push(`/projects/${response.data.id}`); + dispatch({ + type: ActionTypes.NEW_PROJECT, + name: response.data.name, + id: response.data.id, + owner: response.data.user, + selectedFile: response.data.selectedFile, + files: response.data.files + }); + }) + .catch(response => dispatch({ + type: ActionTypes.PROJECT_SAVE_FAIL, + error: response.data + })); + }; +} + diff --git a/client/modules/IDE/pages/IDEView.js b/client/modules/IDE/pages/IDEView.js index ec3842a6..6987b726 100644 --- a/client/modules/IDE/pages/IDEView.js +++ b/client/modules/IDE/pages/IDEView.js @@ -30,6 +30,7 @@ class IDEView extends React.Component { createProject={this.props.createProject} saveProject={this.props.saveProject} exportProjectAsZip={this.props.exportProjectAsZip} + cloneProject={this.props.cloneProject} /> { if (err) { return res.json({ success: false }); }