fix merge conflict

This commit is contained in:
mathuramg 2016-07-11 23:03:54 -04:00
commit c8f54dced9
32 changed files with 462 additions and 132 deletions

View file

@ -21,6 +21,13 @@ function Nav(props) {
Save Save
</p> </p>
</li> </li>
<li className="nav__item">
<p className="nav__open">
<Link to="/sketches">
Open
</Link>
</p>
</li>
</ul> </ul>
<ul className="nav__items-right"> <ul className="nav__items-right">
<li className="nav__item"> <li className="nav__item">

View file

@ -1,4 +1,4 @@
export const CHANGE_SELECTED_FILE = 'CHANGE_SELECTED_FILE'; export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const TOGGLE_SKETCH = 'TOGGLE_SKETCH'; export const TOGGLE_SKETCH = 'TOGGLE_SKETCH';
export const START_SKETCH = 'START_SKETCH'; export const START_SKETCH = 'START_SKETCH';
@ -27,6 +27,9 @@ export const PROJECT_SAVE_FAIL = 'PROJECT_SAVE_FAIL';
export const NEW_PROJECT = 'NEW_PROJECT'; export const NEW_PROJECT = 'NEW_PROJECT';
export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECT = 'SET_PROJECT';
export const SET_PROJECTS = 'SET_PROJECTS';
export const SET_SELECTED_FILE = 'SET_SELECTED_FILE';
// eventually, handle errors more specifically and better // eventually, handle errors more specifically and better
export const ERROR = 'ERROR'; export const ERROR = 'ERROR';

View file

@ -1,8 +1,8 @@
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
export function updateFile(name, content) { export function updateFileContent(name, content) {
return { return {
type: ActionTypes.CHANGE_SELECTED_FILE, type: ActionTypes.UPDATE_FILE_CONTENT,
name, name,
content content
}; };

View file

@ -17,3 +17,10 @@ export function stopSketch() {
type: ActionTypes.STOP_SKETCH type: ActionTypes.STOP_SKETCH
}; };
} }
export function setSelectedFile(fileId) {
return {
type: ActionTypes.SET_SELECTED_FILE,
selectedFile: fileId
};
}

View file

