diff --git a/client/components/Nav.js b/client/components/Nav.js
index e00fb8da..87ef80ec 100644
--- a/client/components/Nav.js
+++ b/client/components/Nav.js
@@ -28,6 +28,16 @@ function Nav(props) {
+
+
+ Export (zip)
+
+
+
+
+ Clone
+
+
-
@@ -42,6 +52,8 @@ function Nav(props) {
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/constants.js b/client/constants.js
index 967df39b..ed64f2b0 100644
--- a/client/constants.js
+++ b/client/constants.js
@@ -30,6 +30,12 @@ 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';
+
+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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ No newline at end of file
diff --git a/client/modules/IDE/actions/files.js b/client/modules/IDE/actions/files.js
index f7ebd8f3..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 {
@@ -7,3 +10,41 @@ export function updateFileContent(name, content) {
content
};
}
+
+// TODO make req to server
+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/actions/ide.js b/client/modules/IDE/actions/ide.js
index 0bcbc414..a0b48434 100644
--- a/client/modules/IDE/actions/ide.js
+++ b/client/modules/IDE/actions/ide.js
@@ -24,3 +24,27 @@ export function setSelectedFile(fileId) {
selectedFile: fileId
};
}
+
+export function newFile() {
+ return {
+ type: ActionTypes.SHOW_MODAL
+ };
+}
+
+export function closeNewFileModal() {
+ return {
+ 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/actions/project.js b/client/modules/IDE/actions/project.js
index 8f820c30..804963b8 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';
@@ -8,12 +10,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 +65,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
});
@@ -83,6 +88,7 @@ export function createProject() {
type: ActionTypes.NEW_PROJECT,
name: response.data.name,
id: response.data.id,
+ owner: response.data.user,
selectedFile: response.data.selectedFile,
files: response.data.files
});
@@ -93,3 +99,42 @@ 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`);
+ });
+ };
+}
+
+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/components/Editor.js b/client/modules/IDE/components/Editor.js
index e8f24f4a..680158c5 100644
--- a/client/modules/IDE/components/Editor.js
+++ b/client/modules/IDE/components/Editor.js
@@ -1,7 +1,21 @@
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;
+
+import { debounce } from 'throttle-debounce';
class Editor extends React.Component {
@@ -13,12 +27,18 @@ class Editor extends React.Component {
styleActiveLine: true,
inputStyle: 'contenteditable',
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());
+ 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);
@@ -38,6 +58,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/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..f24ae645
--- /dev/null
+++ b/client/modules/IDE/components/NewFileModal.js
@@ -0,0 +1,66 @@
+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 (
+
+ );
+}
+
+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';
+ } else 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 27e664ca..cb6c5c53 100644
--- a/client/modules/IDE/components/Sidebar.js
+++ b/client/modules/IDE/components/Sidebar.js
@@ -1,10 +1,41 @@
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 (
-