update eslint to latest version, fix lots of linting errors (#308)

* update eslint and dependencies, fix linting errors that can be fixed with --fix

* fix lots of linting errors

* update eslintrc, fix some linting errors

* fix all server side linting errors, untested

* fix errors that fixing linting errors had caused

* fix client side eslint errors

* fix client side linting errors

* fix refs lint errors

* fix more linting errors

* update eslint and dependencies, fix linting errors that can be fixed with --fix

* fix lots of linting errors

* update eslintrc, fix some linting errors

* fix all server side linting errors, untested

* fix errors that fixing linting errors had caused

* fix client side eslint errors

* fix client side linting errors

* fix refs lint errors

* fix more linting errors

* fix some accessibility linting errors

* fix a lot of linting errors

* fix a billion more linting errors

* hopefully fix all linting errors, still need to test

* fix bugs that fixing linting had caused
This commit is contained in:
Cassie Tarakajian 2017-02-22 14:29:35 -05:00 committed by GitHub
parent 059308fbfe
commit e87390adb9
73 changed files with 964 additions and 733 deletions

View file

@ -24,7 +24,20 @@
"no-console": 0,
"no-alert": 0,
"no-underscore-dangle": 0,
"max-len": [1, 120, 4],
"max-len": [1, 120, 2, {ignoreComments: true}],
"quote-props": [1, "consistent-as-needed"],
"no-unused-vars": [1, {"vars": "local", "args": "none"}],
"consistent-return": ["error", { "treatUndefinedAsUnspecified": true }],
"no-param-reassign": [2, { "props": false }],
"react/self-closing-comp": ["error", {
"component": true,
"html": false
}],
"newline-per-chained-call": 0,
"react/prefer-stateless-function": [2,
{ "ignorePureComponents": true
}],
"class-methods-use-this": 0
},
"plugins": [
"react", "jsx-a11y", "import"

View file

@ -1,85 +1,86 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
function Nav(props) {
class Nav extends React.PureComponent {
render() {
return (
<nav className="nav" role="navigation" title="main-navigation">
<ul className="nav__items-left" title="project-menu">
<li className="nav__item">
<a
<button
className="nav__new"
onClick={() => {
if (!props.unsavedChanges) {
props.newProject();
} else if (props.warnIfUnsavedChanges()) {
props.newProject();
if (!this.props.unsavedChanges) {
this.props.newProject();
} else if (this.props.warnIfUnsavedChanges()) {
this.props.newProject();
}
}}
>
New
</a>
</button>
</li>
{(() => { // eslint-disable-line
if (!props.project.owner || props.project.owner && props.project.owner.id === props.user.id) {
if (!this.props.project.owner || (this.props.project.owner && this.props.project.owner.id === this.props.user.id)) {
return (
<li className="nav__item">
<a
<button
className="nav__save"
onClick={() => {
if (props.user.authenticated) {
props.saveProject();
if (this.props.user.authenticated) {
this.props.saveProject();
} else {
props.showErrorModal('forceAuthentication');
this.props.showErrorModal('forceAuthentication');
}
}}
>
Save
</a>
</button>
</li>
);
}
})()}
{(() => { // eslint-disable-line
if (props.project.id && props.user.authenticated) {
if (this.props.project.id && this.props.user.authenticated) {
return (
<li className="nav__item">
<a className="nav__clone" onClick={props.cloneProject}>
<button className="nav__clone" onClick={this.props.cloneProject}>
Duplicate
</a>
</button>
</li>
);
}
})()}
{(() => { // eslint-disable-line
if (props.project.id) {
if (this.props.project.id) {
return (
<li className="nav__item">
<a className="nav__export" onClick={() => props.exportProjectAsZip(props.project.id)}>
<button className="nav__export" onClick={() => this.props.exportProjectAsZip(this.props.project.id)}>
Download
</a>
</button>
</li>
);
}
})()}
{(() => { // eslint-disable-line
if (props.project.id) {
if (this.props.project.id) {
return (
<li className="nav__item">
<a onClick={props.showShareModal}>
<button onClick={this.props.showShareModal}>
Share
</a>
</button>
</li>
);
}
})()}
{(() => { // eslint-disable-line
if (props.user.authenticated) {
if (this.props.user.authenticated) {
return (
<li className="nav__item">
<p className="nav__open">
<Link
to={`/${props.user.username}/sketches`}
onClick={props.stopSketch}
to={`/${this.props.user.username}/sketches`}
onClick={this.props.stopSketch}
>
Open
</Link>
@ -100,6 +101,7 @@ function Nav(props) {
<a
href="https://p5js.org/reference/"
target="_blank"
rel="noopener noreferrer"
>Reference</a>
</p>
</li>
@ -113,7 +115,7 @@ function Nav(props) {
</ul>
<ul className="nav__items-right" title="user-menu">
{(() => {
if (!props.user.authenticated) {
if (!this.props.user.authenticated) {
return (
<li className="nav__item">
<p>
@ -124,29 +126,34 @@ function Nav(props) {
}
return (
<li className="nav__item">
<a>Hello, {props.user.username}!</a>
<a>Hello, {this.props.user.username}!</a>
<ul className="nav__dropdown">
<li className="nav__dropdown-heading">
<a>Hello, {props.user.username}!</a>
<a>Hello, {this.props.user.username}!</a>
</li>
<li>
<Link to={`/${props.user.username}/sketches`}>
<Link to={`/${this.props.user.username}/sketches`}>
My sketches
</Link>
</li>
<li>
<a onClick={props.logoutUser} >
<button onClick={this.props.logoutUser} >
Log out
</a>
</button>
</li>
</ul>
</li>
);
})()}
</ul>
<div className="nav__announce">This is a preview version of the editor, that has not yet been officially released. It is in development, you can report bugs <a href="https://github.com/processing/p5.js-web-editor/issues" target="_blank">here</a>. Please use with caution.</div>
<div className="nav__announce">
This is a preview version of the editor, that has not yet been officially released. It is in development, you can report bugs
<a href="https://github.com/processing/p5.js-web-editor/issues" target="_blank" rel="noopener noreferrer">here</a>.
Please use with caution.
</div>
</nav>
);
}
}
Nav.propTypes = {
@ -168,7 +175,16 @@ Nav.propTypes = {
logoutUser: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired,
showShareModal: PropTypes.func.isRequired,
showErrorModal: PropTypes.func.isRequired
showErrorModal: PropTypes.func.isRequired,
unsavedChanges: PropTypes.bool.isRequired,
warnIfUnsavedChanges: PropTypes.func.isRequired
};
Nav.defaultProps = {
project: {
id: undefined,
owner: undefined
}
};
export default Nav;

View file

@ -53,7 +53,6 @@ export const COLLAPSE_CONSOLE = 'COLLAPSE_CONSOLE';
export const UPDATE_LINT_MESSAGE = 'UPDATE_LINT_MESSAGE';
export const CLEAR_LINT_MESSAGE = 'CLEAR_LINT_MESSAGE';
export const UPDATE_LINENUMBER = 'UPDATE_LINENUMBER';
export const SHOW_FILE_OPTIONS = 'SHOW_FILE_OPTIONS';
export const HIDE_FILE_OPTIONS = 'HIDE_FILE_OPTIONS';

View file

@ -30,11 +30,15 @@ class App extends React.Component {
}
App.propTypes = {
children: PropTypes.object,
children: PropTypes.element,
location: PropTypes.shape({
pathname: PropTypes.string
}),
}).isRequired,
setPreviousPath: PropTypes.func.isRequired,
};
App.defaultProps = {
children: null
};
export default connect(() => ({}), { setPreviousPath })(App);

View file

@ -11,7 +11,11 @@ function Overlay(props) {
}
Overlay.propTypes = {
children: PropTypes.object
children: PropTypes.element
};
Overlay.defaultProps = {
children: null
};
export default Overlay;

View file

@ -14,10 +14,3 @@ export function clearLintMessage() {
type: ActionTypes.CLEAR_LINT_MESSAGE
};
}
export function updateLineNumber(lineNumber) {
return {
type: ActionTypes.UPDATE_LINENUMBER,
lineNumber
};
}

View file

@ -1,9 +1,9 @@
import * as ActionTypes from '../../../constants';
import axios from 'axios';
import objectID from 'bson-objectid';
import blobUtil from 'blob-util';
import { setUnsavedChanges } from './ide';
import { reset } from 'redux-form';
import * as ActionTypes from '../../../constants';
import { setUnsavedChanges } from './ide';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
@ -18,11 +18,11 @@ function createUniqueName(name, parentId, files) {
.children.map(childFileId => files.find(file => file.id === childFileId));
let testName = name;
let index = 1;
let existingName = siblingFiles.find((file) => name === file.name);
let existingName = siblingFiles.find(file => name === file.name);
while (existingName) {
testName = appendToFilename(name, `-${index}`);
index++;
index += 1;
existingName = siblingFiles.find((file) => testName === file.name); // eslint-disable-line
}
return testName;
@ -56,7 +56,7 @@ export function createFile(formProps) {
children: []
};
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
.then(response => {
.then((response) => {
dispatch({
type: ActionTypes.CREATE_FILE,
...response.data,
@ -113,7 +113,7 @@ export function createFolder(formProps) {
fileType: 'folder'
};
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
.then(response => {
.then((response) => {
dispatch({
type: ActionTypes.CREATE_FILE,
...response.data,
@ -200,7 +200,7 @@ export function deleteFile(id, parentId) {
parentId
});
})
.catch(response => {
.catch((response) => {
dispatch({
type: ActionTypes.ERROR,
error: response.data

View file

@ -1,5 +1,5 @@
import * as ActionTypes from '../../../constants';
import axios from 'axios';
import * as ActionTypes from '../../../constants';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
@ -7,7 +7,7 @@ function updatePreferences(formParams, dispatch) {
axios.put(`${ROOT_URL}/preferences`, formParams, { withCredentials: true })
.then(() => {
})
.catch((response) => dispatch({
.catch(response => dispatch({
type: ActionTypes.ERROR,
error: response.data
}));

View file

@ -1,7 +1,8 @@
import * as ActionTypes from '../../../constants';
import { browserHistory } from 'react-router';
import axios from 'axios';
import objectID from 'bson-objectid';
import moment from 'moment';
import * as ActionTypes from '../../../constants';
import { showToast, setToastText } from './toast';
import { setUnsavedChanges,
justOpenedProject,
@ -9,7 +10,6 @@ import { setUnsavedChanges,
setProjectSavedTime,
resetProjectSavedTime,
showErrorModal } from './ide';
import moment from 'moment';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
@ -21,7 +21,7 @@ export function getProject(id) {
dispatch(resetProjectSavedTime());
}
axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
.then(response => {
.then((response) => {
dispatch({
type: ActionTypes.SET_PROJECT,
project: response.data,
@ -86,7 +86,7 @@ export function saveProject(autosave = false) {
});
} else {
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
.then(response => {
.then((response) => {
dispatch(setUnsavedChanges(false));
dispatch(setProjectSavedTime(moment().format()));
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
@ -108,7 +108,7 @@ export function saveProject(autosave = false) {
}
}
})
.catch(response => {
.catch((response) => {
if (response.status === 403) {
dispatch(showErrorModal('staleSession'));
} else {
@ -131,7 +131,7 @@ export function autosaveProject() {
export function createProject() {
return (dispatch) => {
axios.post(`${ROOT_URL}/projects`, {}, { withCredentials: true })
.then(response => {
.then((response) => {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({
type: ActionTypes.NEW_PROJECT,
@ -192,7 +192,7 @@ export function cloneProject() {
// const newFiles = state.files;
const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles });
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
.then(response => {
.then((response) => {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({
type: ActionTypes.NEW_PROJECT,

View file

@ -1,5 +1,5 @@
import * as ActionTypes from '../../../constants';
import axios from 'axios';
import * as ActionTypes from '../../../constants';
import { showErrorModal, setPreviousPath } from './ide';
import { resetProject } from './project';
@ -14,7 +14,7 @@ export function getProjects(username) {
url = `${ROOT_URL}/projects`;
}
axios.get(url, { withCredentials: true })
.then(response => {
.then((response) => {
dispatch({
type: ActionTypes.SET_PROJECTS,
projects: response.data
@ -41,7 +41,7 @@ export function deleteProject(id) {
id
});
})
.catch(response => {
.catch((response) => {
if (response.status === 403) {
dispatch(showErrorModal('staleSession'));
} else {

View file

@ -1,7 +1,7 @@
import axios from 'axios';
import { createFile } from './files';
const textFileRegex = /(text\/|application\/json)/;
const textFileRegex = /(text\/|application\/json)/;
const s3BucketHttps = `https://s3-us-west-2.amazonaws.com/${process.env.S3_BUCKET}/`;
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB
@ -36,11 +36,11 @@ export function dropzoneAcceptCallback(file, done) {
// check mime type
// if text, local interceptor
if (file.type.match(textFileRegex) && file.size < MAX_LOCAL_FILE_SIZE) {
localIntercept(file).then(result => {
localIntercept(file).then((result) => {
file.content = result; // eslint-disable-line
done('Uploading plaintext file locally.');
})
.catch(result => {
.catch((result) => {
done(`Failed to download file ${file.name}: ${result}`);
console.warn(file);
});
@ -55,14 +55,14 @@ export function dropzoneAcceptCallback(file, done) {
{
withCredentials: true
})
.then(response => {
.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 => {
.catch((response) => {
file.custom_status = 'rejected'; // eslint-disable-line
if (response.data.responseText && response.data.responseText.message) {
done(response.data.responseText.message);
@ -76,7 +76,7 @@ export function dropzoneAcceptCallback(file, done) {
export function dropzoneSendingCallback(file, xhr, formData) {
return () => {
if (!file.type.match(textFileRegex) || file.size >= MAX_LOCAL_FILE_SIZE) {
Object.keys(file.postData).forEach(key => {
Object.keys(file.postData).forEach((key) => {
formData.append(key, file.postData[key]);
});
formData.append('Content-type', '');

View file

@ -1,10 +1,11 @@
import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg';
import { browserHistory } from 'react-router';
const exitUrl = require('../../../images/exit.svg');
const squareLogoUrl = require('../../../images/p5js-square-logo.svg');
const playUrl = require('../../../images/play.svg');
const asteriskUrl = require('../../../images/p5-asterisk.svg');
import { browserHistory } from 'react-router';
class About extends React.Component {
constructor(props) {
@ -13,7 +14,7 @@ class About extends React.Component {
}
componentDidMount() {
this.refs.about.focus();
this.aboutSection.focus();
}
closeAboutModal() {
@ -22,7 +23,7 @@ class About extends React.Component {
render() {
return (
<section className="about" ref="about" tabIndex="0">
<section className="about" ref={(element) => { this.aboutSection = element; }} tabIndex="0">
<header className="about__header">
<h2 className="about__header-title">Welcome</h2>
<button className="about__exit-button" onClick={this.closeAboutModal}>
@ -36,6 +37,7 @@ class About extends React.Component {
<a
href="http://hello.p5js.org/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__play-video-button" src={playUrl} alt="Play Hello Video" />
Play hello! video</a>
@ -47,6 +49,7 @@ class About extends React.Component {
<a
href="https://p5js.org/examples/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Examples</a>
@ -55,6 +58,7 @@ class About extends React.Component {
<a
href="https://p5js.org/tutorials/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Tutorials</a>
@ -66,6 +70,7 @@ class About extends React.Component {
<a
href="https://p5js.org/libraries/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Libraries</a>
@ -74,6 +79,7 @@ class About extends React.Component {
<a
href="https://p5js.org/reference/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Reference</a>
@ -82,6 +88,7 @@ class About extends React.Component {
<a
href="https://forum.processing.org/two/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Forum</a>
@ -93,18 +100,21 @@ class About extends React.Component {
<a
href="https://github.com/processing/p5.js-web-editor"
target="_blank"
rel="noopener noreferrer"
>Contribute</a>
</p>
<p className="about__footer-list">
<a
href="https://github.com/processing/p5.js-web-editor/issues/new"
target="_blank"
rel="noopener noreferrer"
>Report a bug</a>
</p>
<p className="about__footer-list">
<a
href="https://twitter.com/p5xjs?lang=en"
target="_blank"
rel="noopener noreferrer"
>Twitter</a>
</p>
<button className="about__ok-button" onClick={this.closeAboutModal}>OK!</button>

View file

@ -1,12 +1,13 @@
import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
const upArrowUrl = require('../../../images/up-arrow.svg');
const downArrowUrl = require('../../../images/down-arrow.svg');
import classNames from 'classnames';
class Console extends React.Component {
componentDidUpdate() {
this.refs.console_messages.scrollTop = this.refs.console_messages.scrollHeight;
this.consoleMessages.scrollTop = this.consoleMessages.scrollHeight;
}
render() {
@ -16,13 +17,13 @@ class Console extends React.Component {
});
return (
<div ref="console" className={consoleClass} role="main" tabIndex="0" title="console">
<div className={consoleClass} role="main" tabIndex="0" title="console">
<div className="preview-console__header">
<h2 className="preview-console__header-title">Console</h2>
<div className="preview-console__header-buttons">
<a className="preview-console__clear" onClick={this.props.clearConsole} aria-label="clear console">
<button className="preview-console__clear" onClick={this.props.clearConsole} aria-label="clear console">
Clear
</a>
</button>
<button className="preview-console__collapse" onClick={this.props.collapseConsole} aria-label="collapse console">
<InlineSVG src={downArrowUrl} />
</button>
@ -31,20 +32,20 @@ class Console extends React.Component {
</button>
</div>
</div>
<div ref="console_messages" className="preview-console__messages">
{this.props.consoleEvents.map((consoleEvent, index) => {
<div ref={(element) => { this.consoleMessages = element; }} className="preview-console__messages">
{this.props.consoleEvents.map((consoleEvent) => {
const args = consoleEvent.arguments;
const method = consoleEvent.method;
if (Object.keys(args).length === 0) {
return (
<div key={index} className="preview-console__undefined">
<span key={`${index}-0`}>{'undefined'}</span>
<div key={consoleEvent.id} className="preview-console__undefined">
<span key={`${consoleEvent.id}-0`}>{'undefined'}</span>
</div>
);
}
return (
<div key={index} className={`preview-console__${method}`}>
{Object.keys(args).map((key) => <span key={`${index}-${key}`}>{args[key]}</span>)}
<div key={consoleEvent.id} className={`preview-console__${method}`}>
{Object.keys(args).map(key => <span key={`${consoleEvent.id}-${key}`}>{args[key]}</span>)}
</div>
);
})}
@ -56,12 +57,18 @@ class Console extends React.Component {
}
Console.propTypes = {
consoleEvents: PropTypes.array,
isPlaying: PropTypes.bool.isRequired,
consoleEvents: PropTypes.arrayOf(PropTypes.shape({
method: PropTypes.string.isRequired,
args: PropTypes.arrayOf(PropTypes.string)
})),
isExpanded: PropTypes.bool.isRequired,
collapseConsole: PropTypes.func.isRequired,
expandConsole: PropTypes.func.isRequired,
clearConsole: PropTypes.func.isRequired
};
Console.defaultProps = {
consoleEvents: []
};
export default Console;

View file

@ -1,12 +1,7 @@
import React, { PropTypes } from 'react';
import EditorAccessibility from '../components/EditorAccessibility';
import CodeMirror from 'codemirror';
import beautifyJS from 'js-beautify';
const beautifyCSS = beautifyJS.css;
const beautifyHTML = beautifyJS.html;
import '../../../utils/p5-javascript';
import 'codemirror/mode/css/css';
import '../../../utils/htmlmixed';
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/lint/lint';
import 'codemirror/addon/lint/javascript-lint';
@ -15,22 +10,27 @@ import 'codemirror/addon/lint/html-lint';
import 'codemirror/addon/comment/comment';
import 'codemirror/keymap/sublime';
import 'codemirror/addon/search/jump-to-line';
import { JSHINT } from 'jshint';
window.JSHINT = JSHINT;
import { CSSLint } from 'csslint';
window.CSSLint = CSSLint;
import { HTMLHint } from 'htmlhint';
window.HTMLHint = HTMLHint;
const beepUrl = require('../../../sounds/audioAlert.mp3');
import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
import { debounce } from 'lodash';
import '../../../utils/htmlmixed';
import '../../../utils/p5-javascript';
import Timer from '../components/Timer';
import EditorAccessibility from '../components/EditorAccessibility';
const beautifyCSS = beautifyJS.css;
const beautifyHTML = beautifyJS.html;
window.JSHINT = JSHINT;
window.CSSLint = CSSLint;
window.HTMLHint = HTMLHint;
const beepUrl = require('../../../sounds/audioAlert.mp3');
const downArrowUrl = require('../../../images/down-arrow.svg');
const unsavedChangesDotUrl = require('../../../images/unsaved-changes-dot.svg');
import classNames from 'classnames';
import { debounce } from 'lodash';
import Timer from '../components/Timer';
const rightArrowUrl = require('../../../images/right-arrow.svg');
const leftArrowUrl = require('../../../images/left-arrow.svg');
@ -42,7 +42,7 @@ class Editor extends React.Component {
componentDidMount() {
this.beep = new Audio(beepUrl);
this.widgets = [];
this._cm = CodeMirror(this.refs.container, { // eslint-disable-line
this._cm = CodeMirror(this.codemirrorContainer, { // eslint-disable-line
theme: `p5-${this.props.theme}`,
lineNumbers: true,
styleActiveLine: true,
@ -64,8 +64,8 @@ class Editor extends React.Component {
}
}, 2000),
options: {
asi: true,
eqeqeq: false,
'asi': true,
'eqeqeq': false,
'-W041': false
}
}
@ -162,7 +162,7 @@ class Editor extends React.Component {
initializeDocuments(files) {
this._docs = {};
files.forEach(file => {
files.forEach((file) => {
if (file.name !== 'root') {
this._docs[file.id] = CodeMirror.Doc(file.content, this.getFileMode(file.name)); // eslint-disable-line
}
@ -189,7 +189,7 @@ class Editor extends React.Component {
if (this.props.editorOptionsVisible) {
this.props.closeEditorOptions();
} else {
this.refs.optionsButton.focus();
this.optionsButton.focus();
this.props.showEditorOptions();
}
}
@ -198,7 +198,7 @@ class Editor extends React.Component {
render() {
const editorSectionClass = classNames({
editor: true,
'editor': true,
'sidebar--contracted': !this.props.isExpanded,
'editor--options': this.props.editorOptionsVisible
});
@ -238,7 +238,7 @@ class Editor extends React.Component {
className="editor__options-button"
aria-label="editor options"
tabIndex="0"
ref="optionsButton"
ref={(element) => { this.optionsButton = element; }}
onClick={() => {
this.toggleEditorOptions();
}}
@ -248,18 +248,17 @@ class Editor extends React.Component {
</button>
<ul className="editor__options" title="editor options">
<li>
<a onClick={this.tidyCode}>Tidy</a>
<button onClick={this.tidyCode}>Tidy</button>
</li>
<li>
<a onClick={this.props.showKeyboardShortcutModal}>Keyboard shortcuts</a>
<button onClick={this.props.showKeyboardShortcutModal}>Keyboard shortcuts</button>
</li>
</ul>
</header>
<div ref="container" className="editor-holder" tabIndex="0">
<div ref={(element) => { this.codemirrorContainer = element; }} className="editor-holder" tabIndex="0">
</div>
<EditorAccessibility
lintMessages={this.props.lintMessages}
lineNumber={this.props.lineNumber}
/>
</section>
);
@ -268,11 +267,14 @@ class Editor extends React.Component {
Editor.propTypes = {
lintWarning: PropTypes.bool.isRequired,
lineNumber: PropTypes.string.isRequired,
lintMessages: PropTypes.array.isRequired,
lintMessages: PropTypes.arrayOf(PropTypes.shape({
severity: PropTypes.string.isRequired,
line: PropTypes.number.isRequired,
message: PropTypes.string.isRequired,
id: PropTypes.number.isRequired
})).isRequired,
updateLintMessage: PropTypes.func.isRequired,
clearLintMessage: PropTypes.func.isRequired,
updateLineNumber: PropTypes.func.isRequired,
indentationAmount: PropTypes.number.isRequired,
isTabIndent: PropTypes.bool.isRequired,
updateFileContent: PropTypes.func.isRequired,
@ -281,7 +283,7 @@ Editor.propTypes = {
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
id: PropTypes.string.isRequired
}),
}).isRequired,
editorOptionsVisible: PropTypes.bool.isRequired,
showEditorOptions: PropTypes.func.isRequired,
closeEditorOptions: PropTypes.func.isRequired,
@ -293,7 +295,11 @@ Editor.propTypes = {
theme: PropTypes.string.isRequired,
unsavedChanges: PropTypes.bool.isRequired,
projectSavedTime: PropTypes.string.isRequired,
files: PropTypes.array.isRequired,
files: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
})).isRequired,
isExpanded: PropTypes.bool.isRequired,
collapseSidebar: PropTypes.func.isRequired,
expandSidebar: PropTypes.func.isRequired,
@ -301,4 +307,8 @@ Editor.propTypes = {
clearConsole: PropTypes.func.isRequired
};
Editor.defaultProps = {
isUserOwner: false
};
export default Editor;

View file

@ -5,11 +5,11 @@ class EditorAccessibility extends React.Component {
}
render() {
let messages = [];
const messages = [];
if (this.props.lintMessages.length > 0) {
this.props.lintMessages.forEach((lintMessage, i) => {
messages.push(
<li key={i}>
<li key={lintMessage.id}>
{lintMessage.severity} in line
{lintMessage.line} :
{lintMessage.message}
@ -35,8 +35,12 @@ class EditorAccessibility extends React.Component {
}
EditorAccessibility.propTypes = {
lintMessages: PropTypes.array.isRequired,
lineNumber: PropTypes.string.isRequired,
lintMessages: PropTypes.arrayOf(PropTypes.shape({
severity: PropTypes.string.isRequired,
line: PropTypes.number.isRequired,
message: PropTypes.string.isRequired,
id: PropTypes.number.isRequired
})).isRequired,
};
export default EditorAccessibility;

View file

@ -1,11 +1,12 @@
import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
import { Link } from 'react-router';
const exitUrl = require('../../../images/exit.svg');
class ErrorModal extends React.Component {
componentDidMount() {
this.refs.modal.focus();
this.errorModal.focus();
}
@ -23,7 +24,7 @@ class ErrorModal extends React.Component {
staleSession() {
return (
<p>
It looks like you've been logged out. Please&nbsp;
It looks like you&apos;ve been logged out. Please&nbsp;
<Link to="/login" onClick={this.props.closeModal}>log in</Link>.
</p>
);
@ -39,7 +40,7 @@ class ErrorModal extends React.Component {
render() {
return (
<section className="error-modal" ref="modal" tabIndex="0">
<section className="error-modal" ref={(element) => { this.errorModal = element; }} tabIndex="0">
<header className="error-modal__header">
<h2 className="error-modal__title">Error</h2>
<button className="error-modal__exit-button" onClick={this.props.closeModal}>
@ -63,7 +64,7 @@ class ErrorModal extends React.Component {
}
ErrorModal.propTypes = {
type: PropTypes.string,
type: PropTypes.string.isRequired,
closeModal: PropTypes.func.isRequired
};

View file

@ -1,14 +1,15 @@
import React, { PropTypes } from 'react';
import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
import * as IDEActions from '../actions/ide';
import * as FileActions from '../actions/files';
const downArrowUrl = require('../../../images/down-arrow.svg');
const folderRightUrl = require('../../../images/triangle-arrow-right.svg');
const folderDownUrl = require('../../../images/triangle-arrow-down.svg');
const fileUrl = require('../../../images/file.svg');
import classNames from 'classnames';
export class FileNode extends React.Component {
constructor(props) {
@ -54,7 +55,7 @@ export class FileNode extends React.Component {
if (this.props.isOptionsOpen) {
this.props.hideFileOptions(this.props.id);
} else {
this.refs[`fileOptions-${this.props.id}`].focus();
this[`fileOptions-${this.props.id}`].focus();
this.props.showFileOptions(this.props.id);
}
}
@ -68,7 +69,7 @@ export class FileNode extends React.Component {
}
render() {
let itemClass = classNames({
const itemClass = classNames({
'sidebar__root-item': this.props.name === 'root',
'sidebar__file-item': this.props.name !== 'root',
'sidebar__file-item--selected': this.props.isSelectedFile,
@ -77,11 +78,7 @@ export class FileNode extends React.Component {
'sidebar__file-item--closed': this.props.isFolderClosed
});
return (
<div
className={itemClass}
onClick={this.handleFileClick}
onBlur={() => setTimeout(() => this.props.hideFileOptions(this.props.id), 200)}
>
<div className={itemClass}>
{(() => { // eslint-disable-line
if (this.props.name !== 'root') {
return (
@ -97,28 +94,28 @@ export class FileNode extends React.Component {
}
return (
<div>
<span
<button
className="sidebar__file-item-closed"
onClick={() => this.props.showFolderChildren(this.props.id)}
>
<InlineSVG className="folder-right" src={folderRightUrl} />
</span>
<span
</button>
<button
className="sidebar__file-item-open"
onClick={() => this.props.hideFolderChildren(this.props.id)}
>
<InlineSVG className="folder-down" src={folderDownUrl} />
</span>
</button>
</div>
);
})()}
<a className="sidebar__file-item-name">{this.props.name}</a>
<button className="sidebar__file-item-name" onClick={this.handleFileClick}>{this.props.name}</button>
<input
type="text"
className="sidebar__file-item-input"
value={this.props.name}
onChange={this.handleFileNameChange}
ref="fileNameInput"
ref={(element) => { this.fileNameInput = element; }}
onBlur={() => {
this.validateFileName();
this.props.hideEditFileName(this.props.id);
@ -128,21 +125,26 @@ export class FileNode extends React.Component {
<button
className="sidebar__file-item-show-options"
aria-label="view file options"
ref={`fileOptions-${this.props.id}`}
ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }}
tabIndex="0"
onClick={this.toggleFileOptions}
onBlur={() => setTimeout(() => this.props.hideFileOptions(this.props.id), 200)}
>
<InlineSVG src={downArrowUrl} />
</button>
<div ref="fileOptions" className="sidebar__file-item-options">
<div className="sidebar__file-item-options">
<ul title="file options">
{(() => { // eslint-disable-line
if (this.props.fileType === 'folder') {
return (
<li>
<a aria-label="add file" onClick={this.props.newFile} >
<button
aria-label="add file"
onClick={this.props.newFile}
className="sidebar__file-item-option"
>
Add File
</a>
</button>
</li>
);
}
@ -151,26 +153,31 @@ export class FileNode extends React.Component {
if (this.props.fileType === 'folder') {
return (
<li>
<a aria-label="add folder" onClick={this.props.newFolder} >
<button
aria-label="add folder"
onClick={this.props.newFolder}
className="sidebar__file-item-option"
>
Add Folder
</a>
</button>
</li>
);
}
})()}
<li>
<a
<button
onClick={() => {
this.originalFileName = this.props.name;
this.props.showEditFileName(this.props.id);
setTimeout(() => this.refs.fileNameInput.focus(), 0);
setTimeout(() => this.fileNameInput.focus(), 0);
}}
className="sidebar__file-item-option"
>
Rename
</a>
</button>
</li>
<li>
<a
<button
onClick={() => {
if (window.confirm(`Are you sure you want to delete ${this.props.name}?`)) {
this.isDeleting = true;
@ -178,9 +185,10 @@ export class FileNode extends React.Component {
setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100);
}
}}
className="sidebar__file-item-option"
>
Delete
</a>
</button>
</li>
</ul>
</div>
@ -205,7 +213,7 @@ export class FileNode extends React.Component {
FileNode.propTypes = {
id: PropTypes.string.isRequired,
parentId: PropTypes.string,
children: PropTypes.array,
children: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
name: PropTypes.string.isRequired,
fileType: PropTypes.string.isRequired,
isSelectedFile: PropTypes.bool,
@ -226,9 +234,22 @@ FileNode.propTypes = {
hideFolderChildren: PropTypes.func.isRequired
};
FileNode.defaultProps = {
id: '1',
name: 'test',
fileType: 'file',
children: [],
parentId: '0',
isSelectedFile: false,
isOptionsOpen: false,
isEditingName: false,
isFolderClosed: false
};
function mapStateToProps(state, ownProps) {
// this is a hack, state is updated before ownProps
return state.files.find((file) => file.id === ownProps.id) || { ...ownProps, name: 'test', fileType: 'file' };
return state.files.find(file => file.id === ownProps.id) || { ...ownProps, name: 'test', fileType: 'file' };
// return state.files.find(file => file.id === ownProps.id);
}
function mapDispatchToProps(dispatch) {

View file

@ -1,9 +1,10 @@
import React, { PropTypes } from 'react';
import Dropzone from 'dropzone';
const s3Bucket = `https://s3-us-west-2.amazonaws.com/${process.env.S3_BUCKET}/`;
import * as UploaderActions from '../actions/uploader';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as UploaderActions from '../actions/uploader';
const s3Bucket = `https://s3-us-west-2.amazonaws.com/${process.env.S3_BUCKET}/`;
class FileUploader extends React.Component {
componentDidMount() {

View file

@ -1,5 +1,6 @@
import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
class KeyboardShortcutModal extends React.Component {

View file

@ -1,11 +1,12 @@
import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import NewFileForm from './NewFileForm';
import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg';
import NewFileForm from './NewFileForm';
import FileUploader from './FileUploader';
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
@ -21,16 +22,16 @@ class NewFileModal extends React.Component {
}
focusOnModal() {
this.refs.modal.focus();
this.modal.focus();
}
render() {
const modalClass = classNames({
modal: true,
'modal': true,
'modal--reduced': !this.props.canUploadMedia
});
return (
<section className={modalClass} tabIndex="0" ref="modal">
<section className={modalClass} tabIndex="0" ref={(element) => { this.modal = element; }}>
<div className="modal-content">
<div className="modal__header">
<h2 className="modal__title">Add File</h2>

View file

@ -8,7 +8,7 @@ class NewFolderForm extends React.Component {
}
componentDidMount() {
this.refs.fileName.focus();
this.fileName.focus();
}
render() {
@ -27,7 +27,7 @@ class NewFolderForm extends React.Component {
id="name"
type="text"
placeholder="Name"
ref="fileName"
ref={(element) => { this.fileName = element; }}
{...domOnlyProps(name)}
/>
<input type="submit" value="Add Folder" aria-label="add folder" />

View file

@ -1,17 +1,18 @@
import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
import NewFolderForm from './NewFolderForm';
const exitUrl = require('../../../images/exit.svg');
class NewFolderModal extends React.Component {
componentDidMount() {
this.refs.modal.focus();
this.newFolderModal.focus();
}
render() {
return (
<section className="modal" ref="modal" tabIndex="0">
<section className="modal" ref={(element) => { this.newFolderModal = element; }} tabIndex="0">
<div className="modal-content-folder">
<div className="modal__header">
<h2 className="modal__title">Add Folder</h2>

View file

@ -49,7 +49,7 @@ class Preferences extends React.Component {
render() {
const beep = new Audio(beepUrl);
const preferencesContainerClass = classNames({
preferences: true,
'preferences': true,
'preferences--selected': this.props.isVisible
});
@ -79,15 +79,14 @@ class Preferences extends React.Component {
</button>
<input
className="preference__value"
aria-live="status"
aria-live="polite"
aria-atomic="true"
role="status"
value={this.props.fontSize}
onChange={this.handleUpdateFont}
ref="fontSizeInput"
ref={(element) => { this.fontSizeInput = element; }}
onClick={() => {
this.refs.fontSizeInput.select();
this.fontSizeInput.select();
}}
>
</input>
@ -113,15 +112,14 @@ class Preferences extends React.Component {
</button>
<input
className="preference__value"
aria-live="status"
aria-live="polite"
aria-atomic="true"
role="status"
value={this.props.indentationAmount}
onChange={this.handleUpdateIndentation}
ref="indentationInput"
ref={(element) => { this.indentationInput = element; }}
onClick={() => {
this.refs.indentationInput.select();
this.indentationInput.select();
}}
>
</input>
@ -248,13 +246,13 @@ class Preferences extends React.Component {
checked={!this.props.lintWarning}
/>
<label htmlFor="lint-warning-off" className="preference__option">Off</label>
<div
<button
className="preference__preview-button"
onClick={() => beep.play()}
aria-label="preview sound"
>
Preview sound
</div>
</button>
</div>
</div>
<div className="preference">
@ -303,7 +301,7 @@ class Preferences extends React.Component {
id="text-output-off"
className="preference__radio-button"
value="Off"
checked={!Boolean(this.props.textOutput)}
checked={!(this.props.textOutput)}
/>
<label htmlFor="text-output-off" className="preference__option preference__canvas">Off</label>

View file

@ -86,9 +86,8 @@ class PreviewFrame extends React.Component {
this.renderFrameContents();
}
if (this.props.dispatchConsoleEvent) {
window.addEventListener('message', (messageEvent) => {
messageEvent.data.forEach(message => {
messageEvent.data.forEach((message) => {
const args = message.arguments;
Object.keys(args).forEach((key) => {
if (args[key].includes('Exiting potential infinite loop')) {
@ -100,7 +99,6 @@ class PreviewFrame extends React.Component {
this.props.dispatchConsoleEvent(messageEvent.data);
});
}
}
componentDidUpdate(prevProps) {
// if sketch starts or stops playing, want to rerender
@ -128,7 +126,6 @@ class PreviewFrame extends React.Component {
if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) {
this.renderSketch();
return;
}
// small bug - if autorefresh is on, and the usr changes files
@ -136,11 +133,11 @@ class PreviewFrame extends React.Component {
}
componentWillUnmount() {
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).contentDocument.body);
ReactDOM.unmountComponentAtNode(this.iframeElement.contentDocument.body);
}
clearPreview() {
const doc = ReactDOM.findDOMNode(this);
const doc = this.iframeElement;
doc.srcDoc = '';
}
@ -204,7 +201,7 @@ class PreviewFrame extends React.Component {
scriptsToInject = scriptsToInject.concat(interceptorScripts);
}
scriptsToInject.forEach(scriptToInject => {
scriptsToInject.forEach((scriptToInject) => {
const script = sketchDoc.createElement('script');
script.src = scriptToInject;
sketchDoc.head.appendChild(script);
@ -223,7 +220,7 @@ class PreviewFrame extends React.Component {
resolvePathsForElementsWithAttribute(attr, sketchDoc, files) {
const elements = sketchDoc.querySelectorAll(`[${attr}]`);
const elementsArray = Array.prototype.slice.call(elements);
elementsArray.forEach(element => {
elementsArray.forEach((element) => {
if (element.getAttribute(attr).match(MEDIA_FILE_REGEX_NO_QUOTES)) {
const resolvedFile = resolvePathToFile(element.getAttribute(attr), files);
if (resolvedFile) {
@ -235,7 +232,7 @@ class PreviewFrame extends React.Component {
resolveJSAndCSSLinks(files) {
const newFiles = [];
files.forEach(file => {
files.forEach((file) => {
const newFile = { ...file };
if (file.name.match(/.*\.js$/i)) {
newFile.content = this.resolveJSLinksInString(newFile.content, files);
@ -251,7 +248,7 @@ class PreviewFrame extends React.Component {
let newContent = content;
let jsFileStrings = content.match(STRING_REGEX);
jsFileStrings = jsFileStrings || [];
jsFileStrings.forEach(jsFileString => {
jsFileStrings.forEach((jsFileString) => {
if (jsFileString.match(MEDIA_FILE_REGEX)) {
const filePath = jsFileString.substr(1, jsFileString.length - 2);
const resolvedFile = resolvePathToFile(filePath, files);
@ -275,7 +272,7 @@ class PreviewFrame extends React.Component {
let newContent = content;
let cssFileStrings = content.match(STRING_REGEX);
cssFileStrings = cssFileStrings || [];
cssFileStrings.forEach(cssFileString => {
cssFileStrings.forEach((cssFileString) => {
if (cssFileString.match(MEDIA_FILE_REGEX)) {
const filePath = cssFileString.substr(1, cssFileString.length - 2);
const resolvedFile = resolvePathToFile(filePath, files);
@ -292,7 +289,7 @@ class PreviewFrame extends React.Component {
resolveScripts(sketchDoc, files) {
const scriptsInHTML = sketchDoc.getElementsByTagName('script');
const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML);
scriptsInHTMLArray.forEach(script => {
scriptsInHTMLArray.forEach((script) => {
if (script.getAttribute('src') && script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null) {
const resolvedFile = resolvePathToFile(script.getAttribute('src'), files);
if (resolvedFile) {
@ -313,13 +310,13 @@ class PreviewFrame extends React.Component {
resolveStyles(sketchDoc, files) {
const inlineCSSInHTML = sketchDoc.getElementsByTagName('style');
const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML);
inlineCSSInHTMLArray.forEach(style => {
inlineCSSInHTMLArray.forEach((style) => {
style.innerHTML = this.resolveCSSLinksInString(style.innerHTML, files); // eslint-disable-line
});
const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]');
const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML);
cssLinksInHTMLArray.forEach(css => {
cssLinksInHTMLArray.forEach((css) => {
if (css.getAttribute('href') && css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null) {
const resolvedFile = resolvePathToFile(css.getAttribute('href'), files);
if (resolvedFile) {
@ -337,7 +334,7 @@ class PreviewFrame extends React.Component {
}
renderSketch() {
const doc = ReactDOM.findDOMNode(this);
const doc = this.iframeElement;
if (this.props.isPlaying) {
srcDoc.set(doc, this.injectLocalFiles());
if (this.props.endSketchRefresh) {
@ -350,7 +347,7 @@ class PreviewFrame extends React.Component {
}
renderFrameContents() {
const doc = ReactDOM.findDOMNode(this).contentDocument;
const doc = this.iframeElement.contentDocument;
if (doc.readyState === 'complete') {
this.renderSketch();
} else {
@ -366,8 +363,8 @@ class PreviewFrame extends React.Component {
role="main"
tabIndex="0"
frameBorder="0"
ref="iframe"
title="sketch output"
ref={(element) => { this.iframeElement = element; }}
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms"
/>
);
@ -379,14 +376,16 @@ PreviewFrame.propTypes = {
isTextOutputPlaying: PropTypes.bool.isRequired,
textOutput: PropTypes.number.isRequired,
setTextOutput: PropTypes.func.isRequired,
content: PropTypes.string,
htmlFile: PropTypes.shape({
content: PropTypes.string.isRequired
}),
files: PropTypes.array.isRequired,
dispatchConsoleEvent: PropTypes.func,
children: PropTypes.element,
autorefresh: PropTypes.bool.isRequired,
}).isRequired,
files: PropTypes.arrayOf(PropTypes.shape({
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
url: PropTypes.string,
id: PropTypes.string.isRequired
})).isRequired,
dispatchConsoleEvent: PropTypes.func.isRequired,
endSketchRefresh: PropTypes.func.isRequired,
previewIsRefreshing: PropTypes.bool.isRequired,
fullView: PropTypes.bool,
@ -395,4 +394,8 @@ PreviewFrame.propTypes = {
expandConsole: PropTypes.func.isRequired
};
PreviewFrame.defaultProps = {
fullView: false
};
export default PreviewFrame;

View file

@ -1,15 +1,16 @@
import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
class ShareModal extends React.Component {
componentDidMount() {
this.refs.shareModal.focus();
this.shareModal.focus();
}
render() {
const hostname = window.location.origin;
return (
<section className="share-modal" ref="shareModal" tabIndex="0">
<section className="share-modal" ref={(element) => { this.shareModal = element; }} tabIndex="0">
<header className="share-modal__header">
<h2>Share Sketch</h2>
<button className="about__exit-button" onClick={this.props.closeShareModal}>
@ -17,26 +18,29 @@ class ShareModal extends React.Component {
</button>
</header>
<div className="share-modal__section">
<label className="share-modal__label">Embed</label>
<label className="share-modal__label" htmlFor="share-modal__embed">Embed</label>
<input
type="text"
className="share-modal__input"
id="share-modal__embed"
value={`<iframe src="${hostname}/embed/${this.props.projectId}"></iframe>`}
/>
</div>
<div className="share-modal__section">
<label className="share-modal__label">Fullscreen</label>
<label className="share-modal__label" htmlFor="share-modal__fullscreen">Fullscreen</label>
<input
type="text"
className="share-modal__input"
id="share-modal__fullscreen"
value={`${hostname}/full/${this.props.projectId}`}
/>
</div>
<div className="share-modal__section">
<label className="share-modal__label">Edit</label>
<label className="share-modal__label" htmlFor="share-modal__edit">Edit</label>
<input
type="text"
className="share-modal__input"
id="share-modal__edit"
value={`${hostname}/${this.props.ownerUsername}/sketches/${this.props.projectId}`}
/>
</div>
@ -48,7 +52,7 @@ class ShareModal extends React.Component {
ShareModal.propTypes = {
projectId: PropTypes.string.isRequired,
closeShareModal: PropTypes.func.isRequired,
ownerUsername: PropTypes.string
ownerUsername: PropTypes.string.isRequired
};
export default ShareModal;

View file

@ -1,10 +1,10 @@
import React, { PropTypes } from 'react';
import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg';
// import SidebarItem from './SidebarItem';
import ConnectedFileNode from './FileNode';
const folderUrl = require('../../../images/folder.svg');
const downArrowUrl = require('../../../images/down-arrow.svg');
import ConnectedFileNode from './FileNode';
class Sidebar extends React.Component {
constructor(props) {
@ -22,14 +22,14 @@ class Sidebar extends React.Component {
if (this.props.projectOptionsVisible) {
this.props.closeProjectOptions();
} else {
this.refs.sidebarOptions.focus();
this.sidebarOptions.focus();
this.props.openProjectOptions();
}
}
render() {
const sidebarClass = classNames({
sidebar: true,
'sidebar': true,
'sidebar--contracted': !this.props.isExpanded,
'sidebar--project-options': this.props.projectOptionsVisible
});
@ -48,7 +48,7 @@ class Sidebar extends React.Component {
aria-label="add file or folder"
className="sidebar__add"
tabIndex="0"
ref="sidebarOptions"
ref={(element) => { this.sidebarOptions = element; }}
onClick={this.toggleProjectOptions}
onBlur={() => setTimeout(this.props.closeProjectOptions, 200)}
>
@ -56,14 +56,14 @@ class Sidebar extends React.Component {
</button>
<ul className="sidebar__project-options">
<li>
<a aria-label="add folder" onClick={this.props.newFolder} >
<button aria-label="add folder" onClick={this.props.newFolder} >
Add folder
</a>
</button>
</li>
<li>
<a aria-label="add file" onClick={this.props.newFile} >
<button aria-label="add file" onClick={this.props.newFile} >
Add file
</a>
</button>
</li>
</ul>
</div>
@ -75,17 +75,14 @@ class Sidebar extends React.Component {
}
Sidebar.propTypes = {
files: PropTypes.array.isRequired,
files: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
id: PropTypes.string.isRequired
})).isRequired,
setSelectedFile: PropTypes.func.isRequired,
isExpanded: PropTypes.bool.isRequired,
projectOptionsVisible: PropTypes.bool.isRequired,
newFile: PropTypes.func.isRequired,
showFileOptions: PropTypes.func.isRequired,
hideFileOptions: PropTypes.func.isRequired,
deleteFile: PropTypes.func.isRequired,
showEditFileName: PropTypes.func.isRequired,
hideEditFileName: PropTypes.func.isRequired,
updateFileName: PropTypes.func.isRequired,
openProjectOptions: PropTypes.func.isRequired,
closeProjectOptions: PropTypes.func.isRequired,
newFolder: PropTypes.func.isRequired

View file

@ -3,10 +3,11 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import moment from 'moment';
import { Link, browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import * as SketchActions from '../actions/projects';
import * as ProjectActions from '../actions/project';
import * as ToastActions from '../actions/toast';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
const trashCan = require('../../../images/trash-can.svg');
@ -47,10 +48,11 @@ class SketchList extends React.Component {
</thead>
<tbody>
{this.props.sketches.map(sketch =>
// eslint-disable-next-line
<tr
className="sketches-table__row visibility-toggle"
key={sketch.id}
onClick={() => browserHistory.push(`/${username}/sketches/${sketch._id}`)}
onClick={() => browserHistory.push(`/${username}/sketches/${sketch.id}`)}
>
<td className="sketch-list__trash-column">
{(() => { // eslint-disable-line
@ -71,7 +73,7 @@ class SketchList extends React.Component {
}
})()}
</td>
<td scope="row"><Link to={`/${username}/sketches/${sketch._id}`}>{sketch.name}</Link></td>
<th scope="row"><Link to={`/${username}/sketches/${sketch.id}`}>{sketch.name}</Link></th>
<td>{moment(sketch.createdAt).format('MMM D, YYYY h:mm A')}</td>
<td>{moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')}</td>
</tr>
@ -85,14 +87,23 @@ class SketchList extends React.Component {
}
SketchList.propTypes = {
user: PropTypes.object.isRequired,
user: PropTypes.shape({
username: PropTypes.string
}).isRequired,
getProjects: PropTypes.func.isRequired,
sketches: PropTypes.array.isRequired,
sketches: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired
})).isRequired,
username: PropTypes.string,
deleteProject: PropTypes.func.isRequired,
previousPath: PropTypes.string.isRequired,
showToast: PropTypes.func.isRequired,
setToastText: PropTypes.func.isRequired
};
SketchList.defaultProps = {
username: undefined
};
function mapStateToProps(state) {

View file

@ -2,14 +2,14 @@ import React from 'react';
class TextOutput extends React.Component {
componentDidMount() {
this.refs.canvasTextOutput.focus();
this.canvasTextOutput.focus();
}
render() {
return (
<section
className="text-output"
id="canvas-sub"
ref="canvasTextOutput"
ref={(element) => { this.canvasTextOutput = element; }}
tabIndex="0"
aria-label="text-output"
title="canvas text output"

View file

@ -49,4 +49,8 @@ Timer.propTypes = {
isUserOwner: PropTypes.bool
};
Timer.defaultProps = {
isUserOwner: false
};
export default Timer;

View file

@ -2,9 +2,10 @@ import React, { PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
import * as ToastActions from '../actions/toast';
const exitUrl = require('../../../images/exit.svg');
function Toast(props) {
return (
<section className="toast">

View file

@ -1,12 +1,13 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
const InlineSVG = require('react-inlinesvg');
import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg';
const playUrl = require('../../../images/play.svg');
const logoUrl = require('../../../images/p5js-logo.svg');
const stopUrl = require('../../../images/stop.svg');
const preferencesUrl = require('../../../images/preferences.svg');
const editProjectNameUrl = require('../../../images/pencil.svg');
import classNames from 'classnames';
class Toolbar extends React.Component {
constructor(props) {
@ -38,19 +39,19 @@ class Toolbar extends React.Component {
}
render() {
let playButtonClass = classNames({
const playButtonClass = classNames({
'toolbar__play-button': true,
'toolbar__play-button--selected': this.props.isPlaying
});
let stopButtonClass = classNames({
const stopButtonClass = classNames({
'toolbar__stop-button': true,
'toolbar__stop-button--selected': !this.props.isPlaying
});
let preferencesButtonClass = classNames({
const preferencesButtonClass = classNames({
'toolbar__preferences-button': true,
'toolbar__preferences-button--selected': this.props.preferencesIsVisible
});
let nameContainerClass = classNames({
const nameContainerClass = classNames({
'toolbar__project-name-container': true,
'toolbar__project-name-container--editing': this.props.project.isEditingName
});
@ -110,7 +111,7 @@ class Toolbar extends React.Component {
e.preventDefault();
this.originalProjectName = this.props.project.name;
this.props.showEditProjectName();
setTimeout(() => this.refs.projectNameInput.focus(), 0);
setTimeout(() => this.projectNameInput.focus(), 0);
}
}}
>
@ -122,7 +123,7 @@ class Toolbar extends React.Component {
className="toolbar__project-name-input"
value={this.props.project.name}
onChange={this.handleProjectNameChange}
ref="projectNameInput"
ref={(element) => { this.projectNameInput = element; }}
onBlur={() => {
this.validateProjectName();
this.props.hideEditProjectName();
@ -157,7 +158,6 @@ class Toolbar extends React.Component {
Toolbar.propTypes = {
isPlaying: PropTypes.bool.isRequired,
preferencesIsVisible: PropTypes.bool.isRequired,
startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired,
startTextOutput: PropTypes.func.isRequired,
stopTextOutput: PropTypes.func.isRequired,
@ -182,4 +182,9 @@ Toolbar.propTypes = {
clearConsole: PropTypes.func.isRequired
};
Toolbar.defaultProps = {
owner: undefined,
currentUser: undefined
};
export default Toolbar;

View file

@ -38,18 +38,34 @@ class FullView extends React.Component {
FullView.propTypes = {
params: PropTypes.shape({
project_id: PropTypes.string
}),
}).isRequired,
project: PropTypes.shape({
name: PropTypes.string,
owner: PropTypes.shape({
username: PropTypes.string
})
}).isRequired,
htmlFile: PropTypes.object,
jsFiles: PropTypes.array,
cssFiles: PropTypes.array,
htmlFile: PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired,
jsFiles: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
})).isRequired,
cssFiles: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
})).isRequired,
getProject: PropTypes.func.isRequired,
files: PropTypes.array
files: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
})).isRequired,
};
function mapStateToProps(state) {

View file

@ -1,4 +1,8 @@
import React, { PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import SplitPane from 'react-split-pane';
import Editor from '../components/Editor';
import Sidebar from '../components/Sidebar';
import PreviewFrame from '../components/PreviewFrame';
@ -13,9 +17,6 @@ import ErrorModal from '../components/ErrorModal';
import Nav from '../../../components/Nav';
import Console from '../components/Console';
import Toast from '../components/Toast';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide';
import * as ProjectActions from '../actions/project';
@ -25,7 +26,6 @@ import * as UserActions from '../../User/actions';
import * as ToastActions from '../actions/toast';
import * as ConsoleActions from '../actions/console';
import { getHTMLFile } from '../reducers/files';
import SplitPane from 'react-split-pane';
import Overlay from '../../App/components/Overlay';
import SketchList from '../components/SketchList';
import About from '../components/About';
@ -55,7 +55,7 @@ class IDEView extends React.Component {
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
document.addEventListener('keydown', this.handleGlobalKeydown, false);
this.props.router.setRouteLeaveHook(this.props.route, (route) => this.warnIfUnsavedChanges(route));
this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route));
window.onbeforeunload = () => this.warnIfUnsavedChanges();
@ -104,7 +104,7 @@ class IDEView extends React.Component {
}
if (this.props.route.path !== prevProps.route.path) {
this.props.router.setRouteLeaveHook(this.props.route, (route) => this.warnIfUnsavedChanges(route));
this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route));
}
}
@ -120,16 +120,16 @@ class IDEView extends React.Component {
}
_handleConsolePaneOnDragFinished() {
this.consoleSize = this.refs.consolePane.state.draggedSize;
this.refs.consolePane.setState({
this.consoleSize = this.consolePane.state.draggedSize;
this.consolePane.setState({
resized: false,
draggedSize: undefined,
});
}
_handleSidebarPaneOnDragFinished() {
this.sidebarSize = this.refs.sidebarPane.state.draggedSize;
this.refs.sidebarPane.setState({
this.sidebarSize = this.sidebarPane.state.draggedSize;
this.sidebarPane.setState({
resized: false,
draggedSize: undefined
});
@ -140,7 +140,7 @@ class IDEView extends React.Component {
if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) {
e.preventDefault();
e.stopPropagation();
if (this.isUserOwner() || this.props.user.authenticated && !this.props.project.owner) {
if (this.isUserOwner() || (this.props.user.authenticated && !this.props.project.owner)) {
this.props.saveProject();
}
// 13 === enter
@ -170,7 +170,7 @@ class IDEView extends React.Component {
warnIfUnsavedChanges(route) { // eslint-disable-line
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
// don't warn
} else if (route && this.props.location.pathname === '/login' || this.props.location.pathname === '/signup') {
} else if (route && (this.props.location.pathname === '/login' || this.props.location.pathname === '/signup')) {
// don't warn
} else if (this.props.ide.unsavedChanges) {
if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) {
@ -202,7 +202,6 @@ class IDEView extends React.Component {
<Toolbar
className="Toolbar"
isPlaying={this.props.ide.isPlaying}
startSketch={this.props.startSketch}
stopSketch={this.props.stopSketch}
startTextOutput={this.props.startTextOutput}
stopTextOutput={this.props.stopTextOutput}
@ -246,7 +245,7 @@ class IDEView extends React.Component {
<SplitPane
split="vertical"
defaultSize={this.sidebarSize}
ref="sidebarPane"
ref={(element) => { this.sidebarPane = element; }}
onDragFinished={this._handleSidebarPaneOnDragFinished}
allowResize={this.props.ide.sidebarIsExpanded}
minSize={20}
@ -270,15 +269,15 @@ class IDEView extends React.Component {
<SplitPane
split="vertical"
defaultSize={'50%'}
onChange={() => (this.refs.overlay.style.display = 'block')}
onDragFinished={() => (this.refs.overlay.style.display = 'none')}
onChange={() => (this.overlay.style.display = 'block')}
onDragFinished={() => (this.overlay.style.display = 'none')}
>
<SplitPane
split="horizontal"
primary="second"
defaultSize={this.consoleSize}
minSize={29}
ref="consolePane"
ref={(element) => { this.consolePane = element; }}
onDragFinished={this._handleConsolePaneOnDragFinished}
allowResize={this.props.ide.consoleIsExpanded}
className="editor-preview-subpanel"
@ -286,7 +285,6 @@ class IDEView extends React.Component {
<Editor
lintWarning={this.props.preferences.lintWarning}
lintMessages={this.props.editorAccessibility.lintMessages}
updateLineNumber={this.props.updateLineNumber}
updateLintMessage={this.props.updateLintMessage}
clearLintMessage={this.props.clearLintMessage}
file={this.props.selectedFile}
@ -295,8 +293,6 @@ class IDEView extends React.Component {
indentationAmount={this.props.preferences.indentationAmount}
isTabIndent={this.props.preferences.isTabIndent}
files={this.props.files}
lintMessages={this.props.editorAccessibility.lintMessages}
lineNumber={this.props.editorAccessibility.lineNumber}
editorOptionsVisible={this.props.ide.editorOptionsVisible}
showEditorOptions={this.props.showEditorOptions}
closeEditorOptions={this.props.closeEditorOptions}
@ -317,7 +313,6 @@ class IDEView extends React.Component {
/>
<Console
consoleEvents={this.props.console}
isPlaying={this.props.ide.isPlaying}
isExpanded={this.props.ide.consoleIsExpanded}
expandConsole={this.props.expandConsole}
collapseConsole={this.props.collapseConsole}
@ -325,7 +320,7 @@ class IDEView extends React.Component {
/>
</SplitPane>
<div className="preview-frame-holder">
<div className="preview-frame-overlay" ref="overlay">
<div className="preview-frame-overlay" ref={(element) => { this.overlay = element; }}>
</div>
<div>
{(() => {
@ -351,7 +346,6 @@ class IDEView extends React.Component {
endSketchRefresh={this.props.endSketchRefresh}
stopSketch={this.props.stopSketch}
setBlobUrl={this.props.setBlobUrl}
stopSketch={this.props.stopSketch}
expandConsole={this.props.expandConsole}
/>
</div>
@ -449,10 +443,10 @@ IDEView.propTypes = {
project_id: PropTypes.string,
username: PropTypes.string,
reset_password_token: PropTypes.string,
}),
}).isRequired,
location: PropTypes.shape({
pathname: PropTypes.string
}),
}).isRequired,
getProject: PropTypes.func.isRequired,
user: PropTypes.shape({
authenticated: PropTypes.bool.isRequired,
@ -483,12 +477,9 @@ IDEView.propTypes = {
justOpenedProject: PropTypes.bool.isRequired,
errorType: PropTypes.string
}).isRequired,
startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired,
startTextOutput: PropTypes.func.isRequired,
stopTextOutput: PropTypes.func.isRequired,
detectInfiniteLoops: PropTypes.func.isRequired,
resetInfiniteLoops: PropTypes.func.isRequired,
project: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string.isRequired,
@ -501,11 +492,9 @@ IDEView.propTypes = {
openPreferences: PropTypes.func.isRequired,
editorAccessibility: PropTypes.shape({
lintMessages: PropTypes.array.isRequired,
lineNumber: PropTypes.string.isRequired
}).isRequired,
updateLintMessage: PropTypes.func.isRequired,
clearLintMessage: PropTypes.func.isRequired,
updateLineNumber: PropTypes.func.isRequired,
preferences: PropTypes.shape({
fontSize: PropTypes.number.isRequired,
indentationAmount: PropTypes.number.isRequired,
@ -524,14 +513,22 @@ IDEView.propTypes = {
setAutosave: PropTypes.func.isRequired,
setLintWarning: PropTypes.func.isRequired,
setTextOutput: PropTypes.func.isRequired,
files: PropTypes.array.isRequired,
files: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
})).isRequired,
updateFileContent: PropTypes.func.isRequired,
selectedFile: PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
}),
}).isRequired,
setSelectedFile: PropTypes.func.isRequired,
htmlFile: PropTypes.object.isRequired,
htmlFile: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
}).isRequired,
dispatchConsoleEvent: PropTypes.func.isRequired,
newFile: PropTypes.func.isRequired,
closeNewFileModal: PropTypes.func.isRequired,
@ -565,13 +562,11 @@ IDEView.propTypes = {
toast: PropTypes.shape({
isVisible: PropTypes.bool.isRequired
}).isRequired,
showToast: PropTypes.func.isRequired,
setToastText: PropTypes.func.isRequired,
autosaveProject: PropTypes.func.isRequired,
router: PropTypes.shape({
setRouteLeaveHook: PropTypes.func
}).isRequired,
route: PropTypes.object.isRequired,
route: PropTypes.oneOfType([PropTypes.object, PropTypes.element]).isRequired,
setUnsavedChanges: PropTypes.func.isRequired,
setTheme: PropTypes.func.isRequired,
setAutorefresh: PropTypes.func.isRequired,
@ -580,8 +575,10 @@ IDEView.propTypes = {
startRefreshSketch: PropTypes.func.isRequired,
setBlobUrl: PropTypes.func.isRequired,
setPreviousPath: PropTypes.func.isRequired,
resetProject: PropTypes.func.isRequired,
console: PropTypes.array.isRequired,
console: PropTypes.arrayOf(PropTypes.shape({
method: PropTypes.string.isRequired,
args: PropTypes.arrayOf(PropTypes.string)
})).isRequired,
clearConsole: PropTypes.func.isRequired,
showErrorModal: PropTypes.func.isRequired,
hideErrorModal: PropTypes.func.isRequired

View file

@ -2,11 +2,18 @@ import * as ActionTypes from '../../../constants';
const consoleMax = 200;
const initialState = [];
let messageId = 0;
const console = (state = initialState, action) => {
let messages;
switch (action.type) {
case ActionTypes.CONSOLE_EVENT:
return state.concat(action.event).slice(-consoleMax);
messages = [...action.event];
messages.forEach((message) => {
message.id = messageId;
messageId += 1;
});
return state.concat(messages).slice(-consoleMax);
case ActionTypes.CLEAR_CONSOLE:
return [];
default:

View file

@ -1,21 +1,20 @@
import * as ActionTypes from '../../../constants';
const initialState = {
lineNumber: 'line',
lintMessages: []
};
let messageId = 0;
const editorAccessibility = (state = initialState, action) => {
switch (action.type) {
case ActionTypes.UPDATE_LINT_MESSAGE:
messageId += 1;
return Object.assign({}, state, {
lintMessages: state.lintMessages.concat(
{ severity: action.severity, line: action.line, message: action.message })
{ severity: action.severity, line: action.line, message: action.message, id: messageId })
});
case ActionTypes.CLEAR_LINT_MESSAGE:
return Object.assign({}, state, { lintMessages: [] });
case ActionTypes.UPDATE_LINENUMBER:
return Object.assign({}, state, { lineNumber: `line ${action.lineNumber}` });
default:
return state;
}

View file

@ -1,5 +1,5 @@
import * as ActionTypes from '../../../constants';
import objectID from 'bson-objectid';
import * as ActionTypes from '../../../constants';
const defaultSketch = `function setup() {
createCanvas(400, 400);
@ -42,7 +42,8 @@ const initialState = () => {
id: r,
_id: r,
children: [a, b, c],
fileType: 'folder'
fileType: 'folder',
content: ''
},
{
name: 'sketch.js',
@ -92,7 +93,7 @@ function deleteChild(state, parentId, id) {
function deleteMany(state, ids) {
const newState = [...state];
ids.forEach(id => {
ids.forEach((id) => {
let fileIndex;
newState.find((file, index) => {
if (file.id === id) {
@ -111,7 +112,7 @@ const files = (state, action) => {
}
switch (action.type) {
case ActionTypes.UPDATE_FILE_CONTENT:
return state.map(file => {
return state.map((file) => {
if (file.name !== action.name) {
return file;
}
@ -119,7 +120,7 @@ const files = (state, action) => {
return Object.assign({}, file, { content: action.content });
});
case ActionTypes.SET_BLOB_URL:
return state.map(file => {
return state.map((file) => {
if (file.name !== action.name) {
return file;
}
@ -151,7 +152,7 @@ const files = (state, action) => {
fileType: action.fileType || 'file' }];
}
case ActionTypes.SHOW_FILE_OPTIONS:
return state.map(file => {
return state.map((file) => {
if (file.id !== action.id) {
return file;
}
@ -159,7 +160,7 @@ const files = (state, action) => {
return Object.assign({}, file, { isOptionsOpen: true });
});
case ActionTypes.HIDE_FILE_OPTIONS:
return state.map(file => {
return state.map((file) => {
if (file.id !== action.id) {
return file;
}
@ -167,7 +168,7 @@ const files = (state, action) => {
return Object.assign({}, file, { isOptionsOpen: false });
});
case ActionTypes.UPDATE_FILE_NAME:
return state.map(file => {
return state.map((file) => {
if (file.id !== action.id) {
return file;
}
@ -188,7 +189,7 @@ const files = (state, action) => {
// return newState.filter(file => file.id !== action.id);
}
case ActionTypes.SHOW_EDIT_FILE_NAME:
return state.map(file => {
return state.map((file) => {
if (file.id !== action.id) {
return file;
}
@ -196,7 +197,7 @@ const files = (state, action) => {
return Object.assign({}, file, { isEditingName: true });
});
case ActionTypes.HIDE_EDIT_FILE_NAME:
return state.map(file => {
return state.map((file) => {
if (file.id !== action.id) {
return file;
}
@ -204,21 +205,21 @@ const files = (state, action) => {
return Object.assign({}, file, { isEditingName: false });
});
case ActionTypes.SET_SELECTED_FILE:
return state.map(file => {
return state.map((file) => {
if (file.id === action.selectedFile) {
return Object.assign({}, file, { isSelectedFile: true });
}
return Object.assign({}, file, { isSelectedFile: false });
});
case ActionTypes.SHOW_FOLDER_CHILDREN:
return state.map(file => {
return state.map((file) => {
if (file.id === action.id) {
return Object.assign({}, file, { isFolderClosed: false });
}
return file;
});
case ActionTypes.HIDE_FOLDER_CHILDREN:
return state.map(file => {
return state.map((file) => {
if (file.id === action.id) {
return Object.assign({}, file, { isFolderClosed: true });
}
@ -229,9 +230,9 @@ const files = (state, action) => {
}
};
export const getHTMLFile = (state) => state.filter(file => file.name.match(/.*\.html$/i))[0];
export const getJSFiles = (state) => state.filter(file => file.name.match(/.*\.js$/i));
export const getCSSFiles = (state) => state.filter(file => file.name.match(/.*\.css$/i));
export const getLinkedFiles = (state) => state.filter(file => file.url);
export const getHTMLFile = state => state.filter(file => file.name.match(/.*\.html$/i))[0];
export const getJSFiles = state => state.filter(file => file.name.match(/.*\.js$/i));
export const getCSSFiles = state => state.filter(file => file.name.match(/.*\.css$/i));
export const getLinkedFiles = state => state.filter(file => file.url);
export default files;

View file

@ -1,5 +1,5 @@
import * as ActionTypes from '../../../constants';
import generate from 'project-name-generator';
import * as ActionTypes from '../../../constants';
const initialState = () => {
const generatedString = generate({ words: 2 }).spaced;

View file

@ -5,7 +5,7 @@ const sketches = (state = [], action) => {
case ActionTypes.SET_PROJECTS:
return action.projects;
case ActionTypes.DELETE_PROJECT:
return state.filter((sketch) =>
return state.filter(sketch =>
sketch.id !== action.id
);
default:

View file

@ -1,6 +1,6 @@
import * as ActionTypes from '../../constants';
import { browserHistory } from 'react-router';
import axios from 'axios';
import * as ActionTypes from '../../constants';
import { showErrorModal, justOpenedProject } from '../IDE/actions/ide';
@ -16,7 +16,7 @@ export function authError(error) {
export function signUpUser(previousPath, formValues) {
return (dispatch) => {
axios.post(`${ROOT_URL}/signup`, formValues, { withCredentials: true })
.then(response => {
.then((response) => {
dispatch({ type: ActionTypes.AUTH_USER,
user: response.data
});
@ -48,7 +48,7 @@ export function loginUserFailure(error) {
export function validateAndLoginUser(previousPath, formProps, dispatch) {
return new Promise((resolve, reject) => {
loginUser(formProps)
.then(response => {
.then((response) => {
dispatch({ type: ActionTypes.AUTH_USER,
user: response.data
});
@ -60,7 +60,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) {
browserHistory.push(previousPath);
resolve();
})
.catch(response => {
.catch((response) => {
reject({ password: response.data.message, _error: 'Login failed!' });
});
});
@ -69,7 +69,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) {
export function getUser() {
return (dispatch) => {
axios.get(`${ROOT_URL}/session`, { withCredentials: true })
.then(response => {
.then((response) => {
dispatch({
type: ActionTypes.AUTH_USER,
user: response.data
@ -79,7 +79,7 @@ export function getUser() {
preferences: response.data.preferences
});
})
.catch(response => {
.catch((response) => {
dispatch(authError(response.data.error));
});
};
@ -88,13 +88,13 @@ export function getUser() {
export function validateSession() {
return (dispatch, getState) => {
axios.get(`${ROOT_URL}/session`, { withCredentials: true })
.then(response => {
.then((response) => {
const state = getState();
if (state.user.username !== response.data.username) {
dispatch(showErrorModal('staleSession'));
}
})
.catch(response => {
.catch((response) => {
if (response.status === 404) {
dispatch(showErrorModal('staleSession'));
}

View file

@ -40,9 +40,14 @@ LoginForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
validateAndLoginUser: PropTypes.func.isRequired,
submitting: PropTypes.bool,
invalid: PropTypes.bool,
pristine: PropTypes.bool,
previousPath: PropTypes.string.isRequired
};
LoginForm.defaultProps = {
submitting: false,
pristine: true,
invalid: false
};
export default LoginForm;

View file

@ -44,7 +44,13 @@ NewPasswordForm.propTypes = {
pristine: PropTypes.bool,
params: PropTypes.shape({
reset_password_token: PropTypes.string,
}),
}).isRequired,
};
NewPasswordForm.defaultProps = {
invalid: false,
pristine: true,
submitting: false
};
export default NewPasswordForm;

View file

@ -31,7 +31,13 @@ ResetPasswordForm.propTypes = {
pristine: PropTypes.bool,
user: PropTypes.shape({
resetPasswordInitiate: PropTypes.bool
})
}).isRequired
};
ResetPasswordForm.defaultProps = {
submitting: false,
pristine: true,
invalid: false
};
export default ResetPasswordForm;

View file

@ -69,4 +69,10 @@ SignupForm.propTypes = {
previousPath: PropTypes.string.isRequired
};
SignupForm.defaultProps = {
submitting: false,
pristine: true,
invalid: false
};
export default SignupForm;

View file

@ -1,10 +1,10 @@
import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import { Link, browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import { validateAndLoginUser } from '../actions';
import LoginForm from '../components/LoginForm';
// import GithubButton from '../components/GithubButton';
import { Link, browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
const logoUrl = require('../../../images/p5js-logo.svg');
@ -41,7 +41,7 @@ class LoginView extends React.Component {
{/* <h2 className="form-container__divider">Or</h2>
<GithubButton buttonText="Login with Github" /> */}
<p className="form__navigation-options">
Don't have an account?&nbsp;
Don&apos;t have an account?&nbsp;
<Link className="form__signup-button" to="/signup">Sign Up</Link>
</p>
<p className="form__navigation-options">

View file

@ -1,11 +1,12 @@
import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import NewPasswordForm from '../components/NewPasswordForm';
import * as UserActions from '../actions';
import { bindActionCreators } from 'redux';
import classNames from 'classnames';
import { browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import { bindActionCreators } from 'redux';
import NewPasswordForm from '../components/NewPasswordForm';
import * as UserActions from '../actions';
const exitUrl = require('../../../images/exit.svg');
const logoUrl = require('../../../images/p5js-logo.svg');
@ -55,11 +56,11 @@ class NewPasswordView extends React.Component {
NewPasswordView.propTypes = {
params: PropTypes.shape({
reset_password_token: PropTypes.string,
}),
}).isRequired,
validateResetPasswordToken: PropTypes.func.isRequired,
user: PropTypes.shape({
resetPasswordInvalid: PropTypes.bool
})
}).isRequired
};
function validate(formProps) {

View file

@ -1,11 +1,12 @@
import React, { PropTypes } from 'react';
import { Link, browserHistory } from 'react-router';
import * as UserActions from '../actions';
import { bindActionCreators } from 'redux';
import { reduxForm } from 'redux-form';
import ResetPasswordForm from '../components/ResetPasswordForm';
import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg';
import { bindActionCreators } from 'redux';
import { reduxForm } from 'redux-form';
import * as UserActions from '../actions';
import ResetPasswordForm from '../components/ResetPasswordForm';
const exitUrl = require('../../../images/exit.svg');
const logoUrl = require('../../../images/p5js-logo.svg');
@ -43,7 +44,7 @@ class ResetPasswordView extends React.Component {
<h2 className="form-container__title">Reset Your Password</h2>
<ResetPasswordForm {...this.props} />
<p className="reset-password__submitted">
Your password reset email should arrive shortly. If you don't see it, check
Your password reset email should arrive shortly. If you don&apos;t see it, check
in your spam folder as sometimes it can end up there.
</p>
<p className="form__navigation-options">

View file

@ -1,11 +1,12 @@
import React, { PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import * as UserActions from '../actions';
import { reduxForm } from 'redux-form';
import SignupForm from '../components/SignupForm';
import axios from 'axios';
import { Link, browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import { reduxForm } from 'redux-form';
import * as UserActions from '../actions';
import SignupForm from '../components/SignupForm';
const exitUrl = require('../../../images/exit.svg');
const logoUrl = require('../../../images/p5js-logo.svg');
@ -66,7 +67,7 @@ function asyncValidate(formProps, dispatch, props) {
queryParams[fieldToValidate] = formProps[fieldToValidate];
queryParams.check_type = fieldToValidate;
return axios.get('/api/signup/duplicate_check', { params: queryParams })
.then(response => {
.then((response) => {
if (response.data.exists) {
const error = {};
error[fieldToValidate] = response.data.message;
@ -90,7 +91,7 @@ function validate(formProps) {
if (!formProps.email) {
errors.email = 'Please enter an email.';
} else if (!formProps.email.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i)) {
} else if (!formProps.email.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i)) {
errors.email = 'Please enter a valid email address.';
}

View file

@ -1,4 +1,5 @@
import { combineReducers } from 'redux';
import { reducer as form } from 'redux-form';
import files from './modules/IDE/reducers/files';
import ide from './modules/IDE/reducers/ide';
import preferences from './modules/IDE/reducers/preferences';
@ -8,7 +9,6 @@ import user from './modules/User/reducers';
import sketches from './modules/IDE/reducers/projects';
import toast from './modules/IDE/reducers/toast';
import console from './modules/IDE/reducers/console';
import { reducer as form } from 'redux-form';
const rootReducer = combineReducers({
form,

View file

@ -14,7 +14,7 @@ const checkAuth = (store) => {
store.dispatch(getUser());
};
const routes = (store) =>
const routes = store =>
(
<Route path="/" component={App}>
<IndexRoute component={IDEView} onEnter={checkAuth(store)} />

View file

@ -175,3 +175,16 @@
height:1px;
overflow:hidden;
}
%link {
@include themify() {
text-decoration: none;
color: getThemifyVariable('inactive-text-color');
cursor: pointer;
&:hover {
text-decoration: none;
color: getThemifyVariable('primary-text-color');
}
}
}

View file

@ -6,10 +6,13 @@ html, body {
font-size: #{$base-font-size}px;
}
body, input, button {
body, input {
@include themify() {
color: getThemifyVariable('primary-text-color');
}
}
body, input, button {
font-family: Montserrat, sans-serif;
}
@ -20,13 +23,7 @@ body, input, button {
a {
@include themify() {
text-decoration: none;
color: getThemifyVariable('inactive-text-color');
cursor: pointer;
&:hover {
text-decoration: none;
color: getThemifyVariable('primary-text-color');
}
@extend %link;
}
}
@ -34,10 +31,6 @@ input, button {
font-size: 1rem;
}
button:focus {
outline: none;
}
input {
padding: #{5 / $base-font-size}rem;
border: 1px solid ;
@ -55,6 +48,14 @@ input[type="submit"] {
}
}
button {
@include themify() {
@extend %link;
}
background: transparent;
border: none;
}
h2 {
font-size: #{21 / $base-font-size}em;
}
@ -71,3 +72,6 @@ h6 {
thead {
text-align: left;
}
th {
text-align: left;
}

View file

@ -97,6 +97,11 @@
}
.preview-console__clear {
@include themify() {
@extend %link;
}
background: transparent;
border: none;
padding-right: #{10 / $base-font-size}rem;
.preview-console--collapsed & {
display: none;

View file

@ -24,7 +24,8 @@
margin-right: #{-6 / $base-font-size}rem;
}
.preference button {
.preference__minus-button,
.preference__plus-button {
@include themify() {
@extend %preferences-button;
width: #{32 / $base-font-size}rem;

View file

@ -56,6 +56,7 @@
height: #{20 / $base-font-size}rem;
font-size: #{12 / $base-font-size}rem;
cursor: pointer;
position: relative;
@include themify() {
color: map-get($theme-map, 'inactive-text-color');
&:hover > .file-item__content .sidebar__file-item-name {
@ -106,6 +107,15 @@
.sidebar__file-item--editing & {
display: none;
}
&:before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: '';
width: 100%;
}
}
.sidebar__file-item-show-options {
@ -225,6 +235,8 @@
& svg {
height: #{10 / $base-font-size}rem;
}
background-color: transparent;
border: none;
}
.sidebar__file-item-closed {
@ -260,6 +272,14 @@
width: #{145 / $base-font-size}rem;
}
.sidebar__file-item-option {
@include themify() {
@extend %link;
}
background-color: transparent;
border: none;
}
.sidebar__file-item--closed .file-item__children {
display: none;

View file

@ -10,7 +10,7 @@
"build": "NODE_ENV=production webpack --config webpack.config.prod.js --progress",
"test": "echo \"Error: no test specified\" && exit 1",
"fetch-examples": "node fetch-examples.js",
"postinstall" : "git submodule update --remote --recursive"
"postinstall": "git submodule update --remote --recursive"
},
"main": "index.js",
"author": "Cassie Tarakajian",
@ -20,27 +20,25 @@
"url": "git+https://github.com/catarak/p5.js-web-editor.git"
},
"devDependencies": {
"babel-eslint": "^6.1.0",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.4",
"babel-plugin-transform-react-constant-elements": "^6.8.0",
"babel-plugin-transform-react-inline-elements": "^6.8.0",
"babel-plugin-transform-react-remove-prop-types": "^0.2.6",
"babel-polyfill": "^6.8.0",
"babel-preset-es2015": "^6.6.0",
"babel-preset-es2015-native-modules": "^6.9.2",
"babel-preset-react": "^6.5.0",
"babel-preset-react-hmre": "^1.1.1",
"babel-preset-react-optimize": "^1.0.1",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.8.0",
"chunk-manifest-webpack-plugin": "^0.1.0",
"css-loader": "^0.23.1",
"cssnano": "^3.7.1",
"eslint": "^2.13.1",
"eslint-config-airbnb": "^9.0.1",
"eslint-plugin-import": "^1.9.2",
"eslint-plugin-jsx-a11y": "^1.5.3",
"eslint-plugin-react": "^5.2.2",
"eslint": "^3.14.0",
"eslint-config-airbnb": "^14.0.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^3.0.2",
"eslint-plugin-react": "^6.9.0",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5",
"node-sass": "^3.7.0",
@ -49,14 +47,8 @@
"postcss-focus": "^1.0.0",
"postcss-loader": "^0.9.1",
"postcss-reporter": "^1.3.3",
"redux-devtools": "^3.3.1",
"redux-devtools-dock-monitor": "^1.1.1",
"redux-devtools-log-monitor": "^1.0.11",
"sass-loader": "^3.2.0",
"style-loader": "^0.13.1",
"webpack": "^1.14.0",
"webpack-dev-middleware": "^1.6.1",
"webpack-hot-middleware": "^2.10.0",
"webpack-manifest-plugin": "^1.1.0"
},
"engines": {
@ -67,6 +59,8 @@
"async": "^2.0.0",
"axios": "^0.12.0",
"babel-core": "^6.8.0",
"babel-polyfill": "^6.8.0",
"babel-register": "^6.8.0",
"bcrypt-nodejs": "0.0.3",
"blob-util": "^1.2.1",
"body-parser": "^1.15.1",
@ -106,6 +100,9 @@
"react-router": "^2.6.0",
"react-split-pane": "^0.1.44",
"redux": "^3.5.2",
"redux-devtools": "^3.3.1",
"redux-devtools-dock-monitor": "^1.1.1",
"redux-devtools-log-monitor": "^1.0.11",
"redux-form": "^5.3.3",
"redux-thunk": "^2.1.0",
"request": "^2.76.0",
@ -113,6 +110,9 @@
"s3-policy": "^0.2.0",
"shortid": "^2.2.6",
"srcdoc-polyfill": "^0.2.0",
"webpack": "^1.14.0",
"webpack-dev-middleware": "^1.6.1",
"webpack-hot-middleware": "^2.10.0",
"xhr": "^2.2.1"
}
}

View file

@ -1,9 +1,9 @@
import User from '../models/user';
const passport = require('passport');
const GitHubStrategy = require('passport-github').Strategy;
const LocalStrategy = require('passport-local').Strategy;
import User from '../models/user';
passport.serializeUser((user, done) => {
done(null, user.id);
});
@ -30,7 +30,7 @@ passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, don
return done(null, false, { msg: 'Invalid email or password.' });
});
})
.catch((err) => done(null, false, { msg: err }));
.catch(err => done(null, false, { msg: err }));
}));
/**
@ -42,20 +42,19 @@ passport.use(new GitHubStrategy({
callbackURL: '/auth/github/callback',
passReqToCallback: true
}, (req, accessToken, refreshToken, profile, done) => {
User.findOne({ github: profile.id }, (err, existingUser) => {
User.findOne({ github: profile.id }, (findByGithubErr, existingUser) => {
if (existingUser) {
return done(null, existingUser);
done(null, existingUser);
return;
}
User.findOne({ email: profile._json.email }, (err, existingEmailUser) => {
User.findOne({ email: profile._json.email }, (findByEmailErr, existingEmailUser) => {
if (existingEmailUser) {
existingEmailUser.email = existingEmailUser.email || profile._json.email;
existingEmailUser.github = profile.id;
existingEmailUser.username = existingEmailUser.username || profile.username;
existingEmailUser.tokens.push({ kind: 'github', accessToken });
existingEmailUser.name = existingEmailUser.name || profile.displayName;
existingEmailUser.save((err) => {
return done(null, existingEmailUser);
});
existingEmailUser.save(saveErr => done(null, existingEmailUser));
} else {
const user = new User();
user.email = profile._json.email;
@ -63,9 +62,7 @@ passport.use(new GitHubStrategy({
user.username = profile.username;
user.tokens.push({ kind: 'github', accessToken });
user.name = profile.displayName;
user.save((err) => {
return done(null, user);
});
user.save(saveErr => done(null, user));
}
});
});

View file

@ -26,3 +26,5 @@ export function signS3(req, res) {
};
return res.json(result);
}
export default signS3;

View file

@ -1,10 +1,10 @@
import jsdom, { serializeDocument } from 'jsdom';
import Project from '../models/project';
import {
injectMediaUrls,
resolvePathsForElementsWithAttribute,
resolveScripts,
resolveStyles } from '../utils/previewGeneration';
import jsdom, { serializeDocument } from 'jsdom';
export function serveProject(req, res) {
Project.findById(req.params.project_id)
@ -32,3 +32,5 @@ export function serveProject(req, res) {
});
});
}
export default serveProject;

View file

@ -16,16 +16,18 @@ export function createFile(req, res) {
}, (err, updatedProject) => {
if (err) {
console.log(err);
return res.json({ success: false });
res.json({ success: false });
return;
}
const newFile = updatedProject.files[updatedProject.files.length - 1];
updatedProject.files.id(req.body.parentId).children.push(newFile.id);
updatedProject.save(innerErr => {
updatedProject.save((innerErr) => {
if (innerErr) {
console.log(innerErr);
return res.json({ success: false });
res.json({ success: false });
return;
}
return res.json(updatedProject.files[updatedProject.files.length - 1]);
res.json(updatedProject.files[updatedProject.files.length - 1]);
});
});
}
@ -38,13 +40,13 @@ function getAllDescendantIds(files, nodeId) {
}
function deleteMany(files, ids) {
ids.forEach(id => {
ids.forEach((id) => {
files.id(id).remove();
});
}
function deleteChild(files, parentId, id) {
files = files.map((file) => {
return files.map((file) => {
if (file.id === parentId) {
file.children = file.children.filter(child => child !== id);
return file;
@ -57,11 +59,11 @@ export function deleteFile(req, res) {
Project.findById(req.params.project_id, (err, project) => {
const idsToDelete = getAllDescendantIds(project.files, req.params.file_id);
deleteMany(project.files, [req.params.file_id, ...idsToDelete]);
deleteChild(project.files, req.query.parentId, req.params.file_id);
project.files = deleteChild(project.files, req.query.parentId, req.params.file_id);
// project.files.id(req.params.file_id).remove();
// const childrenArray = project.files.id(req.query.parentId).children;
// project.files.id(req.query.parentId).children = childrenArray.filter(id => id !== req.params.file_id);
project.save(innerErr => {
project.save((innerErr) => {
res.json(project.files);
});
});
@ -70,12 +72,14 @@ export function deleteFile(req, res) {
export function getFileContent(req, res) {
Project.findById(req.params.project_id, (err, project) => {
if (err) {
return res.status(404).send({ success: false, message: 'Project with that id does not exist.' });
res.status(404).send({ success: false, message: 'Project with that id does not exist.' });
return;
}
const filePath = req.params[0];
const resolvedFile = resolvePathToFile(filePath, project.files);
if (!resolvedFile) {
return res.status(404).send({ success: false, message: 'File with that name and path does not exist.' });
res.status(404).send({ success: false, message: 'File with that name and path does not exist.' });
return;
}
res.send(resolvedFile.content);
});

View file

@ -1,13 +1,13 @@
import Project from '../models/project';
import User from '../models/user';
import archiver from 'archiver';
import request from 'request';
import moment from 'moment';
import Project from '../models/project';
import User from '../models/user';
export function createProject(req, res) {
if (!req.user) {
return res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
return;
}
let projectValues = {
@ -17,20 +17,27 @@ export function createProject(req, res) {
projectValues = Object.assign(projectValues, req.body);
Project.create(projectValues, (err, newProject) => {
if (err) { return res.json({ success: false }); }
if (err) {
res.json({ success: false });
return;
}
Project.populate(newProject,
{ path: 'user', select: 'username' },
(innerErr, newProjectWithUser) => {
if (innerErr) { return res.json({ success: false }); }
return res.json(newProjectWithUser);
if (innerErr) {
res.json({ success: false });
return;
}
res.json(newProjectWithUser);
});
});
}
export function updateProject(req, res) {
Project.findById(req.params.project_id, (err, project) => {
Project.findById(req.params.project_id, (findProjectErr, project) => {
if (!req.user || !project.user.equals(req.user._id)) {
return res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
return;
}
// if (req.body.updatedAt && moment(req.body.updatedAt) < moment(project.updatedAt)) {
// return res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' });
@ -40,27 +47,29 @@ export function updateProject(req, res) {
$set: req.body
})
.populate('user', 'username')
.exec((err, updatedProject) => {
if (err) {
console.log(err);
return res.json({ success: false });
.exec((updateProjectErr, updatedProject) => {
if (updateProjectErr) {
console.log(updateProjectErr);
res.json({ success: false });
return;
}
if (updatedProject.files.length !== req.body.files.length) {
const oldFileIds = updatedProject.files.map(file => file.id);
const newFileIds = req.body.files.map(file => file.id);
const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1);
staleIds.forEach(staleId => {
staleIds.forEach((staleId) => {
updatedProject.files.id(staleId).remove();
});
updatedProject.save((innerErr) => {
if (innerErr) {
console.log(innerErr);
return res.json({ success: false });
res.json({ success: false });
return;
}
return res.json(updatedProject);
res.json(updatedProject);
});
}
return res.json(updatedProject);
res.json(updatedProject);
});
});
}
@ -77,15 +86,17 @@ export function getProject(req, res) {
}
export function deleteProject(req, res) {
Project.findById(req.params.project_id, (err, project) => {
Project.findById(req.params.project_id, (findProjectErr, project) => {
if (!req.user || !project.user.equals(req.user._id)) {
return res.status(403).json({ success: false, message: 'Session does not match owner of project.' });
res.status(403).json({ success: false, message: 'Session does not match owner of project.' });
return;
}
Project.remove({ _id: req.params.project_id }, (err) => {
if (err) {
return res.status(404).send({ message: 'Project with that id does not exist' });
Project.remove({ _id: req.params.project_id }, (removeProjectError) => {
if (removeProjectError) {
res.status(404).send({ message: 'Project with that id does not exist' });
return;
}
return res.json({ success: true });
res.json({ success: true });
});
});
}
@ -100,7 +111,7 @@ export function getProjects(req, res) {
});
} else {
// could just move this to client side
return res.json([]);
res.json([]);
}
}
@ -108,18 +119,18 @@ export function getProjectsForUser(req, res) {
if (req.params.username) {
User.findOne({ username: req.params.username }, (err, user) => {
if (!user) {
return res.status(404).json({ message: 'User with that username does not exist.' });
res.status(404).json({ message: 'User with that username does not exist.' });
return;
}
Project.find({ user: user._id }) // eslint-disable-line no-underscore-dangle
.sort('-createdAt')
.select('name files id createdAt updatedAt')
.exec((err, projects) => res.json(projects));
.exec((innerErr, projects) => res.json(projects));
});
} else {
// could just move this to client side
return res.json([]);
res.json([]);
}
return null;
}
function buildZip(project, req, res) {
@ -127,7 +138,6 @@ function buildZip(project, req, res) {
const rootFile = project.files.find(file => file.name === 'root');
const numFiles = project.files.filter(file => file.fileType !== 'folder').length;
const files = project.files;
const projectName = project.name;
let numCompletedFiles = 0;
zip.on('error', (err) => {
@ -140,14 +150,13 @@ function buildZip(project, req, res) {
function addFileToZip(file, path) {
if (file.fileType === 'folder') {
const newPath = file.name === 'root' ? path : `${path}${file.name}/`;
file.children.forEach(fileId => {
file.children.forEach((fileId) => {
const childFile = files.find(f => f.id === fileId);
(() => {
addFileToZip(childFile, newPath);
})();
});
} else {
if (file.url) {
} else if (file.url) {
request({ method: 'GET', url: file.url, encoding: null }, (err, response, body) => {
zip.append(body, { name: `${path}${file.name}` });
numCompletedFiles += 1;
@ -163,7 +172,6 @@ function buildZip(project, req, res) {
}
}
}
}
addFileToZip(rootFile, '/');
}

View file

@ -1,8 +1,8 @@
import User from '../models/user';
import crypto from 'crypto';
import async from 'async';
import nodemailer from 'nodemailer';
import mg from 'nodemailer-mailgun-transport';
import User from '../models/user';
export function createUser(req, res, next) {
const user = new User({
@ -12,17 +12,25 @@ export function createUser(req, res, next) {
});
User.findOne({ email: req.body.email },
(err, existingUser) => { // eslint-disable-line consistent-return
if (err) { res.status(404).send({ error: err }); }
(err, existingUser) => {
if (err) {
res.status(404).send({ error: err });
return;
}
if (existingUser) {
return res.status(422).send({ error: 'Email is in use' });
res.status(422).send({ error: 'Email is in use' });
return;
}
user.save((saveErr) => { // eslint-disable-line consistent-return
if (saveErr) { return next(saveErr); }
req.logIn(user, (loginErr) => { // eslint-disable-line consistent-return
user.save((saveErr) => {
if (saveErr) {
next(saveErr);
return;
}
req.logIn(user, (loginErr) => {
if (loginErr) {
return next(loginErr);
next(loginErr);
return;
}
res.json({
email: req.user.email,
@ -58,21 +66,24 @@ export function duplicateUserCheck(req, res) {
export function updatePreferences(req, res) {
User.findById(req.user.id, (err, user) => {
if (err) {
return res.status(500).json({ error: err });
res.status(500).json({ error: err });
return;
}
if (!user) {
return res.status(404).json({ error: 'Document not found' });
res.status(404).json({ error: 'Document not found' });
return;
}
const preferences = Object.assign({}, user.preferences, req.body.preferences);
user.preferences = preferences;
user.save((err) => {
if (err) {
return res.status(500).json({ error: err });
user.save((saveErr) => {
if (saveErr) {
res.status(500).json({ error: saveErr });
return;
}
return res.json(user.preferences);
res.json(user.preferences);
});
});
}
@ -88,13 +99,14 @@ export function resetPasswordInitiate(req, res) {
(token, done) => {
User.findOne({ email: req.body.email }, (err, user) => {
if (!user) {
return res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
return;
}
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
user.save((err) => {
done(err, token, user);
user.save((saveErr) => {
done(saveErr, token, user);
});
});
},
@ -124,41 +136,41 @@ export function resetPasswordInitiate(req, res) {
], (err) => {
if (err) {
console.log(err);
return res.json({ success: false });
res.json({ success: false });
return;
}
// send email here
return res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
});
}
export function validateResetPasswordToken(req, res) {
User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => {
if (!user) {
return res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' });
res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' });
return;
}
res.json({ success: true });
});
}
export function updatePassword(req, res) {
User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function (err, user) {
User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => {
if (!user) {
return res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' });
res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' });
return;
}
user.password = req.body.password;
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
user.save(function (err) {
req.logIn(user, function (err) {
return res.json({
user.save((saveErr) => {
req.logIn(user, loginErr => res.json({
email: req.user.email,
username: req.user.username,
preferences: req.user.preferences,
id: req.user._id
});
});
}));
});
});

View file

@ -3,10 +3,9 @@ import Q from 'q';
import mongoose from 'mongoose';
import objectID from 'bson-objectid';
import shortid from 'shortid';
import eachSeries from 'async/eachSeries';
import User from './models/user';
import Project from './models/project';
import async from 'async';
import eachSeries from 'async/eachSeries';
const defaultHTML =
`<!DOCTYPE html>
@ -30,8 +29,8 @@ const defaultCSS =
}
`;
const client_id = process.env.GITHUB_ID;
const client_secret = process.env.GITHUB_SECRET;
const clientId = process.env.GITHUB_ID;
const clientSecret = process.env.GITHUB_SECRET;
const headers = { 'User-Agent': 'p5js-web-editor/0.0.1' };
@ -41,144 +40,106 @@ mongoose.connection.on('error', () => {
process.exit(1);
});
getp5User();
function getp5User() {
User.findOne({ username: 'p5' }, (err, user) => {
if (err) throw err;
if (!user) {
user = new User({
username: 'p5',
email: 'p5-examples@gmail.com',
password: 'test'
});
user.save(err => {
if (err) throw err;
console.log('Created a user p5' + user);
});
}
Project.find({ user: user._id }, (err, projects) => {
// if there are already some sketches, delete them
console.log('Deleting old projects...');
projects.forEach(project => {
Project.remove({ _id: project._id }, err => {
if (err) throw err;
});
});
});
return getCategories()
.then(getSketchesInCategories)
.then(getSketchContent)
.then(createProjectsInP5user);
});
}
function getCategories() {
let categories = [];
const categories = [];
const options = {
url: 'https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/en?client_id=' +
client_id + '&client_secret=' + client_secret,
url: `https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/en?client_id=${
clientId}&client_secret=${clientSecret}`,
method: 'GET',
headers
};
return rp(options).then(res => {
return rp(options).then((res) => {
const json = JSON.parse(res);
json.forEach(metadata => {
json.forEach((metadata) => {
let category = '';
for (let j = 1; j < metadata.name.split('_').length; j++) {
category += metadata.name.split('_')[j] + ' ';
for (let j = 1; j < metadata.name.split('_').length; j += 1) {
category += `${metadata.name.split('_')[j]} `;
}
categories.push({ url: metadata.url, name: category });
});
return categories;
}).catch(err => {
}).catch((err) => {
throw err;
});
}
function getSketchesInCategories(categories) {
return Q.all(categories.map(category => {
return Q.all(categories.map((category) => {
const options = {
url: category.url.replace('?ref=master', '') + '?client_id=' + client_id + '&client_secret=' + client_secret,
url: `${category.url.replace('?ref=master', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
method: 'GET',
headers
};
return rp(options).then(res => {
let projectsInOneCategory = [];
return rp(options).then((res) => {
const projectsInOneCategory = [];
const examples = JSON.parse(res);
examples.forEach(example => {
examples.forEach((example) => {
let projectName;
if (example.name === '02_Instance_Container.js') {
for (let i = 1; i < 5; i++) {
const instanceProjectName = category.name + ': ' + 'Instance Container ' + i;
for (let i = 1; i < 5; i += 1) {
const instanceProjectName = `${category.name}: Instance Container ${i}`;
projectsInOneCategory.push({ sketchUrl: example.download_url, projectName: instanceProjectName });
}
} else {
if (example.name.split('_')[1]) {
projectName = category.name + ': ' + example.name.split('_').slice(1).join(' ').replace('.js', '');
projectName = `${category.name}: ${example.name.split('_').slice(1).join(' ').replace('.js', '')}`;
} else {
projectName = category.name + ': ' + example.name.replace('.js', '');
projectName = `${category.name}: ${example.name.replace('.js', '')}`;
}
projectsInOneCategory.push({ sketchUrl: example.download_url, projectName });
}
});
return projectsInOneCategory;
}).catch(err => {
}).catch((err) => {
throw err;
});
}));
}
function getSketchContent(projectsInAllCategories) {
return Q.all(projectsInAllCategories.map(projectsInOneCategory => {
return Q.all(projectsInOneCategory.map(project => {
return Q.all(projectsInAllCategories.map(projectsInOneCategory => Q.all(projectsInOneCategory.map((project) => {
const options = {
url: project.sketchUrl.replace('?ref=master', '') + '?client_id=' + client_id + '&client_secret=' + client_secret,
url: `${project.sketchUrl.replace('?ref=master', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
method: 'GET',
headers
};
return rp(options).then(res => {
return rp(options).then((res) => {
const noNumberprojectName = project.projectName.replace(/(\d+)/g, '');
if (noNumberprojectName === 'Instance Mode : Instance Container ') {
for (let i = 0; i < 4; i++) {
let splitedRes = res.split('*/')[1].split('</html>')[i] + '</html>\n';
for (let i = 0; i < 4; i += 1) {
const splitedRes = `${res.split('*/')[1].split('</html>')[i]}</html>\n`;
project.sketchContent = splitedRes.replace('p5.js', 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js');
}
} else {
project.sketchContent = res;
}
return project;
}).catch(err => {
}).catch((err) => {
throw err;
});
}));
}));
}))));
}
function createProjectsInP5user(projectsInAllCategories) {
let assetsfiles = [];
const options = {
url: 'https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/assets?client_id=' +
client_id + '&client_secret=' + client_secret,
url: `https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/assets?client_id=${
clientId}&client_secret=${clientSecret}`,
method: 'GET',
headers
};
rp(options).then(res => {
rp(options).then((res) => {
const assets = JSON.parse(res);
User.findOne({ username: 'p5' }, (err, user) => {
if (err) throw err;
async.eachSeries(projectsInAllCategories, (projectsInOneCategory, categoryCallback) => {
async.eachSeries(projectsInOneCategory, (project, projectCallback) => {
eachSeries(projectsInAllCategories, (projectsInOneCategory, categoryCallback) => {
eachSeries(projectsInOneCategory, (project, projectCallback) => {
let newProject;
const a = objectID().toHexString();
const b = objectID().toHexString();
@ -267,12 +228,12 @@ function createProjectsInP5user(projectsInAllCategories) {
});
}
let assetsInProject = project.sketchContent.match(/assets\/[\w-]+\.[\w]*/g) || project.sketchContent.match(/assets\/[\w-]*/g) || [];
const assetsInProject = project.sketchContent.match(/assets\/[\w-]+\.[\w]*/g) || project.sketchContent.match(/assets\/[\w-]*/g) || [];
assetsInProject.forEach((assetName, i) => {
assetName = assetName.split('assets/')[1];
assetsInProject.forEach((assetNamePath, i) => {
let assetName = assetNamePath.split('assets/')[1];
assets.forEach(asset => {
assets.forEach((asset) => {
if (asset.name === assetName || asset.name.split('.')[0] === assetName) {
assetName = asset.name;
}
@ -301,25 +262,61 @@ function createProjectsInP5user(projectsInAllCategories) {
children: [],
fileType: 'file'
});
console.log('create assets: ' + assetName);
console.log(`create assets: ${assetName}`);
// add asset file inside the newly created assets folder at index 4
newProject.files[4].children.push(fileID);
}
});
newProject.save((err, newProject) => {
if (err) throw err;
console.log('Created a new project in p5 user: ' + newProject.name);
newProject.save((saveErr, savedProject) => {
if (saveErr) throw saveErr;
console.log(`Created a new project in p5 user: ${savedProject.name}`);
projectCallback();
});
}, (err) => {
}, (categoryErr) => {
categoryCallback();
});
}, (err) => {
}, (examplesErr) => {
process.exit();
});
});
}).catch(err => {
}).catch((err) => {
throw err;
});
}
function getp5User() {
User.findOne({ username: 'p5' }, (err, user) => {
if (err) throw err;
let p5User = user;
if (!p5User) {
p5User = new User({
username: 'p5',
email: 'p5-examples@gmail.com',
password: 'test'
});
p5User.save((saveErr) => {
if (saveErr) throw saveErr;
console.log(`Created a user p5${p5User}`);
});
}
Project.find({ user: p5User._id }, (projectsErr, projects) => {
// if there are already some sketches, delete them
console.log('Deleting old projects...');
projects.forEach((project) => {
Project.remove({ _id: project._id }, (removeErr) => {
if (removeErr) throw removeErr;
});
});
});
return getCategories()
.then(getSketchesInCategories)
.then(getSketchContent)
.then(createProjectsInP5user);
});
}
getp5User();

View file

@ -1,7 +1,8 @@
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
import shortid from 'shortid';
const Schema = mongoose.Schema;
const fileSchema = new Schema({
name: { type: String, default: 'sketch.js' },
content: { type: String, default: '' },
@ -11,7 +12,7 @@ const fileSchema = new Schema({
isSelectedFile: { type: Boolean }
}, { timestamps: true, _id: true });
fileSchema.virtual('id').get(function () {
fileSchema.virtual('id').get(function getFileId() {
return this._id.toHexString();
});
@ -26,7 +27,7 @@ const projectSchema = new Schema({
_id: { type: String, default: shortid.generate }
}, { timestamps: true });
projectSchema.virtual('id').get(function () {
projectSchema.virtual('id').get(function getProjectId() {
return this._id;
});

View file

@ -1,7 +1,9 @@
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const bcrypt = require('bcrypt-nodejs');
const Schema = mongoose.Schema;
const userSchema = new Schema({
name: { type: String, default: '' },
username: { type: String, required: true, unique: true },
@ -39,7 +41,7 @@ userSchema.pre('save', function checkPassword(next) { // eslint-disable-line con
});
});
userSchema.virtual('id').get(function () {
userSchema.virtual('id').get(function idToString() {
return this._id.toHexString();
});

View file

@ -1,8 +1,9 @@
import { Router } from 'express';
const router = new Router();
import { renderIndex } from '../views/index';
import { get404Sketch } from '../views/404Page';
import { userExists } from '../controllers/user.controller.js';
import { userExists } from '../controllers/user.controller';
const router = new Router();
// this is intended to be a temporary file
// until i figure out isomorphic rendering
@ -48,7 +49,7 @@ router.route('/about').get((req, res) => {
});
router.route('/:username/sketches').get((req, res) => {
userExists(req.params.username, (exists) => (
userExists(req.params.username, exists => (
exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html))
));
});

View file

@ -1,5 +1,6 @@
import { Router } from 'express';
import * as UserController from '../controllers/user.controller';
const router = new Router();
router.route('/signup').post(UserController.createUser);

View file

@ -3,24 +3,15 @@ import mongoose from 'mongoose';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import session from 'express-session';
const MongoStore = require('connect-mongo')(session);
import connectMongo from 'connect-mongo';
import passport from 'passport';
import path from 'path';
// Webpack Requirements
import webpack from 'webpack';
import config from '../webpack.config.dev';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
const app = new Express();
// Run Webpack dev server in development mode
if (process.env.NODE_ENV === 'development') {
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
app.use(webpackHotMiddleware(compiler));
}
import config from '../webpack.config.dev';
// Import all required modules
import serverConfig from './config';
@ -35,6 +26,16 @@ import embedRoutes from './routes/embed.routes';
import { renderIndex } from './views/index';
import { get404Sketch } from './views/404Page';
const app = new Express();
const MongoStore = connectMongo(session);
// Run Webpack dev server in development mode
if (process.env.NODE_ENV === 'development') {
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
app.use(webpackHotMiddleware(compiler));
}
// Body parser, cookie parser, sessions, serve public assets
app.use(Express.static(path.resolve(__dirname, '../static')));

View file

@ -15,7 +15,7 @@ export function resolvePathToFile(filePath, files) {
file._id.valueOf().toString() === childFileId.valueOf()
)
);
childFiles.some(childFile => {
childFiles.some((childFile) => {
if (childFile.name === filePathSegment) {
currentFile = childFile;
foundChild = true;
@ -30,3 +30,5 @@ export function resolvePathToFile(filePath, files) {
});
return resolvedFile;
}
export default resolvePathToFile;

View file

@ -10,7 +10,7 @@ function resolveLinksInString(content, files, projectId) {
let fileStrings = content.match(STRING_REGEX);
const fileStringRegex = /^('|")(?!(http:\/\/|https:\/\/)).*('|")$/i;
fileStrings = fileStrings || [];
fileStrings.forEach(fileString => {
fileStrings.forEach((fileString) => {
// if string does not begin with http or https
if (fileString.match(fileStringRegex)) {
const filePath = fileString.substr(1, fileString.length - 2);
@ -35,7 +35,7 @@ function resolveLinksInString(content, files, projectId) {
}
export function injectMediaUrls(filesToInject, allFiles, projectId) {
filesToInject.forEach(file => {
filesToInject.forEach((file) => {
file.content = resolveLinksInString(file.content, allFiles, projectId);
});
}
@ -43,7 +43,7 @@ export function injectMediaUrls(filesToInject, allFiles, projectId) {
export function resolvePathsForElementsWithAttribute(attr, sketchDoc, files) {
const elements = sketchDoc.querySelectorAll(`[${attr}]`);
const elementsArray = Array.prototype.slice.call(elements);
elementsArray.forEach(element => {
elementsArray.forEach((element) => {
if (element.getAttribute(attr).match(MEDIA_FILE_REGEX_NO_QUOTES)) {
const resolvedFile = resolvePathToFile(element.getAttribute(attr), files);
if (resolvedFile) {
@ -56,7 +56,7 @@ export function resolvePathsForElementsWithAttribute(attr, sketchDoc, files) {
export function resolveScripts(sketchDoc, files, projectId) {
const scriptsInHTML = sketchDoc.getElementsByTagName('script');
const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML);
scriptsInHTMLArray.forEach(script => {
scriptsInHTMLArray.forEach((script) => {
if (script.getAttribute('src') && script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null) {
const resolvedFile = resolvePathToFile(script.getAttribute('src'), files);
if (resolvedFile) {
@ -76,13 +76,13 @@ export function resolveScripts(sketchDoc, files, projectId) {
export function resolveStyles(sketchDoc, files, projectId) {
const inlineCSSInHTML = sketchDoc.getElementsByTagName('style');
const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML);
inlineCSSInHTMLArray.forEach(style => {
inlineCSSInHTMLArray.forEach((style) => {
style.innerHTML = resolveLinksInString(style.innerHTML, files, projectId);
});
const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]');
const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML);
cssLinksInHTMLArray.forEach(css => {
cssLinksInHTMLArray.forEach((css) => {
if (css.getAttribute('href') && css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null) {
const resolvedFile = resolvePathToFile(css.getAttribute('href'), files);
if (resolvedFile) {

View file

@ -20,19 +20,19 @@ export function get404Sketch(callback) {
instanceMode = jsFiles.find(file => file.name === 'sketch.js').content.includes('Instance Mode');
jsFiles.forEach(file => { // Add js files as script tags
jsFiles.forEach((file) => { // Add js files as script tags
const html = htmlFile.split('</body>');
html[0] = `${html[0]}<script>${file.content}</script>`;
htmlFile = html.join('</body>');
});
cssFiles.forEach(file => { // Add css files as style tags
cssFiles.forEach((file) => { // Add css files as style tags
const html = htmlFile.split('</head>');
html[0] = `${html[0]}<style>${file.content}</style>`;
htmlFile = html.join('</head>');
});
linkedFiles.forEach(file => { // Add linked files as link tags
linkedFiles.forEach((file) => { // Add linked files as link tags
const html = htmlFile.split('<head>');
html[1] = `<link href=${file.url}>${html[1]}`;
htmlFile = html.join('<head>');
@ -112,3 +112,6 @@ export function get404Sketch(callback) {
}
});
}
export default get404Sketch;

@ -1 +1 @@
Subproject commit a1c126721ac667f7750ec181c8ccf363f2658d8b
Subproject commit 9fa934af4ca85853ba2fe911558d5f658621ea0b