Merge branch 'master' into feature-runtime-error-highlight
This commit is contained in:
commit
069f974989
82 changed files with 2362 additions and 721 deletions
|
@ -55,6 +55,10 @@ The automatic redirection to HTTPS is turned off by default in development. If y
|
||||||
S3_BUCKET=<your-s3-bucket>
|
S3_BUCKET=<your-s3-bucket>
|
||||||
GITHUB_ID=<your-github-client-id>
|
GITHUB_ID=<your-github-client-id>
|
||||||
GITHUB_SECRET=<your-github-client-secret>
|
GITHUB_SECRET=<your-github-client-secret>
|
||||||
|
EMAIL_SENDER=<email-address-to-send-from>
|
||||||
|
MAILGUN_KEY=<mailgun-api-key>
|
||||||
|
MAILGUN_DOMAIN=<mailgun-domain>
|
||||||
|
EMAIL_VERIFY_SECRET_TOKEN=whatever_you_want_this_to_be_it_only_matters_for_production
|
||||||
```
|
```
|
||||||
For production, you will need to have real Github and Amazon credentions. Refer to [this gist](https://gist.github.com/catarak/70c9301f0fd1ac2d6b58de03f61997e3) for creating an S3 bucket for testing.
|
For production, you will need to have real Github and Amazon credentions. Refer to [this gist](https://gist.github.com/catarak/70c9301f0fd1ac2d6b58de03f61997e3) for creating an S3 bucket for testing.
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,10 @@ class Nav extends React.PureComponent {
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{(() => { // eslint-disable-line
|
{(() => { // eslint-disable-line
|
||||||
if (!this.props.project.owner || (this.props.project.owner && this.props.project.owner.id === this.props.user.id)) {
|
if (
|
||||||
|
!this.props.project.owner ||
|
||||||
|
(this.props.project.owner && this.props.project.owner.id === this.props.user.id)
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<button
|
<button
|
||||||
|
@ -80,7 +83,6 @@ class Nav extends React.PureComponent {
|
||||||
<p className="nav__open">
|
<p className="nav__open">
|
||||||
<Link
|
<Link
|
||||||
to={`/${this.props.user.username}/sketches`}
|
to={`/${this.props.user.username}/sketches`}
|
||||||
onClick={this.props.stopSketch}
|
|
||||||
>
|
>
|
||||||
Open
|
Open
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -91,7 +93,9 @@ class Nav extends React.PureComponent {
|
||||||
})()}
|
})()}
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<p className="nav__open">
|
<p className="nav__open">
|
||||||
<Link to="/p5/sketches">
|
<Link
|
||||||
|
to="/p5/sketches"
|
||||||
|
>
|
||||||
Examples
|
Examples
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
@ -119,7 +123,9 @@ class Nav extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<p>
|
<p>
|
||||||
<Link to="/login">Log in</Link> <span className="nav__item-spacer">or</span> <Link to="/signup">Sign up</Link>
|
<Link to="/login">Log in</Link>
|
||||||
|
<span className="nav__item-spacer">or</span>
|
||||||
|
<Link to="/signup">Sign up</Link>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -136,6 +142,11 @@ class Nav extends React.PureComponent {
|
||||||
My sketches
|
My sketches
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to={`/${this.props.user.username}/assets`}>
|
||||||
|
My assets
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to={`/${this.props.user.username}/account`}>
|
<Link to={`/${this.props.user.username}/account`}>
|
||||||
My account
|
My account
|
||||||
|
@ -153,7 +164,11 @@ class Nav extends React.PureComponent {
|
||||||
</ul>
|
</ul>
|
||||||
<div className="nav__announce">
|
<div className="nav__announce">
|
||||||
This is a preview version of the editor, that has not yet been officially released.
|
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>.
|
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.
|
Please use with caution.
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -178,7 +193,6 @@ Nav.propTypes = {
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
logoutUser: PropTypes.func.isRequired,
|
logoutUser: PropTypes.func.isRequired,
|
||||||
stopSketch: PropTypes.func.isRequired,
|
|
||||||
showShareModal: PropTypes.func.isRequired,
|
showShareModal: PropTypes.func.isRequired,
|
||||||
showErrorModal: PropTypes.func.isRequired,
|
showErrorModal: PropTypes.func.isRequired,
|
||||||
unsavedChanges: PropTypes.bool.isRequired,
|
unsavedChanges: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
|
// TODO Organize this file by reducer type, ot break this apart into
|
||||||
|
// multiple files
|
||||||
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
|
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
|
||||||
export const TOGGLE_SKETCH = 'TOGGLE_SKETCH';
|
export const TOGGLE_SKETCH = 'TOGGLE_SKETCH';
|
||||||
|
|
||||||
export const START_SKETCH = 'START_SKETCH';
|
export const START_SKETCH = 'START_SKETCH';
|
||||||
export const STOP_SKETCH = 'STOP_SKETCH';
|
export const STOP_SKETCH = 'STOP_SKETCH';
|
||||||
|
|
||||||
export const START_TEXT_OUTPUT = 'START_TEXT_OUTPUT';
|
export const START_ACCESSIBLE_OUTPUT = 'START_ACCESSIBLE_OUTPUT';
|
||||||
export const STOP_TEXT_OUTPUT = 'STOP_TEXT_OUTPUT';
|
export const STOP_ACCESSIBLE_OUTPUT = 'STOP_ACCESSIBLE_OUTPUT';
|
||||||
|
|
||||||
export const OPEN_PREFERENCES = 'OPEN_PREFERENCES';
|
export const OPEN_PREFERENCES = 'OPEN_PREFERENCES';
|
||||||
export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES';
|
export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES';
|
||||||
|
@ -69,6 +71,8 @@ export const SET_AUTOSAVE = 'SET_AUTOSAVE';
|
||||||
export const SET_LINT_WARNING = 'SET_LINT_WARNING';
|
export const SET_LINT_WARNING = 'SET_LINT_WARNING';
|
||||||
export const SET_PREFERENCES = 'SET_PREFERENCES';
|
export const SET_PREFERENCES = 'SET_PREFERENCES';
|
||||||
export const SET_TEXT_OUTPUT = 'SET_TEXT_OUTPUT';
|
export const SET_TEXT_OUTPUT = 'SET_TEXT_OUTPUT';
|
||||||
|
export const SET_GRID_OUTPUT = 'SET_GRID_OUTPUT';
|
||||||
|
export const SET_SOUND_OUTPUT = 'SET_SOUND_OUTPUT';
|
||||||
|
|
||||||
export const OPEN_PROJECT_OPTIONS = 'OPEN_PROJECT_OPTIONS';
|
export const OPEN_PROJECT_OPTIONS = 'OPEN_PROJECT_OPTIONS';
|
||||||
export const CLOSE_PROJECT_OPTIONS = 'CLOSE_PROJECT_OPTIONS';
|
export const CLOSE_PROJECT_OPTIONS = 'CLOSE_PROJECT_OPTIONS';
|
||||||
|
@ -100,6 +104,11 @@ export const RESET_PASSWORD_INITIATE = 'RESET_PASSWORD_INITIATE';
|
||||||
export const RESET_PASSWORD_RESET = 'RESET_PASSWORD_RESET';
|
export const RESET_PASSWORD_RESET = 'RESET_PASSWORD_RESET';
|
||||||
export const INVALID_RESET_PASSWORD_TOKEN = 'INVALID_RESET_PASSWORD_TOKEN';
|
export const INVALID_RESET_PASSWORD_TOKEN = 'INVALID_RESET_PASSWORD_TOKEN';
|
||||||
|
|
||||||
|
export const EMAIL_VERIFICATION_INITIATE = 'EMAIL_VERIFICATION_INITIATE';
|
||||||
|
export const EMAIL_VERIFICATION_VERIFY = 'EMAIL_VERIFICATION_VERIFY';
|
||||||
|
export const EMAIL_VERIFICATION_VERIFIED = 'EMAIL_VERIFICATION_VERIFIED';
|
||||||
|
export const EMAIL_VERIFICATION_INVALID = 'EMAIL_VERIFICATION_INVALID';
|
||||||
|
|
||||||
// eventually, handle errors more specifically and better
|
// eventually, handle errors more specifically and better
|
||||||
export const ERROR = 'ERROR';
|
export const ERROR = 'ERROR';
|
||||||
|
|
||||||
|
@ -120,3 +129,4 @@ export const HIDE_HELP_MODAL = 'HIDE_HELP_MODAL';
|
||||||
|
|
||||||
export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
|
export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
|
||||||
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
|
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
|
||||||
|
export const SET_ASSETS = 'SET_ASSETS';
|
||||||
|
|
|
@ -1,21 +1,70 @@
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
import { browserHistory } from 'react-router';
|
||||||
|
|
||||||
function Overlay(props) {
|
const exitUrl = require('../../../images/exit.svg');
|
||||||
|
|
||||||
|
class Overlay extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.close = this.close.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.overlay.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.props.closeOverlay) {
|
||||||
|
browserHistory.push(this.props.previousPath);
|
||||||
|
} else {
|
||||||
|
this.props.closeOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
ariaLabel,
|
||||||
|
title,
|
||||||
|
children
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="overlay">
|
<div className="overlay">
|
||||||
<div className="overlay-content">
|
<div className="overlay__content">
|
||||||
{props.children}
|
<section
|
||||||
|
tabIndex="0"
|
||||||
|
role="main"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
ref={(element) => { this.overlay = element; }}
|
||||||
|
className="overlay__body"
|
||||||
|
>
|
||||||
|
<header className="overlay__header">
|
||||||
|
<h2 className="overlay__title">{title}</h2>
|
||||||
|
<button className="overlay__close-button" onClick={this.close}>
|
||||||
|
<InlineSVG src={exitUrl} alt="close overlay" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Overlay.propTypes = {
|
Overlay.propTypes = {
|
||||||
children: PropTypes.element
|
children: PropTypes.element,
|
||||||
|
closeOverlay: PropTypes.func,
|
||||||
|
title: PropTypes.string,
|
||||||
|
ariaLabel: PropTypes.string,
|
||||||
|
previousPath: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
Overlay.defaultProps = {
|
Overlay.defaultProps = {
|
||||||
children: null
|
children: null,
|
||||||
|
title: 'Modal',
|
||||||
|
closeOverlay: null,
|
||||||
|
ariaLabel: 'modal'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Overlay;
|
export default Overlay;
|
||||||
|
|
30
client/modules/IDE/actions/assets.js
Normal file
30
client/modules/IDE/actions/assets.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import * as ActionTypes from '../../../constants';
|
||||||
|
|
||||||
|
const ROOT_URL = process.env.API_URL;
|
||||||
|
|
||||||
|
function setAssets(assets) {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.SET_ASSETS,
|
||||||
|
assets
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAssets(username) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
axios.get(`${ROOT_URL}/S3/${username}/objects`, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
dispatch(setAssets(response.data.assets));
|
||||||
|
})
|
||||||
|
.catch(response => dispatch({
|
||||||
|
type: ActionTypes.ERROR
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAsset(assetKey, userId) {
|
||||||
|
return {
|
||||||
|
type: 'PLACEHOLDER'
|
||||||
|
};
|
||||||
|
}
|
|
@ -31,15 +31,15 @@ export function endSketchRefresh() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startTextOutput() {
|
export function startAccessibleOutput() {
|
||||||
return {
|
return {
|
||||||
type: ActionTypes.START_TEXT_OUTPUT
|
type: ActionTypes.START_ACCESSIBLE_OUTPUT
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stopTextOutput() {
|
export function stopAccessibleOutput() {
|
||||||
return {
|
return {
|
||||||
type: ActionTypes.STOP_TEXT_OUTPUT
|
type: ActionTypes.STOP_ACCESSIBLE_OUTPUT
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,42 @@ export function setTextOutput(value) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setGridOutput(value) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.SET_GRID_OUTPUT,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
const state = getState();
|
||||||
|
if (state.user.authenticated) {
|
||||||
|
const formParams = {
|
||||||
|
preferences: {
|
||||||
|
gridOutput: value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updatePreferences(formParams, dispatch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSoundOutput(value) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.SET_SOUND_OUTPUT,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
const state = getState();
|
||||||
|
if (state.user.authenticated) {
|
||||||
|
const formParams = {
|
||||||
|
preferences: {
|
||||||
|
soundOutput: value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updatePreferences(formParams, dispatch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function setTheme(value) {
|
export function setTheme(value) {
|
||||||
// return {
|
// return {
|
||||||
// type: ActionTypes.SET_THEME,
|
// type: ActionTypes.SET_THEME,
|
||||||
|
@ -180,4 +216,3 @@ export function setAutorefresh(value) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,12 @@
|
||||||
import React, { PropTypes } from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
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 squareLogoUrl = require('../../../images/p5js-square-logo.svg');
|
||||||
const playUrl = require('../../../images/play.svg');
|
const playUrl = require('../../../images/play.svg');
|
||||||
const asteriskUrl = require('../../../images/p5-asterisk.svg');
|
const asteriskUrl = require('../../../images/p5-asterisk.svg');
|
||||||
|
|
||||||
class About extends React.Component {
|
function About(props) {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.closeAboutModal = this.closeAboutModal.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.aboutSection.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAboutModal() {
|
|
||||||
browserHistory.push(this.props.previousPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<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}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close About Overlay" />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
<div className="about__content">
|
<div className="about__content">
|
||||||
<div className="about__content-column">
|
<div className="about__content-column">
|
||||||
<InlineSVG className="about__logo" src={squareLogoUrl} alt="p5js Square Logo" />
|
<InlineSVG className="about__logo" src={squareLogoUrl} alt="p5js Square Logo" />
|
||||||
|
@ -94,7 +71,6 @@ class About extends React.Component {
|
||||||
Forum</a>
|
Forum</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="about__footer">
|
<div className="about__footer">
|
||||||
<p className="about__footer-list">
|
<p className="about__footer-list">
|
||||||
<a
|
<a
|
||||||
|
@ -117,15 +93,9 @@ class About extends React.Component {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>Twitter</a>
|
>Twitter</a>
|
||||||
</p>
|
</p>
|
||||||
<button className="about__ok-button" onClick={this.closeAboutModal}>OK!</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
About.propTypes = {
|
|
||||||
previousPath: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default About;
|
export default About;
|
||||||
|
|
51
client/modules/IDE/components/AccessibleOutput.jsx
Normal file
51
client/modules/IDE/components/AccessibleOutput.jsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import GridOutput from '../components/GridOutput';
|
||||||
|
import TextOutput from '../components/TextOutput';
|
||||||
|
|
||||||
|
class AccessibleOutput extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
this.accessibleOutputModal.focus();
|
||||||
|
}
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
// if the user explicitly clicks on the play button, want to refocus on the text output
|
||||||
|
if (this.props.isPlaying && this.props.previewIsRefreshing) {
|
||||||
|
this.accessibleOutputModal.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="accessible-output"
|
||||||
|
id="canvas-sub"
|
||||||
|
ref={(element) => { this.accessibleOutputModal = element; }}
|
||||||
|
tabIndex="0"
|
||||||
|
aria-label="accessible-output"
|
||||||
|
title="canvas text output"
|
||||||
|
>
|
||||||
|
{(() => { // eslint-disable-line
|
||||||
|
if (this.props.textOutput) {
|
||||||
|
return (
|
||||||
|
<TextOutput />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
{(() => { // eslint-disable-line
|
||||||
|
if (this.props.gridOutput) {
|
||||||
|
return (
|
||||||
|
<GridOutput />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessibleOutput.propTypes = {
|
||||||
|
isPlaying: PropTypes.bool.isRequired,
|
||||||
|
previewIsRefreshing: PropTypes.bool.isRequired,
|
||||||
|
textOutput: PropTypes.bool.isRequired,
|
||||||
|
gridOutput: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccessibleOutput;
|
71
client/modules/IDE/components/AssetList.jsx
Normal file
71
client/modules/IDE/components/AssetList.jsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
|
||||||
|
import * as AssetActions from '../actions/assets';
|
||||||
|
|
||||||
|
|
||||||
|
class AssetList extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.props.getAssets(this.props.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="asset-table-container">
|
||||||
|
{this.props.assets.length === 0 &&
|
||||||
|
<p className="asset-table__empty">No uploaded assets.</p>
|
||||||
|
}
|
||||||
|
{this.props.assets.length > 0 &&
|
||||||
|
<table className="asset-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>View</th>
|
||||||
|
<th>Sketch</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{this.props.assets.map(asset =>
|
||||||
|
<tr className="asset-table__row" key={asset.key}>
|
||||||
|
<td>{asset.name}</td>
|
||||||
|
<td>{prettyBytes(asset.size)}</td>
|
||||||
|
<td><Link to={asset.url} target="_blank">View</Link></td>
|
||||||
|
<td><Link to={`/${this.props.username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link></td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetList.propTypes = {
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
assets: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
key: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
sketchName: PropTypes.string.isRequired,
|
||||||
|
sketchId: PropTypes.string.isRequired
|
||||||
|
})).isRequired,
|
||||||
|
getAssets: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
user: state.user,
|
||||||
|
assets: state.assets
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(Object.assign({}, AssetActions), dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(AssetList);
|
|
@ -24,7 +24,11 @@ class Console extends React.Component {
|
||||||
<button 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
|
Clear
|
||||||
</button>
|
</button>
|
||||||
<button className="preview-console__collapse" onClick={this.props.collapseConsole} aria-label="collapse console">
|
<button
|
||||||
|
className="preview-console__collapse"
|
||||||
|
onClick={this.props.collapseConsole}
|
||||||
|
aria-label="collapse console"
|
||||||
|
>
|
||||||
<InlineSVG src={downArrowUrl} />
|
<InlineSVG src={downArrowUrl} />
|
||||||
</button>
|
</button>
|
||||||
<button className="preview-console__expand" onClick={this.props.expandConsole} aria-label="expand console">
|
<button className="preview-console__expand" onClick={this.props.expandConsole} aria-label="expand console">
|
||||||
|
|
|
@ -9,7 +9,11 @@ import 'codemirror/addon/lint/css-lint';
|
||||||
import 'codemirror/addon/lint/html-lint';
|
import 'codemirror/addon/lint/html-lint';
|
||||||
import 'codemirror/addon/comment/comment';
|
import 'codemirror/addon/comment/comment';
|
||||||
import 'codemirror/keymap/sublime';
|
import 'codemirror/keymap/sublime';
|
||||||
|
import 'codemirror/addon/search/searchcursor';
|
||||||
|
import 'codemirror/addon/search/matchesonscrollbar';
|
||||||
|
import 'codemirror/addon/search/match-highlighter';
|
||||||
import 'codemirror/addon/search/jump-to-line';
|
import 'codemirror/addon/search/jump-to-line';
|
||||||
|
|
||||||
import { JSHINT } from 'jshint';
|
import { JSHINT } from 'jshint';
|
||||||
import { CSSLint } from 'csslint';
|
import { CSSLint } from 'csslint';
|
||||||
import { HTMLHint } from 'htmlhint';
|
import { HTMLHint } from 'htmlhint';
|
||||||
|
@ -20,6 +24,13 @@ import '../../../utils/htmlmixed';
|
||||||
import '../../../utils/p5-javascript';
|
import '../../../utils/p5-javascript';
|
||||||
import Timer from '../components/Timer';
|
import Timer from '../components/Timer';
|
||||||
import EditorAccessibility from '../components/EditorAccessibility';
|
import EditorAccessibility from '../components/EditorAccessibility';
|
||||||
|
import {
|
||||||
|
metaKey,
|
||||||
|
} from '../../../utils/metaKey';
|
||||||
|
|
||||||
|
import search from '../../../utils/codemirror-search';
|
||||||
|
|
||||||
|
search(CodeMirror);
|
||||||
|
|
||||||
const beautifyCSS = beautifyJS.css;
|
const beautifyCSS = beautifyJS.css;
|
||||||
const beautifyHTML = beautifyJS.html;
|
const beautifyHTML = beautifyJS.html;
|
||||||
|
@ -63,6 +74,7 @@ class Editor extends React.Component {
|
||||||
fixedGutter: false,
|
fixedGutter: false,
|
||||||
gutters: ['CodeMirror-lint-markers'],
|
gutters: ['CodeMirror-lint-markers'],
|
||||||
keyMap: 'sublime',
|
keyMap: 'sublime',
|
||||||
|
highlightSelectionMatches: true, // highlight current search match
|
||||||
lint: {
|
lint: {
|
||||||
onUpdateLinting: ((annotations) => {
|
onUpdateLinting: ((annotations) => {
|
||||||
this.props.hideRuntimeErrorWarning();
|
this.props.hideRuntimeErrorWarning();
|
||||||
|
@ -77,10 +89,11 @@ class Editor extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
this._cm.setOption('extraKeys', {
|
this._cm.setOption('extraKeys', {
|
||||||
'Cmd-Enter': () => null,
|
[`${metaKey}-Enter`]: () => null,
|
||||||
'Shift-Cmd-Enter': () => null,
|
[`Shift-${metaKey}-Enter`]: () => null,
|
||||||
'Ctrl-Enter': () => null,
|
[`${metaKey}-F`]: 'findPersistent',
|
||||||
'Shift-Ctrl-Enter': () => null
|
[`${metaKey}-G`]: 'findNext',
|
||||||
|
[`Shift-${metaKey}-G`]: 'findPrev',
|
||||||
});
|
});
|
||||||
|
|
||||||
this.initializeDocuments(this.props.files);
|
this.initializeDocuments(this.props.files);
|
||||||
|
@ -184,7 +197,6 @@ class Editor extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeDocuments(files) {
|
initializeDocuments(files) {
|
||||||
console.log('calling initialize documents');
|
|
||||||
this._docs = {};
|
this._docs = {};
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
if (file.name !== 'root') {
|
if (file.name !== 'root') {
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
|
||||||
|
|
||||||
class ErrorModal extends React.Component {
|
class ErrorModal extends React.Component {
|
||||||
componentDidMount() {
|
|
||||||
this.errorModal.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
forceAuthentication() {
|
forceAuthentication() {
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
|
@ -40,13 +32,6 @@ class ErrorModal extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<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}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close Error Modal" />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
<div className="error-modal__content">
|
<div className="error-modal__content">
|
||||||
{(() => { // eslint-disable-line
|
{(() => { // eslint-disable-line
|
||||||
if (this.props.type === 'forceAuthentication') {
|
if (this.props.type === 'forceAuthentication') {
|
||||||
|
@ -58,7 +43,6 @@ class ErrorModal extends React.Component {
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,11 @@ export class FileNode extends React.Component {
|
||||||
if (oldFileExtension && !newFileExtension) {
|
if (oldFileExtension && !newFileExtension) {
|
||||||
this.props.updateFileName(this.props.id, this.originalFileName);
|
this.props.updateFileName(this.props.id, this.originalFileName);
|
||||||
}
|
}
|
||||||
if (oldFileExtension && newFileExtension && oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase()) {
|
if (
|
||||||
|
oldFileExtension &&
|
||||||
|
newFileExtension &&
|
||||||
|
oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase()
|
||||||
|
) {
|
||||||
this.props.updateFileName(this.props.id, this.originalFileName);
|
this.props.updateFileName(this.props.id, this.originalFileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
38
client/modules/IDE/components/GridOutput.jsx
Normal file
38
client/modules/IDE/components/GridOutput.jsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
class GridOutput extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
this.GridOutputModal.focus();
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="gridOutput-content"
|
||||||
|
ref={(element) => { this.GridOutputModal = element; }}
|
||||||
|
>
|
||||||
|
<h2> Grid Output </h2>
|
||||||
|
<p
|
||||||
|
tabIndex="0"
|
||||||
|
role="main"
|
||||||
|
id="gridOutput-content-summary"
|
||||||
|
aria-label="grid output summary"
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<table
|
||||||
|
id="gridOutput-content-table"
|
||||||
|
summary="grid output details"
|
||||||
|
>
|
||||||
|
</table>
|
||||||
|
<div
|
||||||
|
tabIndex="0"
|
||||||
|
role="main"
|
||||||
|
id="gridOutput-content-details"
|
||||||
|
aria-label="grid output details"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GridOutput;
|
|
@ -1,82 +1,84 @@
|
||||||
import React, { PropTypes } from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
import {
|
||||||
|
metaKeyName,
|
||||||
|
} from '../../../utils/metaKey';
|
||||||
|
|
||||||
class KeyboardShortcutModal extends React.Component {
|
function KeyboardShortcutModal() {
|
||||||
componentDidMount() {
|
|
||||||
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<section className="keyboard-shortcuts">
|
<ul className="keyboard-shortcuts" title="keyboard shortcuts">
|
||||||
<header className="keyboard-shortcuts__header">
|
|
||||||
<h2>Keyboard Shortcuts</h2>
|
|
||||||
<button className="keyboard-shortcuts__close" onClick={this.props.closeModal}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close Keyboard Shortcuts Overlay" />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
<ul title="keyboard shortcuts">
|
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">Shift + Tab</span>
|
<span className="keyboard-shortcut__command">Shift + Tab</span>
|
||||||
<span>Tidy</span>
|
<span>Tidy</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{this.isMac ? 'Command + S' : 'Control + S'}
|
{metaKeyName} + S
|
||||||
</span>
|
</span>
|
||||||
<span>Save</span>
|
<span>Save</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{this.isMac ? 'Command + [' : 'Control + ['}
|
{metaKeyName} + F
|
||||||
|
</span>
|
||||||
|
<span>Find Text</span>
|
||||||
|
</li>
|
||||||
|
<li className="keyboard-shortcut-item">
|
||||||
|
<span className="keyboard-shortcut__command">
|
||||||
|
{metaKeyName} + G
|
||||||
|
</span>
|
||||||
|
<span>Find Next Text Match</span>
|
||||||
|
</li>
|
||||||
|
<li className="keyboard-shortcut-item">
|
||||||
|
<span className="keyboard-shortcut__command">
|
||||||
|
{metaKeyName} + Shift + G
|
||||||
|
</span>
|
||||||
|
<span>Find Previous Text Match</span>
|
||||||
|
</li>
|
||||||
|
<li className="keyboard-shortcut-item">
|
||||||
|
<span className="keyboard-shortcut__command">
|
||||||
|
{metaKeyName} + [
|
||||||
</span>
|
</span>
|
||||||
<span>Indent Code Left</span>
|
<span>Indent Code Left</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{this.isMac ? 'Command + ]' : 'Control + ]'}
|
{metaKeyName} + ]
|
||||||
</span>
|
</span>
|
||||||
<span>Indent Code Right</span>
|
<span>Indent Code Right</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{this.isMac ? 'Command + /' : 'Control + /'}
|
{metaKeyName} + /
|
||||||
</span>
|
</span>
|
||||||
<span>Comment Line</span>
|
<span>Comment Line</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{this.isMac ? 'Command + Enter' : 'Control + Enter'}</span>
|
{metaKeyName} + Enter
|
||||||
|
</span>
|
||||||
<span>Start Sketch</span>
|
<span>Start Sketch</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{this.isMac ? 'Command + Shift + Enter' : 'Control + Shift + Enter'}
|
{metaKeyName} + Shift + Enter
|
||||||
</span>
|
</span>
|
||||||
<span>Stop Sketch</span>
|
<span>Stop Sketch</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{this.isMac ? 'Command + Shift + 1' : 'Control + Shift + 1'}
|
{metaKeyName} + Shift + 1
|
||||||
</span>
|
</span>
|
||||||
<span>Toggle Text-based Canvas</span>
|
<span>Toggle Text-based Canvas</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{this.isMac ? 'Command + Shift + 2' : 'Control + Shift + 2'}
|
{metaKeyName} + Shift + 2
|
||||||
</span>
|
</span>
|
||||||
<span>Turn Off Text-based Canvas</span>
|
<span>Turn Off Text-based Canvas</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
KeyboardShortcutModal.propTypes = {
|
|
||||||
closeModal: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default KeyboardShortcutModal;
|
export default KeyboardShortcutModal;
|
||||||
|
|
|
@ -261,50 +261,41 @@ class Preferences extends React.Component {
|
||||||
|
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="checkbox"
|
||||||
onChange={() => this.props.setTextOutput(1)}
|
onChange={(event) => {
|
||||||
|
this.props.setTextOutput(event.target.checked);
|
||||||
|
}}
|
||||||
aria-label="text output on"
|
aria-label="text output on"
|
||||||
name="text output"
|
name="text output"
|
||||||
id="text-output-on"
|
id="text-output-on"
|
||||||
className="preference__radio-button"
|
|
||||||
value="On"
|
value="On"
|
||||||
checked={Boolean(this.props.textOutput === 1)}
|
checked={(this.props.textOutput)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="text-output-on" className="preference__option preference__canvas">Plain-text</label>
|
<label htmlFor="text-output-on" className="preference__option preference__canvas">Plain-text</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="checkbox"
|
||||||
onChange={() => this.props.setTextOutput(2)}
|
onChange={(event) => {
|
||||||
aria-label="table text output on"
|
this.props.setGridOutput(event.target.checked);
|
||||||
name="table text output"
|
}}
|
||||||
id="grid-output-on"
|
aria-label="table output on"
|
||||||
className="preference__radio-button"
|
name="table output"
|
||||||
value="Grid On"
|
id="table-output-on"
|
||||||
checked={Boolean(this.props.textOutput === 2)}
|
value="On"
|
||||||
|
checked={(this.props.gridOutput)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="grid-output-on" className="preference__option preference__canvas">Table-text</label>
|
<label htmlFor="table-output-on" className="preference__option preference__canvas">Table-text</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="checkbox"
|
||||||
onChange={() => this.props.setTextOutput(3)}
|
onChange={(event) => {
|
||||||
|
this.props.setSoundOutput(event.target.checked);
|
||||||
|
}}
|
||||||
aria-label="sound output on"
|
aria-label="sound output on"
|
||||||
name="sound output"
|
name="sound output"
|
||||||
id="sound-output-on"
|
id="sound-output-on"
|
||||||
className="preference__radio-button"
|
|
||||||
value="On"
|
value="On"
|
||||||
checked={Boolean(this.props.textOutput === 3)}
|
checked={(this.props.soundOutput)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="sound-output-on" className="preference__option preference__canvas">Sound</label>
|
<label htmlFor="sound-output-on" className="preference__option preference__canvas">Sound</label>
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
onChange={() => this.props.setTextOutput(0)}
|
|
||||||
aria-label="text output off"
|
|
||||||
name="text output"
|
|
||||||
id="text-output-off"
|
|
||||||
className="preference__radio-button"
|
|
||||||
value="Off"
|
|
||||||
checked={!(this.props.textOutput)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="text-output-off" className="preference__option preference__canvas">Off</label>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -324,8 +315,12 @@ Preferences.propTypes = {
|
||||||
setFontSize: PropTypes.func.isRequired,
|
setFontSize: PropTypes.func.isRequired,
|
||||||
autosave: PropTypes.bool.isRequired,
|
autosave: PropTypes.bool.isRequired,
|
||||||
setAutosave: PropTypes.func.isRequired,
|
setAutosave: PropTypes.func.isRequired,
|
||||||
textOutput: PropTypes.number.isRequired,
|
textOutput: PropTypes.bool.isRequired,
|
||||||
|
gridOutput: PropTypes.bool.isRequired,
|
||||||
|
soundOutput: PropTypes.bool.isRequired,
|
||||||
setTextOutput: PropTypes.func.isRequired,
|
setTextOutput: PropTypes.func.isRequired,
|
||||||
|
setGridOutput: PropTypes.func.isRequired,
|
||||||
|
setSoundOutput: PropTypes.func.isRequired,
|
||||||
lintWarning: PropTypes.bool.isRequired,
|
lintWarning: PropTypes.bool.isRequired,
|
||||||
setLintWarning: PropTypes.func.isRequired,
|
setLintWarning: PropTypes.func.isRequired,
|
||||||
theme: PropTypes.string.isRequired,
|
theme: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -7,8 +7,12 @@ import loopProtect from 'loop-protect';
|
||||||
import { getBlobUrl } from '../actions/files';
|
import { getBlobUrl } from '../actions/files';
|
||||||
import { resolvePathToFile } from '../../../../server/utils/filePath';
|
import { resolvePathToFile } from '../../../../server/utils/filePath';
|
||||||
|
|
||||||
|
const decomment = require('decomment');
|
||||||
|
|
||||||
const startTag = '@fs-';
|
const startTag = '@fs-';
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
const MEDIA_FILE_REGEX = /^('|")(?!(http:\/\/|https:\/\/)).*\.(png|jpg|jpeg|gif|bmp|mp3|wav|aiff|ogg|json|txt|csv|svg|obj|mp4|ogg|webm|mov|otf|ttf|m4a)('|")$/i;
|
const MEDIA_FILE_REGEX = /^('|")(?!(http:\/\/|https:\/\/)).*\.(png|jpg|jpeg|gif|bmp|mp3|wav|aiff|ogg|json|txt|csv|svg|obj|mp4|ogg|webm|mov|otf|ttf|m4a)('|")$/i;
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
const MEDIA_FILE_REGEX_NO_QUOTES = /^(?!(http:\/\/|https:\/\/)).*\.(png|jpg|jpeg|gif|bmp|mp3|wav|aiff|ogg|json|txt|csv|svg|obj|mp4|ogg|webm|mov|otf|ttf|m4a)$/i;
|
const MEDIA_FILE_REGEX_NO_QUOTES = /^(?!(http:\/\/|https:\/\/)).*\.(png|jpg|jpeg|gif|bmp|mp3|wav|aiff|ogg|json|txt|csv|svg|obj|mp4|ogg|webm|mov|otf|ttf|m4a)$/i;
|
||||||
const STRING_REGEX = /(['"])((\\\1|.)*?)\1/gm;
|
const STRING_REGEX = /(['"])((\\\1|.)*?)\1/gm;
|
||||||
const TEXT_FILE_REGEX = /(.+\.json$|.+\.txt$|.+\.csv$)/i;
|
const TEXT_FILE_REGEX = /(.+\.json$|.+\.txt$|.+\.csv$)/i;
|
||||||
|
@ -54,20 +58,17 @@ function hijackConsoleErrorsScript(offs) {
|
||||||
}
|
}
|
||||||
return [line - l, file];
|
return [line - l, file];
|
||||||
}
|
}
|
||||||
|
|
||||||
// catch reference errors, via http://stackoverflow.com/a/12747364/2994108
|
// catch reference errors, via http://stackoverflow.com/a/12747364/2994108
|
||||||
window.onerror = function (msg, url, lineNumber, columnNo, error) {
|
window.onerror = function (msg, url, lineNumber, columnNo, error) {
|
||||||
var string = msg.toLowerCase();
|
var string = msg.toLowerCase();
|
||||||
var substring = "script error";
|
var substring = "script error";
|
||||||
var data = {};
|
var data = {};
|
||||||
|
|
||||||
if (string.indexOf(substring) !== -1){
|
if (string.indexOf(substring) !== -1){
|
||||||
data = 'Script Error: See Browser Console for Detail';
|
data = 'Script Error: See Browser Console for Detail';
|
||||||
} else {
|
} else {
|
||||||
var fileInfo = getScriptOff(lineNumber);
|
var fileInfo = getScriptOff(lineNumber);
|
||||||
data = msg + ' (' + fileInfo[1] + ': line ' + fileInfo[0] + ')';
|
data = msg + ' (' + fileInfo[1] + ': line ' + fileInfo[0] + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
window.parent.postMessage([{
|
window.parent.postMessage([{
|
||||||
method: 'error',
|
method: 'error',
|
||||||
arguments: data,
|
arguments: data,
|
||||||
|
@ -114,7 +115,7 @@ class PreviewFrame extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if user switches textoutput preferences
|
// if user switches textoutput preferences
|
||||||
if (this.props.isTextOutputPlaying !== prevProps.isTextOutputPlaying) {
|
if (this.props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying) {
|
||||||
this.renderSketch();
|
this.renderSketch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -124,6 +125,16 @@ class PreviewFrame extends React.Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.gridOutput !== prevProps.gridOutput) {
|
||||||
|
this.renderSketch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.soundOutput !== prevProps.soundOutput) {
|
||||||
|
this.renderSketch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) {
|
if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) {
|
||||||
this.renderSketch();
|
this.renderSketch();
|
||||||
}
|
}
|
||||||
|
@ -165,38 +176,43 @@ class PreviewFrame extends React.Component {
|
||||||
'/loop-protect.min.js',
|
'/loop-protect.min.js',
|
||||||
'/hijackConsole.js'
|
'/hijackConsole.js'
|
||||||
];
|
];
|
||||||
if (this.props.isTextOutputPlaying || (this.props.textOutput !== 0 && this.props.isPlaying)) {
|
if (
|
||||||
|
this.props.isAccessibleOutputPlaying ||
|
||||||
|
((this.props.textOutput || this.props.gridOutput || this.props.soundOutput) && this.props.isPlaying)) {
|
||||||
let interceptorScripts = [];
|
let interceptorScripts = [];
|
||||||
if (this.props.textOutput === 0) {
|
interceptorScripts = [
|
||||||
this.props.setTextOutput(1);
|
'/p5-interceptor/registry.js',
|
||||||
|
'/p5-interceptor/loadData.js',
|
||||||
|
'/p5-interceptor/interceptorHelperFunctions.js',
|
||||||
|
'/p5-interceptor/baseInterceptor.js',
|
||||||
|
'/p5-interceptor/entities/entity.min.js',
|
||||||
|
'/p5-interceptor/ntc.min.js'
|
||||||
|
];
|
||||||
|
if (!this.props.textOutput && !this.props.gridOutput && !this.props.soundOutput) {
|
||||||
|
this.props.setTextOutput(true);
|
||||||
}
|
}
|
||||||
if (this.props.textOutput === 1) {
|
if (this.props.textOutput) {
|
||||||
interceptorScripts = [
|
let textInterceptorScripts = [];
|
||||||
'/p5-interceptor/registry.js',
|
textInterceptorScripts = [
|
||||||
'/p5-interceptor/loadData.js',
|
|
||||||
'/p5-interceptor/interceptorHelperFunctions.js',
|
|
||||||
'/p5-interceptor/baseInterceptor.js',
|
|
||||||
'/p5-interceptor/entities/entity.min.js',
|
|
||||||
'/p5-interceptor/textInterceptor/interceptorFunctions.js',
|
'/p5-interceptor/textInterceptor/interceptorFunctions.js',
|
||||||
'/p5-interceptor/textInterceptor/interceptorP5.js',
|
'/p5-interceptor/textInterceptor/interceptorP5.js'
|
||||||
'/p5-interceptor/ntc.min.js'
|
|
||||||
];
|
];
|
||||||
} else if (this.props.textOutput === 2) {
|
interceptorScripts = interceptorScripts.concat(textInterceptorScripts);
|
||||||
interceptorScripts = [
|
}
|
||||||
'/p5-interceptor/registry.js',
|
if (this.props.gridOutput) {
|
||||||
'/p5-interceptor/loadData.js',
|
let gridInterceptorScripts = [];
|
||||||
'/p5-interceptor/interceptorHelperFunctions.js',
|
gridInterceptorScripts = [
|
||||||
'/p5-interceptor/baseInterceptor.js',
|
|
||||||
'/p5-interceptor/entities/entity.min.js',
|
|
||||||
'/p5-interceptor/gridInterceptor/interceptorFunctions.js',
|
'/p5-interceptor/gridInterceptor/interceptorFunctions.js',
|
||||||
'/p5-interceptor/gridInterceptor/interceptorP5.js',
|
'/p5-interceptor/gridInterceptor/interceptorP5.js'
|
||||||
'/p5-interceptor/ntc.min.js'
|
|
||||||
];
|
];
|
||||||
} else if (this.props.textOutput === 3) {
|
interceptorScripts = interceptorScripts.concat(gridInterceptorScripts);
|
||||||
interceptorScripts = [
|
}
|
||||||
'/p5-interceptor/loadData.js',
|
if (this.props.soundOutput) {
|
||||||
|
let soundInterceptorScripts = [];
|
||||||
|
soundInterceptorScripts = [
|
||||||
'/p5-interceptor/soundInterceptor/interceptorP5.js'
|
'/p5-interceptor/soundInterceptor/interceptorP5.js'
|
||||||
];
|
];
|
||||||
|
interceptorScripts = interceptorScripts.concat(soundInterceptorScripts);
|
||||||
}
|
}
|
||||||
scriptsToInject = scriptsToInject.concat(interceptorScripts);
|
scriptsToInject = scriptsToInject.concat(interceptorScripts);
|
||||||
}
|
}
|
||||||
|
@ -264,6 +280,7 @@ class PreviewFrame extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
newContent = decomment(newContent, { ignore: /noprotect/g });
|
||||||
newContent = loopProtect(newContent);
|
newContent = loopProtect(newContent);
|
||||||
return newContent;
|
return newContent;
|
||||||
}
|
}
|
||||||
|
@ -373,8 +390,10 @@ class PreviewFrame extends React.Component {
|
||||||
|
|
||||||
PreviewFrame.propTypes = {
|
PreviewFrame.propTypes = {
|
||||||
isPlaying: PropTypes.bool.isRequired,
|
isPlaying: PropTypes.bool.isRequired,
|
||||||
isTextOutputPlaying: PropTypes.bool.isRequired,
|
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
|
||||||
textOutput: PropTypes.number.isRequired,
|
textOutput: PropTypes.bool.isRequired,
|
||||||
|
gridOutput: PropTypes.bool.isRequired,
|
||||||
|
soundOutput: PropTypes.bool.isRequired,
|
||||||
setTextOutput: PropTypes.func.isRequired,
|
setTextOutput: PropTypes.func.isRequired,
|
||||||
htmlFile: PropTypes.shape({
|
htmlFile: PropTypes.shape({
|
||||||
content: PropTypes.string.isRequired
|
content: PropTypes.string.isRequired
|
||||||
|
|
|
@ -1,29 +1,20 @@
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
function ShareModal(props) {
|
||||||
|
const {
|
||||||
class ShareModal extends React.Component {
|
projectId,
|
||||||
componentDidMount() {
|
ownerUsername
|
||||||
this.shareModal.focus();
|
} = props;
|
||||||
}
|
|
||||||
render() {
|
|
||||||
const hostname = window.location.origin;
|
const hostname = window.location.origin;
|
||||||
return (
|
return (
|
||||||
<section className="share-modal" ref={(element) => { this.shareModal = element; }} tabIndex="0">
|
<div className="share-modal">
|
||||||
<header className="share-modal__header">
|
|
||||||
<h2>Share Sketch</h2>
|
|
||||||
<button className="about__exit-button" onClick={this.props.closeShareModal}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close Share Overlay" />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
<div className="share-modal__section">
|
<div className="share-modal__section">
|
||||||
<label className="share-modal__label" htmlFor="share-modal__embed">Embed</label>
|
<label className="share-modal__label" htmlFor="share-modal__embed">Embed</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="share-modal__input"
|
className="share-modal__input"
|
||||||
id="share-modal__embed"
|
id="share-modal__embed"
|
||||||
value={`<iframe src="${hostname}/embed/${this.props.projectId}"></iframe>`}
|
value={`<iframe src="${hostname}/embed/${projectId}"></iframe>`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="share-modal__section">
|
<div className="share-modal__section">
|
||||||
|
@ -32,7 +23,7 @@ class ShareModal extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
className="share-modal__input"
|
className="share-modal__input"
|
||||||
id="share-modal__fullscreen"
|
id="share-modal__fullscreen"
|
||||||
value={`${hostname}/full/${this.props.projectId}`}
|
value={`${hostname}/full/${projectId}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="share-modal__section">
|
<div className="share-modal__section">
|
||||||
|
@ -41,17 +32,15 @@ class ShareModal extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
className="share-modal__input"
|
className="share-modal__input"
|
||||||
id="share-modal__edit"
|
id="share-modal__edit"
|
||||||
value={`${hostname}/${this.props.ownerUsername}/sketches/${this.props.projectId}`}
|
value={`${hostname}/${ownerUsername}/sketches/${projectId}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ShareModal.propTypes = {
|
ShareModal.propTypes = {
|
||||||
projectId: PropTypes.string.isRequired,
|
projectId: PropTypes.string.isRequired,
|
||||||
closeShareModal: PropTypes.func.isRequired,
|
|
||||||
ownerUsername: PropTypes.string.isRequired
|
ownerUsername: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,35 +8,22 @@ import * as SketchActions from '../actions/projects';
|
||||||
import * as ProjectActions from '../actions/project';
|
import * as ProjectActions from '../actions/project';
|
||||||
import * as ToastActions from '../actions/toast';
|
import * as ToastActions from '../actions/toast';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
|
||||||
const trashCan = require('../../../images/trash-can.svg');
|
const trashCan = require('../../../images/trash-can.svg');
|
||||||
|
|
||||||
class SketchList extends React.Component {
|
class SketchList extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.closeSketchList = this.closeSketchList.bind(this);
|
|
||||||
this.props.getProjects(this.props.username);
|
this.props.getProjects(this.props.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
document.getElementById('sketchlist').focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeSketchList() {
|
|
||||||
browserHistory.push(this.props.previousPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
||||||
return (
|
return (
|
||||||
<section className="sketch-list" aria-label="project list" tabIndex="0" role="main" id="sketchlist">
|
|
||||||
<header className="sketch-list__header">
|
|
||||||
<h2 className="sketch-list__header-title">Open a Sketch</h2>
|
|
||||||
<button className="sketch-list__exit-button" onClick={this.closeSketchList}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close Sketch List Overlay" />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
<div className="sketches-table-container">
|
<div className="sketches-table-container">
|
||||||
|
{ this.props.sketches.length === 0 &&
|
||||||
|
<p className="sketches-table__empty">No sketches.</p>
|
||||||
|
}
|
||||||
|
{ this.props.sketches.length > 0 &&
|
||||||
<table className="sketches-table" summary="table containing all saved projects">
|
<table className="sketches-table" summary="table containing all saved projects">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -79,9 +66,8 @@ class SketchList extends React.Component {
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,8 +84,7 @@ SketchList.propTypes = {
|
||||||
updatedAt: PropTypes.string.isRequired
|
updatedAt: PropTypes.string.isRequired
|
||||||
})).isRequired,
|
})).isRequired,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
deleteProject: PropTypes.func.isRequired,
|
deleteProject: PropTypes.func.isRequired
|
||||||
previousPath: PropTypes.string.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
SketchList.defaultProps = {
|
SketchList.defaultProps = {
|
||||||
|
|
|
@ -1,28 +1,16 @@
|
||||||
import React, { PropTypes } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
class TextOutput extends React.Component {
|
class TextOutput extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.canvasTextOutput.focus();
|
this.TextOutputModal.focus();
|
||||||
}
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
// if the user explicitly clicks on the play button, want to refocus on the text output
|
|
||||||
if (this.props.isPlaying && this.props.previewIsRefreshing) {
|
|
||||||
this.canvasTextOutput.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="text-output"
|
id="textOutput-content"
|
||||||
id="canvas-sub"
|
ref={(element) => { this.TextOutputModal = element; }}
|
||||||
ref={(element) => { this.canvasTextOutput = element; }}
|
|
||||||
tabIndex="0"
|
|
||||||
aria-label="text-output"
|
|
||||||
title="canvas text output"
|
|
||||||
>
|
>
|
||||||
<h2> Output </h2>
|
<h2> Text Output </h2>
|
||||||
<section id="textOutput-content">
|
|
||||||
</section>
|
|
||||||
<p
|
<p
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
role="main"
|
role="main"
|
||||||
|
@ -31,10 +19,8 @@ class TextOutput extends React.Component {
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<table
|
<table
|
||||||
tabIndex="0"
|
|
||||||
role="main"
|
|
||||||
id="textOutput-content-table"
|
id="textOutput-content-table"
|
||||||
aria-label="text output details"
|
summary="text output details"
|
||||||
>
|
>
|
||||||
</table>
|
</table>
|
||||||
<div
|
<div
|
||||||
|
@ -49,9 +35,4 @@ class TextOutput extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextOutput.propTypes = {
|
|
||||||
isPlaying: PropTypes.bool.isRequired,
|
|
||||||
previewIsRefreshing: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TextOutput;
|
export default TextOutput;
|
||||||
|
|
|
@ -64,7 +64,7 @@ class Toolbar extends React.Component {
|
||||||
className="toolbar__play-sketch-button"
|
className="toolbar__play-sketch-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.props.clearConsole();
|
this.props.clearConsole();
|
||||||
this.props.startTextOutput();
|
this.props.startAccessibleOutput();
|
||||||
this.props.startSketchAndRefresh();
|
this.props.startSketchAndRefresh();
|
||||||
}}
|
}}
|
||||||
aria-label="play sketch"
|
aria-label="play sketch"
|
||||||
|
@ -85,7 +85,7 @@ class Toolbar extends React.Component {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={stopButtonClass}
|
className={stopButtonClass}
|
||||||
onClick={() => { this.props.stopTextOutput(); this.props.stopSketch(); }}
|
onClick={() => { this.props.stopAccessibleOutput(); this.props.stopSketch(); }}
|
||||||
aria-label="stop sketch"
|
aria-label="stop sketch"
|
||||||
>
|
>
|
||||||
<InlineSVG src={stopUrl} alt="Stop Sketch" />
|
<InlineSVG src={stopUrl} alt="Stop Sketch" />
|
||||||
|
@ -141,7 +141,10 @@ class Toolbar extends React.Component {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.props.project.name}
|
{this.props.project.name}
|
||||||
{this.canEditProjectName() && <InlineSVG className="toolbar__edit-name-button" src={editProjectNameUrl} alt="Edit Project Name" />}
|
{
|
||||||
|
this.canEditProjectName() &&
|
||||||
|
<InlineSVG className="toolbar__edit-name-button" src={editProjectNameUrl} alt="Edit Project Name" />
|
||||||
|
}
|
||||||
</a>
|
</a>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -184,8 +187,8 @@ Toolbar.propTypes = {
|
||||||
isPlaying: PropTypes.bool.isRequired,
|
isPlaying: PropTypes.bool.isRequired,
|
||||||
preferencesIsVisible: PropTypes.bool.isRequired,
|
preferencesIsVisible: PropTypes.bool.isRequired,
|
||||||
stopSketch: PropTypes.func.isRequired,
|
stopSketch: PropTypes.func.isRequired,
|
||||||
startTextOutput: PropTypes.func.isRequired,
|
startAccessibleOutput: PropTypes.func.isRequired,
|
||||||
stopTextOutput: PropTypes.func.isRequired,
|
stopAccessibleOutput: PropTypes.func.isRequired,
|
||||||
setProjectName: PropTypes.func.isRequired,
|
setProjectName: PropTypes.func.isRequired,
|
||||||
openPreferences: PropTypes.func.isRequired,
|
openPreferences: PropTypes.func.isRequired,
|
||||||
owner: PropTypes.shape({
|
owner: PropTypes.shape({
|
||||||
|
|
|
@ -2,12 +2,13 @@ import React, { PropTypes } from 'react';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import SplitPane from 'react-split-pane';
|
import SplitPane from 'react-split-pane';
|
||||||
import Editor from '../components/Editor';
|
import Editor from '../components/Editor';
|
||||||
import Sidebar from '../components/Sidebar';
|
import Sidebar from '../components/Sidebar';
|
||||||
import PreviewFrame from '../components/PreviewFrame';
|
import PreviewFrame from '../components/PreviewFrame';
|
||||||
import Toolbar from '../components/Toolbar';
|
import Toolbar from '../components/Toolbar';
|
||||||
import TextOutput from '../components/TextOutput';
|
import AccessibleOutput from '../components/AccessibleOutput';
|
||||||
import Preferences from '../components/Preferences';
|
import Preferences from '../components/Preferences';
|
||||||
import NewFileModal from '../components/NewFileModal';
|
import NewFileModal from '../components/NewFileModal';
|
||||||
import NewFolderModal from '../components/NewFolderModal';
|
import NewFolderModal from '../components/NewFolderModal';
|
||||||
|
@ -29,6 +30,7 @@ import * as ConsoleActions from '../actions/console';
|
||||||
import { getHTMLFile } from '../reducers/files';
|
import { getHTMLFile } from '../reducers/files';
|
||||||
import Overlay from '../../App/components/Overlay';
|
import Overlay from '../../App/components/Overlay';
|
||||||
import SketchList from '../components/SketchList';
|
import SketchList from '../components/SketchList';
|
||||||
|
import AssetList from '../components/AssetList';
|
||||||
import About from '../components/About';
|
import About from '../components/About';
|
||||||
|
|
||||||
class IDEView extends React.Component {
|
class IDEView extends React.Component {
|
||||||
|
@ -97,7 +99,9 @@ class IDEView extends React.Component {
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.isUserOwner() && this.props.project.id) {
|
if (this.isUserOwner() && this.props.project.id) {
|
||||||
if (this.props.preferences.autosave && this.props.ide.unsavedChanges && !this.props.ide.justOpenedProject) {
|
if (this.props.preferences.autosave && this.props.ide.unsavedChanges && !this.props.ide.justOpenedProject) {
|
||||||
if (this.props.selectedFile.name === prevProps.selectedFile.name && this.props.selectedFile.content !== prevProps.selectedFile.content) {
|
if (
|
||||||
|
this.props.selectedFile.name === prevProps.selectedFile.name &&
|
||||||
|
this.props.selectedFile.content !== prevProps.selectedFile.content) {
|
||||||
if (this.autosaveInterval) {
|
if (this.autosaveInterval) {
|
||||||
clearTimeout(this.autosaveInterval);
|
clearTimeout(this.autosaveInterval);
|
||||||
}
|
}
|
||||||
|
@ -165,15 +169,14 @@ class IDEView extends React.Component {
|
||||||
this.props.startSketchAndRefresh();
|
this.props.startSketchAndRefresh();
|
||||||
} else if (e.keyCode === 50 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac)) && e.shiftKey) {
|
} else if (e.keyCode === 50 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac)) && e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.setTextOutput(0);
|
this.props.setTextOutput(false);
|
||||||
|
this.props.setGridOutput(false);
|
||||||
|
this.props.setSoundOutput(false);
|
||||||
} else if (e.keyCode === 49 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac)) && e.shiftKey) {
|
} else if (e.keyCode === 49 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac)) && e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.props.preferences.textOutput === 3) {
|
this.props.setTextOutput(true);
|
||||||
this.props.preferences.textOutput = 1;
|
this.props.setGridOutput(true);
|
||||||
} else {
|
this.props.setSoundOutput(true);
|
||||||
this.props.preferences.textOutput += 1;
|
|
||||||
}
|
|
||||||
this.props.setTextOutput(this.props.preferences.textOutput);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,6 +201,9 @@ class IDEView extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="ide">
|
<div className="ide">
|
||||||
|
<Helmet>
|
||||||
|
<title>{this.props.project.name}</title>
|
||||||
|
</Helmet>
|
||||||
{this.props.toast.isVisible && <Toast />}
|
{this.props.toast.isVisible && <Toast />}
|
||||||
<Nav
|
<Nav
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
|
@ -217,8 +223,8 @@ class IDEView extends React.Component {
|
||||||
className="Toolbar"
|
className="Toolbar"
|
||||||
isPlaying={this.props.ide.isPlaying}
|
isPlaying={this.props.ide.isPlaying}
|
||||||
stopSketch={this.props.stopSketch}
|
stopSketch={this.props.stopSketch}
|
||||||
startTextOutput={this.props.startTextOutput}
|
startAccessibleOutput={this.props.startAccessibleOutput}
|
||||||
stopTextOutput={this.props.stopTextOutput}
|
stopAccessibleOutput={this.props.stopAccessibleOutput}
|
||||||
projectName={this.props.project.name}
|
projectName={this.props.project.name}
|
||||||
setProjectName={this.props.setProjectName}
|
setProjectName={this.props.setProjectName}
|
||||||
showEditProjectName={this.props.showEditProjectName}
|
showEditProjectName={this.props.showEditProjectName}
|
||||||
|
@ -228,6 +234,8 @@ class IDEView extends React.Component {
|
||||||
serveSecure={this.props.project.serveSecure}
|
serveSecure={this.props.project.serveSecure}
|
||||||
setServeSecure={this.props.setServeSecure}
|
setServeSecure={this.props.setServeSecure}
|
||||||
setTextOutput={this.props.setTextOutput}
|
setTextOutput={this.props.setTextOutput}
|
||||||
|
setGridOutput={this.props.setGridOutput}
|
||||||
|
setSoundOutput={this.props.setSoundOutput}
|
||||||
owner={this.props.project.owner}
|
owner={this.props.project.owner}
|
||||||
project={this.props.project}
|
project={this.props.project}
|
||||||
infiniteLoop={this.props.ide.infiniteLoop}
|
infiniteLoop={this.props.ide.infiniteLoop}
|
||||||
|
@ -254,7 +262,11 @@ class IDEView extends React.Component {
|
||||||
lintWarning={this.props.preferences.lintWarning}
|
lintWarning={this.props.preferences.lintWarning}
|
||||||
setLintWarning={this.props.setLintWarning}
|
setLintWarning={this.props.setLintWarning}
|
||||||
textOutput={this.props.preferences.textOutput}
|
textOutput={this.props.preferences.textOutput}
|
||||||
|
gridOutput={this.props.preferences.gridOutput}
|
||||||
|
soundOutput={this.props.preferences.soundOutput}
|
||||||
setTextOutput={this.props.setTextOutput}
|
setTextOutput={this.props.setTextOutput}
|
||||||
|
setGridOutput={this.props.setGridOutput}
|
||||||
|
setSoundOutput={this.props.setSoundOutput}
|
||||||
theme={this.props.preferences.theme}
|
theme={this.props.preferences.theme}
|
||||||
setTheme={this.props.setTheme}
|
setTheme={this.props.setTheme}
|
||||||
/>
|
/>
|
||||||
|
@ -350,11 +362,19 @@ class IDEView extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{(() => {
|
{(() => {
|
||||||
if ((this.props.preferences.textOutput && this.props.ide.isPlaying) || this.props.ide.isTextOutputPlaying) {
|
if (
|
||||||
|
(
|
||||||
|
(this.props.preferences.textOutput ||
|
||||||
|
this.props.preferences.gridOutput ||
|
||||||
|
this.props.preferences.soundOutput
|
||||||
|
) && this.props.ide.isPlaying
|
||||||
|
) || this.props.ide.isAccessibleOutputPlaying) {
|
||||||
return (
|
return (
|
||||||
<TextOutput
|
<AccessibleOutput
|
||||||
isPlaying={this.props.ide.isPlaying}
|
isPlaying={this.props.ide.isPlaying}
|
||||||
previewIsRefreshing={this.props.ide.previewIsRefreshing}
|
previewIsRefreshing={this.props.ide.previewIsRefreshing}
|
||||||
|
textOutput={this.props.preferences.textOutput}
|
||||||
|
gridOutput={this.props.preferences.gridOutput}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -366,9 +386,13 @@ class IDEView extends React.Component {
|
||||||
files={this.props.files}
|
files={this.props.files}
|
||||||
content={this.props.selectedFile.content}
|
content={this.props.selectedFile.content}
|
||||||
isPlaying={this.props.ide.isPlaying}
|
isPlaying={this.props.ide.isPlaying}
|
||||||
isTextOutputPlaying={this.props.ide.isTextOutputPlaying}
|
isAccessibleOutputPlaying={this.props.ide.isAccessibleOutputPlaying}
|
||||||
textOutput={this.props.preferences.textOutput}
|
textOutput={this.props.preferences.textOutput}
|
||||||
|
gridOutput={this.props.preferences.gridOutput}
|
||||||
|
soundOutput={this.props.preferences.soundOutput}
|
||||||
setTextOutput={this.props.setTextOutput}
|
setTextOutput={this.props.setTextOutput}
|
||||||
|
setGridOutput={this.props.setGridOutput}
|
||||||
|
setSoundOutput={this.props.setSoundOutput}
|
||||||
dispatchConsoleEvent={this.props.dispatchConsoleEvent}
|
dispatchConsoleEvent={this.props.dispatchConsoleEvent}
|
||||||
autorefresh={this.props.preferences.autorefresh}
|
autorefresh={this.props.preferences.autorefresh}
|
||||||
previewIsRefreshing={this.props.ide.previewIsRefreshing}
|
previewIsRefreshing={this.props.ide.previewIsRefreshing}
|
||||||
|
@ -407,10 +431,30 @@ class IDEView extends React.Component {
|
||||||
{(() => { // eslint-disable-line
|
{(() => { // eslint-disable-line
|
||||||
if (this.props.location.pathname.match(/sketches$/)) {
|
if (this.props.location.pathname.match(/sketches$/)) {
|
||||||
return (
|
return (
|
||||||
<Overlay>
|
<Overlay
|
||||||
|
ariaLabel="project list"
|
||||||
|
title="Open a Sketch"
|
||||||
|
previousPath={this.props.ide.previousPath}
|
||||||
|
>
|
||||||
<SketchList
|
<SketchList
|
||||||
username={this.props.params.username}
|
username={this.props.params.username}
|
||||||
|
user={this.props.user}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
{(() => { // eslint-disable-line
|
||||||
|
if (this.props.location.pathname.match(/assets$/)) {
|
||||||
|
return (
|
||||||
|
<Overlay
|
||||||
|
title="Assets"
|
||||||
|
ariaLabel="asset list"
|
||||||
previousPath={this.props.ide.previousPath}
|
previousPath={this.props.ide.previousPath}
|
||||||
|
>
|
||||||
|
<AssetList
|
||||||
|
username={this.props.params.username}
|
||||||
|
user={this.props.user}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
);
|
);
|
||||||
|
@ -419,7 +463,11 @@ class IDEView extends React.Component {
|
||||||
{(() => { // eslint-disable-line
|
{(() => { // eslint-disable-line
|
||||||
if (this.props.location.pathname === '/about') {
|
if (this.props.location.pathname === '/about') {
|
||||||
return (
|
return (
|
||||||
<Overlay>
|
<Overlay
|
||||||
|
previousPath={this.props.ide.previousPath}
|
||||||
|
title="Welcome"
|
||||||
|
ariaLabel="about"
|
||||||
|
>
|
||||||
<About previousPath={this.props.ide.previousPath} />
|
<About previousPath={this.props.ide.previousPath} />
|
||||||
</Overlay>
|
</Overlay>
|
||||||
);
|
);
|
||||||
|
@ -428,10 +476,13 @@ class IDEView extends React.Component {
|
||||||
{(() => { // eslint-disable-line
|
{(() => { // eslint-disable-line
|
||||||
if (this.props.ide.shareModalVisible) {
|
if (this.props.ide.shareModalVisible) {
|
||||||
return (
|
return (
|
||||||
<Overlay>
|
<Overlay
|
||||||
|
title="Share Sketch"
|
||||||
|
ariaLabel="share"
|
||||||
|
closeOverlay={this.props.closeShareModal}
|
||||||
|
>
|
||||||
<ShareModal
|
<ShareModal
|
||||||
projectId={this.props.project.id}
|
projectId={this.props.project.id}
|
||||||
closeShareModal={this.props.closeShareModal}
|
|
||||||
ownerUsername={this.props.project.owner.username}
|
ownerUsername={this.props.project.owner.username}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
@ -441,10 +492,12 @@ class IDEView extends React.Component {
|
||||||
{(() => { // eslint-disable-line
|
{(() => { // eslint-disable-line
|
||||||
if (this.props.ide.keyboardShortcutVisible) {
|
if (this.props.ide.keyboardShortcutVisible) {
|
||||||
return (
|
return (
|
||||||
<Overlay>
|
<Overlay
|
||||||
<KeyboardShortcutModal
|
title="Keyboard Shortcuts"
|
||||||
closeModal={this.props.closeKeyboardShortcutModal}
|
ariaLabel="keyboard shortcuts"
|
||||||
/>
|
closeOverlay={this.props.closeKeyboardShortcutModal}
|
||||||
|
>
|
||||||
|
<KeyboardShortcutModal />
|
||||||
</Overlay>
|
</Overlay>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -452,10 +505,13 @@ class IDEView extends React.Component {
|
||||||
{(() => { // eslint-disable-line
|
{(() => { // eslint-disable-line
|
||||||
if (this.props.ide.errorType) {
|
if (this.props.ide.errorType) {
|
||||||
return (
|
return (
|
||||||
<Overlay>
|
<Overlay
|
||||||
|
title="Error"
|
||||||
|
ariaLabel="error"
|
||||||
|
closeOverlay={this.props.hideErrorModal}
|
||||||
|
>
|
||||||
<ErrorModal
|
<ErrorModal
|
||||||
type={this.props.ide.errorType}
|
type={this.props.ide.errorType}
|
||||||
closeModal={this.props.hideErrorModal}
|
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
);
|
);
|
||||||
|
@ -498,7 +554,7 @@ IDEView.propTypes = {
|
||||||
saveProject: PropTypes.func.isRequired,
|
saveProject: PropTypes.func.isRequired,
|
||||||
ide: PropTypes.shape({
|
ide: PropTypes.shape({
|
||||||
isPlaying: PropTypes.bool.isRequired,
|
isPlaying: PropTypes.bool.isRequired,
|
||||||
isTextOutputPlaying: PropTypes.bool.isRequired,
|
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
|
||||||
consoleEvent: PropTypes.array,
|
consoleEvent: PropTypes.array,
|
||||||
modalIsVisible: PropTypes.bool.isRequired,
|
modalIsVisible: PropTypes.bool.isRequired,
|
||||||
sidebarIsExpanded: PropTypes.bool.isRequired,
|
sidebarIsExpanded: PropTypes.bool.isRequired,
|
||||||
|
@ -513,7 +569,7 @@ IDEView.propTypes = {
|
||||||
infiniteLoop: PropTypes.bool.isRequired,
|
infiniteLoop: PropTypes.bool.isRequired,
|
||||||
previewIsRefreshing: PropTypes.bool.isRequired,
|
previewIsRefreshing: PropTypes.bool.isRequired,
|
||||||
infiniteLoopMessage: PropTypes.string.isRequired,
|
infiniteLoopMessage: PropTypes.string.isRequired,
|
||||||
projectSavedTime: PropTypes.string.isRequired,
|
projectSavedTime: PropTypes.string,
|
||||||
previousPath: PropTypes.string.isRequired,
|
previousPath: PropTypes.string.isRequired,
|
||||||
justOpenedProject: PropTypes.bool.isRequired,
|
justOpenedProject: PropTypes.bool.isRequired,
|
||||||
errorType: PropTypes.string,
|
errorType: PropTypes.string,
|
||||||
|
@ -521,8 +577,8 @@ IDEView.propTypes = {
|
||||||
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
stopSketch: PropTypes.func.isRequired,
|
stopSketch: PropTypes.func.isRequired,
|
||||||
startTextOutput: PropTypes.func.isRequired,
|
startAccessibleOutput: PropTypes.func.isRequired,
|
||||||
stopTextOutput: PropTypes.func.isRequired,
|
stopAccessibleOutput: PropTypes.func.isRequired,
|
||||||
project: PropTypes.shape({
|
project: PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
@ -547,7 +603,9 @@ IDEView.propTypes = {
|
||||||
isTabIndent: PropTypes.bool.isRequired,
|
isTabIndent: PropTypes.bool.isRequired,
|
||||||
autosave: PropTypes.bool.isRequired,
|
autosave: PropTypes.bool.isRequired,
|
||||||
lintWarning: PropTypes.bool.isRequired,
|
lintWarning: PropTypes.bool.isRequired,
|
||||||
textOutput: PropTypes.number.isRequired,
|
textOutput: PropTypes.bool.isRequired,
|
||||||
|
gridOutput: PropTypes.bool.isRequired,
|
||||||
|
soundOutput: PropTypes.bool.isRequired,
|
||||||
theme: PropTypes.string.isRequired,
|
theme: PropTypes.string.isRequired,
|
||||||
autorefresh: PropTypes.bool.isRequired
|
autorefresh: PropTypes.bool.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
@ -559,6 +617,8 @@ IDEView.propTypes = {
|
||||||
setAutosave: PropTypes.func.isRequired,
|
setAutosave: PropTypes.func.isRequired,
|
||||||
setLintWarning: PropTypes.func.isRequired,
|
setLintWarning: PropTypes.func.isRequired,
|
||||||
setTextOutput: PropTypes.func.isRequired,
|
setTextOutput: PropTypes.func.isRequired,
|
||||||
|
setGridOutput: PropTypes.func.isRequired,
|
||||||
|
setSoundOutput: PropTypes.func.isRequired,
|
||||||
files: PropTypes.arrayOf(PropTypes.shape({
|
files: PropTypes.arrayOf(PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
|
12
client/modules/IDE/reducers/assets.js
Normal file
12
client/modules/IDE/reducers/assets.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import * as ActionTypes from '../../../constants';
|
||||||
|
|
||||||
|
const assets = (state = [], action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.SET_ASSETS:
|
||||||
|
return action.assets;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default assets;
|
|
@ -13,9 +13,9 @@ const defaultHTML =
|
||||||
`<!DOCTYPE html>
|
`<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.10/p5.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/p5.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.10/addons/p5.dom.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/addons/p5.dom.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.10/addons/p5.sound.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/addons/p5.sound.min.js"></script>
|
||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
<link rel="stylesheet" type="text/css" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as ActionTypes from '../../../constants';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
isTextOutputPlaying: false,
|
isAccessibleOutputPlaying: false,
|
||||||
modalIsVisible: false,
|
modalIsVisible: false,
|
||||||
sidebarIsExpanded: false,
|
sidebarIsExpanded: false,
|
||||||
consoleIsExpanded: true,
|
consoleIsExpanded: true,
|
||||||
|
@ -28,10 +28,10 @@ const ide = (state = initialState, action) => {
|
||||||
return Object.assign({}, state, { isPlaying: true });
|
return Object.assign({}, state, { isPlaying: true });
|
||||||
case ActionTypes.STOP_SKETCH:
|
case ActionTypes.STOP_SKETCH:
|
||||||
return Object.assign({}, state, { isPlaying: false });
|
return Object.assign({}, state, { isPlaying: false });
|
||||||
case ActionTypes.START_TEXT_OUTPUT:
|
case ActionTypes.START_ACCESSIBLE_OUTPUT:
|
||||||
return Object.assign({}, state, { isTextOutputPlaying: true });
|
return Object.assign({}, state, { isAccessibleOutputPlaying: true });
|
||||||
case ActionTypes.STOP_TEXT_OUTPUT:
|
case ActionTypes.STOP_ACCESSIBLE_OUTPUT:
|
||||||
return Object.assign({}, state, { isTextOutputPlaying: false });
|
return Object.assign({}, state, { isAccessibleOutputPlaying: false });
|
||||||
case ActionTypes.CONSOLE_EVENT:
|
case ActionTypes.CONSOLE_EVENT:
|
||||||
return Object.assign({}, state, { consoleEvent: action.event });
|
return Object.assign({}, state, { consoleEvent: action.event });
|
||||||
case ActionTypes.SHOW_MODAL:
|
case ActionTypes.SHOW_MODAL:
|
||||||
|
|
|
@ -6,7 +6,9 @@ const initialState = {
|
||||||
isTabIndent: true,
|
isTabIndent: true,
|
||||||
autosave: true,
|
autosave: true,
|
||||||
lintWarning: false,
|
lintWarning: false,
|
||||||
textOutput: 0,
|
textOutput: false,
|
||||||
|
gridOutput: false,
|
||||||
|
soundOutput: false,
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
autorefresh: false
|
autorefresh: false
|
||||||
};
|
};
|
||||||
|
@ -31,6 +33,10 @@ const preferences = (state = initialState, action) => {
|
||||||
return Object.assign({}, state, { lintWarning: action.value });
|
return Object.assign({}, state, { lintWarning: action.value });
|
||||||
case ActionTypes.SET_TEXT_OUTPUT:
|
case ActionTypes.SET_TEXT_OUTPUT:
|
||||||
return Object.assign({}, state, { textOutput: action.value });
|
return Object.assign({}, state, { textOutput: action.value });
|
||||||
|
case ActionTypes.SET_GRID_OUTPUT:
|
||||||
|
return Object.assign({}, state, { gridOutput: action.value });
|
||||||
|
case ActionTypes.SET_SOUND_OUTPUT:
|
||||||
|
return Object.assign({}, state, { soundOutput: action.value });
|
||||||
case ActionTypes.SET_PREFERENCES:
|
case ActionTypes.SET_PREFERENCES:
|
||||||
return action.preferences;
|
return action.preferences;
|
||||||
case ActionTypes.SET_THEME:
|
case ActionTypes.SET_THEME:
|
||||||
|
|
|
@ -8,6 +8,7 @@ const initialState = () => {
|
||||||
return {
|
return {
|
||||||
name: generatedName,
|
name: generatedName,
|
||||||
serveSecure: isSecurePage(),
|
serveSecure: isSecurePage(),
|
||||||
|
updatedAt: ''
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -130,6 +130,41 @@ export function initiateResetPassword(formValues) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function initiateVerification() {
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.EMAIL_VERIFICATION_INITIATE
|
||||||
|
});
|
||||||
|
axios.post(`${ROOT_URL}/verify/send`, {}, { withCredentials: true })
|
||||||
|
.then(() => {
|
||||||
|
// do nothing
|
||||||
|
})
|
||||||
|
.catch(response => dispatch({
|
||||||
|
type: ActionTypes.ERROR,
|
||||||
|
message: response.data
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyEmailConfirmation(token) {
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.EMAIL_VERIFICATION_VERIFY,
|
||||||
|
state: 'checking',
|
||||||
|
});
|
||||||
|
return axios.get(`${ROOT_URL}/verify?t=${token}`, {}, { withCredentials: true })
|
||||||
|
.then(response => dispatch({
|
||||||
|
type: ActionTypes.EMAIL_VERIFICATION_VERIFIED,
|
||||||
|
message: response.data,
|
||||||
|
}))
|
||||||
|
.catch(response => dispatch({
|
||||||
|
type: ActionTypes.EMAIL_VERIFICATION_INVALID,
|
||||||
|
message: response.data
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function resetPasswordReset() {
|
export function resetPasswordReset() {
|
||||||
return {
|
return {
|
||||||
type: ActionTypes.RESET_PASSWORD_RESET
|
type: ActionTypes.RESET_PASSWORD_RESET
|
||||||
|
|
|
@ -4,11 +4,19 @@ import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||||
function AccountForm(props) {
|
function AccountForm(props) {
|
||||||
const {
|
const {
|
||||||
fields: { username, email, currentPassword, newPassword },
|
fields: { username, email, currentPassword, newPassword },
|
||||||
|
user,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
initiateVerification,
|
||||||
submitting,
|
submitting,
|
||||||
invalid,
|
invalid,
|
||||||
pristine
|
pristine
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const handleInitiateVerification = (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
initiateVerification();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="form" onSubmit={handleSubmit(props.updateSettings)}>
|
<form className="form" onSubmit={handleSubmit(props.updateSettings)}>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
|
@ -22,6 +30,26 @@ function AccountForm(props) {
|
||||||
/>
|
/>
|
||||||
{email.touched && email.error && <span className="form-error">{email.error}</span>}
|
{email.touched && email.error && <span className="form-error">{email.error}</span>}
|
||||||
</p>
|
</p>
|
||||||
|
{
|
||||||
|
user.verified !== 'verified' &&
|
||||||
|
(
|
||||||
|
<p className="form__context">
|
||||||
|
<span className="form__status">Unconfirmed.</span>
|
||||||
|
{
|
||||||
|
user.emailVerificationInitiate === true ?
|
||||||
|
(
|
||||||
|
<span className="form__status"> Confirmation sent, check your email.</span>
|
||||||
|
) :
|
||||||
|
(
|
||||||
|
<button
|
||||||
|
className="form__action"
|
||||||
|
onClick={handleInitiateVerification}
|
||||||
|
>Resend confirmation email</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="username" className="form__label">User Name</label>
|
<label htmlFor="username" className="form__label">User Name</label>
|
||||||
<input
|
<input
|
||||||
|
@ -43,7 +71,11 @@ function AccountForm(props) {
|
||||||
id="currentPassword"
|
id="currentPassword"
|
||||||
{...domOnlyProps(currentPassword)}
|
{...domOnlyProps(currentPassword)}
|
||||||
/>
|
/>
|
||||||
{currentPassword.touched && currentPassword.error && <span className="form-error">{currentPassword.error}</span>}
|
{
|
||||||
|
currentPassword.touched &&
|
||||||
|
currentPassword.error &&
|
||||||
|
<span className="form-error">{currentPassword.error}</span>
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="new password" className="form__label">New Password</label>
|
<label htmlFor="new password" className="form__label">New Password</label>
|
||||||
|
@ -56,7 +88,12 @@ function AccountForm(props) {
|
||||||
/>
|
/>
|
||||||
{newPassword.touched && newPassword.error && <span className="form-error">{newPassword.error}</span>}
|
{newPassword.touched && newPassword.error && <span className="form-error">{newPassword.error}</span>}
|
||||||
</p>
|
</p>
|
||||||
<input type="submit" disabled={submitting || invalid || pristine} value="Save All Settings" aria-label="updateSettings" />
|
<input
|
||||||
|
type="submit"
|
||||||
|
disabled={submitting || invalid || pristine}
|
||||||
|
value="Save All Settings"
|
||||||
|
aria-label="updateSettings"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -66,9 +103,14 @@ AccountForm.propTypes = {
|
||||||
username: PropTypes.object.isRequired,
|
username: PropTypes.object.isRequired,
|
||||||
email: PropTypes.object.isRequired,
|
email: PropTypes.object.isRequired,
|
||||||
currentPassword: PropTypes.object.isRequired,
|
currentPassword: PropTypes.object.isRequired,
|
||||||
newPassword: PropTypes.object.isRequired
|
newPassword: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
user: PropTypes.shape({
|
||||||
|
verified: PropTypes.number.isRequired,
|
||||||
|
emailVerificationInitiate: PropTypes.bool.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
handleSubmit: PropTypes.func.isRequired,
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
initiateVerification: PropTypes.func.isRequired,
|
||||||
updateSettings: PropTypes.func.isRequired,
|
updateSettings: PropTypes.func.isRequired,
|
||||||
submitting: PropTypes.bool,
|
submitting: PropTypes.bool,
|
||||||
invalid: PropTypes.bool,
|
invalid: PropTypes.bool,
|
||||||
|
|
|
@ -25,7 +25,11 @@ function NewPasswordForm(props) {
|
||||||
id="confirm password"
|
id="confirm password"
|
||||||
{...domOnlyProps(confirmPassword)}
|
{...domOnlyProps(confirmPassword)}
|
||||||
/>
|
/>
|
||||||
{confirmPassword.touched && confirmPassword.error && <span className="form-error">{confirmPassword.error}</span>}
|
{
|
||||||
|
confirmPassword.touched &&
|
||||||
|
confirmPassword.error &&
|
||||||
|
<span className="form-error">{confirmPassword.error}</span>
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<input type="submit" disabled={submitting || invalid || pristine} value="Set New Password" aria-label="sign up" />
|
<input type="submit" disabled={submitting || invalid || pristine} value="Set New Password" aria-label="sign up" />
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -15,7 +15,12 @@ function ResetPasswordForm(props) {
|
||||||
{...domOnlyProps(email)}
|
{...domOnlyProps(email)}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<input type="submit" disabled={submitting || invalid || pristine || props.user.resetPasswordInitiate} value="Send Password Reset Email" aria-label="Send email to reset password" />
|
<input
|
||||||
|
type="submit"
|
||||||
|
disabled={submitting || invalid || pristine || props.user.resetPasswordInitiate}
|
||||||
|
value="Send Password Reset Email"
|
||||||
|
aria-label="Send email to reset password"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,11 @@ function SignupForm(props) {
|
||||||
id="confirm password"
|
id="confirm password"
|
||||||
{...domOnlyProps(confirmPassword)}
|
{...domOnlyProps(confirmPassword)}
|
||||||
/>
|
/>
|
||||||
{confirmPassword.touched && confirmPassword.error && <span className="form-error">{confirmPassword.error}</span>}
|
{
|
||||||
|
confirmPassword.touched &&
|
||||||
|
confirmPassword.error &&
|
||||||
|
<span className="form-error">{confirmPassword.error}</span>
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<input type="submit" disabled={submitting || invalid || pristine} value="Sign Up" aria-label="sign up" />
|
<input type="submit" disabled={submitting || invalid || pristine} value="Sign Up" aria-label="sign up" />
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { bindActionCreators } from 'redux';
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
import InlineSVG from 'react-inlinesvg';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { updateSettings } from '../actions';
|
import { updateSettings, initiateVerification } from '../actions';
|
||||||
import AccountForm from '../components/AccountForm';
|
import AccountForm from '../components/AccountForm';
|
||||||
import { validateSettings } from '../../../utils/reduxFormUtils';
|
import { validateSettings } from '../../../utils/reduxFormUtils';
|
||||||
import GithubButton from '../components/GithubButton';
|
import GithubButton from '../components/GithubButton';
|
||||||
|
@ -59,7 +59,7 @@ function mapStateToProps(state) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return bindActionCreators({ updateSettings }, dispatch);
|
return bindActionCreators({ updateSettings, initiateVerification }, dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
function asyncValidate(formProps, dispatch, props) {
|
function asyncValidate(formProps, dispatch, props) {
|
||||||
|
@ -81,7 +81,7 @@ function asyncValidate(formProps, dispatch, props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountView.propTypes = {
|
AccountView.propTypes = {
|
||||||
previousPath: PropTypes.string.isRequired
|
previousPath: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm({
|
export default reduxForm({
|
||||||
|
|
110
client/modules/User/pages/EmailVerificationView.jsx
Normal file
110
client/modules/User/pages/EmailVerificationView.jsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { browserHistory } from 'react-router';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import { verifyEmailConfirmation } from '../actions';
|
||||||
|
|
||||||
|
const exitUrl = require('../../../images/exit.svg');
|
||||||
|
const logoUrl = require('../../../images/p5js-logo.svg');
|
||||||
|
|
||||||
|
|
||||||
|
class EmailVerificationView extends React.Component {
|
||||||
|
static defaultProps = {
|
||||||
|
emailVerificationTokenState: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.closeLoginPage = this.closeLoginPage.bind(this);
|
||||||
|
this.gotoHomePage = this.gotoHomePage.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
const verificationToken = this.verificationToken();
|
||||||
|
if (verificationToken != null) {
|
||||||
|
this.props.verifyEmailConfirmation(verificationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verificationToken = () => get(this.props, 'location.query.t', null);
|
||||||
|
|
||||||
|
closeLoginPage() {
|
||||||
|
browserHistory.push(this.props.previousPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
gotoHomePage() {
|
||||||
|
browserHistory.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let status = null;
|
||||||
|
const {
|
||||||
|
emailVerificationTokenState,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (this.verificationToken() == null) {
|
||||||
|
status = (
|
||||||
|
<p>That link is invalid</p>
|
||||||
|
);
|
||||||
|
} else if (emailVerificationTokenState === 'checking') {
|
||||||
|
status = (
|
||||||
|
<p>Validating token, please wait...</p>
|
||||||
|
);
|
||||||
|
} else if (emailVerificationTokenState === 'verified') {
|
||||||
|
status = (
|
||||||
|
<p>All done, your email address has been verified.</p>
|
||||||
|
);
|
||||||
|
} else if (emailVerificationTokenState === 'invalid') {
|
||||||
|
status = (
|
||||||
|
<p>Something went wrong.</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-container">
|
||||||
|
<div className="form-container__header">
|
||||||
|
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
||||||
|
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
||||||
|
</button>
|
||||||
|
<button className="form-container__exit-button" onClick={this.closeLoginPage}>
|
||||||
|
<InlineSVG src={exitUrl} alt="Close Login Page" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="form-container__content">
|
||||||
|
<h2 className="form-container__title">Verify your email</h2>
|
||||||
|
{status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
emailVerificationTokenState: state.user.emailVerificationTokenState,
|
||||||
|
previousPath: state.ide.previousPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
verifyEmailConfirmation,
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
EmailVerificationView.propTypes = {
|
||||||
|
previousPath: PropTypes.string.isRequired,
|
||||||
|
emailVerificationTokenState: PropTypes.oneOf([
|
||||||
|
'checking', 'verified', 'invalid'
|
||||||
|
]),
|
||||||
|
verifyEmailConfirmation: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(EmailVerificationView);
|
|
@ -19,6 +19,14 @@ const user = (state = { authenticated: false }, action) => {
|
||||||
return Object.assign({}, state, { resetPasswordInitiate: false });
|
return Object.assign({}, state, { resetPasswordInitiate: false });
|
||||||
case ActionTypes.INVALID_RESET_PASSWORD_TOKEN:
|
case ActionTypes.INVALID_RESET_PASSWORD_TOKEN:
|
||||||
return Object.assign({}, state, { resetPasswordInvalid: true });
|
return Object.assign({}, state, { resetPasswordInvalid: true });
|
||||||
|
case ActionTypes.EMAIL_VERIFICATION_INITIATE:
|
||||||
|
return Object.assign({}, state, { emailVerificationInitiate: true });
|
||||||
|
case ActionTypes.EMAIL_VERIFICATION_VERIFY:
|
||||||
|
return Object.assign({}, state, { emailVerificationTokenState: 'checking' });
|
||||||
|
case ActionTypes.EMAIL_VERIFICATION_VERIFIED:
|
||||||
|
return Object.assign({}, state, { emailVerificationTokenState: 'verified' });
|
||||||
|
case ActionTypes.EMAIL_VERIFICATION_INVALID:
|
||||||
|
return Object.assign({}, state, { emailVerificationTokenState: 'invalid' });
|
||||||
case ActionTypes.SETTINGS_UPDATED:
|
case ActionTypes.SETTINGS_UPDATED:
|
||||||
return { ...state, ...action.user };
|
return { ...state, ...action.user };
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -9,6 +9,7 @@ import user from './modules/User/reducers';
|
||||||
import sketches from './modules/IDE/reducers/projects';
|
import sketches from './modules/IDE/reducers/projects';
|
||||||
import toast from './modules/IDE/reducers/toast';
|
import toast from './modules/IDE/reducers/toast';
|
||||||
import console from './modules/IDE/reducers/console';
|
import console from './modules/IDE/reducers/console';
|
||||||
|
import assets from './modules/IDE/reducers/assets';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
form,
|
form,
|
||||||
|
@ -20,7 +21,8 @@ const rootReducer = combineReducers({
|
||||||
sketches,
|
sketches,
|
||||||
editorAccessibility,
|
editorAccessibility,
|
||||||
toast,
|
toast,
|
||||||
console
|
console,
|
||||||
|
assets
|
||||||
});
|
});
|
||||||
|
|
||||||
export default rootReducer;
|
export default rootReducer;
|
||||||
|
|
|
@ -7,15 +7,21 @@ import FullView from './modules/IDE/pages/FullView';
|
||||||
import LoginView from './modules/User/pages/LoginView';
|
import LoginView from './modules/User/pages/LoginView';
|
||||||
import SignupView from './modules/User/pages/SignupView';
|
import SignupView from './modules/User/pages/SignupView';
|
||||||
import ResetPasswordView from './modules/User/pages/ResetPasswordView';
|
import ResetPasswordView from './modules/User/pages/ResetPasswordView';
|
||||||
|
import EmailVerificationView from './modules/User/pages/EmailVerificationView';
|
||||||
import NewPasswordView from './modules/User/pages/NewPasswordView';
|
import NewPasswordView from './modules/User/pages/NewPasswordView';
|
||||||
import AccountView from './modules/User/pages/AccountView';
|
import AccountView from './modules/User/pages/AccountView';
|
||||||
// import SketchListView from './modules/Sketch/pages/SketchListView';
|
// import SketchListView from './modules/Sketch/pages/SketchListView';
|
||||||
import { getUser } from './modules/User/actions';
|
import { getUser } from './modules/User/actions';
|
||||||
|
import { stopSketch } from './modules/IDE/actions/ide';
|
||||||
|
|
||||||
const checkAuth = (store) => {
|
const checkAuth = (store) => {
|
||||||
store.dispatch(getUser());
|
store.dispatch(getUser());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRouteChange = (store) => {
|
||||||
|
store.dispatch(stopSketch());
|
||||||
|
};
|
||||||
|
|
||||||
const routes = (store) => {
|
const routes = (store) => {
|
||||||
const sourceProtocol = findSourceProtocol(store.getState());
|
const sourceProtocol = findSourceProtocol(store.getState());
|
||||||
|
|
||||||
|
@ -28,11 +34,12 @@ const routes = (store) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Route path="/" component={App}>
|
<Route path="/" component={App} onChange={() => { onRouteChange(store); }}>
|
||||||
<IndexRoute component={IDEView} onEnter={checkAuth(store)} />
|
<IndexRoute component={IDEView} onEnter={checkAuth(store)} />
|
||||||
<Route path="/login" component={forceToHttps(LoginView)} />
|
<Route path="/login" component={forceToHttps(LoginView)} />
|
||||||
<Route path="/signup" component={forceToHttps(SignupView)} />
|
<Route path="/signup" component={forceToHttps(SignupView)} />
|
||||||
<Route path="/reset-password" component={forceToHttps(ResetPasswordView)} />
|
<Route path="/reset-password" component={forceToHttps(ResetPasswordView)} />
|
||||||
|
<Route path="/verify" component={forceToHttps(EmailVerificationView)} />
|
||||||
<Route
|
<Route
|
||||||
path="/reset-password/:reset_password_token"
|
path="/reset-password/:reset_password_token"
|
||||||
component={forceToHttps(NewPasswordView)}
|
component={forceToHttps(NewPasswordView)}
|
||||||
|
@ -42,6 +49,7 @@ const routes = (store) => {
|
||||||
<Route path="/sketches" component={IDEView} />
|
<Route path="/sketches" component={IDEView} />
|
||||||
<Route path="/:username/sketches/:project_id" component={IDEView} />
|
<Route path="/:username/sketches/:project_id" component={IDEView} />
|
||||||
<Route path="/:username/sketches" component={IDEView} />
|
<Route path="/:username/sketches" component={IDEView} />
|
||||||
|
<Route path="/:username/assets" component={IDEView} />
|
||||||
<Route path="/:username/account" component={forceToHttps(AccountView)} />
|
<Route path="/:username/account" component={forceToHttps(AccountView)} />
|
||||||
<Route path="/about" component={IDEView} />
|
<Route path="/about" component={IDEView} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -17,3 +17,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin icon() {
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('icon-color');
|
||||||
|
& g {
|
||||||
|
fill: getThemifyVariable('icon-color');
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: getThemifyVariable('icon-hover-color');
|
||||||
|
& g {
|
||||||
|
opacity: 1;
|
||||||
|
fill: getThemifyVariable('icon-hover-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
}
|
|
@ -31,26 +31,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
%icon {
|
|
||||||
@include themify() {
|
|
||||||
color: getThemifyVariable('icon-color');
|
|
||||||
& g {
|
|
||||||
fill: getThemifyVariable('icon-color');
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
color: getThemifyVariable('icon-hover-color');
|
|
||||||
& g {
|
|
||||||
opacity: 1;
|
|
||||||
fill: getThemifyVariable('icon-hover-color');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
%icon-toast{
|
%icon-toast{
|
||||||
@include themify() {
|
@include themify() {
|
||||||
color: $toast-text-color
|
color: $toast-text-color
|
||||||
|
|
|
@ -44,6 +44,7 @@ $themes: (
|
||||||
input-text-color: #333,
|
input-text-color: #333,
|
||||||
input-border-color: #b5b5b5,
|
input-border-color: #b5b5b5,
|
||||||
about-list-text-color: #4a4a4a,
|
about-list-text-color: #4a4a4a,
|
||||||
|
search-background-color: #ebebeb
|
||||||
),
|
),
|
||||||
dark: (
|
dark: (
|
||||||
logo-color: $p5js-pink,
|
logo-color: $p5js-pink,
|
||||||
|
@ -79,6 +80,7 @@ $themes: (
|
||||||
input-text-color: #333,
|
input-text-color: #333,
|
||||||
input-border-color: #b5b5b5,
|
input-border-color: #b5b5b5,
|
||||||
about-list-text-color: #f4f4f4,
|
about-list-text-color: #f4f4f4,
|
||||||
|
search-background-color: #ebebeb
|
||||||
),
|
),
|
||||||
contrast: (
|
contrast: (
|
||||||
logo-color: $yellow,
|
logo-color: $yellow,
|
||||||
|
@ -113,6 +115,7 @@ $themes: (
|
||||||
input-text-color: #333,
|
input-text-color: #333,
|
||||||
input-border-color: #b5b5b5,
|
input-border-color: #b5b5b5,
|
||||||
about-list-text-color: #f4f4f4,
|
about-list-text-color: #f4f4f4,
|
||||||
|
search-background-color: $white
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,3 @@
|
||||||
.about {
|
|
||||||
@extend %modal;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-flow: column;
|
|
||||||
width: #{720 / $base-font-size}rem;
|
|
||||||
outline: none;
|
|
||||||
& a {
|
|
||||||
color: $form-navigation-options-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.about__header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-top: #{12 / $base-font-size}rem;
|
|
||||||
padding-right: #{14 / $base-font-size}rem;
|
|
||||||
padding-bottom: #{20 / $base-font-size}rem;
|
|
||||||
padding-left: #{21 / $base-font-size}rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about__header-title {
|
|
||||||
font-size: #{38 / $base-font-size}rem;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about__exit-button {
|
|
||||||
@include themify() {
|
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.about__logo {
|
.about__logo {
|
||||||
@include themify() {
|
@include themify() {
|
||||||
& path {
|
& path {
|
||||||
|
@ -69,10 +37,15 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
padding-top: #{17 / $base-font-size}rem;
|
padding-top: #{17 / $base-font-size}rem;
|
||||||
padding-right: #{78 / $base-font-size}rem;
|
padding-right: #{78 / $base-font-size}rem;
|
||||||
padding-bottom: #{20 / $base-font-size}rem;
|
padding-bottom: #{20 / $base-font-size}rem;
|
||||||
padding-left: #{20 / $base-font-size}rem;
|
padding-left: #{20 / $base-font-size}rem;
|
||||||
|
width: #{720 / $base-font-size}rem;
|
||||||
|
& a {
|
||||||
|
color: $form-navigation-options-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.about__content-column {
|
.about__content-column {
|
||||||
|
@ -112,6 +85,7 @@
|
||||||
padding-right: #{20 / $base-font-size}rem;
|
padding-right: #{20 / $base-font-size}rem;
|
||||||
padding-bottom: #{21 / $base-font-size}rem;
|
padding-bottom: #{21 / $base-font-size}rem;
|
||||||
padding-left: #{291 / $base-font-size}rem;
|
padding-left: #{291 / $base-font-size}rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about__footer-list {
|
.about__footer-list {
|
||||||
|
|
56
client/styles/components/_asset-list.scss
Normal file
56
client/styles/components/_asset-list.scss
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
.asset-table-container {
|
||||||
|
// flex: 1 1 0%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
max-width: 100%;
|
||||||
|
width: #{1000 / $base-font-size}rem;
|
||||||
|
min-height: #{400 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table {
|
||||||
|
width: 100%;
|
||||||
|
padding: #{10 / $base-font-size}rem #{20 / $base-font-size}rem;
|
||||||
|
max-height: 100%;
|
||||||
|
border-spacing: 0;
|
||||||
|
& .asset-list__delete-column {
|
||||||
|
width: #{23 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& thead {
|
||||||
|
font-size: #{12 / $base-font-size}rem;
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('inactive-text-color')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& th {
|
||||||
|
height: #{32 / $base-font-size}rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table__row {
|
||||||
|
margin: #{10 / $base-font-size}rem;
|
||||||
|
height: #{72 / $base-font-size}rem;
|
||||||
|
font-size: #{16 / $base-font-size}rem;
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
@include themify() {
|
||||||
|
background: getThemifyVariable('console-header-background-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& a {
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('primary-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& td:first-child {
|
||||||
|
padding-left: #{10 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table__empty {
|
||||||
|
text-align: center;
|
||||||
|
font-size: #{16 / $base-font-size}rem;
|
||||||
|
}
|
|
@ -72,9 +72,7 @@
|
||||||
|
|
||||||
.preview-console__collapse {
|
.preview-console__collapse {
|
||||||
padding-top: #{3 / $base-font-size}rem;
|
padding-top: #{3 / $base-font-size}rem;
|
||||||
@include themify() {
|
@include icon();
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
.preview-console--collapsed & {
|
.preview-console--collapsed & {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -82,9 +80,7 @@
|
||||||
|
|
||||||
.preview-console__expand {
|
.preview-console__expand {
|
||||||
padding-top: #{3 / $base-font-size}rem;
|
padding-top: #{3 / $base-font-size}rem;
|
||||||
@include themify() {
|
@include icon();
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
display: none;
|
display: none;
|
||||||
.preview-console--collapsed & {
|
.preview-console--collapsed & {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -105,6 +105,169 @@
|
||||||
width: #{48 / $base-font-size}rem;
|
width: #{48 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Search dialog
|
||||||
|
*/
|
||||||
|
|
||||||
|
.CodeMirror-dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: - #{365/2/$base-font-size}rem;
|
||||||
|
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: #{365 / $base-font-size}rem;
|
||||||
|
|
||||||
|
font-family: Montserrat, sans-serif;
|
||||||
|
|
||||||
|
padding: #{14 / $base-font-size}rem #{20 / $base-font-size}rem #{14 / $base-font-size}rem #{18 / $base-font-size}rem;
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
background-color: getThemifyVariable('modal-background-color');
|
||||||
|
box-shadow: 0 12px 12px 0 getThemifyVariable('shadow-color');
|
||||||
|
border: solid 0.5px getThemifyVariable('modal-border-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-search-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: #{12 / $base-font-size}rem;
|
||||||
|
|
||||||
|
font-size: #{21 / $base-font-size}rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-search-field {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: #{12 / $base-font-size}rem;
|
||||||
|
@include themify() {
|
||||||
|
background-color: getThemifyVariable('search-background-color');
|
||||||
|
border: solid 0.5px getThemifyVariable('button-border-color');
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
background-color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-search-count {
|
||||||
|
display: block;
|
||||||
|
height: #{20 / $base-font-size}rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-search-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
*/
|
||||||
|
.CodeMirror-search-modifiers {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-regexp-button,
|
||||||
|
.CodeMirror-case-button,
|
||||||
|
.CodeMirror-word-button {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
margin-left: #{10 / $base-font-size}rem;
|
||||||
|
|
||||||
|
word-break: keep-all;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('inactive-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-regexp-button .label,
|
||||||
|
.CodeMirror-case-button .label,
|
||||||
|
.CodeMirror-word-button .label {
|
||||||
|
@extend %hidden-element;
|
||||||
|
}
|
||||||
|
|
||||||
|
[aria-checked="true"] {
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('primary-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Previous / Next buttons
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Visually hide button text
|
||||||
|
.CodeMirror-search-button .label {
|
||||||
|
@extend %hidden-element;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-search-button {
|
||||||
|
margin-right: #{10 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-search-button::after {
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
|
||||||
|
@include icon();
|
||||||
|
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previous button
|
||||||
|
.CodeMirror-search-button.prev::after {
|
||||||
|
background-image: url(../images/up-arrow.svg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next button
|
||||||
|
.CodeMirror-search-button.next::after {
|
||||||
|
background-image: url(../images/down-arrow.svg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Close button
|
||||||
|
*/
|
||||||
|
.CodeMirror-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: #{14 / $base-font-size}rem;
|
||||||
|
right: #{18 / $base-font-size}rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visually hide button text
|
||||||
|
.CodeMirror-close-button .label {
|
||||||
|
@extend %hidden-element;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-close-button:after {
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
margin-left: #{8 / $base-font-size}rem;
|
||||||
|
|
||||||
|
@include icon();
|
||||||
|
|
||||||
|
background: transparent url(../images/exit.svg) no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-holder {
|
.editor-holder {
|
||||||
height: calc(100% - #{29 / $base-font-size}rem);
|
height: calc(100% - #{29 / $base-font-size}rem);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -116,9 +279,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor__options-button {
|
.editor__options-button {
|
||||||
@include themify() {
|
@include icon();
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: #{10 / $base-font-size}rem;
|
top: #{10 / $base-font-size}rem;
|
||||||
right: #{2 / $base-font-size}rem;
|
right: #{2 / $base-font-size}rem;
|
||||||
|
|
|
@ -12,9 +12,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-modal__exit-button {
|
.error-modal__exit-button {
|
||||||
@include themify() {
|
@include icon();
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-modal__content {
|
.error-modal__content {
|
||||||
|
|
|
@ -33,6 +33,15 @@
|
||||||
border-color: $secondary-form-title-color;
|
border-color: $secondary-form-title-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form__context {
|
||||||
|
text-align: left;
|
||||||
|
margin-top: #{15 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form__status {
|
||||||
|
color: $form-navigation-options-color;
|
||||||
|
}
|
||||||
|
|
||||||
.form input[type="submit"] {
|
.form input[type="submit"] {
|
||||||
@extend %forms-button;
|
@extend %forms-button;
|
||||||
}
|
}
|
||||||
|
|
20
client/styles/components/_keyboard-shortcuts.scss
Normal file
20
client/styles/components/_keyboard-shortcuts.scss
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
.keyboard-shortcuts {
|
||||||
|
padding: #{20 / $base-font-size}rem;
|
||||||
|
padding-bottom: #{40 / $base-font-size}rem;
|
||||||
|
width: #{450 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-shortcut-item {
|
||||||
|
display: flex;
|
||||||
|
& + & {
|
||||||
|
margin-top: #{10 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-shortcut__command {
|
||||||
|
width: 50%;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: #{10 / $base-font-size}rem;
|
||||||
|
}
|
|
@ -22,9 +22,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__exit-button {
|
.modal__exit-button {
|
||||||
@include themify() {
|
@include icon();
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__header {
|
.modal__header {
|
||||||
|
@ -50,68 +48,3 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: #{20 / $base-font-size}rem 0;
|
margin: #{20 / $base-font-size}rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploader {
|
|
||||||
min-height: #{200 / $base-font-size}rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-modal {
|
|
||||||
@extend %modal;
|
|
||||||
padding: #{20 / $base-font-size}rem;
|
|
||||||
width: #{500 / $base-font-size}rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-modal__header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-modal__section {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: #{10 / $base-font-size}rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-modal__label {
|
|
||||||
width: #{86 / $base-font-size}rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-modal__input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyboard-shortcuts {
|
|
||||||
@extend %modal;
|
|
||||||
padding: #{20 / $base-font-size}rem;
|
|
||||||
width: #{450 / $base-font-size}rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyboard-shortcuts__header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: #{20 / $base-font-size}rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyboard-shortcuts__close {
|
|
||||||
@include themify() {
|
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyboard-shortcut-item {
|
|
||||||
display: flex;
|
|
||||||
& + & {
|
|
||||||
margin-top: #{10 / $base-font-size}rem;
|
|
||||||
}
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyboard-shortcut__command {
|
|
||||||
width: 50%;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: right;
|
|
||||||
padding-right: #{10 / $base-font-size}rem;
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,10 +9,31 @@
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-content {
|
.overlay__content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overlay__body {
|
||||||
|
@extend %modal;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-flow: column;
|
||||||
|
max-height: 80%;
|
||||||
|
max-width: 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: #{40 / $base-font-size}rem #{20 / $base-font-size}rem #{30 / $base-font-size}rem #{20 / $base-font-size}rem;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay__close-button {
|
||||||
|
@include icon();
|
||||||
|
padding: #{12 / $base-font-size}rem #{16 / $base-font-size}rem;
|
||||||
|
}
|
|
@ -17,9 +17,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.preferences__exit-button {
|
.preferences__exit-button {
|
||||||
@include themify() {
|
@include icon();
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
padding-top: #{5 / $base-font-size}rem;
|
padding-top: #{5 / $base-font-size}rem;
|
||||||
margin-right: #{-6 / $base-font-size}rem;
|
margin-right: #{-6 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
@ -151,5 +149,5 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.preference__option.preference__canvas:not(:last-child) {
|
.preference__option.preference__canvas:not(:last-child) {
|
||||||
padding-right: #{20 / $base-font-size}rem;
|
padding-right: #{14 / $base-font-size}rem;
|
||||||
}
|
}
|
24
client/styles/components/_share.scss
Normal file
24
client/styles/components/_share.scss
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
.share-modal {
|
||||||
|
padding: #{20 / $base-font-size}rem;
|
||||||
|
width: #{500 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-modal__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-modal__section {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: #{10 / $base-font-size}rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-modal__label {
|
||||||
|
width: #{86 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-modal__input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
|
@ -25,9 +25,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__add {
|
.sidebar__add {
|
||||||
@include themify() {
|
@include icon();
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
.sidebar--contracted & {
|
.sidebar--contracted & {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -124,8 +122,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__file-item-show-options {
|
.sidebar__file-item-show-options {
|
||||||
|
@include icon();
|
||||||
@include themify() {
|
@include themify() {
|
||||||
@extend %icon;
|
|
||||||
padding: #{4 / $base-font-size}rem 0;
|
padding: #{4 / $base-font-size}rem 0;
|
||||||
background-color: map-get($theme-map, 'file-selected-color');
|
background-color: map-get($theme-map, 'file-selected-color');
|
||||||
padding-right: #{6 / $base-font-size}rem;
|
padding-right: #{6 / $base-font-size}rem;
|
||||||
|
@ -183,9 +181,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__expand {
|
.sidebar__expand {
|
||||||
@include themify() {
|
@include icon();
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: #{7 / $base-font-size}rem;
|
top: #{7 / $base-font-size}rem;
|
||||||
left: #{1 / $base-font-size}rem;
|
left: #{1 / $base-font-size}rem;
|
||||||
|
@ -200,9 +196,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__contract {
|
.sidebar__contract {
|
||||||
@include themify() {
|
@include icon();
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: #{7 / $base-font-size}rem;
|
top: #{7 / $base-font-size}rem;
|
||||||
left: #{34 / $base-font-size}rem;
|
left: #{34 / $base-font-size}rem;
|
||||||
|
|
|
@ -1,25 +1,8 @@
|
||||||
.sketch-list {
|
|
||||||
@extend %modal;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-flow: column;
|
|
||||||
width: #{1000 / $base-font-size}rem;
|
|
||||||
height: 80%;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sketch-list__header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sketch-list__header-title {
|
|
||||||
padding: #{40 / $base-font-size}rem #{16 / $base-font-size}rem #{12 / $base-font-size}rem #{21 / $base-font-size}rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sketches-table-container {
|
.sketches-table-container {
|
||||||
flex: 1 0 0%;
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
max-width: 100%;
|
||||||
|
width: #{1000 / $base-font-size}rem;
|
||||||
|
min-height: #{400 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sketches-table {
|
.sketches-table {
|
||||||
|
@ -67,13 +50,6 @@
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sketch-list__exit-button {
|
|
||||||
@include themify() {
|
|
||||||
@extend %icon;
|
|
||||||
}
|
|
||||||
margin: #{12 / $base-font-size}rem #{16 / $base-font-size}rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visibility-toggle .sketch-list__trash-button {
|
.visibility-toggle .sketch-list__trash-button {
|
||||||
@extend %hidden-element;
|
@extend %hidden-element;
|
||||||
width:#{20 / $base-font-size}rem;
|
width:#{20 / $base-font-size}rem;
|
||||||
|
@ -95,3 +71,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sketches-table__empty {
|
||||||
|
text-align: center;
|
||||||
|
font-size: #{16 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
.dropzone {
|
.dropzone {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uploader {
|
||||||
|
min-height: #{200 / $base-font-size}rem;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
|
@ -31,7 +31,7 @@
|
||||||
@extend %hidden-element;
|
@extend %hidden-element;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-output {
|
.accessible-output {
|
||||||
@extend %hidden-element;
|
@extend %hidden-element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,9 @@
|
||||||
@import 'components/error-modal';
|
@import 'components/error-modal';
|
||||||
@import 'components/preview-frame';
|
@import 'components/preview-frame';
|
||||||
@import 'components/help-modal';
|
@import 'components/help-modal';
|
||||||
|
@import 'components/share';
|
||||||
|
@import 'components/asset-list';
|
||||||
|
@import 'components/keyboard-shortcuts';
|
||||||
|
|
||||||
@import 'layout/ide';
|
@import 'layout/ide';
|
||||||
@import 'layout/fullscreen';
|
@import 'layout/fullscreen';
|
||||||
|
|
433
client/utils/codemirror-search.js
Normal file
433
client/utils/codemirror-search.js
Normal file
|
@ -0,0 +1,433 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
|
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||||
|
|
||||||
|
// Define search commands. Depends on dialog.js or another
|
||||||
|
// implementation of the openDialog method.
|
||||||
|
|
||||||
|
// Replace works a little oddly -- it will do the replace on the next
|
||||||
|
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
|
||||||
|
// replace by making sure the match is no longer selected when hitting
|
||||||
|
// Ctrl-G.
|
||||||
|
|
||||||
|
export default function(CodeMirror) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function searchOverlay(query) {
|
||||||
|
return {token: function(stream) {
|
||||||
|
query.lastIndex = stream.pos;
|
||||||
|
var match = query.exec(stream.string);
|
||||||
|
if (match && match.index == stream.pos) {
|
||||||
|
stream.pos += match[0].length || 1;
|
||||||
|
return "searching";
|
||||||
|
} else if (match) {
|
||||||
|
stream.pos = match.index;
|
||||||
|
} else {
|
||||||
|
stream.skipToEnd();
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchState() {
|
||||||
|
this.posFrom = this.posTo = this.lastQuery = this.query = null;
|
||||||
|
this.overlay = null;
|
||||||
|
this.regexp = false;
|
||||||
|
this.caseInsensitive = true;
|
||||||
|
this.wholeWord = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchState(cm) {
|
||||||
|
return cm.state.search || (cm.state.search = new SearchState());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchCursor(cm, query, pos) {
|
||||||
|
return cm.getSearchCursor(query, pos, getSearchState(cm).caseInsensitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
|
||||||
|
var searchField = document.getElementsByClassName("CodeMirror-search-field")[0];
|
||||||
|
if (!searchField) {
|
||||||
|
cm.openDialog(text, onEnter, {
|
||||||
|
value: deflt,
|
||||||
|
selectValueOnOpen: true,
|
||||||
|
closeOnEnter: false,
|
||||||
|
onClose: function () {
|
||||||
|
clearSearch(cm);
|
||||||
|
},
|
||||||
|
onKeyDown: onKeyDown,
|
||||||
|
closeOnBlur: false
|
||||||
|
});
|
||||||
|
|
||||||
|
searchField = document.getElementsByClassName("CodeMirror-search-field")[0];
|
||||||
|
|
||||||
|
var dialog = document.getElementsByClassName("CodeMirror-dialog")[0];
|
||||||
|
var closeButton = dialog.getElementsByClassName("close")[0];
|
||||||
|
|
||||||
|
var state = getSearchState(cm);
|
||||||
|
|
||||||
|
CodeMirror.on(searchField, "keyup", function (e) {
|
||||||
|
if (e.keyCode !== 13 && searchField.value.length > 1) { // not enter and more than 1 character to search
|
||||||
|
startSearch(cm, getSearchState(cm), searchField.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.on(closeButton, "click", function () {
|
||||||
|
clearSearch(cm);
|
||||||
|
dialog.parentNode.removeChild(dialog);
|
||||||
|
cm.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
var upArrow = dialog.getElementsByClassName("up-arrow")[0];
|
||||||
|
CodeMirror.on(upArrow, "click", function () {
|
||||||
|
CodeMirror.commands.findPrev(cm);
|
||||||
|
searchField.blur();
|
||||||
|
cm.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
var downArrow = dialog.getElementsByClassName("down-arrow")[0];
|
||||||
|
CodeMirror.on(downArrow, "click", function () {
|
||||||
|
CodeMirror.commands.findNext(cm);
|
||||||
|
searchField.blur();
|
||||||
|
cm.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
var regexpButton = dialog.getElementsByClassName("CodeMirror-regexp-button")[0];
|
||||||
|
CodeMirror.on(regexpButton, "click", function () {
|
||||||
|
var state = getSearchState(cm);
|
||||||
|
state.regexp = toggle(regexpButton);
|
||||||
|
startSearch(cm, getSearchState(cm), searchField.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
toggle(regexpButton, state.regexp);
|
||||||
|
|
||||||
|
var caseSensitiveButton = dialog.getElementsByClassName("CodeMirror-case-button")[0];
|
||||||
|
CodeMirror.on(caseSensitiveButton, "click", function () {
|
||||||
|
var state = getSearchState(cm);
|
||||||
|
state.caseInsensitive = !toggle(caseSensitiveButton);
|
||||||
|
startSearch(cm, getSearchState(cm), searchField.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
toggle(caseSensitiveButton, !state.caseInsensitive);
|
||||||
|
|
||||||
|
var wholeWordButton = dialog.getElementsByClassName("CodeMirror-word-button")[0];
|
||||||
|
CodeMirror.on(wholeWordButton, "click", function () {
|
||||||
|
var state = getSearchState(cm);
|
||||||
|
state.wholeWord = toggle(wholeWordButton);
|
||||||
|
startSearch(cm, getSearchState(cm), searchField.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
toggle(wholeWordButton, state.wholeWord);
|
||||||
|
|
||||||
|
function toggle(el, initialState) {
|
||||||
|
var currentState, nextState;
|
||||||
|
|
||||||
|
if (initialState == null) {
|
||||||
|
currentState = el.getAttribute('aria-checked') === 'true';
|
||||||
|
nextState = !currentState;
|
||||||
|
} else {
|
||||||
|
nextState = initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.setAttribute('aria-checked', nextState);
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searchField.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dialog(cm, text, shortText, deflt, f) {
|
||||||
|
if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
|
||||||
|
else f(prompt(shortText, deflt));
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastSelectedIndex = 0;
|
||||||
|
function confirmDialog(cm, text, shortText, fs) {
|
||||||
|
if (cm.openConfirm) cm.openConfirm(text, fs);
|
||||||
|
else if (confirm(shortText)) fs[0]();
|
||||||
|
|
||||||
|
var dialog = document.getElementsByClassName("CodeMirror-dialog")[0];
|
||||||
|
var buttons = dialog.getElementsByTagName("button");
|
||||||
|
buttons[lastSelectedIndex].focus();
|
||||||
|
for (var i = 0; i < buttons.length; i += 1) {
|
||||||
|
(function (index) {
|
||||||
|
var button = buttons[index];
|
||||||
|
button.addEventListener("focus", function (e) {
|
||||||
|
lastSelectedIndex = index === buttons.length - 1 ? 0 : index;
|
||||||
|
});
|
||||||
|
button.addEventListener("keyup", function (e) {
|
||||||
|
if (e.keyCode === 37) { // arrow left
|
||||||
|
var prevButton = index === 0 ? buttons.length - 1 : index - 1;
|
||||||
|
buttons[prevButton].focus();
|
||||||
|
}
|
||||||
|
if (e.keyCode === 39) { // arrow right
|
||||||
|
var nextButton = index === buttons.length - 1 ? 0 : index + 1;
|
||||||
|
buttons[nextButton].focus();
|
||||||
|
}
|
||||||
|
if (e.keyCode === 27) { // esc
|
||||||
|
cm.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
if (index === buttons.length - 1) { // "done"
|
||||||
|
lastSelectedIndex = 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseString(string) {
|
||||||
|
return string.replace(/\\(.)/g, function(_, ch) {
|
||||||
|
if (ch == "n") return "\n"
|
||||||
|
if (ch == "r") return "\r"
|
||||||
|
return ch
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parses the query text and state and returns
|
||||||
|
a RegExp ready for searching
|
||||||
|
*/
|
||||||
|
function parseQuery(query, state) {
|
||||||
|
var emptyQuery = 'x^'; // matches nothing
|
||||||
|
|
||||||
|
if (query === '') { // empty string matches nothing
|
||||||
|
query = emptyQuery;
|
||||||
|
} else {
|
||||||
|
if (state.regexp === false) {
|
||||||
|
query = parseString(query);
|
||||||
|
query = query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.wholeWord) {
|
||||||
|
query += '\\b';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var regexp;
|
||||||
|
|
||||||
|
try {
|
||||||
|
regexp = new RegExp(query, state.caseInsensitive ? "gi" : "g");
|
||||||
|
} catch (e) {
|
||||||
|
regexp = new RegExp(emptyQuery, 'g');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the resulting regexp will match everything, do not use it
|
||||||
|
if (regexp.test('')) {
|
||||||
|
return new RegExp(emptyQuery, 'g');
|
||||||
|
}
|
||||||
|
|
||||||
|
return regexp;
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryDialog = `
|
||||||
|
<h3 class="CodeMirror-search-title">Find</h3>
|
||||||
|
<input type="text" class="search-input CodeMirror-search-field" placeholder="Find in files" />
|
||||||
|
<div class="CodeMirror-search-actions">
|
||||||
|
<div class="CodeMirror-search-modifiers button-wrap">
|
||||||
|
<button
|
||||||
|
title="Regular expression"
|
||||||
|
aria-label="Regular expression"
|
||||||
|
role="checkbox"
|
||||||
|
class="CodeMirror-search-modifier-button CodeMirror-regexp-button"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" class="button">.*</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Case sensitive"
|
||||||
|
aria-label="Case sensitive"
|
||||||
|
role="checkbox"
|
||||||
|
class="CodeMirror-search-modifier-button CodeMirror-case-button"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" class="button">Aa</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Whole words"
|
||||||
|
aria-label="Whole words"
|
||||||
|
role="checkbox"
|
||||||
|
class="CodeMirror-search-modifier-button CodeMirror-word-button"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" class="button">" "</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="CodeMirror-search-nav">
|
||||||
|
<button
|
||||||
|
title="Previous"
|
||||||
|
aria-label="Previous"
|
||||||
|
class="CodeMirror-search-button icon up-arrow prev"
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Next"
|
||||||
|
aria-label="Next"
|
||||||
|
class="CodeMirror-search-button icon down-arrow next"
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
title="Close"
|
||||||
|
aria-label="Close"
|
||||||
|
class="CodeMirror-close-button close icon">
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function startSearch(cm, state, originalQuery) {
|
||||||
|
state.queryText = originalQuery;
|
||||||
|
state.query = parseQuery(originalQuery, state);
|
||||||
|
|
||||||
|
cm.removeOverlay(state.overlay, state.caseInsensitive);
|
||||||
|
state.overlay = searchOverlay(state.query);
|
||||||
|
cm.addOverlay(state.overlay);
|
||||||
|
if (cm.showMatchesOnScrollbar) {
|
||||||
|
if (state.annotate) { state.annotate.clear(); state.annotate = null; }
|
||||||
|
state.annotate = cm.showMatchesOnScrollbar(state.query, state.caseInsensitive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSearch(cm, rev, persistent, immediate, ignoreQuery) {
|
||||||
|
var state = getSearchState(cm);
|
||||||
|
if (!ignoreQuery && state.query) return findNext(cm, rev);
|
||||||
|
var q = cm.getSelection() || state.lastQuery;
|
||||||
|
if (persistent && cm.openDialog) {
|
||||||
|
var hiding = null
|
||||||
|
var searchNext = function(query, event) {
|
||||||
|
CodeMirror.e_stop(event);
|
||||||
|
if (!query) return;
|
||||||
|
if (query != state.queryText) {
|
||||||
|
startSearch(cm, state, query);
|
||||||
|
state.posFrom = state.posTo = cm.getCursor();
|
||||||
|
}
|
||||||
|
if (hiding) hiding.style.opacity = 1
|
||||||
|
findNext(cm, event.shiftKey, function(_, to) {
|
||||||
|
var dialog
|
||||||
|
if (to.line < 3 && document.querySelector &&
|
||||||
|
(dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
|
||||||
|
dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
|
||||||
|
(hiding = dialog).style.opacity = .4
|
||||||
|
})
|
||||||
|
};
|
||||||
|
persistentDialog(cm, queryDialog, q, searchNext, function(event, query) {
|
||||||
|
var keyName = CodeMirror.keyName(event)
|
||||||
|
var cmd = CodeMirror.keyMap[cm.getOption("keyMap")][keyName]
|
||||||
|
if (!cmd) cmd = cm.getOption('extraKeys')[keyName]
|
||||||
|
if (cmd == "findNext" || cmd == "findPrev" ||
|
||||||
|
cmd == "findPersistentNext" || cmd == "findPersistentPrev") {
|
||||||
|
CodeMirror.e_stop(event);
|
||||||
|
startSearch(cm, getSearchState(cm), query);
|
||||||
|
cm.execCommand(cmd);
|
||||||
|
} else if (cmd == "find" || cmd == "findPersistent") {
|
||||||
|
CodeMirror.e_stop(event);
|
||||||
|
searchNext(query, event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (immediate && q) {
|
||||||
|
startSearch(cm, state, q);
|
||||||
|
findNext(cm, rev);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dialog(cm, queryDialog, "Search for:", q, function(query) {
|
||||||
|
if (query && !state.query) cm.operation(function() {
|
||||||
|
startSearch(cm, state, query);
|
||||||
|
state.posFrom = state.posTo = cm.getCursor();
|
||||||
|
findNext(cm, rev);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNext(cm, rev, callback) {cm.operation(function() {
|
||||||
|
var state = getSearchState(cm);
|
||||||
|
var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
|
||||||
|
if (!cursor.find(rev)) {
|
||||||
|
cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
|
||||||
|
if (!cursor.find(rev)) return;
|
||||||
|
}
|
||||||
|
cm.setSelection(cursor.from(), cursor.to());
|
||||||
|
cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 60);
|
||||||
|
state.posFrom = cursor.from(); state.posTo = cursor.to();
|
||||||
|
if (callback) callback(cursor.from(), cursor.to())
|
||||||
|
});}
|
||||||
|
|
||||||
|
function clearSearch(cm) {cm.operation(function() {
|
||||||
|
var state = getSearchState(cm);
|
||||||
|
state.lastQuery = state.queryText;
|
||||||
|
if (!state.query) return;
|
||||||
|
state.query = state.queryText = null;
|
||||||
|
cm.removeOverlay(state.overlay);
|
||||||
|
if (state.annotate) { state.annotate.clear(); state.annotate = null; }
|
||||||
|
});}
|
||||||
|
|
||||||
|
var replaceQueryDialog =
|
||||||
|
'<input type="text" class="search-input CodeMirror-search-field"/><div class="close icon"></div>';
|
||||||
|
|
||||||
|
var replacementQueryDialog = 'With: <input type="text" class="replace-input CodeMirror-search-field"/>';
|
||||||
|
var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>";
|
||||||
|
|
||||||
|
function replaceAll(cm, query, text) {
|
||||||
|
cm.operation(function() {
|
||||||
|
for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
|
||||||
|
if (typeof query != "string") {
|
||||||
|
var match = cm.getRange(cursor.from(), cursor.to()).match(query);
|
||||||
|
cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
|
||||||
|
} else cursor.replace(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This will need updating if replace is implemented
|
||||||
|
function replace(cm, all) {
|
||||||
|
var prevDialog = document.getElementsByClassName("CodeMirror-dialog")[0];
|
||||||
|
if (prevDialog) {
|
||||||
|
clearSearch(cm);
|
||||||
|
prevDialog.parentNode.removeChild(prevDialog);
|
||||||
|
cm.focus();
|
||||||
|
}
|
||||||
|
if (cm.getOption("readOnly")) return;
|
||||||
|
var query = cm.getSelection() || getSearchState(cm).lastQuery;
|
||||||
|
var dialogText = all ? "Replace all:" : "Replace:"
|
||||||
|
dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
|
||||||
|
if (!query) return;
|
||||||
|
query = parseQuery(query);
|
||||||
|
dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
|
||||||
|
text = parseString(text)
|
||||||
|
if (all) {
|
||||||
|
replaceAll(cm, query, text)
|
||||||
|
} else {
|
||||||
|
clearSearch(cm);
|
||||||
|
var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
|
||||||
|
var advance = function() {
|
||||||
|
var start = cursor.from(), match;
|
||||||
|
if (!(match = cursor.findNext())) {
|
||||||
|
cursor = getSearchCursor(cm, query);
|
||||||
|
if (!(match = cursor.findNext()) ||
|
||||||
|
(start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
|
||||||
|
}
|
||||||
|
cm.setSelection(cursor.from(), cursor.to());
|
||||||
|
cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 60);
|
||||||
|
confirmDialog(cm, doReplaceConfirm, "Replace?",
|
||||||
|
[function() {doReplace(match);}, advance,
|
||||||
|
function() {replaceAll(cm, query, text)}]);
|
||||||
|
};
|
||||||
|
var doReplace = function(match) {
|
||||||
|
cursor.replace(typeof query == "string" ? text :
|
||||||
|
text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
|
||||||
|
advance();
|
||||||
|
};
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeMirror.commands.find = function(cm) {doSearch(cm);};
|
||||||
|
CodeMirror.commands.findPersistent = function(cm) { doSearch(cm, false, true, false, true);};
|
||||||
|
CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);};
|
||||||
|
CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);};
|
||||||
|
CodeMirror.commands.findNext = doSearch;
|
||||||
|
CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
|
||||||
|
CodeMirror.commands.clearSearch = clearSearch;
|
||||||
|
// CodeMirror.commands.replace = replace;
|
||||||
|
// CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
|
||||||
|
};
|
16
client/utils/metaKey.js
Normal file
16
client/utils/metaKey.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
const metaKey = (() => {
|
||||||
|
if (navigator != null && navigator.platform != null) {
|
||||||
|
return /^MAC/i.test(navigator.platform) ?
|
||||||
|
'Cmd' :
|
||||||
|
'Ctrl';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Ctrl';
|
||||||
|
})();
|
||||||
|
|
||||||
|
const metaKeyName = metaKey === 'Cmd' ? 'Command' : 'Control';
|
||||||
|
|
||||||
|
export {
|
||||||
|
metaKey,
|
||||||
|
metaKeyName,
|
||||||
|
};
|
|
@ -26,7 +26,9 @@ function validateNameEmail(formProps, errors) {
|
||||||
|
|
||||||
if (!formProps.email) {
|
if (!formProps.email) {
|
||||||
errors.email = 'Please enter an 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 (
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
!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.';
|
errors.email = 'Please enter a valid email address.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"cookie-parser": "^1.4.1",
|
"cookie-parser": "^1.4.1",
|
||||||
"cors": "^2.8.1",
|
"cors": "^2.8.1",
|
||||||
"csslint": "^0.10.0",
|
"csslint": "^0.10.0",
|
||||||
|
"csurf": "^1.9.0",
|
||||||
|
"decomment": "^0.8.7",
|
||||||
"dotenv": "^2.0.0",
|
"dotenv": "^2.0.0",
|
||||||
"dropzone": "^4.3.0",
|
"dropzone": "^4.3.0",
|
||||||
"escape-string-regexp": "^1.0.5",
|
"escape-string-regexp": "^1.0.5",
|
||||||
|
@ -78,12 +80,16 @@
|
||||||
"express": "^4.13.4",
|
"express": "^4.13.4",
|
||||||
"express-session": "^1.13.0",
|
"express-session": "^1.13.0",
|
||||||
"file-type": "^3.8.0",
|
"file-type": "^3.8.0",
|
||||||
|
"fs-promise": "^1.0.0",
|
||||||
"htmlhint": "^0.9.13",
|
"htmlhint": "^0.9.13",
|
||||||
|
"is_js": "^0.9.0",
|
||||||
|
"is-url": "^1.2.2",
|
||||||
"js-beautify": "^1.6.4",
|
"js-beautify": "^1.6.4",
|
||||||
"jsdom": "^9.8.3",
|
"jsdom": "^9.8.3",
|
||||||
"jshint": "^2.9.4",
|
"jshint": "^2.9.4",
|
||||||
"lodash": "^4.16.4",
|
"lodash": "^4.16.4",
|
||||||
"loop-protect": "git+https://git@github.com/catarak/loop-protect.git",
|
"loop-protect": "git+https://git@github.com/catarak/loop-protect.git",
|
||||||
|
"mjml": "^3.3.2",
|
||||||
"moment": "^2.14.1",
|
"moment": "^2.14.1",
|
||||||
"mongoose": "^4.4.16",
|
"mongoose": "^4.4.16",
|
||||||
"node-uuid": "^1.4.7",
|
"node-uuid": "^1.4.7",
|
||||||
|
@ -92,10 +98,13 @@
|
||||||
"passport": "^0.3.2",
|
"passport": "^0.3.2",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
"pretty-bytes": "^3.0.1",
|
||||||
"project-name-generator": "^2.1.3",
|
"project-name-generator": "^2.1.3",
|
||||||
|
"pug": "^2.0.0-beta6",
|
||||||
"q": "^1.4.1",
|
"q": "^1.4.1",
|
||||||
"react": "^15.1.0",
|
"react": "^15.1.0",
|
||||||
"react-dom": "^15.1.0",
|
"react-dom": "^15.1.0",
|
||||||
|
"react-helmet": "^5.1.3",
|
||||||
"react-inlinesvg": "^0.4.2",
|
"react-inlinesvg": "^0.4.2",
|
||||||
"react-redux": "^4.4.5",
|
"react-redux": "^4.4.5",
|
||||||
"react-router": "^2.6.0",
|
"react-router": "^2.6.0",
|
||||||
|
|
|
@ -84,6 +84,7 @@ passport.use(new GitHubStrategy({
|
||||||
existingEmailUser.username = existingEmailUser.username || profile.username;
|
existingEmailUser.username = existingEmailUser.username || profile.username;
|
||||||
existingEmailUser.tokens.push({ kind: 'github', accessToken });
|
existingEmailUser.tokens.push({ kind: 'github', accessToken });
|
||||||
existingEmailUser.name = existingEmailUser.name || profile.displayName;
|
existingEmailUser.name = existingEmailUser.name || profile.displayName;
|
||||||
|
existingEmailUser.verified = User.EmailConfirmation.Verified;
|
||||||
existingEmailUser.save(saveErr => done(null, existingEmailUser));
|
existingEmailUser.save(saveErr => done(null, existingEmailUser));
|
||||||
} else {
|
} else {
|
||||||
const user = new User();
|
const user = new User();
|
||||||
|
@ -92,6 +93,7 @@ passport.use(new GitHubStrategy({
|
||||||
user.username = profile.username;
|
user.username = profile.username;
|
||||||
user.tokens.push({ kind: 'github', accessToken });
|
user.tokens.push({ kind: 'github', accessToken });
|
||||||
user.name = profile.displayName;
|
user.name = profile.displayName;
|
||||||
|
user.verified = User.EmailConfirmation.Verified;
|
||||||
user.save(saveErr => done(null, user));
|
user.save(saveErr => done(null, user));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import uuid from 'node-uuid';
|
import uuid from 'node-uuid';
|
||||||
import policy from 's3-policy';
|
import policy from 's3-policy';
|
||||||
import s3 from 's3';
|
import s3 from 's3';
|
||||||
|
import { getProjectsForUserId } from './project.controller';
|
||||||
|
import { findUserByUsername } from './user.controller';
|
||||||
|
|
||||||
const client = s3.createClient({
|
const client = s3.createClient({
|
||||||
maxAsyncS3: 20,
|
maxAsyncS3: 20,
|
||||||
|
@ -28,7 +30,7 @@ export function getObjectKey(url) {
|
||||||
if (urlArray.length === 6) {
|
if (urlArray.length === 6) {
|
||||||
const key = urlArray.pop();
|
const key = urlArray.pop();
|
||||||
const userId = urlArray.pop();
|
const userId = urlArray.pop();
|
||||||
objectKey = `${userId}/${key}`
|
objectKey = `${userId}/${key}`;
|
||||||
} else {
|
} else {
|
||||||
const key = urlArray.pop();
|
const key = urlArray.pop();
|
||||||
objectKey = key;
|
objectKey = key;
|
||||||
|
@ -104,3 +106,45 @@ export function copyObjectInS3(req, res) {
|
||||||
res.json({ url: `${s3Bucket}${userId}/${newFilename}` });
|
res.json({ url: `${s3Bucket}${userId}/${newFilename}` });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listObjectsInS3ForUser(req, res) {
|
||||||
|
const username = req.params.username;
|
||||||
|
findUserByUsername(username, (user) => {
|
||||||
|
const userId = user.id;
|
||||||
|
const params = {
|
||||||
|
s3Params: {
|
||||||
|
Bucket: `${process.env.S3_BUCKET}`,
|
||||||
|
Prefix: `${userId}/`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let assets = [];
|
||||||
|
const list = client.listObjects(params)
|
||||||
|
.on('data', (data) => {
|
||||||
|
assets = assets.concat(data["Contents"].map((object) => {
|
||||||
|
return { key: object["Key"], size: object["Size"] };
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.on('end', () => {
|
||||||
|
const projectAssets = [];
|
||||||
|
getProjectsForUserId(userId).then((projects) => {
|
||||||
|
projects.forEach((project) => {
|
||||||
|
project.files.forEach((file) => {
|
||||||
|
if (!file.url) return;
|
||||||
|
|
||||||
|
const foundAsset = assets.find((asset) => file.url.includes(asset.key));
|
||||||
|
if (!foundAsset) return;
|
||||||
|
projectAssets.push({
|
||||||
|
name: file.name,
|
||||||
|
sketchName: project.name,
|
||||||
|
sketchId: project.id,
|
||||||
|
url: file.url,
|
||||||
|
key: foundAsset.key,
|
||||||
|
size: foundAsset.size
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
res.json({assets: projectAssets});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -48,7 +48,8 @@ function deleteMany(files, ids) {
|
||||||
|
|
||||||
each(ids, (id, cb) => {
|
each(ids, (id, cb) => {
|
||||||
if (files.id(id).url) {
|
if (files.id(id).url) {
|
||||||
if (!process.env.S3_DATE || (process.env.S3_DATE && moment(process.env.S3_DATE) < moment(files.id(id).createdAt))) {
|
if (!process.env.S3_DATE
|
||||||
|
|| (process.env.S3_DATE && moment(process.env.S3_DATE) < moment(files.id(id).createdAt))) {
|
||||||
const objectKey = getObjectKey(files.id(id).url);
|
const objectKey = getObjectKey(files.id(id).url);
|
||||||
objectKeys.push(objectKey);
|
objectKeys.push(objectKey);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import archiver from 'archiver';
|
import archiver from 'archiver';
|
||||||
import request from 'request';
|
import request from 'request';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import isUrl from 'is-url';
|
||||||
|
import jsdom, { serializeDocument } from 'jsdom';
|
||||||
import Project from '../models/project';
|
import Project from '../models/project';
|
||||||
import User from '../models/user';
|
import User from '../models/user';
|
||||||
import { deleteObjectsFromS3, getObjectKey } from './aws.controller';
|
import { deleteObjectsFromS3, getObjectKey } from './aws.controller';
|
||||||
|
@ -100,10 +102,7 @@ function deleteFilesFromS3(files) {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.map((file) => {
|
.map(file => getObjectKey(file.url)));
|
||||||
return getObjectKey(file.url);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteProject(req, res) {
|
export function deleteProject(req, res) {
|
||||||
|
@ -123,12 +122,28 @@ export function deleteProject(req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProjects(req, res) {
|
export function getProjectsForUserId(userId) {
|
||||||
if (req.user) {
|
return new Promise((resolve, reject) => {
|
||||||
Project.find({ user: req.user._id }) // eslint-disable-line no-underscore-dangle
|
Project.find({ user: userId })
|
||||||
.sort('-createdAt')
|
.sort('-createdAt')
|
||||||
.select('name files id createdAt updatedAt')
|
.select('name files id createdAt updatedAt')
|
||||||
.exec((err, projects) => {
|
.exec((err, projects) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
resolve(projects);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectsForUserName(username) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjects(req, res) {
|
||||||
|
if (req.user) {
|
||||||
|
getProjectsForUserId(req.user._id)
|
||||||
|
.then((projects) => {
|
||||||
res.json(projects);
|
res.json(projects);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -144,7 +159,7 @@ export function getProjectsForUser(req, res) {
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
Project.find({ user: user._id }) // eslint-disable-line no-underscore-dangle
|
Project.find({ user: user._id })
|
||||||
.sort('-createdAt')
|
.sort('-createdAt')
|
||||||
.select('name files id createdAt updatedAt')
|
.select('name files id createdAt updatedAt')
|
||||||
.exec((innerErr, projects) => res.json(projects));
|
.exec((innerErr, projects) => res.json(projects));
|
||||||
|
@ -155,6 +170,48 @@ export function getProjectsForUser(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bundleExternalLibs(project, zip, callback) {
|
||||||
|
const rootFile = project.files.find(file => file.name === 'root');
|
||||||
|
const indexHtml = project.files.find(file => file.name === 'index.html');
|
||||||
|
let numScriptsResolved = 0;
|
||||||
|
let numScriptTags = 0;
|
||||||
|
|
||||||
|
function resolveScriptTagSrc(scriptTag, document) {
|
||||||
|
const path = scriptTag.src.split('/');
|
||||||
|
const filename = path[path.length - 1];
|
||||||
|
const src = scriptTag.src;
|
||||||
|
|
||||||
|
if (!isUrl(src)) {
|
||||||
|
numScriptsResolved += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
request({ method: 'GET', url: src, encoding: null }, (err, response, body) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
zip.append(body, { name: filename });
|
||||||
|
scriptTag.src = filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
numScriptsResolved += 1;
|
||||||
|
if (numScriptsResolved === numScriptTags) {
|
||||||
|
indexHtml.content = serializeDocument(document);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
jsdom.env(indexHtml.content, (innerErr, window) => {
|
||||||
|
const indexHtmlDoc = window.document;
|
||||||
|
const scriptTags = indexHtmlDoc.getElementsByTagName('script');
|
||||||
|
numScriptTags = scriptTags.length;
|
||||||
|
for (let i = 0; i < numScriptTags; i += 1) {
|
||||||
|
resolveScriptTagSrc(scriptTags[i], indexHtmlDoc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function buildZip(project, req, res) {
|
function buildZip(project, req, res) {
|
||||||
const zip = archiver('zip');
|
const zip = archiver('zip');
|
||||||
const rootFile = project.files.find(file => file.name === 'root');
|
const rootFile = project.files.find(file => file.name === 'root');
|
||||||
|
@ -194,7 +251,10 @@ function buildZip(project, req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bundleExternalLibs(project, zip, () => {
|
||||||
addFileToZip(rootFile, '/');
|
addFileToZip(rootFile, '/');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadProjectAsZip(req, res) {
|
export function downloadProjectAsZip(req, res) {
|
||||||
|
|
|
@ -13,6 +13,7 @@ export function createSession(req, res, next) {
|
||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
username: req.user.username,
|
username: req.user.username,
|
||||||
preferences: req.user.preferences,
|
preferences: req.user.preferences,
|
||||||
|
verified: req.user.verified,
|
||||||
id: req.user._id
|
id: req.user._id
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -25,6 +26,7 @@ export function getSession(req, res) {
|
||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
username: req.user.username,
|
username: req.user.username,
|
||||||
preferences: req.user.preferences,
|
preferences: req.user.preferences,
|
||||||
|
verified: req.user.verified,
|
||||||
id: req.user._id
|
id: req.user._id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,38 @@
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import async from 'async';
|
import async from 'async';
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
import mg from 'nodemailer-mailgun-transport';
|
|
||||||
import User from '../models/user';
|
import User from '../models/user';
|
||||||
|
import mail from '../utils/mail';
|
||||||
|
import {
|
||||||
|
renderEmailConfirmation,
|
||||||
|
renderResetPassword,
|
||||||
|
} from '../views/mail';
|
||||||
|
|
||||||
|
const random = (done) => {
|
||||||
|
crypto.randomBytes(20, (err, buf) => {
|
||||||
|
const token = buf.toString('hex');
|
||||||
|
done(err, token);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function findUserByUsername(username, cb) {
|
||||||
|
User.findOne({ username },
|
||||||
|
(err, user) => {
|
||||||
|
cb(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours
|
||||||
|
|
||||||
export function createUser(req, res, next) {
|
export function createUser(req, res, next) {
|
||||||
|
random((tokenError, token) => {
|
||||||
const user = new User({
|
const user = new User({
|
||||||
username: req.body.username,
|
username: req.body.username,
|
||||||
email: req.body.email,
|
email: req.body.email,
|
||||||
password: req.body.password
|
password: req.body.password,
|
||||||
|
verified: User.EmailConfirmation.Sent,
|
||||||
|
verifiedToken: token,
|
||||||
|
verifiedTokenExpires: EMAIL_VERIFY_TOKEN_EXPIRY_TIME,
|
||||||
});
|
});
|
||||||
|
|
||||||
User.findOne({ email: req.body.email },
|
User.findOne({ email: req.body.email },
|
||||||
|
@ -32,15 +56,29 @@ export function createUser(req, res, next) {
|
||||||
next(loginErr);
|
next(loginErr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
|
const mailOptions = renderEmailConfirmation({
|
||||||
|
body: {
|
||||||
|
domain: `${protocol}://${req.headers.host}`,
|
||||||
|
link: `${protocol}://${req.headers.host}/verify?t=${token}`
|
||||||
|
},
|
||||||
|
to: req.user.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars
|
||||||
res.json({
|
res.json({
|
||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
username: req.user.username,
|
username: req.user.username,
|
||||||
preferences: req.user.preferences,
|
preferences: req.user.preferences,
|
||||||
|
verified: req.user.verified,
|
||||||
id: req.user._id
|
id: req.user._id
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function duplicateUserCheck(req, res) {
|
export function duplicateUserCheck(req, res) {
|
||||||
|
@ -90,12 +128,7 @@ export function updatePreferences(req, res) {
|
||||||
|
|
||||||
export function resetPasswordInitiate(req, res) {
|
export function resetPasswordInitiate(req, res) {
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
(done) => {
|
random,
|
||||||
crypto.randomBytes(20, (err, buf) => {
|
|
||||||
const token = buf.toString('hex');
|
|
||||||
done(err, token);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(token, done) => {
|
(token, done) => {
|
||||||
User.findOne({ email: req.body.email }, (err, user) => {
|
User.findOne({ email: req.body.email }, (err, user) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -111,27 +144,16 @@ export function resetPasswordInitiate(req, res) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(token, user, done) => {
|
(token, user, done) => {
|
||||||
const auth = {
|
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
auth: {
|
const mailOptions = renderResetPassword({
|
||||||
api_key: process.env.MAILGUN_KEY,
|
body: {
|
||||||
domain: process.env.MAILGUN_DOMAIN
|
domain: `${protocol}://${req.headers.host}`,
|
||||||
}
|
link: `${protocol}://${req.headers.host}/reset-password/${token}`,
|
||||||
};
|
},
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport(mg(auth));
|
|
||||||
const message = {
|
|
||||||
to: user.email,
|
to: user.email,
|
||||||
from: 'p5.js Web Editor <noreply@p5js.org>',
|
|
||||||
subject: 'p5.js Web Editor Password Reset',
|
|
||||||
text: `You are receiving this email because you (or someone else) have requested the reset of the password for your account.
|
|
||||||
\n\nPlease click on the following link, or paste this into your browser to complete the process:
|
|
||||||
\n\nhttp://${req.headers.host}/reset-password/${token}
|
|
||||||
\n\nIf you did not request this, please ignore this email and your password will remain unchanged.
|
|
||||||
\n\nThanks for using the p5.js Web Editor!\n`
|
|
||||||
};
|
|
||||||
transporter.sendMail(message, (error) => {
|
|
||||||
done(error);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mail.send(mailOptions, done);
|
||||||
}
|
}
|
||||||
], (err) => {
|
], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -153,6 +175,76 @@ export function validateResetPasswordToken(req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function emailVerificationInitiate(req, res) {
|
||||||
|
async.waterfall([
|
||||||
|
random,
|
||||||
|
(token, done) => {
|
||||||
|
User.findById(req.user.id, (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({ error: err });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({ error: 'Document not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.verified === User.EmailConfirmation.Verified) {
|
||||||
|
res.status(409).json({ error: 'Email already verified' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
|
const mailOptions = renderEmailConfirmation({
|
||||||
|
body: {
|
||||||
|
domain: `${protocol}://${req.headers.host}`,
|
||||||
|
link: `${protocol}://${req.headers.host}/verify?t=${token}`
|
||||||
|
},
|
||||||
|
to: user.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars
|
||||||
|
if (mailErr != null) {
|
||||||
|
res.status(500).send({ error: 'Error sending mail' });
|
||||||
|
} else {
|
||||||
|
user.verified = User.EmailConfirmation.Resent;
|
||||||
|
user.verifiedToken = token;
|
||||||
|
user.verifiedTokenExpires = EMAIL_VERIFY_TOKEN_EXPIRY_TIME; // 24 hours
|
||||||
|
user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
email: req.user.email,
|
||||||
|
username: req.user.username,
|
||||||
|
preferences: req.user.preferences,
|
||||||
|
verified: user.verified,
|
||||||
|
id: req.user._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyEmail(req, res) {
|
||||||
|
const token = req.query.t;
|
||||||
|
|
||||||
|
User.findOne({ verifiedToken: token, verifiedTokenExpires: { $gt: Date.now() } }, (err, user) => {
|
||||||
|
if (!user) {
|
||||||
|
res.status(401).json({ success: false, message: 'Token is invalid or has expired.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.verified = User.EmailConfirmation.Verified;
|
||||||
|
user.verifiedToken = null;
|
||||||
|
user.verifiedTokenExpires = null;
|
||||||
|
user.save()
|
||||||
|
.then((result) => { // eslint-disable-line
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function updatePassword(req, res) {
|
export function updatePassword(req, res) {
|
||||||
User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => {
|
User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -205,7 +297,6 @@ export function updateSettings(req, res) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
user.email = req.body.email;
|
|
||||||
user.username = req.body.username;
|
user.username = req.body.username;
|
||||||
|
|
||||||
if (req.body.currentPassword) {
|
if (req.body.currentPassword) {
|
||||||
|
@ -218,6 +309,28 @@ export function updateSettings(req, res) {
|
||||||
user.password = req.body.newPassword;
|
user.password = req.body.newPassword;
|
||||||
saveUser(res, user);
|
saveUser(res, user);
|
||||||
});
|
});
|
||||||
|
} else if (user.email !== req.body.email) {
|
||||||
|
user.verified = User.EmailConfirmation.Sent;
|
||||||
|
|
||||||
|
user.email = req.body.email;
|
||||||
|
|
||||||
|
random((error, token) => {
|
||||||
|
user.verifiedToken = token;
|
||||||
|
user.verifiedTokenExpires = EMAIL_VERIFY_TOKEN_EXPIRY_TIME;
|
||||||
|
|
||||||
|
saveUser(res, user);
|
||||||
|
|
||||||
|
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
|
const mailOptions = renderEmailConfirmation({
|
||||||
|
body: {
|
||||||
|
domain: `${protocol}://${req.headers.host}`,
|
||||||
|
link: `${protocol}://${req.headers.host}/verify?t=${token}`
|
||||||
|
},
|
||||||
|
to: user.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
mail.send(mailOptions);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
saveUser(res, user);
|
saveUser(res, user);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,9 @@ const defaultHTML =
|
||||||
`<!DOCTYPE html>
|
`<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.10/p5.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/p5.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.10/addons/p5.dom.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/addons/p5.dom.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.10/addons/p5.sound.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/addons/p5.sound.min.js"></script>
|
||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
<link rel="stylesheet" type="text/css" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -112,7 +112,8 @@ function getSketchContent(projectsInAllCategories) {
|
||||||
if (noNumberprojectName === 'Instance Mode : Instance Container ') {
|
if (noNumberprojectName === 'Instance Mode : Instance Container ') {
|
||||||
for (let i = 0; i < 4; i += 1) {
|
for (let i = 0; i < 4; i += 1) {
|
||||||
const splitedRes = `${res.split('*/')[1].split('</html>')[i]}</html>\n`;
|
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');
|
project.sketchContent = splitedRes.replace('p5.js',
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
project.sketchContent = res;
|
project.sketchContent = res;
|
||||||
|
@ -228,7 +229,8 @@ function createProjectsInP5user(projectsInAllCategories) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const 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((assetNamePath, i) => {
|
assetsInProject.forEach((assetNamePath, i) => {
|
||||||
let assetName = assetNamePath.split('assets/')[1];
|
let assetName = assetNamePath.split('assets/')[1];
|
||||||
|
|
|
@ -29,7 +29,7 @@ Project.find({}, (err, projects) => {
|
||||||
if (!project.user) return;
|
if (!project.user) return;
|
||||||
const userId = project.user.valueOf();
|
const userId = project.user.valueOf();
|
||||||
project.files.forEach((file, fileIndex) => {
|
project.files.forEach((file, fileIndex) => {
|
||||||
if (file.url && file.url.includes(process.env.S3_BUCKET)) {
|
if (file.url && file.url.includes(process.env.S3_BUCKET) && !file.url.includes(userId)) {
|
||||||
const key = file.url.split('/').pop();
|
const key = file.url.split('/').pop();
|
||||||
console.log(key);
|
console.log(key);
|
||||||
const params = {
|
const params = {
|
||||||
|
@ -37,6 +37,7 @@ Project.find({}, (err, projects) => {
|
||||||
CopySource: `${process.env.S3_BUCKET}/${key}`,
|
CopySource: `${process.env.S3_BUCKET}/${key}`,
|
||||||
Key: `${userId}/${key}`
|
Key: `${userId}/${key}`
|
||||||
};
|
};
|
||||||
|
try {
|
||||||
client.moveObject(params)
|
client.moveObject(params)
|
||||||
.on('err', (err) => {
|
.on('err', (err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
@ -47,6 +48,9 @@ Project.find({}, (err, projects) => {
|
||||||
console.log(`updated file ${key}`);
|
console.log(`updated file ${key}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,12 @@ import mongoose from 'mongoose';
|
||||||
|
|
||||||
const bcrypt = require('bcrypt-nodejs');
|
const bcrypt = require('bcrypt-nodejs');
|
||||||
|
|
||||||
|
const EmailConfirmationStates = {
|
||||||
|
Verified: 'verified',
|
||||||
|
Sent: 'sent',
|
||||||
|
Resent: 'resent',
|
||||||
|
};
|
||||||
|
|
||||||
const Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
const userSchema = new Schema({
|
const userSchema = new Schema({
|
||||||
|
@ -10,6 +16,9 @@ const userSchema = new Schema({
|
||||||
password: { type: String },
|
password: { type: String },
|
||||||
resetPasswordToken: String,
|
resetPasswordToken: String,
|
||||||
resetPasswordExpires: Date,
|
resetPasswordExpires: Date,
|
||||||
|
verified: { type: String },
|
||||||
|
verifiedToken: String,
|
||||||
|
verifiedTokenExpires: Date,
|
||||||
github: { type: String },
|
github: { type: String },
|
||||||
email: { type: String, unique: true },
|
email: { type: String, unique: true },
|
||||||
tokens: Array,
|
tokens: Array,
|
||||||
|
@ -19,7 +28,9 @@ const userSchema = new Schema({
|
||||||
isTabIndent: { type: Boolean, default: false },
|
isTabIndent: { type: Boolean, default: false },
|
||||||
autosave: { type: Boolean, default: true },
|
autosave: { type: Boolean, default: true },
|
||||||
lintWarning: { type: Boolean, default: false },
|
lintWarning: { type: Boolean, default: false },
|
||||||
textOutput: { type: Number, default: 0 },
|
textOutput: { type: Boolean, default: false },
|
||||||
|
gridOutput: { type: Boolean, default: false },
|
||||||
|
soundOutput: { type: Boolean, default: false },
|
||||||
theme: { type: String, default: 'light' },
|
theme: { type: String, default: 'light' },
|
||||||
autorefresh: { type: Boolean, default: false }
|
autorefresh: { type: Boolean, default: false }
|
||||||
}
|
}
|
||||||
|
@ -71,4 +82,6 @@ userSchema.statics.findByMailOrName = function findByMailOrName(email) {
|
||||||
return this.findOne(query).exec();
|
return this.findOne(query).exec();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
userSchema.statics.EmailConfirmation = EmailConfirmationStates;
|
||||||
|
|
||||||
export default mongoose.model('User', userSchema);
|
export default mongoose.model('User', userSchema);
|
||||||
|
|
|
@ -6,5 +6,6 @@ const router = new Router();
|
||||||
router.route('/S3/sign').post(AWSController.signS3);
|
router.route('/S3/sign').post(AWSController.signS3);
|
||||||
router.route('/S3/copy').post(AWSController.copyObjectInS3);
|
router.route('/S3/copy').post(AWSController.copyObjectInS3);
|
||||||
router.route('/S3/:object_key').delete(AWSController.deleteObjectFromS3);
|
router.route('/S3/:object_key').delete(AWSController.deleteObjectFromS3);
|
||||||
|
router.route('/S3/:username/objects').get(AWSController.listObjectsInS3ForUser);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -40,6 +40,10 @@ router.route('/reset-password/:reset_password_token').get((req, res) => {
|
||||||
res.send(renderIndex());
|
res.send(renderIndex());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.route('/verify').get((req, res) => {
|
||||||
|
res.send(renderIndex());
|
||||||
|
});
|
||||||
|
|
||||||
router.route('/sketches').get((req, res) => {
|
router.route('/sketches').get((req, res) => {
|
||||||
res.send(renderIndex());
|
res.send(renderIndex());
|
||||||
});
|
});
|
||||||
|
@ -54,6 +58,12 @@ router.route('/:username/sketches').get((req, res) => {
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.route('/:username/assets').get((req, res) => {
|
||||||
|
userExists(req.params.username, exists => (
|
||||||
|
exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html))
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
router.route('/:username/account').get((req, res) => {
|
router.route('/:username/account').get((req, res) => {
|
||||||
userExists(req.params.username, exists => (
|
userExists(req.params.username, exists => (
|
||||||
exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html))
|
exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html))
|
||||||
|
|
|
@ -17,4 +17,8 @@ router.route('/reset-password/:token').post(UserController.updatePassword);
|
||||||
|
|
||||||
router.route('/account').put(UserController.updateSettings);
|
router.route('/account').put(UserController.updateSettings);
|
||||||
|
|
||||||
|
router.route('/verify/send').post(UserController.emailVerificationInitiate);
|
||||||
|
|
||||||
|
router.route('/verify').get(UserController.verifyEmail);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import session from 'express-session';
|
||||||
import connectMongo from 'connect-mongo';
|
import connectMongo from 'connect-mongo';
|
||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import csurf from 'csurf';
|
||||||
|
|
||||||
// Webpack Requirements
|
// Webpack Requirements
|
||||||
import webpack from 'webpack';
|
import webpack from 'webpack';
|
||||||
|
@ -23,6 +24,7 @@ import files from './routes/file.routes';
|
||||||
import aws from './routes/aws.routes';
|
import aws from './routes/aws.routes';
|
||||||
import serverRoutes from './routes/server.routes';
|
import serverRoutes from './routes/server.routes';
|
||||||
import embedRoutes from './routes/embed.routes';
|
import embedRoutes from './routes/embed.routes';
|
||||||
|
import { requestsOfTypeJSON } from './utils/requestsOfType';
|
||||||
|
|
||||||
import { renderIndex } from './views/index';
|
import { renderIndex } from './views/index';
|
||||||
import { get404Sketch } from './views/404Page';
|
import { get404Sketch } from './views/404Page';
|
||||||
|
@ -73,18 +75,27 @@ app.use(session({
|
||||||
autoReconnect: true
|
autoReconnect: true
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Enables CSRF protection and stores secret in session
|
||||||
|
app.use(csurf());
|
||||||
|
// Middleware to add CSRF token as cookie to some requests
|
||||||
|
const csrfToken = (req, res, next) => {
|
||||||
|
res.cookie('XSRF-TOKEN', req.csrfToken());
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
app.use(passport.initialize());
|
app.use(passport.initialize());
|
||||||
app.use(passport.session());
|
app.use(passport.session());
|
||||||
app.use('/api', users);
|
app.use('/api', requestsOfTypeJSON(), users);
|
||||||
app.use('/api', sessions);
|
app.use('/api', requestsOfTypeJSON(), sessions);
|
||||||
app.use('/api', projects);
|
app.use('/api', requestsOfTypeJSON(), projects);
|
||||||
app.use('/api', files);
|
app.use('/api', requestsOfTypeJSON(), files);
|
||||||
app.use('/api', aws);
|
app.use('/api', requestsOfTypeJSON(), aws);
|
||||||
// this is supposed to be TEMPORARY -- until i figure out
|
// this is supposed to be TEMPORARY -- until i figure out
|
||||||
// isomorphic rendering
|
// isomorphic rendering
|
||||||
app.use('/', serverRoutes);
|
app.use('/', csrfToken, serverRoutes);
|
||||||
|
|
||||||
app.use('/', embedRoutes);
|
app.use('/', csrfToken, embedRoutes);
|
||||||
app.get('/auth/github', passport.authenticate('github'));
|
app.get('/auth/github', passport.authenticate('github'));
|
||||||
app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), (req, res) => {
|
app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), (req, res) => {
|
||||||
res.redirect('/');
|
res.redirect('/');
|
||||||
|
|
48
server/utils/mail.js
Normal file
48
server/utils/mail.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* Mail service wrapping around mailgun
|
||||||
|
*/
|
||||||
|
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import mg from 'nodemailer-mailgun-transport';
|
||||||
|
|
||||||
|
const auth = {
|
||||||
|
api_key: process.env.MAILGUN_KEY,
|
||||||
|
domain: process.env.MAILGUN_DOMAIN,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Mail {
|
||||||
|
constructor() {
|
||||||
|
this.client = nodemailer.createTransport(mg({ auth }));
|
||||||
|
this.sendOptions = {
|
||||||
|
from: process.env.EMAIL_SENDER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMail(mailOptions) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client.sendMail(mailOptions, (err, info) => {
|
||||||
|
resolve(err, info);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchMail(data, callback) {
|
||||||
|
const mailOptions = {
|
||||||
|
to: data.to,
|
||||||
|
subject: data.subject,
|
||||||
|
from: this.sendOptions.from,
|
||||||
|
html: data.html,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.sendMail(mailOptions)
|
||||||
|
.then((err, res) => {
|
||||||
|
callback(err, res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data, callback) {
|
||||||
|
return this.dispatchMail(data, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Mail();
|
|
@ -1,5 +1,5 @@
|
||||||
import { resolvePathToFile } from '../utils/filePath';
|
import { resolvePathToFile } from '../utils/filePath';
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
const MEDIA_FILE_REGEX_NO_QUOTES = /^(?!(http:\/\/|https:\/\/)).*\.(png|jpg|jpeg|gif|bmp|mp3|wav|aiff|ogg|json|txt|csv|svg|obj|mp4|ogg|webm|mov|otf|ttf|m4a)$/i;
|
const MEDIA_FILE_REGEX_NO_QUOTES = /^(?!(http:\/\/|https:\/\/)).*\.(png|jpg|jpeg|gif|bmp|mp3|wav|aiff|ogg|json|txt|csv|svg|obj|mp4|ogg|webm|mov|otf|ttf|m4a)$/i;
|
||||||
const STRING_REGEX = /(['"])((\\\1|.)*?)\1/gm;
|
const STRING_REGEX = /(['"])((\\\1|.)*?)\1/gm;
|
||||||
const EXTERNAL_LINK_REGEX = /^(http:\/\/|https:\/\/)/;
|
const EXTERNAL_LINK_REGEX = /^(http:\/\/|https:\/\/)/;
|
||||||
|
|
12
server/utils/renderMjml.js
Normal file
12
server/utils/renderMjml.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { mjml2html } from 'mjml';
|
||||||
|
|
||||||
|
export default (template) => {
|
||||||
|
try {
|
||||||
|
const output = mjml2html(template);
|
||||||
|
return output.html;
|
||||||
|
} catch (e) {
|
||||||
|
// fall through to null
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
15
server/utils/requestsOfType.js
Normal file
15
server/utils/requestsOfType.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
express middleware that sends a 406 Unacceptable
|
||||||
|
response if an incoming request's Content-Type
|
||||||
|
header does not match `type`
|
||||||
|
*/
|
||||||
|
const requestsOfType = type => (req, res, next) => {
|
||||||
|
if (req.get('content-type') != null && !req.is(type)) {
|
||||||
|
return next({ statusCode: 406 }); // 406 UNACCEPTABLE
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default requestsOfType;
|
||||||
|
export const requestsOfTypeJSON = () => requestsOfType('application/json');
|
56
server/views/mail.js
Normal file
56
server/views/mail.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import renderMjml from '../utils/renderMjml';
|
||||||
|
import mailLayout from './mailLayout';
|
||||||
|
|
||||||
|
export const renderResetPassword = (data) => {
|
||||||
|
const subject = 'p5.js Web Editor Password Reset';
|
||||||
|
const templateOptions = {
|
||||||
|
domain: data.body.domain,
|
||||||
|
headingText: 'Reset your password',
|
||||||
|
greetingText: 'Hello,',
|
||||||
|
messageText: 'We received a request to reset the password for your account. To reset your password, click on the button below:', // eslint-disable-line max-len
|
||||||
|
link: data.body.link,
|
||||||
|
buttonText: 'Reset password',
|
||||||
|
directLinkText: 'Or copy and paste the URL into your browser:',
|
||||||
|
noteText: 'If you did not request this, please ignore this email and your password will remain unchanged. Thanks for using the p5.js Web Editor!', // eslint-disable-line max-len
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return MJML string
|
||||||
|
const template = mailLayout(templateOptions);
|
||||||
|
|
||||||
|
// Render MJML to HTML string
|
||||||
|
const html = renderMjml(template);
|
||||||
|
|
||||||
|
// Return options to send mail
|
||||||
|
return Object.assign(
|
||||||
|
{},
|
||||||
|
data,
|
||||||
|
{ html, subject },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderEmailConfirmation = (data) => {
|
||||||
|
const subject = 'p5.js Email Verification';
|
||||||
|
const templateOptions = {
|
||||||
|
domain: data.body.domain,
|
||||||
|
headingText: 'Email Verification',
|
||||||
|
greetingText: 'Hello,',
|
||||||
|
messageText: 'To verify your email, click on the button below:',
|
||||||
|
link: data.body.link,
|
||||||
|
buttonText: 'Verify Email',
|
||||||
|
directLinkText: 'Or copy and paste the URL into your browser:',
|
||||||
|
noteText: 'This link is only valid for the next 24 hours. Thanks for using the p5.js Web Editor!',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return MJML string
|
||||||
|
const template = mailLayout(templateOptions);
|
||||||
|
|
||||||
|
// Render MJML to HTML string
|
||||||
|
const html = renderMjml(template);
|
||||||
|
|
||||||
|
// Return options to send mail
|
||||||
|
return Object.assign(
|
||||||
|
{},
|
||||||
|
data,
|
||||||
|
{ html, subject },
|
||||||
|
);
|
||||||
|
};
|
59
server/views/mailLayout.js
Normal file
59
server/views/mailLayout.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
export default ({
|
||||||
|
domain,
|
||||||
|
headingText,
|
||||||
|
greetingText,
|
||||||
|
messageText,
|
||||||
|
link,
|
||||||
|
buttonText,
|
||||||
|
directLinkText,
|
||||||
|
noteText,
|
||||||
|
}) => (
|
||||||
|
`
|
||||||
|
<mjml>
|
||||||
|
<mj-body>
|
||||||
|
<mj-container>
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-image width="192" src="${domain}/images/p5js-square-logo.png" alt="p5.js" />
|
||||||
|
<mj-divider border-color="#ed225d"></mj-divider>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text font-size="20px" color="#333333" font-family="sans-serif">
|
||||||
|
${headingText}
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text color="#333333">
|
||||||
|
${greetingText}
|
||||||
|
</mj-text>
|
||||||
|
<mj-text color="#333333">
|
||||||
|
${messageText}
|
||||||
|
</mj-text>
|
||||||
|
<mj-button background-color="#ed225d" href="${link}">
|
||||||
|
${buttonText}
|
||||||
|
</mj-button>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text color="#333333">
|
||||||
|
${directLinkText}
|
||||||
|
</mj-text>
|
||||||
|
<mj-text align="center" color="#333333"><a href="${link}">${link}</a></mj-text>
|
||||||
|
<mj-text color="#333333">
|
||||||
|
${noteText}
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
</mj-container>
|
||||||
|
</mj-body>
|
||||||
|
</mjml>
|
||||||
|
`
|
||||||
|
);
|
BIN
static/images/p5js-square-logo.png
Normal file
BIN
static/images/p5js-square-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
|
@ -1 +1 @@
|
||||||
Subproject commit 344fedf8d868c62adc571bf4212c6bea3cd20247
|
Subproject commit 0958be54482722821159cd3e07777988ee349f37
|
Loading…
Reference in a new issue