Merge pull request #1528 from ghalestrilo/feature/mobile-examples

Feature/mobile examples
This commit is contained in:
ghalestrilo 2020-08-17 18:00:11 -03:00 committed by GitHub
commit 127660ab4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 701 additions and 213 deletions

View file

@ -26,3 +26,4 @@ S3_BUCKET_URL_BASE=<alt-for-s3-url>
SESSION_SECRET=whatever_you_want_this_to_be_it_only_matters_for_production SESSION_SECRET=whatever_you_want_this_to_be_it_only_matters_for_production
UI_ACCESS_TOKEN_ENABLED=false UI_ACCESS_TOKEN_ENABLED=false
UPLOAD_LIMIT=250000000 UPLOAD_LIMIT=250000000
MOBILE_ENABLED=true

View file

@ -3,16 +3,22 @@ import styled from 'styled-components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { prop, remSize } from '../../theme'; import { prop, remSize } from '../../theme';
const background = transparent => prop(transparent ? 'backgroundColor' : 'MobilePanel.default.background');
const textColor = prop('primaryTextColor'); const background = ({ transparent, inverted }) => prop(transparent === true
? 'backgroundColor'
: `MobilePanel.default.${inverted === true ? 'foreground' : 'background'}`);
const textColor = ({ transparent, inverted }) => prop((transparent === false && inverted === true)
? 'MobilePanel.default.background'
: 'primaryTextColor');
const HeaderDiv = styled.div` const HeaderDiv = styled.div`
position: fixed; position: fixed;
width: 100%; width: 100%;
background: ${props => background(props.transparent === true)}; background: ${props => background(props)};
color: ${textColor}; color: ${textColor};
padding: ${remSize(12)}; padding: ${props => remSize(props.slim === true ? 2 : 12)};
padding-left: ${remSize(16)}; padding-left: ${remSize(16)};
padding-right: ${remSize(16)}; padding-right: ${remSize(16)};
z-index: 1; z-index: 1;
@ -25,8 +31,10 @@ const HeaderDiv = styled.div`
svg { svg {
max-height: ${remSize(32)}; max-height: ${remSize(32)};
padding: ${remSize(4)} padding: ${remSize(4)};
} }
& svg path { fill: ${textColor} !important; }
`; `;
const IconContainer = styled.div` const IconContainer = styled.div`
@ -48,9 +56,10 @@ const TitleContainer = styled.div`
`; `;
const Header = ({ const Header = ({
title, subtitle, leftButton, children, transparent title, subtitle, leftButton, children,
transparent, inverted, slim
}) => ( }) => (
<HeaderDiv transparent={transparent}> <HeaderDiv transparent={transparent} slim={slim} inverted={inverted}>
{leftButton} {leftButton}
<TitleContainer padded={subtitle === null}> <TitleContainer padded={subtitle === null}>
{title && <h2>{title}</h2>} {title && <h2>{title}</h2>}
@ -67,7 +76,9 @@ Header.propTypes = {
subtitle: PropTypes.string, subtitle: PropTypes.string,
leftButton: PropTypes.element, leftButton: PropTypes.element,
children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]), children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]),
transparent: PropTypes.bool transparent: PropTypes.bool,
inverted: PropTypes.bool,
slim: PropTypes.bool,
}; };
Header.defaultProps = { Header.defaultProps = {
@ -75,7 +86,9 @@ Header.defaultProps = {
subtitle: null, subtitle: null,
leftButton: null, leftButton: null,
children: [], children: [],
transparent: false transparent: false,
inverted: false,
slim: false
}; };
export default Header; export default Header;

View file

@ -0,0 +1,18 @@
import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router';
import { prop, remSize } from '../../theme';
export default styled(Link)`
box-sizing: border-box;
background: transparent;
/* border-top: ${remSize(4)} solid ${props => prop(props.selected ? 'colors.p5jsPink' : 'MobilePanel.default.background')}; */
border-top: ${remSize(4)} solid ${props => (props.selected ? prop('TabHighlight') : 'transparent')};
color: ${prop('primaryTextColor')};
padding: ${remSize(8)} ${remSize(16)};
width: 30%;
`;

View file

@ -0,0 +1,15 @@
import React from 'react';
import styled from 'styled-components';
import { prop, remSize } from '../../theme';
export default styled.div`
display: flex;
justify-content: space-between;
h3 { text-align: center; width: 100%; }
border-top: 1px solid ${prop('Separator')};
background: ${props => prop('backgroundColor')};
`;

View file

@ -17,6 +17,7 @@ export function getCollections(username) {
} else { } else {
url = '/collections'; url = '/collections';
} }
console.log(url);
apiClient.get(url) apiClient.get(url)
.then((response) => { .then((response) => {
dispatch({ dispatch({

View file

@ -21,6 +21,7 @@ import CollectionListRow from './CollectionListRow';
import ArrowUpIcon from '../../../../images/sort-arrow-up.svg'; import ArrowUpIcon from '../../../../images/sort-arrow-up.svg';
import ArrowDownIcon from '../../../../images/sort-arrow-down.svg'; import ArrowDownIcon from '../../../../images/sort-arrow-down.svg';
class CollectionList extends React.Component { class CollectionList extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -127,6 +128,7 @@ class CollectionList extends React.Component {
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;
const { mobile } = this.props;
return ( return (
<article className="sketches-table-container"> <article className="sketches-table-container">
@ -141,15 +143,16 @@ class CollectionList extends React.Component {
<thead> <thead>
<tr> <tr>
{this._renderFieldHeader('name', 'Name')} {this._renderFieldHeader('name', 'Name')}
{this._renderFieldHeader('createdAt', 'Date Created')} {this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)}
{this._renderFieldHeader('updatedAt', 'Date Updated')} {this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)}
{this._renderFieldHeader('numItems', '# sketches')} {this._renderFieldHeader('numItems', mobile ? 'Sketches' : '# sketches')}
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{this.props.collections.map(collection => {this.props.collections.map(collection =>
(<CollectionListRow (<CollectionListRow
mobile={mobile}
key={collection.id} key={collection.id}
collection={collection} collection={collection}
user={this.props.user} user={this.props.user}
@ -209,7 +212,8 @@ CollectionList.propTypes = {
owner: PropTypes.shape({ owner: PropTypes.shape({
id: PropTypes.string id: PropTypes.string
}) })
}) }),
mobile: PropTypes.bool,
}; };
CollectionList.defaultProps = { CollectionList.defaultProps = {
@ -218,7 +222,8 @@ CollectionList.defaultProps = {
id: undefined, id: undefined,
owner: undefined owner: undefined
}, },
username: undefined username: undefined,
mobile: false
}; };
function mapStateToProps(state, ownProps) { function mapStateToProps(state, ownProps) {

View file

@ -11,6 +11,8 @@ import * as ToastActions from '../../actions/toast';
import DownFilledTriangleIcon from '../../../../images/down-filled-triangle.svg'; import DownFilledTriangleIcon from '../../../../images/down-filled-triangle.svg';
const formatDateCell = (date, mobile = false) => format(new Date(date), 'MMM D, YYYY');
class CollectionListRowBase extends React.Component { class CollectionListRowBase extends React.Component {
static projectInCollection(project, collection) { static projectInCollection(project, collection) {
return collection.items.find(item => item.project.id === project.id) != null; return collection.items.find(item => item.project.id === project.id) != null;
@ -199,7 +201,7 @@ class CollectionListRowBase extends React.Component {
} }
render() { render() {
const { collection } = this.props; const { collection, mobile } = this.props;
return ( return (
<tr <tr
@ -211,9 +213,9 @@ class CollectionListRowBase extends React.Component {
{this.renderCollectionName()} {this.renderCollectionName()}
</span> </span>
</th> </th>
<td>{format(new Date(collection.createdAt), 'MMM D, YYYY')}</td> <td>{mobile && 'Created: '}{format(new Date(collection.createdAt), 'MMM D, YYYY')}</td>
<td>{format(new Date(collection.updatedAt), 'MMM D, YYYY')}</td> <td>{mobile && 'Updated: '}{formatDateCell(collection.updatedAt)}</td>
<td>{(collection.items || []).length}</td> <td>{mobile && '# sketches: '}{(collection.items || []).length}</td>
<td className="sketch-list__dropdown-column"> <td className="sketch-list__dropdown-column">
{this.renderActions()} {this.renderActions()}
</td> </td>
@ -245,6 +247,11 @@ CollectionListRowBase.propTypes = {
deleteCollection: PropTypes.func.isRequired, deleteCollection: PropTypes.func.isRequired,
editCollection: PropTypes.func.isRequired, editCollection: PropTypes.func.isRequired,
onAddSketches: PropTypes.func.isRequired, onAddSketches: PropTypes.func.isRequired,
mobile: PropTypes.bool,
};
CollectionListRowBase.defaultProps = {
mobile: false,
}; };
function mapDispatchToPropsSketchListRow(dispatch) { function mapDispatchToPropsSketchListRow(dispatch) {

View file

@ -5,13 +5,20 @@ import EditIcon from '../../../images/pencil.svg';
// TODO I think this needs a description prop so that it's accessible // TODO I think this needs a description prop so that it's accessible
function EditableInput({ function EditableInput({
validate, value, emptyPlaceholder, InputComponent, inputProps, onChange validate,
value,
emptyPlaceholder,
InputComponent,
inputProps,
onChange,
}) { }) {
const [isEditing, setIsEditing] = React.useState(false); const [isEditing, setIsEditing] = React.useState(false);
const [currentValue, setCurrentValue] = React.useState(value || ''); const [currentValue, setCurrentValue] = React.useState(value || '');
const displayValue = currentValue || emptyPlaceholder; const displayValue = currentValue || emptyPlaceholder;
const hasValue = currentValue !== ''; const hasValue = currentValue !== '';
const classes = `editable-input editable-input--${isEditing ? 'is-editing' : 'is-not-editing'} editable-input--${hasValue ? 'has-value' : 'has-placeholder'}`; const classes = `editable-input editable-input--${
isEditing ? 'is-editing' : 'is-not-editing'
} editable-input--${hasValue ? 'has-value' : 'has-placeholder'}`;
const inputRef = React.createRef(); const inputRef = React.createRef();
React.useEffect(() => { React.useEffect(() => {
@ -54,7 +61,11 @@ function EditableInput({
aria-label={`Edit ${displayValue} value`} aria-label={`Edit ${displayValue} value`}
> >
<span>{displayValue}</span> <span>{displayValue}</span>
<EditIcon className="editable-input__icon" focusable="false" aria-hidden="true" /> <EditIcon
className="editable-input__icon"
focusable="false"
aria-hidden="true"
/>
</button> </button>
<InputComponent <InputComponent
@ -68,7 +79,7 @@ function EditableInput({
ref={inputRef} ref={inputRef}
value={currentValue} value={currentValue}
/> />
</span > </span>
); );
} }
@ -84,7 +95,7 @@ EditableInput.propTypes = {
emptyPlaceholder: PropTypes.string, emptyPlaceholder: PropTypes.string,
InputComponent: PropTypes.elementType, InputComponent: PropTypes.elementType,
// eslint-disable-next-line react/forbid-prop-types // eslint-disable-next-line react/forbid-prop-types
inputProps: PropTypes.object, inputProps: PropTypes.object, // eslint-disable-line
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
validate: PropTypes.func, validate: PropTypes.func,
value: PropTypes.string, value: PropTypes.string,

View file

@ -16,7 +16,10 @@ class NewFileForm extends React.Component {
} }
render() { render() {
const { fields: { name }, handleSubmit } = this.props; const {
fields: { name },
handleSubmit,
} = this.props;
return ( return (
<form <form
className="new-file-form" className="new-file-form"
@ -26,7 +29,9 @@ class NewFileForm extends React.Component {
}} }}
> >
<div className="new-file-form__input-wrapper"> <div className="new-file-form__input-wrapper">
<label className="new-file-form__name-label" htmlFor="name">Name:</label> <label className="new-file-form__name-label" htmlFor="name">
Name:
</label>
<input <input
className="new-file-form__name-input" className="new-file-form__name-input"
id="name" id="name"
@ -34,14 +39,18 @@ class NewFileForm extends React.Component {
placeholder={this.props.t('NewFileForm.Placeholder')} placeholder={this.props.t('NewFileForm.Placeholder')}
maxLength="128" maxLength="128"
{...domOnlyProps(name)} {...domOnlyProps(name)}
ref={(element) => { this.fileName = element; }} ref={(element) => {
this.fileName = element;
}}
/> />
<Button <Button
type="submit" type="submit"
>{this.props.t('NewFileForm.AddFileSubmit')} >{this.props.t('NewFileForm.AddFileSubmit')}
</Button> </Button>
</div> </div>
{name.touched && name.error && <span className="form-error">{name.error}</span>} {name.touched && name.error && (
<span className="form-error">{name.error}</span>
)}
</form> </form>
); );
} }

View file

@ -18,7 +18,8 @@ class NewFolderForm extends React.Component {
render() { render() {
const { const {
fields: { name }, handleSubmit fields: { name },
handleSubmit,
} = this.props; } = this.props;
return ( return (
<form <form
@ -28,7 +29,9 @@ class NewFolderForm extends React.Component {
}} }}
> >
<div className="new-folder-form__input-wrapper"> <div className="new-folder-form__input-wrapper">
<label className="new-folder-form__name-label" htmlFor="name">Name:</label> <label className="new-folder-form__name-label" htmlFor="name">
Name:
</label>
<input <input
className="new-folder-form__name-input" className="new-folder-form__name-input"
id="name" id="name"
@ -43,7 +46,9 @@ class NewFolderForm extends React.Component {
>{this.props.t('NewFolderForm.AddFolderSubmit')} >{this.props.t('NewFolderForm.AddFolderSubmit')}
</Button> </Button>
</div> </div>
{name.touched && name.error && <span className="form-error">{name.error}</span>} {name.touched && name.error && (
<span className="form-error">{name.error}</span>
)}
</form> </form>
); );
} }
@ -62,6 +67,6 @@ NewFolderForm.propTypes = {
}; };
NewFolderForm.defaultProps = { NewFolderForm.defaultProps = {
submitting: false, submitting: false,
pristine: true pristine: true,
}; };
export default withTranslation()(NewFolderForm); export default withTranslation()(NewFolderForm);

View file

@ -22,6 +22,9 @@ import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg'; import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg';
const formatDateCell = (date, mobile = false) => format(new Date(date), mobile ? 'MMM D, YYYY' : 'MMM D, YYYY h:mm A');
class SketchListRowBase extends React.Component { class SketchListRowBase extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -251,6 +254,7 @@ class SketchListRowBase extends React.Component {
const { const {
sketch, sketch,
username, username,
mobile
} = this.props; } = this.props;
const { renameOpen, renameValue } = this.state; const { renameOpen, renameValue } = this.state;
let url = `/${username}/sketches/${sketch.id}`; let url = `/${username}/sketches/${sketch.id}`;
@ -258,6 +262,8 @@ class SketchListRowBase extends React.Component {
url = `/${username}/sketches/${slugify(sketch.name, '_')}`; url = `/${username}/sketches/${slugify(sketch.name, '_')}`;
} }
if (this.props.mobile) url = `/mobile${url}`;
const name = ( const name = (
<React.Fragment> <React.Fragment>
<Link to={url}> <Link to={url}>
@ -287,8 +293,8 @@ class SketchListRowBase extends React.Component {
<th scope="row"> <th scope="row">
{name} {name}
</th> </th>
<td>{format(new Date(sketch.createdAt), 'MMM D, YYYY h:mm A')}</td> <td>{mobile && 'Created: '}{formatDateCell(sketch.createdAt, mobile)}</td>
<td>{format(new Date(sketch.updatedAt), 'MMM D, YYYY h:mm A')}</td> <td>{mobile && 'Updated: '}{formatDateCell(sketch.updatedAt, mobile)}</td>
{this.renderDropdown()} {this.renderDropdown()}
</tr> </tr>
</React.Fragment>); </React.Fragment>);
@ -312,7 +318,12 @@ SketchListRowBase.propTypes = {
cloneProject: PropTypes.func.isRequired, cloneProject: PropTypes.func.isRequired,
exportProjectAsZip: PropTypes.func.isRequired, exportProjectAsZip: PropTypes.func.isRequired,
changeProjectName: PropTypes.func.isRequired, changeProjectName: PropTypes.func.isRequired,
onAddToCollection: PropTypes.func.isRequired onAddToCollection: PropTypes.func.isRequired,
mobile: PropTypes.bool
};
SketchListRowBase.defaultProps = {
mobile: false
}; };
function mapDispatchToPropsSketchListRow(dispatch) { function mapDispatchToPropsSketchListRow(dispatch) {
@ -413,6 +424,7 @@ class SketchList extends React.Component {
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;
const { mobile } = this.props;
return ( return (
<article className="sketches-table-container"> <article className="sketches-table-container">
<Helmet> <Helmet>
@ -425,14 +437,15 @@ class SketchList extends React.Component {
<thead> <thead>
<tr> <tr>
{this._renderFieldHeader('name', 'Sketch')} {this._renderFieldHeader('name', 'Sketch')}
{this._renderFieldHeader('createdAt', 'Date Created')} {this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)}
{this._renderFieldHeader('updatedAt', 'Date Updated')} {this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)}
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{this.props.sketches.map(sketch => {this.props.sketches.map(sketch =>
(<SketchListRow (<SketchListRow
mobile={mobile}
key={sketch.id} key={sketch.id}
sketch={sketch} sketch={sketch}
user={this.props.user} user={this.props.user}
@ -482,10 +495,12 @@ SketchList.propTypes = {
field: PropTypes.string.isRequired, field: PropTypes.string.isRequired,
direction: PropTypes.string.isRequired direction: PropTypes.string.isRequired
}).isRequired, }).isRequired,
mobile: PropTypes.bool,
}; };
SketchList.defaultProps = { SketchList.defaultProps = {
username: undefined username: undefined,
mobile: false,
}; };
function mapStateToProps(state) { function mapStateToProps(state) {

View file

@ -45,13 +45,22 @@ function isUserOwner(props) {
return props.project.owner && props.project.owner.id === props.user.id; return props.project.owner && props.project.owner.id === props.user.id;
} }
function warnIfUnsavedChanges(props) { // eslint-disable-line function warnIfUnsavedChanges(props) {
// eslint-disable-line
const { route } = props.route; const { route } = props.route;
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) { if (
route &&
route.action === 'PUSH' &&
(route.pathname === '/login' || route.pathname === '/signup')
) {
// don't warn // don't warn
props.persistState(); props.persistState();
window.onbeforeunload = null; window.onbeforeunload = null;
} else if (route && (props.location.pathname === '/login' || props.location.pathname === '/signup')) { } else if (
route &&
(props.location.pathname === '/login' ||
props.location.pathname === '/signup')
) {
// don't warn // don't warn
props.persistState(); props.persistState();
window.onbeforeunload = null; window.onbeforeunload = null;
@ -62,6 +71,7 @@ function warnIfUnsavedChanges(props) { // eslint-disable-line
props.setUnsavedChanges(false); props.setUnsavedChanges(false);
return true; return true;
} }
return true;
} }
class IDEView extends React.Component { class IDEView extends React.Component {
@ -71,7 +81,7 @@ class IDEView extends React.Component {
this.state = { this.state = {
consoleSize: props.ide.consoleIsExpanded ? 150 : 29, consoleSize: props.ide.consoleIsExpanded ? 150 : 29,
sidebarSize: props.ide.sidebarIsExpanded ? 160 : 20 sidebarSize: props.ide.sidebarIsExpanded ? 160 : 20,
}; };
} }
@ -91,7 +101,10 @@ class IDEView extends React.Component {
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1; this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
document.addEventListener('keydown', this.handleGlobalKeydown, false); document.addEventListener('keydown', this.handleGlobalKeydown, false);
this.props.router.setRouteLeaveHook(this.props.route, this.handleUnsavedChanges); this.props.router.setRouteLeaveHook(
this.props.route,
this.handleUnsavedChanges
);
window.onbeforeunload = this.handleUnsavedChanges; window.onbeforeunload = this.handleUnsavedChanges;
@ -104,11 +117,15 @@ class IDEView extends React.Component {
} }
if (this.props.ide.consoleIsExpanded !== nextProps.ide.consoleIsExpanded) { if (this.props.ide.consoleIsExpanded !== nextProps.ide.consoleIsExpanded) {
this.setState({ consoleSize: nextProps.ide.consoleIsExpanded ? 150 : 29 }); this.setState({
consoleSize: nextProps.ide.consoleIsExpanded ? 150 : 29,
});
} }
if (this.props.ide.sidebarIsExpanded !== nextProps.ide.sidebarIsExpanded) { if (this.props.ide.sidebarIsExpanded !== nextProps.ide.sidebarIsExpanded) {
this.setState({ sidebarSize: nextProps.ide.sidebarIsExpanded ? 160 : 20 }); this.setState({
sidebarSize: nextProps.ide.sidebarIsExpanded ? 160 : 20,
});
} }
} }
@ -122,10 +139,15 @@ class IDEView extends React.Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (isUserOwner(this.props) && this.props.project.id) { if (isUserOwner(this.props) && 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 ( if (
this.props.selectedFile.name === prevProps.selectedFile.name && this.props.selectedFile.name === prevProps.selectedFile.name &&
this.props.selectedFile.content !== prevProps.selectedFile.content) { this.props.selectedFile.content !== prevProps.selectedFile.content
) {
if (this.autosaveInterval) { if (this.autosaveInterval) {
clearTimeout(this.autosaveInterval); clearTimeout(this.autosaveInterval);
} }
@ -142,7 +164,8 @@ class IDEView extends React.Component {
} }
if (this.props.route.path !== prevProps.route.path) { if (this.props.route.path !== prevProps.route.path) {
this.props.router.setRouteLeaveHook(this.props.route, () => warnIfUnsavedChanges(this.props)); this.props.router.setRouteLeaveHook(this.props.route, () =>
warnIfUnsavedChanges(this.props));
} }
} }
componentWillUnmount() { componentWillUnmount() {
@ -152,10 +175,16 @@ class IDEView extends React.Component {
} }
handleGlobalKeydown(e) { handleGlobalKeydown(e) {
// 83 === s // 83 === s
if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) { if (
e.keyCode === 83 &&
((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))
) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (isUserOwner(this.props) || (this.props.user.authenticated && !this.props.project.owner)) { if (
isUserOwner(this.props) ||
(this.props.user.authenticated && !this.props.project.owner)
) {
this.props.saveProject(this.cmController.getContent()); this.props.saveProject(this.cmController.getContent());
} else if (this.props.user.authenticated) { } else if (this.props.user.authenticated) {
this.props.cloneProject(); this.props.cloneProject();
@ -163,23 +192,41 @@ class IDEView extends React.Component {
this.props.showErrorModal('forceAuthentication'); this.props.showErrorModal('forceAuthentication');
} }
// 13 === enter // 13 === enter
} else if (e.keyCode === 13 && e.shiftKey && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) { } else if (
e.keyCode === 13 &&
e.shiftKey &&
((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))
) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.props.stopSketch(); this.props.stopSketch();
} else if (e.keyCode === 13 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) { } else if (
e.keyCode === 13 &&
((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))
) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.props.startSketch(); this.props.startSketch();
// 50 === 2 // 50 === 2
} 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.setAllAccessibleOutput(false); this.props.setAllAccessibleOutput(false);
// 49 === 1 // 49 === 1
} 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();
this.props.setAllAccessibleOutput(true); this.props.setAllAccessibleOutput(true);
} else if (e.keyCode === 66 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) { } else if (
e.keyCode === 66 &&
((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))
) {
e.preventDefault(); e.preventDefault();
if (!this.props.ide.sidebarIsExpanded) { if (!this.props.ide.sidebarIsExpanded) {
this.props.expandSidebar(); this.props.expandSidebar();
@ -218,7 +265,7 @@ class IDEView extends React.Component {
cmController={this.cmController} cmController={this.cmController}
/> />
<Toolbar key={this.props.project.id} /> <Toolbar key={this.props.project.id} />
{this.props.ide.preferencesIsVisible && {this.props.ide.preferencesIsVisible && (
<Overlay <Overlay
title={this.props.t('Preferences.Settings')} title={this.props.t('Preferences.Settings')}
ariaLabel={this.props.t('Preferences.Settings')} ariaLabel={this.props.t('Preferences.Settings')}
@ -245,7 +292,7 @@ class IDEView extends React.Component {
setTheme={this.props.setTheme} setTheme={this.props.setTheme}
/> />
</Overlay> </Overlay>
} )}
<main className="editor-preview-container"> <main className="editor-preview-container">
<SplitPane <SplitPane
split="vertical" split="vertical"
@ -274,10 +321,17 @@ class IDEView extends React.Component {
<SplitPane <SplitPane
split="vertical" split="vertical"
defaultSize="50%" defaultSize="50%"
onChange={() => { this.overlay.style.display = 'block'; }} onChange={() => {
onDragFinished={() => { this.overlay.style.display = 'none'; }} this.overlay.style.display = 'block';
}}
onDragFinished={() => {
this.overlay.style.display = 'none';
}}
resizerStyle={{ resizerStyle={{
borderLeftWidth: '2px', borderRightWidth: '2px', width: '2px', margin: '0px 0px' borderLeftWidth: '2px',
borderRightWidth: '2px',
width: '2px',
margin: '0px 0px',
}} }}
> >
<SplitPane <SplitPane
@ -303,7 +357,9 @@ class IDEView extends React.Component {
editorOptionsVisible={this.props.ide.editorOptionsVisible} editorOptionsVisible={this.props.ide.editorOptionsVisible}
showEditorOptions={this.props.showEditorOptions} showEditorOptions={this.props.showEditorOptions}
closeEditorOptions={this.props.closeEditorOptions} closeEditorOptions={this.props.closeEditorOptions}
showKeyboardShortcutModal={this.props.showKeyboardShortcutModal} showKeyboardShortcutModal={
this.props.showKeyboardShortcutModal
}
setUnsavedChanges={this.props.setUnsavedChanges} setUnsavedChanges={this.props.setUnsavedChanges}
isPlaying={this.props.ide.isPlaying} isPlaying={this.props.ide.isPlaying}
theme={this.props.preferences.theme} theme={this.props.preferences.theme}
@ -320,8 +376,12 @@ class IDEView extends React.Component {
consoleEvents={this.props.console} consoleEvents={this.props.console}
showRuntimeErrorWarning={this.props.showRuntimeErrorWarning} showRuntimeErrorWarning={this.props.showRuntimeErrorWarning}
hideRuntimeErrorWarning={this.props.hideRuntimeErrorWarning} hideRuntimeErrorWarning={this.props.hideRuntimeErrorWarning}
runtimeErrorWarningVisible={this.props.ide.runtimeErrorWarningVisible} runtimeErrorWarningVisible={
provideController={(ctl) => { this.cmController = ctl; }} this.props.ide.runtimeErrorWarningVisible
}
provideController={(ctl) => {
this.cmController = ctl;
}}
/> />
<Console /> <Console />
</SplitPane> </SplitPane>
@ -330,27 +390,28 @@ class IDEView extends React.Component {
<h2 className="preview-frame__title">{this.props.t('Toolbar.Preview')}</h2> <h2 className="preview-frame__title">{this.props.t('Toolbar.Preview')}</h2>
</header> </header>
<div className="preview-frame__content"> <div className="preview-frame__content">
<div className="preview-frame-overlay" ref={(element) => { this.overlay = element; }}> <div
className="preview-frame-overlay"
ref={(element) => {
this.overlay = element;
}}
>
</div> </div>
<div> <div>
{( {((this.props.preferences.textOutput ||
(
(this.props.preferences.textOutput ||
this.props.preferences.gridOutput || this.props.preferences.gridOutput ||
this.props.preferences.soundOutput this.props.preferences.soundOutput) &&
) && this.props.ide.isPlaying) ||
this.props.ide.isPlaying this.props.ide.isAccessibleOutputPlaying}
) ||
this.props.ide.isAccessibleOutputPlaying
)
}
</div> </div>
<PreviewFrame <PreviewFrame
htmlFile={this.props.htmlFile} htmlFile={this.props.htmlFile}
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}
isAccessibleOutputPlaying={this.props.ide.isAccessibleOutputPlaying} isAccessibleOutputPlaying={
this.props.ide.isAccessibleOutputPlaying
}
textOutput={this.props.preferences.textOutput} textOutput={this.props.preferences.textOutput}
gridOutput={this.props.preferences.gridOutput} gridOutput={this.props.preferences.gridOutput}
soundOutput={this.props.preferences.soundOutput} soundOutput={this.props.preferences.soundOutput}
@ -373,21 +434,17 @@ class IDEView extends React.Component {
</SplitPane> </SplitPane>
</SplitPane> </SplitPane>
</main> </main>
{ this.props.ide.modalIsVisible && {this.props.ide.modalIsVisible && <NewFileModal />}
<NewFileModal /> {this.props.ide.newFolderModalVisible && (
}
{this.props.ide.newFolderModalVisible &&
<NewFolderModal <NewFolderModal
closeModal={this.props.closeNewFolderModal} closeModal={this.props.closeNewFolderModal}
createFolder={this.props.createFolder} createFolder={this.props.createFolder}
/> />
} )}
{this.props.ide.uploadFileModalVisible && {this.props.ide.uploadFileModalVisible && (
<UploadFileModal <UploadFileModal closeModal={this.props.closeUploadFileModal} />
closeModal={this.props.closeUploadFileModal} )}
/> {this.props.location.pathname === '/about' && (
}
{ this.props.location.pathname === '/about' &&
<Overlay <Overlay
title={this.props.t('About.Title')} title={this.props.t('About.Title')}
previousPath={this.props.ide.previousPath} previousPath={this.props.ide.previousPath}
@ -395,8 +452,8 @@ class IDEView extends React.Component {
> >
<About previousPath={this.props.ide.previousPath} /> <About previousPath={this.props.ide.previousPath} />
</Overlay> </Overlay>
} )}
{this.props.location.pathname === '/feedback' && {this.props.location.pathname === '/feedback' && (
<Overlay <Overlay
title={this.props.t('IDEView.SubmitFeedback')} title={this.props.t('IDEView.SubmitFeedback')}
previousPath={this.props.ide.previousPath} previousPath={this.props.ide.previousPath}
@ -404,8 +461,8 @@ class IDEView extends React.Component {
> >
<Feedback previousPath={this.props.ide.previousPath} /> <Feedback previousPath={this.props.ide.previousPath} />
</Overlay> </Overlay>
} )}
{this.props.location.pathname.match(/add-to-collection$/) && {this.props.location.pathname.match(/add-to-collection$/) && (
<Overlay <Overlay
ariaLabel="add to collection" ariaLabel="add to collection"
title="Add to collection" title="Add to collection"
@ -419,8 +476,8 @@ class IDEView extends React.Component {
user={this.props.user} user={this.props.user}
/> />
</Overlay> </Overlay>
} )}
{this.props.ide.shareModalVisible && {this.props.ide.shareModalVisible && (
<Overlay <Overlay
title="Share" title="Share"
ariaLabel="share" ariaLabel="share"
@ -432,8 +489,8 @@ class IDEView extends React.Component {
ownerUsername={this.props.ide.shareModalProjectUsername} ownerUsername={this.props.ide.shareModalProjectUsername}
/> />
</Overlay> </Overlay>
} )}
{this.props.ide.keyboardShortcutVisible && {this.props.ide.keyboardShortcutVisible && (
<Overlay <Overlay
title={this.props.t('KeyboardShortcuts.Title')} title={this.props.t('KeyboardShortcuts.Title')}
ariaLabel={this.props.t('KeyboardShortcuts.Title')} ariaLabel={this.props.t('KeyboardShortcuts.Title')}
@ -441,8 +498,8 @@ class IDEView extends React.Component {
> >
<KeyboardShortcutModal /> <KeyboardShortcutModal />
</Overlay> </Overlay>
} )}
{this.props.ide.errorType && {this.props.ide.errorType && (
<Overlay <Overlay
title="Error" title="Error"
ariaLabel={this.props.t('Common.Error')} ariaLabel={this.props.t('Common.Error')}
@ -453,7 +510,7 @@ class IDEView extends React.Component {
closeModal={this.props.hideErrorModal} closeModal={this.props.hideErrorModal}
/> />
</Overlay> </Overlay>
} )}
</div> </div>
); );
} }
@ -466,19 +523,19 @@ IDEView.propTypes = {
reset_password_token: PropTypes.string, reset_password_token: PropTypes.string,
}).isRequired, }).isRequired,
location: PropTypes.shape({ location: PropTypes.shape({
pathname: PropTypes.string pathname: PropTypes.string,
}).isRequired, }).isRequired,
getProject: PropTypes.func.isRequired, getProject: PropTypes.func.isRequired,
user: PropTypes.shape({ user: PropTypes.shape({
authenticated: PropTypes.bool.isRequired, authenticated: PropTypes.bool.isRequired,
id: PropTypes.string, id: PropTypes.string,
username: PropTypes.string username: PropTypes.string,
}).isRequired, }).isRequired,
saveProject: PropTypes.func.isRequired, saveProject: PropTypes.func.isRequired,
ide: PropTypes.shape({ ide: PropTypes.shape({
isPlaying: PropTypes.bool.isRequired, isPlaying: PropTypes.bool.isRequired,
isAccessibleOutputPlaying: PropTypes.bool.isRequired, isAccessibleOutputPlaying: PropTypes.bool.isRequired,
consoleEvent: PropTypes.array, consoleEvent: PropTypes.array, // eslint-disable-line
modalIsVisible: PropTypes.bool.isRequired, modalIsVisible: PropTypes.bool.isRequired,
sidebarIsExpanded: PropTypes.bool.isRequired, sidebarIsExpanded: PropTypes.bool.isRequired,
consoleIsExpanded: PropTypes.bool.isRequired, consoleIsExpanded: PropTypes.bool.isRequired,
@ -500,7 +557,7 @@ IDEView.propTypes = {
justOpenedProject: PropTypes.bool.isRequired, justOpenedProject: PropTypes.bool.isRequired,
errorType: PropTypes.string, errorType: PropTypes.string,
runtimeErrorWarningVisible: PropTypes.bool.isRequired, runtimeErrorWarningVisible: PropTypes.bool.isRequired,
uploadFileModalVisible: PropTypes.bool.isRequired uploadFileModalVisible: PropTypes.bool.isRequired,
}).isRequired, }).isRequired,
stopSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired,
project: PropTypes.shape({ project: PropTypes.shape({
@ -508,12 +565,12 @@ IDEView.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
owner: PropTypes.shape({ owner: PropTypes.shape({
username: PropTypes.string, username: PropTypes.string,
id: PropTypes.string id: PropTypes.string,
}), }),
updatedAt: PropTypes.string updatedAt: PropTypes.string,
}).isRequired, }).isRequired,
editorAccessibility: PropTypes.shape({ editorAccessibility: PropTypes.shape({
lintMessages: PropTypes.array.isRequired, lintMessages: PropTypes.array.isRequired, // eslint-disable-line
}).isRequired, }).isRequired,
updateLintMessage: PropTypes.func.isRequired, updateLintMessage: PropTypes.func.isRequired,
clearLintMessage: PropTypes.func.isRequired, clearLintMessage: PropTypes.func.isRequired,
@ -543,19 +600,19 @@ IDEView.propTypes = {
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,
content: PropTypes.string.isRequired content: PropTypes.string.isRequired,
})).isRequired, })).isRequired,
updateFileContent: PropTypes.func.isRequired, updateFileContent: PropTypes.func.isRequired,
selectedFile: PropTypes.shape({ selectedFile: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired, content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired name: PropTypes.string.isRequired,
}).isRequired, }).isRequired,
setSelectedFile: PropTypes.func.isRequired, setSelectedFile: PropTypes.func.isRequired,
htmlFile: PropTypes.shape({ htmlFile: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired content: PropTypes.string.isRequired,
}).isRequired, }).isRequired,
dispatchConsoleEvent: PropTypes.func.isRequired, dispatchConsoleEvent: PropTypes.func.isRequired,
newFile: PropTypes.func.isRequired, newFile: PropTypes.func.isRequired,
@ -578,11 +635,11 @@ IDEView.propTypes = {
showKeyboardShortcutModal: PropTypes.func.isRequired, showKeyboardShortcutModal: PropTypes.func.isRequired,
closeKeyboardShortcutModal: PropTypes.func.isRequired, closeKeyboardShortcutModal: PropTypes.func.isRequired,
toast: PropTypes.shape({ toast: PropTypes.shape({
isVisible: PropTypes.bool.isRequired isVisible: PropTypes.bool.isRequired,
}).isRequired, }).isRequired,
autosaveProject: PropTypes.func.isRequired, autosaveProject: PropTypes.func.isRequired,
router: PropTypes.shape({ router: PropTypes.shape({
setRouteLeaveHook: PropTypes.func setRouteLeaveHook: PropTypes.func,
}).isRequired, }).isRequired,
route: PropTypes.oneOfType([PropTypes.object, PropTypes.element]).isRequired, route: PropTypes.oneOfType([PropTypes.object, PropTypes.element]).isRequired,
setUnsavedChanges: PropTypes.func.isRequired, setUnsavedChanges: PropTypes.func.isRequired,
@ -593,7 +650,7 @@ IDEView.propTypes = {
setPreviousPath: PropTypes.func.isRequired, setPreviousPath: PropTypes.func.isRequired,
console: PropTypes.arrayOf(PropTypes.shape({ console: PropTypes.arrayOf(PropTypes.shape({
method: PropTypes.string.isRequired, method: PropTypes.string.isRequired,
args: PropTypes.arrayOf(PropTypes.string) args: PropTypes.arrayOf(PropTypes.string),
})).isRequired, })).isRequired,
clearConsole: PropTypes.func.isRequired, clearConsole: PropTypes.func.isRequired,
showErrorModal: PropTypes.func.isRequired, showErrorModal: PropTypes.func.isRequired,
@ -604,13 +661,14 @@ IDEView.propTypes = {
startSketch: PropTypes.func.isRequired, startSketch: PropTypes.func.isRequired,
openUploadFileModal: PropTypes.func.isRequired, openUploadFileModal: PropTypes.func.isRequired,
closeUploadFileModal: PropTypes.func.isRequired, closeUploadFileModal: PropTypes.func.isRequired,
t: PropTypes.func.isRequired t: PropTypes.func.isRequired,
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
files: state.files, files: state.files,
selectedFile: state.files.find(file => file.isSelectedFile) || selectedFile:
state.files.find(file => file.isSelectedFile) ||
state.files.find(file => file.name === 'sketch.js') || state.files.find(file => file.name === 'sketch.js') ||
state.files.find(file => file.name !== 'root'), state.files.find(file => file.name !== 'root'),
htmlFile: getHTMLFile(state.files), htmlFile: getHTMLFile(state.files),
@ -620,7 +678,7 @@ function mapStateToProps(state) {
user: state.user, user: state.user,
project: state.project, project: state.project,
toast: state.toast, toast: state.toast,
console: state.console console: state.console,
}; };
} }

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
@ -20,7 +20,7 @@ import { getHTMLFile } from '../reducers/files';
// Local Imports // Local Imports
import Editor from '../components/Editor'; import Editor from '../components/Editor';
import { PlayIcon, ExitIcon, MoreIcon } from '../../../common/icons'; import { PlayIcon, MoreIcon } from '../../../common/icons';
import IconButton from '../../../components/mobile/IconButton'; import IconButton from '../../../components/mobile/IconButton';
import Header from '../../../components/mobile/Header'; import Header from '../../../components/mobile/Header';
@ -35,8 +35,8 @@ import useAsModal from '../../../components/useAsModal';
import { PreferencesIcon } from '../../../common/icons'; import { PreferencesIcon } from '../../../common/icons';
import Dropdown from '../../../components/Dropdown'; import Dropdown from '../../../components/Dropdown';
const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id); const isUserOwner = ({ project, user }) =>
project.owner && project.owner.id === user.id;
const Expander = styled.div` const Expander = styled.div`
height: ${props => (props.expanded ? remSize(160) : remSize(27))}; height: ${props => (props.expanded ? remSize(160) : remSize(27))};
@ -46,35 +46,61 @@ const NavItem = styled.li`
position: relative; position: relative;
`; `;
const headerNavOptions = [ const getNatOptions = (username = undefined) =>
(username
? [
{ icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', }, { icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', },
{ icon: PreferencesIcon, title: 'Examples', href: '/mobile/examples' }, { icon: PreferencesIcon, title: 'My Stuff', href: `/mobile/${username}/sketches` },
{ icon: PreferencesIcon, title: 'Examples', href: '/mobile/p5/sketches' },
{ icon: PreferencesIcon, title: 'Original Editor', href: '/', }, { icon: PreferencesIcon, title: 'Original Editor', href: '/', },
]; ]
: [
{ icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', },
{ icon: PreferencesIcon, title: 'Examples', href: '/mobile/p5/sketches' },
{ icon: PreferencesIcon, title: 'Original Editor', href: '/', },
]
);
const MobileIDEView = (props) => { const MobileIDEView = (props) => {
const { const {
preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage, preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage,
selectedFile, updateFileContent, files, selectedFile, updateFileContent, files, user, params,
closeEditorOptions, showEditorOptions, closeEditorOptions, showEditorOptions,
startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console, startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch, getProject, clearPersistedState
} = props; } = props;
const [tmController, setTmController] = useState(null); // eslint-disable-line const [tmController, setTmController] = useState(null); // eslint-disable-line
const { username } = user;
const [triggerNavDropdown, NavDropDown] = useAsModal(<Dropdown
items={getNatOptions(username)}
align="right"
/>);
// Force state reset
useEffect(clearPersistedState, []);
useEffect(stopSketch, []);
// Load Project
const [currentProjectID, setCurrentProjectID] = useState(null);
useEffect(() => {
if (!username) return;
if (params.project_id && !currentProjectID) {
if (params.project_id !== project.id) {
getProject(params.project_id, params.username);
}
}
setCurrentProjectID(params.project_id);
}, [params, project, username]);
const [triggerNavDropdown, NavDropDown] = useAsModal(<Dropdown align="right" items={headerNavOptions} />);
return ( return (
<Screen fullscreen> <Screen fullscreen>
<Header <Header
title={project.name} title={project.name}
subtitle={selectedFile.name} subtitle={selectedFile.name}
leftButton={
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
}
> >
<NavItem> <NavItem>
<IconButton <IconButton
@ -125,7 +151,11 @@ const MobileIDEView = (props) => {
</IDEWrapper> </IDEWrapper>
<Footer> <Footer>
{ide.consoleIsExpanded && <Expander expanded><Console /></Expander>} {ide.consoleIsExpanded && (
<Expander expanded>
<Console />
</Expander>
)}
<ActionStrip /> <ActionStrip />
</Footer> </Footer>
</Screen> </Screen>
@ -133,7 +163,6 @@ const MobileIDEView = (props) => {
}; };
MobileIDEView.propTypes = { MobileIDEView.propTypes = {
preferences: PropTypes.shape({ preferences: PropTypes.shape({
fontSize: PropTypes.number.isRequired, fontSize: PropTypes.number.isRequired,
autosave: PropTypes.bool.isRequired, autosave: PropTypes.bool.isRequired,
@ -144,13 +173,13 @@ MobileIDEView.propTypes = {
gridOutput: PropTypes.bool.isRequired, gridOutput: PropTypes.bool.isRequired,
soundOutput: 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,
ide: PropTypes.shape({ ide: PropTypes.shape({
isPlaying: PropTypes.bool.isRequired, isPlaying: PropTypes.bool.isRequired,
isAccessibleOutputPlaying: PropTypes.bool.isRequired, isAccessibleOutputPlaying: PropTypes.bool.isRequired,
consoleEvent: PropTypes.arrayOf(PropTypes.shape({})), consoleEvent: PropTypes.array,
modalIsVisible: PropTypes.bool.isRequired, modalIsVisible: PropTypes.bool.isRequired,
sidebarIsExpanded: PropTypes.bool.isRequired, sidebarIsExpanded: PropTypes.bool.isRequired,
consoleIsExpanded: PropTypes.bool.isRequired, consoleIsExpanded: PropTypes.bool.isRequired,
@ -172,11 +201,11 @@ MobileIDEView.propTypes = {
justOpenedProject: PropTypes.bool.isRequired, justOpenedProject: PropTypes.bool.isRequired,
errorType: PropTypes.string, errorType: PropTypes.string,
runtimeErrorWarningVisible: PropTypes.bool.isRequired, runtimeErrorWarningVisible: PropTypes.bool.isRequired,
uploadFileModalVisible: PropTypes.bool.isRequired uploadFileModalVisible: PropTypes.bool.isRequired,
}).isRequired, }).isRequired,
editorAccessibility: PropTypes.shape({ editorAccessibility: PropTypes.shape({
lintMessages: PropTypes.arrayOf(PropTypes.shape({})).isRequired, lintMessages: PropTypes.array.isRequired,
}).isRequired, }).isRequired,
project: PropTypes.shape({ project: PropTypes.shape({
@ -184,9 +213,9 @@ MobileIDEView.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
owner: PropTypes.shape({ owner: PropTypes.shape({
username: PropTypes.string, username: PropTypes.string,
id: PropTypes.string id: PropTypes.string,
}), }),
updatedAt: PropTypes.string updatedAt: PropTypes.string,
}).isRequired, }).isRequired,
startSketch: PropTypes.func.isRequired, startSketch: PropTypes.func.isRequired,
@ -198,7 +227,7 @@ MobileIDEView.propTypes = {
selectedFile: PropTypes.shape({ selectedFile: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired, content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired name: PropTypes.string.isRequired,
}).isRequired, }).isRequired,
updateFileContent: PropTypes.func.isRequired, updateFileContent: PropTypes.func.isRequired,
@ -206,7 +235,7 @@ MobileIDEView.propTypes = {
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,
content: PropTypes.string.isRequired content: PropTypes.string.isRequired,
})).isRequired, })).isRequired,
closeEditorOptions: PropTypes.func.isRequired, closeEditorOptions: PropTypes.func.isRequired,
@ -225,7 +254,7 @@ MobileIDEView.propTypes = {
console: PropTypes.arrayOf(PropTypes.shape({ console: PropTypes.arrayOf(PropTypes.shape({
method: PropTypes.string.isRequired, method: PropTypes.string.isRequired,
args: PropTypes.arrayOf(PropTypes.string) args: PropTypes.arrayOf(PropTypes.string),
})).isRequired, })).isRequired,
showRuntimeErrorWarning: PropTypes.func.isRequired, showRuntimeErrorWarning: PropTypes.func.isRequired,
@ -235,15 +264,22 @@ MobileIDEView.propTypes = {
user: PropTypes.shape({ user: PropTypes.shape({
authenticated: PropTypes.bool.isRequired, authenticated: PropTypes.bool.isRequired,
id: PropTypes.string, id: PropTypes.string,
username: PropTypes.string,
}).isRequired,
getProject: PropTypes.func.isRequired,
clearPersistedState: PropTypes.func.isRequired,
params: PropTypes.shape({
project_id: PropTypes.string,
username: PropTypes.string username: PropTypes.string
}).isRequired, }).isRequired,
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
files: state.files, files: state.files,
selectedFile: state.files.find(file => file.isSelectedFile) || selectedFile:
state.files.find(file => file.isSelectedFile) ||
state.files.find(file => file.name === 'sketch.js') || state.files.find(file => file.name === 'sketch.js') ||
state.files.find(file => file.name !== 'root'), state.files.find(file => file.name !== 'root'),
htmlFile: getHTMLFile(state.files), htmlFile: getHTMLFile(state.files),
@ -253,7 +289,7 @@ function mapStateToProps(state) {
user: state.user, user: state.user,
project: state.project, project: state.project,
toast: state.toast, toast: state.toast,
console: state.console console: state.console,
}; };
} }
@ -274,5 +310,4 @@ function mapDispatchToProps(dispatch) {
); );
} }
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView)); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView));

View file

@ -0,0 +1,229 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { useSelector } from 'react-redux';
import { withRouter } from 'react-router';
import Screen from '../../components/mobile/MobileScreen';
import Header from '../../components/mobile/Header';
import IconButton from '../../components/mobile/IconButton';
import { ExitIcon, MoreIcon } from '../../common/icons';
import Footer from '../../components/mobile/Footer';
import { remSize, prop } from '../../theme';
import SketchList from '../IDE/components/SketchList';
import CollectionList from '../IDE/components/CollectionList';
import AssetList from '../IDE/components/AssetList';
import Content from './MobileViewContent';
import { SketchSearchbar, CollectionSearchbar } from '../IDE/components/Searchbar';
import Button from '../../common/Button';
import useAsModal from '../../components/useAsModal';
import Dropdown from '../../components/Dropdown';
import FooterTabSwitcher from '../../components/mobile/TabSwitcher';
import FooterTab from '../../components/mobile/Tab';
import Loader from '../App/components/loader';
const EXAMPLE_USERNAME = 'p5';
// @ghalestrilo 08/13/2020: I'm sorry
const ContentWrapper = styled(Content)`
table {
table-layout: fixed;
margin-bottom: ${remSize(120)}
}
td ,thead button {
font-size: ${remSize(10)};
text-align: left;
};
tbody th {
font-size: ${remSize(16)};
width: 100%;
padding-right: ${remSize(12)};
font-weight: bold;
display: flex;
grid-area: name;
};
tbody td, thead th {
justify-self: center;
align-self: flex-end;
color: ${prop('primaryTextColor')}
}
thead th svg { margin-left: ${remSize(8)} }
tbody td { justify-self: start; text-align: start; padding: 0 }
tbody td:nth-child(2) { justify-self: start; text-align: start; padding-left: ${remSize(12)}};
tbody td:last-child {
justify-self: end;
text-align: end;
grid-row-start: 1;
grid-column-start: 3;
};
.sketch-list__dropdown-column { width: auto; };
tbody { height: ${remSize(48)}; }
.sketches-table-container {
background: ${prop('SketchList.background')};
}
.sketches-table__row {
background: ${prop('SketchList.card.background')} !important;
height: auto
}
tr {
align-self: start;
display: grid;
box-shadow: 0 0 18px 0 ${prop('shadowColor')};
};
thead tr {
grid-template-columns: repeat(${props => props.fieldcount}, 1fr) 0fr;
${props => props.noheader && 'display: none;'}
}
tbody tr {
padding: ${remSize(8)};
border-radius: ${remSize(4)};
grid-template-columns: repeat(${props => props.fieldcount - 1}) 1fr;
grid-template-areas: "name name name" "content content content";
grid-row-gap: ${remSize(12)}
}
.loader-container { position: fixed ; padding-bottom: 32% }
.sketches-table thead th {
background-color: transparent;
}
.asset-table thead th {
height: initial;
align-self: center;
}
.asset-table thead tr {
height: ${remSize(32)}
}
`;
const Subheader = styled.div`
display: flex;
flex-direction: row;
* { border-radius: 0px; }
.searchbar {
display: flex;
width: 100%;
}
.searchbar__input { width: 100%; }
`;
const SubheaderButton = styled(Button)`
border-radius: 0px !important;
`;
const Panels = {
sketches: SketchList,
collections: CollectionList,
assets: AssetList
};
const navOptions = username => [
{ title: 'Create Sketch', href: '/mobile' },
{ title: 'Create Collection', href: `/mobile/${username}/collections/create` }
];
const getPanel = (pathname) => {
const pathparts = pathname ? pathname.split('/') : [];
const matches = Object.keys(Panels).map(part => part.toLowerCase()).filter(part => pathparts.includes(part));
return matches && matches.length > 0 && matches[0];
};
const NavItem = styled.li`
position: relative;
`;
const isOwner = (user, params) => user && params && user.username === params.username;
const renderPanel = (name, props) => (Component => (Component && <Component {...props} mobile />))(Panels[name]);
const MobileDashboard = ({ params, location }) => {
const user = useSelector(state => state.user);
const { username: paramsUsername } = params;
const { pathname } = location;
const Tabs = Object.keys(Panels);
const isExamples = paramsUsername === EXAMPLE_USERNAME;
const panel = getPanel(pathname);
const [toggleNavDropdown, NavDropdown] = useAsModal(<Dropdown
items={navOptions(user.username)}
align="right"
/>);
return (
<Screen fullscreen key={pathname}>
<Header slim inverted title={isExamples ? 'Examples' : 'My Stuff'}>
<NavItem>
<IconButton
onClick={toggleNavDropdown}
icon={MoreIcon}
aria-label="Options"
/>
<NavDropdown />
</NavItem>
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to ide view" />
</Header>
<ContentWrapper slimheader fieldcount={panel === Tabs[1] ? 4 : 3} noheader={panel === Tabs[2]}>
<Subheader>
{panel === Tabs[0] && <SketchSearchbar />}
{panel === Tabs[1] && <CollectionSearchbar />}
</Subheader>
{renderPanel(panel, { username: paramsUsername, key: pathname })}
</ContentWrapper>
<Footer>
{!isExamples &&
<FooterTabSwitcher>
{Tabs.map(tab => (
<FooterTab
key={`tab-${tab}`}
selected={tab === panel}
to={pathname.replace(panel, tab)}
>
<h3>{(isExamples && tab === 'Sketches') ? 'Examples' : tab}</h3>
</FooterTab>))
}
</FooterTabSwitcher>
}
</Footer>
</Screen>);
};
MobileDashboard.propTypes = {
location: PropTypes.shape({
pathname: PropTypes.string.isRequired
}).isRequired,
params: PropTypes.shape({
username: PropTypes.string.isRequired
})
};
MobileDashboard.defaultProps = { params: {} };
export default withRouter(MobileDashboard);

View file

@ -18,11 +18,7 @@ import { getHTMLFile } from '../IDE/reducers/files';
import { ExitIcon } from '../../common/icons'; import { ExitIcon } from '../../common/icons';
import { remSize } from '../../theme'; import { remSize } from '../../theme';
import Footer from '../../components/mobile/Footer'; import Footer from '../../components/mobile/Footer';
import Content from './MobileViewContent';
const Content = styled.div`
z-index: 0;
margin-top: ${remSize(68)};
`;
const MobileSketchView = () => { const MobileSketchView = () => {
const { files, ide, preferences } = useSelector(state => state); const { files, ide, preferences } = useSelector(state => state);

View file

@ -0,0 +1,10 @@
import React from 'react';
import styled from 'styled-components';
import { remSize } from '../../theme';
export default styled.div`
/* Dashboard Styles */
z-index: 0;
margin-top: ${props => remSize(props.slimheader ? 49 : 68)};
`;

View file

@ -7,8 +7,8 @@ import CopyableInput from '../../IDE/components/CopyableInput';
import APIKeyList from './APIKeyList'; import APIKeyList from './APIKeyList';
export const APIKeyPropType = PropTypes.shape({ export const APIKeyPropType = PropTypes.shape({
id: PropTypes.object.isRequired, id: PropTypes.object.isRequired, // eslint-disable-line
token: PropTypes.object, token: PropTypes.object, // eslint-disable-line
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired, createdAt: PropTypes.string.isRequired,
lastUsedAt: PropTypes.string lastUsedAt: PropTypes.string
@ -29,7 +29,7 @@ class APIKeyForm extends React.Component {
const { keyLabel } = this.state; const { keyLabel } = this.state;
this.setState({ this.setState({
keyLabel: '' keyLabel: '',
}); });
this.props.createApiKey(keyLabel); this.props.createApiKey(keyLabel);

View file

@ -34,7 +34,9 @@ function AccountForm(props) {
id="email" id="email"
{...domOnlyProps(email)} {...domOnlyProps(email)}
/> />
{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' && user.verified !== 'verified' &&
@ -66,7 +68,9 @@ function AccountForm(props) {
defaultValue={username} defaultValue={username}
{...domOnlyProps(username)} {...domOnlyProps(username)}
/> />
{username.touched && username.error && <span className="form-error">{username.error}</span>} {username.touched && username.error && (
<span className="form-error">{username.error}</span>
)}
</p> </p>
<p className="form__field"> <p className="form__field">
<label htmlFor="current password" className="form__label">{t('AccountForm.CurrentPassword')}</label> <label htmlFor="current password" className="form__label">{t('AccountForm.CurrentPassword')}</label>
@ -77,11 +81,9 @@ function AccountForm(props) {
id="currentPassword" id="currentPassword"
{...domOnlyProps(currentPassword)} {...domOnlyProps(currentPassword)}
/> />
{ {currentPassword.touched && currentPassword.error && (
currentPassword.touched &&
currentPassword.error &&
<span className="form-error">{currentPassword.error}</span> <span className="form-error">{currentPassword.error}</span>
} )}
</p> </p>
<p className="form__field"> <p className="form__field">
<label htmlFor="new password" className="form__label">{t('AccountForm.NewPassword')}</label> <label htmlFor="new password" className="form__label">{t('AccountForm.NewPassword')}</label>
@ -92,7 +94,9 @@ function AccountForm(props) {
id="newPassword" id="newPassword"
{...domOnlyProps(newPassword)} {...domOnlyProps(newPassword)}
/> />
{newPassword.touched && newPassword.error && <span className="form-error">{newPassword.error}</span>} {newPassword.touched && newPassword.error && (
<span className="form-error">{newPassword.error}</span>
)}
</p> </p>
<Button <Button
type="submit" type="submit"
@ -105,10 +109,10 @@ function AccountForm(props) {
AccountForm.propTypes = { AccountForm.propTypes = {
fields: PropTypes.shape({ fields: PropTypes.shape({
username: PropTypes.object.isRequired, username: PropTypes.object.isRequired, // eslint-disable-line
email: PropTypes.object.isRequired, email: PropTypes.object.isRequired, // eslint-disable-line
currentPassword: PropTypes.object.isRequired, currentPassword: PropTypes.object.isRequired, // eslint-disable-line
newPassword: PropTypes.object.isRequired, newPassword: PropTypes.object.isRequired, // eslint-disable-line
}).isRequired, }).isRequired,
user: PropTypes.shape({ user: PropTypes.shape({
verified: PropTypes.number.isRequired, verified: PropTypes.number.isRequired,
@ -126,7 +130,7 @@ AccountForm.propTypes = {
AccountForm.defaultProps = { AccountForm.defaultProps = {
submitting: false, submitting: false,
pristine: true, pristine: true,
invalid: false invalid: false,
}; };
export default withTranslation()(AccountForm); export default withTranslation()(AccountForm);

View file

@ -8,10 +8,16 @@ import { domOnlyProps } from '../../../utils/reduxFormUtils';
function LoginForm(props) { function LoginForm(props) {
const { const {
fields: { email, password }, handleSubmit, submitting, pristine fields: { email, password },
handleSubmit,
submitting,
pristine,
} = props; } = props;
return ( return (
<form className="form" onSubmit={handleSubmit(props.validateAndLoginUser.bind(this, props.previousPath))}> <form
className="form"
onSubmit={handleSubmit(props.validateAndLoginUser.bind(this, props.previousPath))}
>
<p className="form__field"> <p className="form__field">
<label htmlFor="email" className="form__label">{props.t('LoginForm.UsernameOrEmail')}</label> <label htmlFor="email" className="form__label">{props.t('LoginForm.UsernameOrEmail')}</label>
<input <input
@ -21,7 +27,9 @@ function LoginForm(props) {
id="email" id="email"
{...domOnlyProps(email)} {...domOnlyProps(email)}
/> />
{email.touched && email.error && <span className="form-error">{email.error}</span>} {email.touched && email.error && (
<span className="form-error">{email.error}</span>
)}
</p> </p>
<p className="form__field"> <p className="form__field">
<label htmlFor="password" className="form__label">{props.t('LoginForm.Password')}</label> <label htmlFor="password" className="form__label">{props.t('LoginForm.Password')}</label>
@ -32,7 +40,9 @@ function LoginForm(props) {
id="password" id="password"
{...domOnlyProps(password)} {...domOnlyProps(password)}
/> />
{password.touched && password.error && <span className="form-error">{password.error}</span>} {password.touched && password.error && (
<span className="form-error">{password.error}</span>
)}
</p> </p>
<Button <Button
type="submit" type="submit"
@ -45,8 +55,8 @@ function LoginForm(props) {
LoginForm.propTypes = { LoginForm.propTypes = {
fields: PropTypes.shape({ fields: PropTypes.shape({
email: PropTypes.object.isRequired, email: PropTypes.object.isRequired, // eslint-disable-line
password: PropTypes.object.isRequired password: PropTypes.object.isRequired, // eslint-disable-line
}).isRequired, }).isRequired,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
validateAndLoginUser: PropTypes.func.isRequired, validateAndLoginUser: PropTypes.func.isRequired,
@ -60,7 +70,7 @@ LoginForm.propTypes = {
LoginForm.defaultProps = { LoginForm.defaultProps = {
submitting: false, submitting: false,
pristine: true, pristine: true,
invalid: false invalid: false,
}; };
export default withTranslation()(LoginForm); export default withTranslation()(LoginForm);

View file

@ -10,7 +10,10 @@ function NewPasswordForm(props) {
t t
} = props; } = props;
return ( return (
<form className="form" onSubmit={handleSubmit(props.updatePassword.bind(this, props.params.reset_password_token))}> <form
className="form"
onSubmit={handleSubmit(props.updatePassword.bind(this, props.params.reset_password_token))}
>
<p className="form__field"> <p className="form__field">
<label htmlFor="password" className="form__label">{t('NewPasswordForm.Title')}</label> <label htmlFor="password" className="form__label">{t('NewPasswordForm.Title')}</label>
<input <input
@ -20,7 +23,9 @@ function NewPasswordForm(props) {
id="Password" id="Password"
{...domOnlyProps(password)} {...domOnlyProps(password)}
/> />
{password.touched && password.error && <span className="form-error">{password.error}</span>} {password.touched && password.error && (
<span className="form-error">{password.error}</span>
)}
</p> </p>
<p className="form__field"> <p className="form__field">
<label htmlFor="confirm password" className="form__label">{t('NewPasswordForm.ConfirmPassword')}</label> <label htmlFor="confirm password" className="form__label">{t('NewPasswordForm.ConfirmPassword')}</label>
@ -31,11 +36,9 @@ function NewPasswordForm(props) {
id="confirm password" id="confirm password"
{...domOnlyProps(confirmPassword)} {...domOnlyProps(confirmPassword)}
/> />
{ {confirmPassword.touched && confirmPassword.error && (
confirmPassword.touched &&
confirmPassword.error &&
<span className="form-error">{confirmPassword.error}</span> <span className="form-error">{confirmPassword.error}</span>
} )}
</p> </p>
<Button type="submit" disabled={submitting || invalid || pristine}>{t('NewPasswordForm.SubmitSetNewPassword')}</Button> <Button type="submit" disabled={submitting || invalid || pristine}>{t('NewPasswordForm.SubmitSetNewPassword')}</Button>
</form> </form>
@ -44,8 +47,8 @@ function NewPasswordForm(props) {
NewPasswordForm.propTypes = { NewPasswordForm.propTypes = {
fields: PropTypes.shape({ fields: PropTypes.shape({
password: PropTypes.object.isRequired, password: PropTypes.object.isRequired, // eslint-disable-line
confirmPassword: PropTypes.object.isRequired confirmPassword: PropTypes.object.isRequired, // eslint-disable-line
}).isRequired, }).isRequired,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
updatePassword: PropTypes.func.isRequired, updatePassword: PropTypes.func.isRequired,
@ -61,7 +64,7 @@ NewPasswordForm.propTypes = {
NewPasswordForm.defaultProps = { NewPasswordForm.defaultProps = {
invalid: false, invalid: false,
pristine: true, pristine: true,
submitting: false submitting: false,
}; };
export default withTranslation()(NewPasswordForm); export default withTranslation()(NewPasswordForm);

View file

@ -9,7 +9,10 @@ function ResetPasswordForm(props) {
fields: { email }, handleSubmit, submitting, invalid, pristine, t fields: { email }, handleSubmit, submitting, invalid, pristine, t
} = props; } = props;
return ( return (
<form className="form" onSubmit={handleSubmit(props.initiateResetPassword.bind(this))}> <form
className="form"
onSubmit={handleSubmit(props.initiateResetPassword.bind(this))}
>
<p className="form__field"> <p className="form__field">
<label htmlFor="email" className="form__label">{t('ResetPasswordForm.Email')}</label> <label htmlFor="email" className="form__label">{t('ResetPasswordForm.Email')}</label>
<input <input
@ -19,7 +22,9 @@ function ResetPasswordForm(props) {
id="email" id="email"
{...domOnlyProps(email)} {...domOnlyProps(email)}
/> />
{email.touched && email.error && <span className="form-error">{email.error}</span>} {email.touched && email.error && (
<span className="form-error">{email.error}</span>
)}
</p> </p>
<Button <Button
type="submit" type="submit"
@ -48,7 +53,7 @@ ResetPasswordForm.propTypes = {
ResetPasswordForm.defaultProps = { ResetPasswordForm.defaultProps = {
submitting: false, submitting: false,
pristine: true, pristine: true,
invalid: false invalid: false,
}; };
export default withTranslation()(ResetPasswordForm); export default withTranslation()(ResetPasswordForm);

View file

@ -9,10 +9,17 @@ function SignupForm(props) {
const { const {
fields: { fields: {
username, email, password, confirmPassword username, email, password, confirmPassword
}, handleSubmit, submitting, invalid, pristine },
handleSubmit,
submitting,
invalid,
pristine,
} = props; } = props;
return ( return (
<form className="form" onSubmit={handleSubmit(props.signUpUser.bind(this, props.previousPath))}> <form
className="form"
onSubmit={handleSubmit(props.signUpUser.bind(this, props.previousPath))}
>
<p className="form__field"> <p className="form__field">
<label htmlFor="username" className="form__label">{props.t('SignupForm.Title')}</label> <label htmlFor="username" className="form__label">{props.t('SignupForm.Title')}</label>
<input <input
@ -22,7 +29,9 @@ function SignupForm(props) {
id="username" id="username"
{...domOnlyProps(username)} {...domOnlyProps(username)}
/> />
{username.touched && username.error && <span className="form-error">{username.error}</span>} {username.touched && username.error && (
<span className="form-error">{username.error}</span>
)}
</p> </p>
<p className="form__field"> <p className="form__field">
<label htmlFor="email" className="form__label">{props.t('SignupForm.Email')}</label> <label htmlFor="email" className="form__label">{props.t('SignupForm.Email')}</label>
@ -33,7 +42,9 @@ function SignupForm(props) {
id="email" id="email"
{...domOnlyProps(email)} {...domOnlyProps(email)}
/> />
{email.touched && email.error && <span className="form-error">{email.error}</span>} {email.touched && email.error && (
<span className="form-error">{email.error}</span>
)}
</p> </p>
<p className="form__field"> <p className="form__field">
<label htmlFor="password" className="form__label">{props.t('SignupForm.Password')}</label> <label htmlFor="password" className="form__label">{props.t('SignupForm.Password')}</label>
@ -44,7 +55,9 @@ function SignupForm(props) {
id="password" id="password"
{...domOnlyProps(password)} {...domOnlyProps(password)}
/> />
{password.touched && password.error && <span className="form-error">{password.error}</span>} {password.touched && password.error && (
<span className="form-error">{password.error}</span>
)}
</p> </p>
<p className="form__field"> <p className="form__field">
<label htmlFor="confirm password" className="form__label">{props.t('SignupForm.ConfirmPassword')}</label> <label htmlFor="confirm password" className="form__label">{props.t('SignupForm.ConfirmPassword')}</label>
@ -55,11 +68,9 @@ function SignupForm(props) {
id="confirm password" id="confirm password"
{...domOnlyProps(confirmPassword)} {...domOnlyProps(confirmPassword)}
/> />
{ {confirmPassword.touched && confirmPassword.error && (
confirmPassword.touched &&
confirmPassword.error &&
<span className="form-error">{confirmPassword.error}</span> <span className="form-error">{confirmPassword.error}</span>
} )}
</p> </p>
<Button <Button
type="submit" type="submit"
@ -72,10 +83,10 @@ function SignupForm(props) {
SignupForm.propTypes = { SignupForm.propTypes = {
fields: PropTypes.shape({ fields: PropTypes.shape({
username: PropTypes.object.isRequired, username: PropTypes.object.isRequired, // eslint-disable-line
email: PropTypes.object.isRequired, email: PropTypes.object.isRequired, // eslint-disable-line
password: PropTypes.object.isRequired, password: PropTypes.object.isRequired, // eslint-disable-line
confirmPassword: PropTypes.object.isRequired confirmPassword: PropTypes.object.isRequired, // eslint-disable-line
}).isRequired, }).isRequired,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
signUpUser: PropTypes.func.isRequired, signUpUser: PropTypes.func.isRequired,
@ -89,7 +100,7 @@ SignupForm.propTypes = {
SignupForm.defaultProps = { SignupForm.defaultProps = {
submitting: false, submitting: false,
pristine: true, pristine: true,
invalid: false invalid: false,
}; };
export default withTranslation()(SignupForm); export default withTranslation()(SignupForm);

View file

@ -15,6 +15,7 @@ import AccountView from './modules/User/pages/AccountView';
import CollectionView from './modules/User/pages/CollectionView'; import CollectionView from './modules/User/pages/CollectionView';
import DashboardView from './modules/User/pages/DashboardView'; import DashboardView from './modules/User/pages/DashboardView';
import createRedirectWithUsername from './components/createRedirectWithUsername'; import createRedirectWithUsername from './components/createRedirectWithUsername';
import MobileDashboardView from './modules/Mobile/MobileDashboardView';
import { getUser } from './modules/User/actions'; import { getUser } from './modules/User/actions';
import { stopSketch } from './modules/IDE/actions/ide'; import { stopSketch } from './modules/IDE/actions/ide';
import { userIsAuthenticated, userIsNotAuthenticated, userIsAuthorized } from './utils/auth'; import { userIsAuthenticated, userIsNotAuthenticated, userIsAuthorized } from './utils/auth';
@ -26,7 +27,7 @@ const checkAuth = (store) => {
// TODO: This short-circuit seems unnecessary - using the mobile <Switch /> navigator (future) should prevent this from being called // TODO: This short-circuit seems unnecessary - using the mobile <Switch /> navigator (future) should prevent this from being called
const onRouteChange = (store) => { const onRouteChange = (store) => {
const path = window.location.pathname; const path = window.location.pathname;
if (path.includes('/mobile')) return; if (path.includes('/mobile/preview')) return;
store.dispatch(stopSketch()); store.dispatch(stopSketch());
}; };
@ -57,9 +58,17 @@ const routes = store => (
<Route path="/:username/collections/:collection_id" component={CollectionView} /> <Route path="/:username/collections/:collection_id" component={CollectionView} />
<Route path="/about" component={IDEView} /> <Route path="/about" component={IDEView} />
<Route path="/mobile" component={MobileIDEView} />
<Route path="/mobile/preview" component={MobileSketchView} /> <Route path="/mobile/preview" component={MobileSketchView} />
<Route path="/mobile/preferences" component={MobilePreferences} /> <Route path="/mobile/preferences" component={MobilePreferences} />
<Route path="/mobile" component={MobileIDEView} />
<Route path="/mobile/:username/sketches/:project_id" component={MobileIDEView} />
<Route path="/mobile/:username/assets" component={userIsAuthenticated(userIsAuthorized(MobileDashboardView))} />
<Route path="/mobile/:username/sketches" component={MobileDashboardView} />
<Route path="/mobile/:username/collections" component={MobileDashboardView} />
<Route path="/mobile/:username/collections/create" component={MobileDashboardView} />
</Route> </Route>
); );

View file

@ -58,6 +58,7 @@ export const prop = key => (props) => {
return value; return value;
}; };
export default { export default {
[Theme.light]: { [Theme.light]: {
colors, colors,
@ -103,6 +104,14 @@ export default {
border: grays.middleLight border: grays.middleLight
}, },
Separator: grays.middleLight, Separator: grays.middleLight,
TabHighlight: colors.p5jsPink,
SketchList: {
background: grays.lighter,
card: {
background: grays.lighter
}
}
}, },
[Theme.dark]: { [Theme.dark]: {
colors, colors,
@ -148,6 +157,14 @@ export default {
border: grays.middleDark border: grays.middleDark
}, },
Separator: grays.middleDark, Separator: grays.middleDark,
TabHighlight: colors.p5jsPink,
SketchList: {
background: grays.darker,
card: {
background: grays.dark
}
}
}, },
[Theme.contrast]: { [Theme.contrast]: {
colors, colors,
@ -193,5 +210,13 @@ export default {
border: grays.middleDark border: grays.middleDark
}, },
Separator: grays.middleDark, Separator: grays.middleDark,
TabHighlight: grays.darker,
SketchList: {
background: colors.yellow,
card: {
background: grays.dark
}
}
}, },
}; };

View file

@ -7,6 +7,8 @@ import { collectionForUserExists } from '../controllers/collection.controller';
const router = new Router(); const router = new Router();
const fallback404 = res => (exists => (exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html))));
// this is intended to be a temporary file // this is intended to be a temporary file
// until i figure out isomorphic rendering // until i figure out isomorphic rendering
@ -114,20 +116,6 @@ router.get('/about', (req, res) => {
res.send(renderIndex()); res.send(renderIndex());
}); });
if (process.env.MOBILE_ENABLED) {
router.get('/mobile', (req, res) => {
res.send(renderIndex());
});
router.get('/mobile/preview', (req, res) => {
res.send(renderIndex());
});
router.get('/mobile/preferences', (req, res) => {
res.send(renderIndex());
});
}
router.get('/:username/collections/create', (req, res) => { router.get('/:username/collections/create', (req, res) => {
userExists(req.params.username, (exists) => { userExists(req.params.username, (exists) => {
const isLoggedInUser = req.user && req.user.username === req.params.username; const isLoggedInUser = req.user && req.user.username === req.params.username;
@ -156,4 +144,5 @@ router.get('/:username/collections', (req, res) => {
)); ));
}); });
export default router; export default router;

View file

@ -132,6 +132,10 @@ app.get(
// isomorphic rendering // isomorphic rendering
app.use('/', serverRoutes); app.use('/', serverRoutes);
if (process.env.MOBILE_ENABLED) {
app.use('/mobile', serverRoutes);
}
app.use(assetRoutes); app.use(assetRoutes);
app.use('/', embedRoutes); app.use('/', embedRoutes);