@ -12,7 +12,8 @@ export function getProject(id) {
dispatch({ dispatch({
type: ActionTypes.SET_PROJECT, type: ActionTypes.SET_PROJECT,
project: response.data, project: response.data,
file: response.data.file files: response.data.files,
selectedFile: response.data.selectedFile
}); });
}) })
.catch(response => dispatch({ .catch(response => dispatch({
@ -34,7 +35,7 @@ export function saveProject() {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const formParams = Object.assign({}, state.project); const formParams = Object.assign({}, state.project);
formParams.file = state.file; formParams.files = [...state.files];
if (state.project.id) { if (state.project.id) {
axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true }) axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
.then(() => { .then(() => {
@ -47,6 +48,12 @@ export function saveProject() {
error: response.data error: response.data
})); }));
} else { } else {
// this might be unnecessary, but to prevent collisions in mongodb
formParams.files.map(file => {
const newFile = Object.assign({}, file);
delete newFile.id;
return newFile;
});
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
.then(response => { .then(response => {
browserHistory.push(`/projects/${response.data.id}`); browserHistory.push(`/projects/${response.data.id}`);
@ -54,10 +61,8 @@ export function saveProject() {
type: ActionTypes.NEW_PROJECT, type: ActionTypes.NEW_PROJECT,
name: response.data.name, name: response.data.name,
id: response.data.id, id: response.data.id,
file: { selectedFile: response.data.selectedFile,
name: response.data.file.name, files: response.data.files
content: response.data.file.content
}
}); });
}) })
.catch(response => dispatch({ .catch(response => dispatch({
@ -78,10 +83,8 @@ export function createProject() {
type: ActionTypes.NEW_PROJECT, type: ActionTypes.NEW_PROJECT,
name: response.data.name, name: response.data.name,
id: response.data.id, id: response.data.id,
file: { selectedFile: response.data.selectedFile,
name: response.data.file.name, files: response.data.files
content: response.data.file.content
}
}); });
}) })
.catch(response => dispatch({ .catch(response => dispatch({

View file

@ -8,13 +8,14 @@ class Editor extends React.Component {
componentDidMount() { componentDidMount() {
this._cm = CodeMirror(this.refs.container, { // eslint-disable-line this._cm = CodeMirror(this.refs.container, { // eslint-disable-line
theme: 'p5-widget', theme: 'p5-widget',
value: this.props.content, value: this.props.file.content,
lineNumbers: true, lineNumbers: true,
styleActiveLine: true, styleActiveLine: true,
mode: 'javascript' mode: 'javascript'
}); });
this._cm.on('change', () => { // eslint-disable-line this._cm.on('change', () => { // eslint-disable-line
this.props.updateFile('sketch.js', this._cm.getValue()); // this.props.updateFileContent('sketch.js', this._cm.getValue());
this.props.updateFileContent(this.props.file.name, this._cm.getValue());
}); });
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
this._cm.setOption('indentWithTabs', this.props.isTabIndent); this._cm.setOption('indentWithTabs', this.props.isTabIndent);
@ -22,9 +23,9 @@ class Editor extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.content !== prevProps.content && if (this.props.file.content !== prevProps.file.content &&
this.props.content !== this._cm.getValue()) { this.props.file.content !== this._cm.getValue()) {
this._cm.setValue(this.props.content); // eslint-disable-line no-underscore-dangle this._cm.setValue(this.props.file.content); // eslint-disable-line no-underscore-dangle
} }
if (this.props.fontSize !== prevProps.fontSize) { if (this.props.fontSize !== prevProps.fontSize) {
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
@ -54,6 +55,12 @@ Editor.propTypes = {
fontSize: PropTypes.number.isRequired, fontSize: PropTypes.number.isRequired,
indentationAmount: PropTypes.number.isRequired, indentationAmount: PropTypes.number.isRequired,
isTabIndent: PropTypes.bool.isRequired isTabIndent: PropTypes.bool.isRequired
file: PropTypes.shape({
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
}),
updateFileContent: PropTypes.func.isRequired,
fontSize: PropTypes.number.isRequired
}; };
export default Editor; export default Editor;

View file

@ -1,5 +1,7 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import escapeStringRegexp from 'escape-string-regexp';
import srcDoc from 'srcdoc-polyfill';
class PreviewFrame extends React.Component { class PreviewFrame extends React.Component {
@ -11,11 +13,7 @@ class PreviewFrame extends React.Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.isPlaying !== prevProps.isPlaying) { if (this.props.isPlaying !== prevProps.isPlaying) {
if (this.props.isPlaying) { this.renderSketch();
this.renderSketch();
} else {
this.clearPreview();
}
} }
if (this.props.isPlaying && this.props.content !== prevProps.content) { if (this.props.isPlaying && this.props.content !== prevProps.content) {
@ -28,22 +26,48 @@ class PreviewFrame extends React.Component {
} }
clearPreview() { clearPreview() {
const doc = ReactDOM.findDOMNode(this).contentDocument; const doc = ReactDOM.findDOMNode(this);
doc.write(''); doc.srcDoc = '';
doc.close(); }
injectLocalFiles() {
let htmlFile = this.props.htmlFile.content;
this.props.jsFiles.forEach(jsFile => {
const fileName = escapeStringRegexp(jsFile.name);
const fileRegex = new RegExp(`<script.*?src=('|")((\.\/)|\/)?${fileName}('|").*?>([\s\S]*?)<\/script>`, 'gmi');
htmlFile = htmlFile.replace(fileRegex, `<script>\n${jsFile.content}\n</script>`);
});
this.props.cssFiles.forEach(cssFile => {
const fileName = escapeStringRegexp(cssFile.name);
const fileRegex = new RegExp(`<link.*?href=('|")((\.\/)|\/)?${fileName}('|").*?>`, 'gmi');
htmlFile = htmlFile.replace(fileRegex, `<style>\n${cssFile.content}\n</style>`);
});
// const htmlHead = htmlFile.match(/(?:<head.*?>)([\s\S]*?)(?:<\/head>)/gmi);
// const headRegex = new RegExp('head', 'i');
// let htmlHeadContents = htmlHead[0].split(headRegex)[1];
// htmlHeadContents = htmlHeadContents.slice(1, htmlHeadContents.length - 2);
// htmlHeadContents += '<link rel="stylesheet" type="text/css" href="/preview-styles.css" />\n';
// htmlFile = htmlFile.replace(/(?:<head.*?>)([\s\S]*?)(?:<\/head>)/gmi, `<head>\n${htmlHeadContents}\n</head>`);
return htmlFile;
} }
renderSketch() { renderSketch() {
const doc = ReactDOM.findDOMNode(this).contentDocument; const doc = ReactDOM.findDOMNode(this);
this.clearPreview(); if (this.props.isPlaying) {
ReactDOM.render(this.props.head, doc.head); // TODO add polyfill for this
const p5Script = doc.createElement('script'); // doc.srcdoc = this.injectLocalFiles();
p5Script.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.0/p5.min.js'); srcDoc.set(doc, this.injectLocalFiles());
doc.body.appendChild(p5Script); } else {
// doc.srcdoc = '';
const sketchScript = doc.createElement('script'); srcDoc.set(doc, '');
sketchScript.textContent = this.props.content; doc.contentWindow.document.open();
doc.body.appendChild(sketchScript); doc.contentWindow.document.write('');
doc.contentWindow.document.close();
}
} }
renderFrameContents() { renderFrameContents() {
@ -60,8 +84,8 @@ class PreviewFrame extends React.Component {
<iframe <iframe
className="preview-frame" className="preview-frame"
frameBorder="0" frameBorder="0"
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-modals allow-forms"
title="sketch output" title="sketch output"
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-modals allow-forms"
></iframe> ></iframe>
); );
} }
@ -70,7 +94,12 @@ class PreviewFrame extends React.Component {
PreviewFrame.propTypes = { PreviewFrame.propTypes = {
isPlaying: PropTypes.bool.isRequired, isPlaying: PropTypes.bool.isRequired,
head: PropTypes.object.isRequired, head: PropTypes.object.isRequired,
content: PropTypes.string.isRequired content: PropTypes.string.isRequired,
htmlFile: PropTypes.shape({
content: PropTypes.string.isRequired
}),
jsFiles: PropTypes.array.isRequired,
cssFiles: PropTypes.array.isRequired
}; };
export default PreviewFrame; export default PreviewFrame;

View file

@ -0,0 +1,34 @@
import React, { PropTypes } from 'react';
import classNames from 'classnames';
function Sidebar(props) {
return (
<section className="sidebar">
<ul className="sidebar__file-list">
{props.files.map(file => {
let itemClass = classNames({
'sidebar__file-item': true,
'sidebar__file-item--selected': file.id === props.selectedFile.id
});
return (
<li
className={itemClass}
key={file.id}
onClick={() => props.setSelectedFile(file.id)}
>{file.name}</li>
);
})}
</ul>
</section>
);
}
Sidebar.propTypes = {
files: PropTypes.array.isRequired,
selectedFile: PropTypes.shape({
id: PropTypes.string.isRequired
}),
setSelectedFile: PropTypes.func.isRequired
};
export default Sidebar;

View file

@ -1,5 +1,6 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import Editor from '../components/Editor'; import Editor from '../components/Editor';
import Sidebar from '../components/Sidebar';
import PreviewFrame from '../components/PreviewFrame'; import PreviewFrame from '../components/PreviewFrame';
import Toolbar from '../components/Toolbar'; import Toolbar from '../components/Toolbar';
import Preferences from '../components/Preferences'; import Preferences from '../components/Preferences';
@ -10,6 +11,7 @@ import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide'; import * as IDEActions from '../actions/ide';
import * as PreferencesActions from '../actions/preferences'; import * as PreferencesActions from '../actions/preferences';
import * as ProjectActions from '../actions/project'; import * as ProjectActions from '../actions/project';
import { getFile, getHTMLFile, getJSFiles, getCSSFiles } from '../reducers/files';
class IDEView extends React.Component { class IDEView extends React.Component {
componentDidMount() { componentDidMount() {
@ -52,15 +54,25 @@ class IDEView extends React.Component {
indentWithSpace={this.props.indentWithSpace} indentWithSpace={this.props.indentWithSpace}
indentWithTab={this.props.indentWithTab} indentWithTab={this.props.indentWithTab}
/> />
<Sidebar
files={this.props.files}
selectedFile={this.props.selectedFile}
setSelectedFile={this.props.setSelectedFile}
/>
<Editor <Editor
content={this.props.file.content} file={this.props.selectedFile}
updateFile={this.props.updateFile} updateFileContent={this.props.updateFileContent}
fontSize={this.props.preferences.fontSize} fontSize={this.props.preferences.fontSize}
indentationAmount={this.props.preferences.indentationAmount} indentationAmount={this.props.preferences.indentationAmount}
isTabIndent={this.props.preferences.isTabIndent} isTabIndent={this.props.preferences.isTabIndent}
files={this.props.files}
/> />
<PreviewFrame <PreviewFrame
content={this.props.file.content} htmlFile={this.props.htmlFile}
jsFiles={this.props.jsFiles}
cssFiles={this.props.cssFiles}
files={this.props.files}
content={this.props.selectedFile.content}
head={ head={
<link type="text/css" rel="stylesheet" href="/preview-styles.css" /> <link type="text/css" rel="stylesheet" href="/preview-styles.css" />
} }
@ -98,6 +110,7 @@ IDEView.propTypes = {
closePreferences: PropTypes.func.isRequired, closePreferences: PropTypes.func.isRequired,
increaseFont: PropTypes.func.isRequired, increaseFont: PropTypes.func.isRequired,
decreaseFont: PropTypes.func.isRequired, decreaseFont: PropTypes.func.isRequired,
<<<<<<< HEAD
updateFont: PropTypes.func.isRequired, updateFont: PropTypes.func.isRequired,
increaseIndentation: PropTypes.func.isRequired, increaseIndentation: PropTypes.func.isRequired,
decreaseIndentation: PropTypes.func.isRequired, decreaseIndentation: PropTypes.func.isRequired,
@ -105,14 +118,27 @@ IDEView.propTypes = {
indentWithSpace: PropTypes.func.isRequired, indentWithSpace: PropTypes.func.isRequired,
indentWithTab: PropTypes.func.isRequired, indentWithTab: PropTypes.func.isRequired,
file: PropTypes.shape({ file: PropTypes.shape({
=======
files: PropTypes.array.isRequired,
updateFileContent: PropTypes.func.isRequired,
selectedFile: PropTypes.shape({
id: PropTypes.string.isRequired,
>>>>>>> b89a1103b9c5616820f4b6b005c118d9f1f9bf13
content: PropTypes.string.isRequired content: PropTypes.string.isRequired
}).isRequired, }),
updateFile: PropTypes.func.isRequired setSelectedFile: PropTypes.func.isRequired,
htmlFile: PropTypes.object.isRequired,
jsFiles: PropTypes.array.isRequired,
cssFiles: PropTypes.array.isRequired
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
file: state.file, files: state.files,
selectedFile: getFile(state.files, state.ide.selectedFile),
htmlFile: getHTMLFile(state.files),
jsFiles: getJSFiles(state.files),
cssFiles: getCSSFiles(state.files),
ide: state.ide, ide: state.ide,
preferences: state.preferences, preferences: state.preferences,
user: state.user, user: state.user,

View file

@ -1,50 +1,75 @@
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
const initialState = { const defaultSketch = `function setup() {
name: 'sketch.js',
content: `function setup() {
createCanvas(400, 400); createCanvas(400, 400);
} }
function draw() { function draw() {
background(220); background(220);
}` }`;
};
const file = (state = initialState, action) => { const defaultHTML =
`<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.0/p5.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<script src="sketch.js"></script>
</body>
</html>
`;
const defaultCSS =
`html, body {
overflow: hidden;
margin: 0;
padding: 0;
}
`;
// if the project has never been saved,
const initialState = [
{
name: 'sketch.js',
content: defaultSketch,
id: '1'
},
{
name: 'index.html',
content: defaultHTML,
id: '2'
},
{
name: 'style.css',
content: defaultCSS,
id: '3'
}];
const files = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case ActionTypes.CHANGE_SELECTED_FILE: case ActionTypes.UPDATE_FILE_CONTENT:
return { return state.map(file => {
name: action.name, if (file.name !== action.name) {
content: action.content return file;
}; }
return Object.assign({}, file, { content: action.content });
});
case ActionTypes.NEW_PROJECT: case ActionTypes.NEW_PROJECT:
return { return [...action.files];
name: action.file.name,
content: action.file.content
};
case ActionTypes.SET_PROJECT: case ActionTypes.SET_PROJECT:
return { return [...action.files];
name: action.file.name,
content: action.file.content
};
default: default:
return state; return state;
} }
}; };
export default file; export const getFile = (state, id) => state.filter(file => file.id === id)[0];
export const getHTMLFile = (state) => state.filter(file => file.name.match(/.*\.html$/))[0];
export const getJSFiles = (state) => state.filter(file => file.name.match(/.*\.js$/));
export const getCSSFiles = (state) => state.filter(file => file.name.match(/.*\.css$/));
// i'll add this in when there are multiple files export default files;
// const files = (state = [], action) => {
// switch (action.type) {
// case ActionTypes.CHANGE_SELECTED_FILE:
// //find the file with the name
// //update it
// //put in into the new array of files
// default:
// return state
// }
// }
// export default files

View file

@ -1,23 +1,22 @@
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
const initialState = { const initialState = {
isPlaying: false isPlaying: false,
selectedFile: '1'
}; };
const ide = (state = initialState, action) => { const ide = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case ActionTypes.TOGGLE_SKETCH: case ActionTypes.TOGGLE_SKETCH:
return { return Object.assign({}, state, { isPlaying: !state.isPlaying });
isPlaying: !state.isPlaying
};
case ActionTypes.START_SKETCH: case ActionTypes.START_SKETCH:
return { return Object.assign({}, state, { isPlaying: true });
isPlaying: true
};
case ActionTypes.STOP_SKETCH: case ActionTypes.STOP_SKETCH:
return { return Object.assign({}, state, { isPlaying: false });
isPlaying: false case ActionTypes.SET_SELECTED_FILE:
}; case ActionTypes.SET_PROJECT:
case ActionTypes.NEW_PROJECT:
return Object.assign({}, state, { selectedFile: action.selectedFile });
default: default:
return state; return state;
} }

View file

@ -0,0 +1,20 @@
import * as ActionTypes from '../../constants';
import axios from 'axios';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
export function getProjects() {
return (dispatch) => {
axios.get(`${ROOT_URL}/projects`, { withCredentials: true })
.then(response => {
dispatch({
type: ActionTypes.SET_PROJECTS,
projects: response.data
});
})
.catch(response => dispatch({
type: ActionTypes.ERROR,
error: response.data
}));
};
}

View file

@ -0,0 +1,63 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import moment from 'moment';
import { Link } from 'react-router';
import Nav from '../../../components/Nav';
import * as SketchActions from '../actions';
import * as ProjectActions from '../../IDE/actions/project';
class SketchListView extends React.Component {
componentDidMount() {
this.props.getProjects();
}
render() {
return (
<div className="sketch-list">
<Nav
user={this.props.user}
createProject={this.props.createProject}
saveProject={this.props.saveProject}
/>
<table className="sketches-table">
<thead>
<th>Name</th>
<th>Created</th>
<th>Last Updated</th>
</thead>
<tbody>
{this.props.sketches.map(sketch =>
<tr className="sketches-table__row">
<td><Link to={`/projects/${sketch._id}`}>{sketch.name}</Link></td>
<td>{moment(sketch.createdAt).format('MMM D, YYYY')}</td>
<td>{moment(sketch.updatedAt).format('MMM D, YYYY')}</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
}
SketchListView.propTypes = {
user: PropTypes.object.isRequired,
createProject: PropTypes.func.isRequired,
saveProject: PropTypes.func.isRequired,
getProjects: PropTypes.func.isRequired,
sketches: PropTypes.array.isRequired
};
function mapStateToProps(state) {
return {
user: state.user,
sketches: state.sketches
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(Object.assign({}, SketchActions, ProjectActions), dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(SketchListView);

View file

@ -0,0 +1,12 @@
import * as ActionTypes from '../../constants';
const sketches = (state = [], action) => {
switch (action.type) {
case ActionTypes.SET_PROJECTS:
return action.projects;
default:
return state;
}
};
export default sketches;

View file

@ -1,18 +1,20 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import file from './modules/IDE/reducers/files'; import files from './modules/IDE/reducers/files';
import ide from './modules/IDE/reducers/ide'; import ide from './modules/IDE/reducers/ide';
import preferences from './modules/IDE/reducers/preferences'; import preferences from './modules/IDE/reducers/preferences';
import project from './modules/IDE/reducers/project'; import project from './modules/IDE/reducers/project';
import user from './modules/User/reducers'; import user from './modules/User/reducers';
import sketches from './modules/Sketch/reducers';
import { reducer as form } from 'redux-form'; import { reducer as form } from 'redux-form';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
form, form,
ide, ide,
file, files,
preferences, preferences,
user, user,
project project,
sketches
}); });
export default rootReducer; export default rootReducer;

View file

@ -4,6 +4,7 @@ import App from './modules/App/App';
import IDEView from './modules/IDE/pages/IDEView'; import IDEView from './modules/IDE/pages/IDEView';
import LoginView from './modules/User/pages/LoginView'; import LoginView from './modules/User/pages/LoginView';
import SignupView from './modules/User/pages/SignupView'; import SignupView from './modules/User/pages/SignupView';
import SketchListView from './modules/Sketch/pages/SketchListView';
import { getUser } from './modules/User/actions'; import { getUser } from './modules/User/actions';
const checkAuth = (store) => { const checkAuth = (store) => {
@ -17,6 +18,7 @@ const routes = (store) =>
<Route path="/login" component={LoginView} /> <Route path="/login" component={LoginView} />
<Route path="/signup" component={SignupView} /> <Route path="/signup" component={SignupView} />
<Route path="/projects/:project_id" component={IDEView} /> <Route path="/projects/:project_id" component={IDEView} />
<Route path="/sketches" component={SketchListView} />
</Route> </Route>
); );

View file

@ -84,7 +84,7 @@
} }
%fake-link { %fake-link {
color: $light-secondary-text-color; color: $light-inactive-text-color;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: $light-primary-text-color; color: $light-primary-text-color;

View file

@ -36,6 +36,6 @@ $dark-button-background-active-color: #f10046;
$dark-button-hover-color: $white; $dark-button-hover-color: $white;
$dark-button-active-color: $white; $dark-button-active-color: $white;
$editor-border-color: #f4f4f4; $ide-border-color: #f4f4f4;
$editor-selected-line-color: #f3f3f3; $editor-selected-line-color: #f3f3f3;
$input-border-color: #979797; $input-border-color: #979797;

View file

@ -18,7 +18,7 @@ body, input, button {
a { a {
text-decoration: none; text-decoration: none;
color: $light-secondary-text-color; color: $light-inactive-text-color;
&:hover { &:hover {
text-decoration: none; text-decoration: none;
color: $light-primary-text-color; color: $light-primary-text-color;
@ -46,11 +46,12 @@ h2 {
h3 { h3 {
font-weight: normal; font-weight: normal;
} }
h4 { h4 {
font-weight: normal; font-weight: normal;
} }
h6 { h6 {
font-weight: normal; font-weight: normal;
} }
thead {
text-align: left;
}

View file

@ -15,3 +15,7 @@ ul, p {
h2, h3 { h2, h3 {
margin: 0; margin: 0;
} }
ul {
list-style: none;
}

View file

@ -1,7 +1,7 @@
.CodeMirror { .CodeMirror {
font-family: Inconsolata, monospace; font-family: Inconsolata, monospace;
height: 100%; height: 100%;
border: 1px solid $editor-border-color; border: 1px solid $ide-border-color;
} }
.CodeMirror-linenumbers { .CodeMirror-linenumbers {

View file

@ -0,0 +1,12 @@
.sidebar__file-list {
border-top: 1px solid $ide-border-color;
}
.sidebar__file-item {
padding: #{8 / $base-font-size}rem #{20 / $base-font-size}rem;
color: $light-inactive-text-color;
}
.sidebar__file-item--selected {
background-color: $ide-border-color;
}

View file

@ -0,0 +1,9 @@
.sketches-table {
width: 100%;
padding: #{10 / $base-font-size}rem 0;
padding-left: #{170 / $base-font-size}rem;
}
.sketches-table__row {
margin: #{10 / $base-font-size}rem;
}

View file

@ -45,12 +45,12 @@
} }
.toolbar__project-name { .toolbar__project-name {
color: $light-secondary-text-color; color: $light-inactive-text-color;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: $light-primary-text-color; color: $light-primary-text-color;
} }
&:focus { &:focus {
color: $light-secondary-text-color; color: $light-inactive-text-color;
} }
} }

View file

@ -6,14 +6,18 @@
} }
.editor-holder { .editor-holder {
width: 50%; flex-grow: 1;
height: 100%; height: 100%;
} }
.preview-frame { .preview-frame {
width: 50%; flex-grow: 1;
} }
.toolbar { .toolbar {
width: 100%; width: 100%;
} }
.sidebar {
width: #{140 / $base-font-size}rem;
}

View file

@ -0,0 +1,6 @@
.sketch-list {
display: flex;
flex-wrap: wrap;
height: 100%;
flex-flow: column;
}

View file

@ -13,5 +13,8 @@
@import 'components/preferences'; @import 'components/preferences';
@import 'components/signup'; @import 'components/signup';
@import 'components/login'; @import 'components/login';
@import 'components/sketch-list';
@import 'components/sidebar';
@import 'layout/ide'; @import 'layout/ide';
@import 'layout/sketch-list';

View file

@ -60,14 +60,17 @@
"babel-core": "^6.8.0", "babel-core": "^6.8.0",
"bcrypt-nodejs": "0.0.3", "bcrypt-nodejs": "0.0.3",
"body-parser": "^1.15.1", "body-parser": "^1.15.1",
"bson-objectid": "^1.1.4",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"codemirror": "^5.14.2", "codemirror": "^5.14.2",
"connect-mongo": "^1.2.0", "connect-mongo": "^1.2.0",
"cookie-parser": "^1.4.1", "cookie-parser": "^1.4.1",
"dotenv": "^2.0.0", "dotenv": "^2.0.0",
"escape-string-regexp": "^1.0.5",
"eslint-loader": "^1.3.0", "eslint-loader": "^1.3.0",
"express": "^4.13.4", "express": "^4.13.4",
"express-session": "^1.13.0", "express-session": "^1.13.0",
"moment": "^2.14.1",
"mongoose": "^4.4.16", "mongoose": "^4.4.16",
"passport": "^0.3.2", "passport": "^0.3.2",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
@ -80,6 +83,7 @@
"redux": "^3.5.2", "redux": "^3.5.2",
"redux-form": "^5.2.5", "redux-form": "^5.2.5",
"redux-thunk": "^2.1.0", "redux-thunk": "^2.1.0",
"shortid": "^2.2.6" "shortid": "^2.2.6",
"srcdoc-polyfill": "^0.2.0"
} }
} }

View file

@ -2,22 +2,14 @@ import Project from '../models/project';
export function createProject(req, res) { export function createProject(req, res) {
const projectValues = { const projectValues = {
user: req.user ? req.user._id : undefined, // eslint-disable-line no-underscore-dangle user: req.user ? req.user._id : undefined // eslint-disable-line no-underscore-dangle
file: {}
}; };
Object.assign(projectValues, req.body); Object.assign(projectValues, req.body);
Project.create(projectValues, (err, newProject) => { Project.create(projectValues, (err, newProject) => {
if (err) { return res.json({ success: false }); } if (err) { return res.json({ success: false }); }
return res.json({ return res.json(newProject);
id: newProject._id, // eslint-disable-line no-underscore-dangle
name: newProject.name,
file: {
name: newProject.file.name,
content: newProject.file.content
}
});
}); });
} }
@ -27,14 +19,7 @@ export function updateProject(req, res) {
$set: req.body $set: req.body
}, (err, updatedProject) => { }, (err, updatedProject) => {
if (err) { return res.json({ success: false }); } if (err) { return res.json({ success: false }); }
return res.json({ return res.json(updatedProject);
id: updatedProject._id, // eslint-disable-line no-underscore-dangle
name: updatedProject.name,
file: {
name: updatedProject.file.name,
content: updatedProject.file.content
}
});
}); });
} }
@ -44,13 +29,21 @@ export function getProject(req, res) {
return res.status(404).send({ message: 'Project with that id does not exist' }); return res.status(404).send({ message: 'Project with that id does not exist' });
} }
return res.json({ return res.json(project);
id: project._id, // eslint-disable-line no-underscore-dangle
name: project.name,
file: {
name: project.file.name,
content: project.file.content
}
});
}); });
} }
export function getProjects(req, res) {
if (req.user) {
Project.find({user: req.user._id}) // eslint-disable-line no-underscore-dangle
.sort('-createdAt')
.select('name file _id createdAt updatedAt')
.exec((err, projects) => {
res.json(projects);
});
} else {
// could just move this to client side
return res.json([]);
}
}

View file

@ -1,24 +1,73 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
const Schema = mongoose.Schema; const Schema = mongoose.Schema;
const ObjectIdSchema = Schema.ObjectId;
const ObjectId = mongoose.Types.ObjectId;
import shortid from 'shortid'; import shortid from 'shortid';
const defaultSketch = `setup() { const defaultSketch = `function setup() {
createCanvas(400, 400); createCanvas(400, 400);
} }
draw() { function draw() {
background(220); background(220);
}` }`
const defaultHTML =
`<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.0/p5.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<script src="sketch.js"></script>
</body>
</html>
`
const defaultCSS =
`html, body {
overflow: hidden;
margin: 0;
padding: 0;
}
`;
const fileSchema = new Schema({ const fileSchema = new Schema({
name: { type: String, default: 'sketch.js' }, name: { type: String, default: 'sketch.js' },
content: { type: String, default: defaultSketch } content: { type: String, default: defaultSketch }
}, { timestamps: true }); }, { timestamps: true, _id: true });
fileSchema.virtual('id').get(function(){
return this._id.toHexString();
});
fileSchema.set('toJSON', {
virtuals: true
});
const projectSchema = new Schema({ const projectSchema = new Schema({
name: { type: String, default: "Hello p5.js, it's the server" }, name: { type: String, default: "Hello p5.js, it's the server" },
user: { type: Schema.Types.ObjectId, ref: 'User' }, user: { type: Schema.Types.ObjectId, ref: 'User' },
file: { type: fileSchema }, files: { type: [ fileSchema ], default: [{ name: 'sketch.js', content: defaultSketch, _id: new ObjectId() },
_id: { type: String, default: shortid.generate } { name: 'index.html', content: defaultHTML, _id: new ObjectId() },
{ name: 'style.css', content: defaultCSS, _id: new ObjectId() }]},
_id: { type: String, default: shortid.generate },
selectedFile: Schema.Types.ObjectId
}, { timestamps: true }); }, { timestamps: true });
projectSchema.virtual('id').get(function(){
return this._id;
});
projectSchema.set('toJSON', {
virtuals: true
});
projectSchema.pre('save', function createSelectedFile(next) {
const project = this;
if (!project.selectedFile) {
project.selectedFile = project.files[0]._id; // eslint-disable-line no-underscore-dangle
return next();
}
});
export default mongoose.model('Project', projectSchema); export default mongoose.model('Project', projectSchema);

View file

@ -9,4 +9,6 @@ router.route('/projects/:project_id').put(ProjectController.updateProject);
router.route('/projects/:project_id').get(ProjectController.getProject); router.route('/projects/:project_id').get(ProjectController.getProject);
router.route('/projects').get(ProjectController.getProjects);
export default router; export default router;

View file

@ -21,4 +21,8 @@ router.route('/login').get((req, res) => {
res.sendFile(path.resolve(`${__dirname}/../../index.html`)); res.sendFile(path.resolve(`${__dirname}/../../index.html`));
}); });
router.route('/sketches').get((req, res) => {
res.sendFile(path.resolve(`${__dirname}/../../index.html`));
});
export default router; export default router;