Merge pull request #1528 from ghalestrilo/feature/mobile-examples
Feature/mobile examples
This commit is contained in:
commit
127660ab4d
26 changed files with 701 additions and 213 deletions
|
@ -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
|
||||
UI_ACCESS_TOKEN_ENABLED=false
|
||||
UPLOAD_LIMIT=250000000
|
||||
MOBILE_ENABLED=true
|
||||
|
|
|
@ -3,16 +3,22 @@ import styled from 'styled-components';
|
|||
import PropTypes from 'prop-types';
|
||||
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`
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background: ${props => background(props.transparent === true)};
|
||||
background: ${props => background(props)};
|
||||
color: ${textColor};
|
||||
padding: ${remSize(12)};
|
||||
padding: ${props => remSize(props.slim === true ? 2 : 12)};
|
||||
padding-left: ${remSize(16)};
|
||||
padding-right: ${remSize(16)};
|
||||
z-index: 1;
|
||||
|
@ -25,8 +31,10 @@ const HeaderDiv = styled.div`
|
|||
|
||||
svg {
|
||||
max-height: ${remSize(32)};
|
||||
padding: ${remSize(4)}
|
||||
padding: ${remSize(4)};
|
||||
}
|
||||
|
||||
& svg path { fill: ${textColor} !important; }
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
|
@ -48,9 +56,10 @@ const TitleContainer = styled.div`
|
|||
`;
|
||||
|
||||
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}
|
||||
<TitleContainer padded={subtitle === null}>
|
||||
{title && <h2>{title}</h2>}
|
||||
|
@ -67,7 +76,9 @@ Header.propTypes = {
|
|||
subtitle: PropTypes.string,
|
||||
leftButton: 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 = {
|
||||
|
@ -75,7 +86,9 @@ Header.defaultProps = {
|
|||
subtitle: null,
|
||||
leftButton: null,
|
||||
children: [],
|
||||
transparent: false
|
||||
transparent: false,
|
||||
inverted: false,
|
||||
slim: false
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
|
18
client/components/mobile/Tab.jsx
Normal file
18
client/components/mobile/Tab.jsx
Normal 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%;
|
||||
`;
|
15
client/components/mobile/TabSwitcher.jsx
Normal file
15
client/components/mobile/TabSwitcher.jsx
Normal 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')};
|
||||
`;
|
||||
|
|
@ -17,6 +17,7 @@ export function getCollections(username) {
|
|||
} else {
|
||||
url = '/collections';
|
||||
}
|
||||
console.log(url);
|
||||
apiClient.get(url)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
|
|
|
@ -21,6 +21,7 @@ import CollectionListRow from './CollectionListRow';
|
|||
import ArrowUpIcon from '../../../../images/sort-arrow-up.svg';
|
||||
import ArrowDownIcon from '../../../../images/sort-arrow-down.svg';
|
||||
|
||||
|
||||
class CollectionList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -127,6 +128,7 @@ class CollectionList extends React.Component {
|
|||
|
||||
render() {
|
||||
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
||||
const { mobile } = this.props;
|
||||
|
||||
return (
|
||||
<article className="sketches-table-container">
|
||||
|
@ -141,15 +143,16 @@ class CollectionList extends React.Component {
|
|||
<thead>
|
||||
<tr>
|
||||
{this._renderFieldHeader('name', 'Name')}
|
||||
{this._renderFieldHeader('createdAt', 'Date Created')}
|
||||
{this._renderFieldHeader('updatedAt', 'Date Updated')}
|
||||
{this._renderFieldHeader('numItems', '# sketches')}
|
||||
{this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)}
|
||||
{this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)}
|
||||
{this._renderFieldHeader('numItems', mobile ? 'Sketches' : '# sketches')}
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.props.collections.map(collection =>
|
||||
(<CollectionListRow
|
||||
mobile={mobile}
|
||||
key={collection.id}
|
||||
collection={collection}
|
||||
user={this.props.user}
|
||||
|
@ -209,7 +212,8 @@ CollectionList.propTypes = {
|
|||
owner: PropTypes.shape({
|
||||
id: PropTypes.string
|
||||
})
|
||||
})
|
||||
}),
|
||||
mobile: PropTypes.bool,
|
||||
};
|
||||
|
||||
CollectionList.defaultProps = {
|
||||
|
@ -218,7 +222,8 @@ CollectionList.defaultProps = {
|
|||
id: undefined,
|
||||
owner: undefined
|
||||
},
|
||||
username: undefined
|
||||
username: undefined,
|
||||
mobile: false
|
||||
};
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
|
|
|
@ -11,6 +11,8 @@ import * as ToastActions from '../../actions/toast';
|
|||
|
||||
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 {
|
||||
static projectInCollection(project, collection) {
|
||||
return collection.items.find(item => item.project.id === project.id) != null;
|
||||
|
@ -199,7 +201,7 @@ class CollectionListRowBase extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { collection } = this.props;
|
||||
const { collection, mobile } = this.props;
|
||||
|
||||
return (
|
||||
<tr
|
||||
|
@ -211,9 +213,9 @@ class CollectionListRowBase extends React.Component {
|
|||
{this.renderCollectionName()}
|
||||
</span>
|
||||
</th>
|
||||
<td>{format(new Date(collection.createdAt), 'MMM D, YYYY')}</td>
|
||||
<td>{format(new Date(collection.updatedAt), 'MMM D, YYYY')}</td>
|
||||
<td>{(collection.items || []).length}</td>
|
||||
<td>{mobile && 'Created: '}{format(new Date(collection.createdAt), 'MMM D, YYYY')}</td>
|
||||
<td>{mobile && 'Updated: '}{formatDateCell(collection.updatedAt)}</td>
|
||||
<td>{mobile && '# sketches: '}{(collection.items || []).length}</td>
|
||||
<td className="sketch-list__dropdown-column">
|
||||
{this.renderActions()}
|
||||
</td>
|
||||
|
@ -245,6 +247,11 @@ CollectionListRowBase.propTypes = {
|
|||
deleteCollection: PropTypes.func.isRequired,
|
||||
editCollection: PropTypes.func.isRequired,
|
||||
onAddSketches: PropTypes.func.isRequired,
|
||||
mobile: PropTypes.bool,
|
||||
};
|
||||
|
||||
CollectionListRowBase.defaultProps = {
|
||||
mobile: false,
|
||||
};
|
||||
|
||||
function mapDispatchToPropsSketchListRow(dispatch) {
|
||||
|
|
|
@ -5,13 +5,20 @@ import EditIcon from '../../../images/pencil.svg';
|
|||
|
||||
// TODO I think this needs a description prop so that it's accessible
|
||||
function EditableInput({
|
||||
validate, value, emptyPlaceholder, InputComponent, inputProps, onChange
|
||||
validate,
|
||||
value,
|
||||
emptyPlaceholder,
|
||||
InputComponent,
|
||||
inputProps,
|
||||
onChange,
|
||||
}) {
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const [currentValue, setCurrentValue] = React.useState(value || '');
|
||||
const displayValue = currentValue || emptyPlaceholder;
|
||||
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();
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -54,7 +61,11 @@ function EditableInput({
|
|||
aria-label={`Edit ${displayValue} value`}
|
||||
>
|
||||
<span>{displayValue}</span>
|
||||
<EditIcon className="editable-input__icon" focusable="false" aria-hidden="true" />
|
||||
<EditIcon
|
||||
className="editable-input__icon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<InputComponent
|
||||
|
@ -68,7 +79,7 @@ function EditableInput({
|
|||
ref={inputRef}
|
||||
value={currentValue}
|
||||
/>
|
||||
</span >
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -84,7 +95,7 @@ EditableInput.propTypes = {
|
|||
emptyPlaceholder: PropTypes.string,
|
||||
InputComponent: PropTypes.elementType,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
inputProps: PropTypes.object,
|
||||
inputProps: PropTypes.object, // eslint-disable-line
|
||||
onChange: PropTypes.func.isRequired,
|
||||
validate: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
|
|
|
@ -16,7 +16,10 @@ class NewFileForm extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { fields: { name }, handleSubmit } = this.props;
|
||||
const {
|
||||
fields: { name },
|
||||
handleSubmit,
|
||||
} = this.props;
|
||||
return (
|
||||
<form
|
||||
className="new-file-form"
|
||||
|
@ -26,7 +29,9 @@ class NewFileForm extends React.Component {
|
|||
}}
|
||||
>
|
||||
<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
|
||||
className="new-file-form__name-input"
|
||||
id="name"
|
||||
|
@ -34,14 +39,18 @@ class NewFileForm extends React.Component {
|
|||
placeholder={this.props.t('NewFileForm.Placeholder')}
|
||||
maxLength="128"
|
||||
{...domOnlyProps(name)}
|
||||
ref={(element) => { this.fileName = element; }}
|
||||
ref={(element) => {
|
||||
this.fileName = element;
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
>{this.props.t('NewFileForm.AddFileSubmit')}
|
||||
</Button>
|
||||
</div>
|
||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||
{name.touched && name.error && (
|
||||
<span className="form-error">{name.error}</span>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,8 @@ class NewFolderForm extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
fields: { name }, handleSubmit
|
||||
fields: { name },
|
||||
handleSubmit,
|
||||
} = this.props;
|
||||
return (
|
||||
<form
|
||||
|
@ -28,7 +29,9 @@ class NewFolderForm extends React.Component {
|
|||
}}
|
||||
>
|
||||
<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
|
||||
className="new-folder-form__name-input"
|
||||
id="name"
|
||||
|
@ -43,7 +46,9 @@ class NewFolderForm extends React.Component {
|
|||
>{this.props.t('NewFolderForm.AddFolderSubmit')}
|
||||
</Button>
|
||||
</div>
|
||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||
{name.touched && name.error && (
|
||||
<span className="form-error">{name.error}</span>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -62,6 +67,6 @@ NewFolderForm.propTypes = {
|
|||
};
|
||||
NewFolderForm.defaultProps = {
|
||||
submitting: false,
|
||||
pristine: true
|
||||
pristine: true,
|
||||
};
|
||||
export default withTranslation()(NewFolderForm);
|
||||
|
|
|
@ -22,6 +22,9 @@ import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
|
|||
import ArrowDownIcon from '../../../images/sort-arrow-down.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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -251,6 +254,7 @@ class SketchListRowBase extends React.Component {
|
|||
const {
|
||||
sketch,
|
||||
username,
|
||||
mobile
|
||||
} = this.props;
|
||||
const { renameOpen, renameValue } = this.state;
|
||||
let url = `/${username}/sketches/${sketch.id}`;
|
||||
|
@ -258,6 +262,8 @@ class SketchListRowBase extends React.Component {
|
|||
url = `/${username}/sketches/${slugify(sketch.name, '_')}`;
|
||||
}
|
||||
|
||||
if (this.props.mobile) url = `/mobile${url}`;
|
||||
|
||||
const name = (
|
||||
<React.Fragment>
|
||||
<Link to={url}>
|
||||
|
@ -287,8 +293,8 @@ class SketchListRowBase extends React.Component {
|
|||
<th scope="row">
|
||||
{name}
|
||||
</th>
|
||||
<td>{format(new Date(sketch.createdAt), 'MMM D, YYYY h:mm A')}</td>
|
||||
<td>{format(new Date(sketch.updatedAt), 'MMM D, YYYY h:mm A')}</td>
|
||||
<td>{mobile && 'Created: '}{formatDateCell(sketch.createdAt, mobile)}</td>
|
||||
<td>{mobile && 'Updated: '}{formatDateCell(sketch.updatedAt, mobile)}</td>
|
||||
{this.renderDropdown()}
|
||||
</tr>
|
||||
</React.Fragment>);
|
||||
|
@ -312,7 +318,12 @@ SketchListRowBase.propTypes = {
|
|||
cloneProject: PropTypes.func.isRequired,
|
||||
exportProjectAsZip: 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) {
|
||||
|
@ -413,6 +424,7 @@ class SketchList extends React.Component {
|
|||
|
||||
render() {
|
||||
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
||||
const { mobile } = this.props;
|
||||
return (
|
||||
<article className="sketches-table-container">
|
||||
<Helmet>
|
||||
|
@ -425,14 +437,15 @@ class SketchList extends React.Component {
|
|||
<thead>
|
||||
<tr>
|
||||
{this._renderFieldHeader('name', 'Sketch')}
|
||||
{this._renderFieldHeader('createdAt', 'Date Created')}
|
||||
{this._renderFieldHeader('updatedAt', 'Date Updated')}
|
||||
{this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)}
|
||||
{this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)}
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.props.sketches.map(sketch =>
|
||||
(<SketchListRow
|
||||
mobile={mobile}
|
||||
key={sketch.id}
|
||||
sketch={sketch}
|
||||
user={this.props.user}
|
||||
|
@ -482,10 +495,12 @@ SketchList.propTypes = {
|
|||
field: PropTypes.string.isRequired,
|
||||
direction: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
mobile: PropTypes.bool,
|
||||
};
|
||||
|
||||
SketchList.defaultProps = {
|
||||
username: undefined
|
||||
username: undefined,
|
||||
mobile: false,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
|
|
|
@ -45,13 +45,22 @@ function isUserOwner(props) {
|
|||
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;
|
||||
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
|
||||
props.persistState();
|
||||
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
|
||||
props.persistState();
|
||||
window.onbeforeunload = null;
|
||||
|
@ -62,6 +71,7 @@ function warnIfUnsavedChanges(props) { // eslint-disable-line
|
|||
props.setUnsavedChanges(false);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class IDEView extends React.Component {
|
||||
|
@ -71,7 +81,7 @@ class IDEView extends React.Component {
|
|||
|
||||
this.state = {
|
||||
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;
|
||||
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;
|
||||
|
||||
|
@ -104,11 +117,15 @@ class IDEView extends React.Component {
|
|||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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 (
|
||||
this.props.selectedFile.name === prevProps.selectedFile.name &&
|
||||
this.props.selectedFile.content !== prevProps.selectedFile.content) {
|
||||
this.props.selectedFile.content !== prevProps.selectedFile.content
|
||||
) {
|
||||
if (this.autosaveInterval) {
|
||||
clearTimeout(this.autosaveInterval);
|
||||
}
|
||||
|
@ -142,7 +164,8 @@ class IDEView extends React.Component {
|
|||
}
|
||||
|
||||
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() {
|
||||
|
@ -152,10 +175,16 @@ class IDEView extends React.Component {
|
|||
}
|
||||
handleGlobalKeydown(e) {
|
||||
// 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.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());
|
||||
} else if (this.props.user.authenticated) {
|
||||
this.props.cloneProject();
|
||||
|
@ -163,23 +192,41 @@ class IDEView extends React.Component {
|
|||
this.props.showErrorModal('forceAuthentication');
|
||||
}
|
||||
// 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.stopPropagation();
|
||||
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.stopPropagation();
|
||||
this.props.startSketch();
|
||||
// 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();
|
||||
this.props.setAllAccessibleOutput(false);
|
||||
// 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();
|
||||
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();
|
||||
if (!this.props.ide.sidebarIsExpanded) {
|
||||
this.props.expandSidebar();
|
||||
|
@ -218,7 +265,7 @@ class IDEView extends React.Component {
|
|||
cmController={this.cmController}
|
||||
/>
|
||||
<Toolbar key={this.props.project.id} />
|
||||
{this.props.ide.preferencesIsVisible &&
|
||||
{this.props.ide.preferencesIsVisible && (
|
||||
<Overlay
|
||||
title={this.props.t('Preferences.Settings')}
|
||||
ariaLabel={this.props.t('Preferences.Settings')}
|
||||
|
@ -245,7 +292,7 @@ class IDEView extends React.Component {
|
|||
setTheme={this.props.setTheme}
|
||||
/>
|
||||
</Overlay>
|
||||
}
|
||||
)}
|
||||
<main className="editor-preview-container">
|
||||
<SplitPane
|
||||
split="vertical"
|
||||
|
@ -274,10 +321,17 @@ class IDEView extends React.Component {
|
|||
<SplitPane
|
||||
split="vertical"
|
||||
defaultSize="50%"
|
||||
onChange={() => { this.overlay.style.display = 'block'; }}
|
||||
onDragFinished={() => { this.overlay.style.display = 'none'; }}
|
||||
onChange={() => {
|
||||
this.overlay.style.display = 'block';
|
||||
}}
|
||||
onDragFinished={() => {
|
||||
this.overlay.style.display = 'none';
|
||||
}}
|
||||
resizerStyle={{
|
||||
borderLeftWidth: '2px', borderRightWidth: '2px', width: '2px', margin: '0px 0px'
|
||||
borderLeftWidth: '2px',
|
||||
borderRightWidth: '2px',
|
||||
width: '2px',
|
||||
margin: '0px 0px',
|
||||
}}
|
||||
>
|
||||
<SplitPane
|
||||
|
@ -303,7 +357,9 @@ class IDEView extends React.Component {
|
|||
editorOptionsVisible={this.props.ide.editorOptionsVisible}
|
||||
showEditorOptions={this.props.showEditorOptions}
|
||||
closeEditorOptions={this.props.closeEditorOptions}
|
||||
showKeyboardShortcutModal={this.props.showKeyboardShortcutModal}
|
||||
showKeyboardShortcutModal={
|
||||
this.props.showKeyboardShortcutModal
|
||||
}
|
||||
setUnsavedChanges={this.props.setUnsavedChanges}
|
||||
isPlaying={this.props.ide.isPlaying}
|
||||
theme={this.props.preferences.theme}
|
||||
|
@ -320,8 +376,12 @@ class IDEView extends React.Component {
|
|||
consoleEvents={this.props.console}
|
||||
showRuntimeErrorWarning={this.props.showRuntimeErrorWarning}
|
||||
hideRuntimeErrorWarning={this.props.hideRuntimeErrorWarning}
|
||||
runtimeErrorWarningVisible={this.props.ide.runtimeErrorWarningVisible}
|
||||
provideController={(ctl) => { this.cmController = ctl; }}
|
||||
runtimeErrorWarningVisible={
|
||||
this.props.ide.runtimeErrorWarningVisible
|
||||
}
|
||||
provideController={(ctl) => {
|
||||
this.cmController = ctl;
|
||||
}}
|
||||
/>
|
||||
<Console />
|
||||
</SplitPane>
|
||||
|
@ -330,27 +390,28 @@ class IDEView extends React.Component {
|
|||
<h2 className="preview-frame__title">{this.props.t('Toolbar.Preview')}</h2>
|
||||
</header>
|
||||
<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>
|
||||
{(
|
||||
(
|
||||
(this.props.preferences.textOutput ||
|
||||
this.props.preferences.gridOutput ||
|
||||
this.props.preferences.soundOutput
|
||||
) &&
|
||||
this.props.ide.isPlaying
|
||||
) ||
|
||||
this.props.ide.isAccessibleOutputPlaying
|
||||
)
|
||||
}
|
||||
{((this.props.preferences.textOutput ||
|
||||
this.props.preferences.gridOutput ||
|
||||
this.props.preferences.soundOutput) &&
|
||||
this.props.ide.isPlaying) ||
|
||||
this.props.ide.isAccessibleOutputPlaying}
|
||||
</div>
|
||||
<PreviewFrame
|
||||
htmlFile={this.props.htmlFile}
|
||||
files={this.props.files}
|
||||
content={this.props.selectedFile.content}
|
||||
isPlaying={this.props.ide.isPlaying}
|
||||
isAccessibleOutputPlaying={this.props.ide.isAccessibleOutputPlaying}
|
||||
isAccessibleOutputPlaying={
|
||||
this.props.ide.isAccessibleOutputPlaying
|
||||
}
|
||||
textOutput={this.props.preferences.textOutput}
|
||||
gridOutput={this.props.preferences.gridOutput}
|
||||
soundOutput={this.props.preferences.soundOutput}
|
||||
|
@ -373,21 +434,17 @@ class IDEView extends React.Component {
|
|||
</SplitPane>
|
||||
</SplitPane>
|
||||
</main>
|
||||
{ this.props.ide.modalIsVisible &&
|
||||
<NewFileModal />
|
||||
}
|
||||
{this.props.ide.newFolderModalVisible &&
|
||||
{this.props.ide.modalIsVisible && <NewFileModal />}
|
||||
{this.props.ide.newFolderModalVisible && (
|
||||
<NewFolderModal
|
||||
closeModal={this.props.closeNewFolderModal}
|
||||
createFolder={this.props.createFolder}
|
||||
/>
|
||||
}
|
||||
{this.props.ide.uploadFileModalVisible &&
|
||||
<UploadFileModal
|
||||
closeModal={this.props.closeUploadFileModal}
|
||||
/>
|
||||
}
|
||||
{ this.props.location.pathname === '/about' &&
|
||||
)}
|
||||
{this.props.ide.uploadFileModalVisible && (
|
||||
<UploadFileModal closeModal={this.props.closeUploadFileModal} />
|
||||
)}
|
||||
{this.props.location.pathname === '/about' && (
|
||||
<Overlay
|
||||
title={this.props.t('About.Title')}
|
||||
previousPath={this.props.ide.previousPath}
|
||||
|
@ -395,8 +452,8 @@ class IDEView extends React.Component {
|
|||
>
|
||||
<About previousPath={this.props.ide.previousPath} />
|
||||
</Overlay>
|
||||
}
|
||||
{this.props.location.pathname === '/feedback' &&
|
||||
)}
|
||||
{this.props.location.pathname === '/feedback' && (
|
||||
<Overlay
|
||||
title={this.props.t('IDEView.SubmitFeedback')}
|
||||
previousPath={this.props.ide.previousPath}
|
||||
|
@ -404,8 +461,8 @@ class IDEView extends React.Component {
|
|||
>
|
||||
<Feedback previousPath={this.props.ide.previousPath} />
|
||||
</Overlay>
|
||||
}
|
||||
{this.props.location.pathname.match(/add-to-collection$/) &&
|
||||
)}
|
||||
{this.props.location.pathname.match(/add-to-collection$/) && (
|
||||
<Overlay
|
||||
ariaLabel="add to collection"
|
||||
title="Add to collection"
|
||||
|
@ -419,8 +476,8 @@ class IDEView extends React.Component {
|
|||
user={this.props.user}
|
||||
/>
|
||||
</Overlay>
|
||||
}
|
||||
{this.props.ide.shareModalVisible &&
|
||||
)}
|
||||
{this.props.ide.shareModalVisible && (
|
||||
<Overlay
|
||||
title="Share"
|
||||
ariaLabel="share"
|
||||
|
@ -432,8 +489,8 @@ class IDEView extends React.Component {
|
|||
ownerUsername={this.props.ide.shareModalProjectUsername}
|
||||
/>
|
||||
</Overlay>
|
||||
}
|
||||
{this.props.ide.keyboardShortcutVisible &&
|
||||
)}
|
||||
{this.props.ide.keyboardShortcutVisible && (
|
||||
<Overlay
|
||||
title={this.props.t('KeyboardShortcuts.Title')}
|
||||
ariaLabel={this.props.t('KeyboardShortcuts.Title')}
|
||||
|
@ -441,8 +498,8 @@ class IDEView extends React.Component {
|
|||
>
|
||||
<KeyboardShortcutModal />
|
||||
</Overlay>
|
||||
}
|
||||
{this.props.ide.errorType &&
|
||||
)}
|
||||
{this.props.ide.errorType && (
|
||||
<Overlay
|
||||
title="Error"
|
||||
ariaLabel={this.props.t('Common.Error')}
|
||||
|
@ -453,7 +510,7 @@ class IDEView extends React.Component {
|
|||
closeModal={this.props.hideErrorModal}
|
||||
/>
|
||||
</Overlay>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -466,19 +523,19 @@ IDEView.propTypes = {
|
|||
reset_password_token: PropTypes.string,
|
||||
}).isRequired,
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string
|
||||
pathname: PropTypes.string,
|
||||
}).isRequired,
|
||||
getProject: PropTypes.func.isRequired,
|
||||
user: PropTypes.shape({
|
||||
authenticated: PropTypes.bool.isRequired,
|
||||
id: PropTypes.string,
|
||||
username: PropTypes.string
|
||||
username: PropTypes.string,
|
||||
}).isRequired,
|
||||
saveProject: PropTypes.func.isRequired,
|
||||
ide: PropTypes.shape({
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
|
||||
consoleEvent: PropTypes.array,
|
||||
consoleEvent: PropTypes.array, // eslint-disable-line
|
||||
modalIsVisible: PropTypes.bool.isRequired,
|
||||
sidebarIsExpanded: PropTypes.bool.isRequired,
|
||||
consoleIsExpanded: PropTypes.bool.isRequired,
|
||||
|
@ -500,7 +557,7 @@ IDEView.propTypes = {
|
|||
justOpenedProject: PropTypes.bool.isRequired,
|
||||
errorType: PropTypes.string,
|
||||
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
||||
uploadFileModalVisible: PropTypes.bool.isRequired
|
||||
uploadFileModalVisible: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
stopSketch: PropTypes.func.isRequired,
|
||||
project: PropTypes.shape({
|
||||
|
@ -508,12 +565,12 @@ IDEView.propTypes = {
|
|||
name: PropTypes.string.isRequired,
|
||||
owner: PropTypes.shape({
|
||||
username: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
id: PropTypes.string,
|
||||
}),
|
||||
updatedAt: PropTypes.string
|
||||
updatedAt: PropTypes.string,
|
||||
}).isRequired,
|
||||
editorAccessibility: PropTypes.shape({
|
||||
lintMessages: PropTypes.array.isRequired,
|
||||
lintMessages: PropTypes.array.isRequired, // eslint-disable-line
|
||||
}).isRequired,
|
||||
updateLintMessage: PropTypes.func.isRequired,
|
||||
clearLintMessage: PropTypes.func.isRequired,
|
||||
|
@ -543,19 +600,19 @@ IDEView.propTypes = {
|
|||
files: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired
|
||||
content: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
updateFileContent: PropTypes.func.isRequired,
|
||||
selectedFile: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
name: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
setSelectedFile: PropTypes.func.isRequired,
|
||||
htmlFile: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired
|
||||
content: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
dispatchConsoleEvent: PropTypes.func.isRequired,
|
||||
newFile: PropTypes.func.isRequired,
|
||||
|
@ -578,11 +635,11 @@ IDEView.propTypes = {
|
|||
showKeyboardShortcutModal: PropTypes.func.isRequired,
|
||||
closeKeyboardShortcutModal: PropTypes.func.isRequired,
|
||||
toast: PropTypes.shape({
|
||||
isVisible: PropTypes.bool.isRequired
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
autosaveProject: PropTypes.func.isRequired,
|
||||
router: PropTypes.shape({
|
||||
setRouteLeaveHook: PropTypes.func
|
||||
setRouteLeaveHook: PropTypes.func,
|
||||
}).isRequired,
|
||||
route: PropTypes.oneOfType([PropTypes.object, PropTypes.element]).isRequired,
|
||||
setUnsavedChanges: PropTypes.func.isRequired,
|
||||
|
@ -593,7 +650,7 @@ IDEView.propTypes = {
|
|||
setPreviousPath: PropTypes.func.isRequired,
|
||||
console: PropTypes.arrayOf(PropTypes.shape({
|
||||
method: PropTypes.string.isRequired,
|
||||
args: PropTypes.arrayOf(PropTypes.string)
|
||||
args: PropTypes.arrayOf(PropTypes.string),
|
||||
})).isRequired,
|
||||
clearConsole: PropTypes.func.isRequired,
|
||||
showErrorModal: PropTypes.func.isRequired,
|
||||
|
@ -604,13 +661,14 @@ IDEView.propTypes = {
|
|||
startSketch: PropTypes.func.isRequired,
|
||||
openUploadFileModal: PropTypes.func.isRequired,
|
||||
closeUploadFileModal: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
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 !== 'root'),
|
||||
htmlFile: getHTMLFile(state.files),
|
||||
|
@ -620,7 +678,7 @@ function mapStateToProps(state) {
|
|||
user: state.user,
|
||||
project: state.project,
|
||||
toast: state.toast,
|
||||
console: state.console
|
||||
console: state.console,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
|
@ -20,7 +20,7 @@ import { getHTMLFile } from '../reducers/files';
|
|||
|
||||
// Local Imports
|
||||
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 Header from '../../../components/mobile/Header';
|
||||
|
@ -35,8 +35,8 @@ import useAsModal from '../../../components/useAsModal';
|
|||
import { PreferencesIcon } from '../../../common/icons';
|
||||
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`
|
||||
height: ${props => (props.expanded ? remSize(160) : remSize(27))};
|
||||
|
@ -46,35 +46,61 @@ const NavItem = styled.li`
|
|||
position: relative;
|
||||
`;
|
||||
|
||||
const headerNavOptions = [
|
||||
{ icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', },
|
||||
{ icon: PreferencesIcon, title: 'Examples', href: '/mobile/examples' },
|
||||
{ icon: PreferencesIcon, title: 'Original Editor', href: '/', },
|
||||
];
|
||||
|
||||
const getNatOptions = (username = undefined) =>
|
||||
(username
|
||||
? [
|
||||
{ icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', },
|
||||
{ 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: 'Preferences', href: '/mobile/preferences', },
|
||||
{ icon: PreferencesIcon, title: 'Examples', href: '/mobile/p5/sketches' },
|
||||
{ icon: PreferencesIcon, title: 'Original Editor', href: '/', },
|
||||
]
|
||||
);
|
||||
|
||||
const MobileIDEView = (props) => {
|
||||
const {
|
||||
preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage,
|
||||
selectedFile, updateFileContent, files,
|
||||
selectedFile, updateFileContent, files, user, params,
|
||||
closeEditorOptions, showEditorOptions,
|
||||
startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
|
||||
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch
|
||||
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch, getProject, clearPersistedState
|
||||
} = props;
|
||||
|
||||
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 (
|
||||
<Screen fullscreen>
|
||||
<Header
|
||||
title={project.name}
|
||||
subtitle={selectedFile.name}
|
||||
leftButton={
|
||||
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
|
||||
}
|
||||
>
|
||||
<NavItem>
|
||||
<IconButton
|
||||
|
@ -125,7 +151,11 @@ const MobileIDEView = (props) => {
|
|||
</IDEWrapper>
|
||||
|
||||
<Footer>
|
||||
{ide.consoleIsExpanded && <Expander expanded><Console /></Expander>}
|
||||
{ide.consoleIsExpanded && (
|
||||
<Expander expanded>
|
||||
<Console />
|
||||
</Expander>
|
||||
)}
|
||||
<ActionStrip />
|
||||
</Footer>
|
||||
</Screen>
|
||||
|
@ -133,7 +163,6 @@ const MobileIDEView = (props) => {
|
|||
};
|
||||
|
||||
MobileIDEView.propTypes = {
|
||||
|
||||
preferences: PropTypes.shape({
|
||||
fontSize: PropTypes.number.isRequired,
|
||||
autosave: PropTypes.bool.isRequired,
|
||||
|
@ -144,13 +173,13 @@ MobileIDEView.propTypes = {
|
|||
gridOutput: PropTypes.bool.isRequired,
|
||||
soundOutput: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.string.isRequired,
|
||||
autorefresh: PropTypes.bool.isRequired
|
||||
autorefresh: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
ide: PropTypes.shape({
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
|
||||
consoleEvent: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
consoleEvent: PropTypes.array,
|
||||
modalIsVisible: PropTypes.bool.isRequired,
|
||||
sidebarIsExpanded: PropTypes.bool.isRequired,
|
||||
consoleIsExpanded: PropTypes.bool.isRequired,
|
||||
|
@ -172,11 +201,11 @@ MobileIDEView.propTypes = {
|
|||
justOpenedProject: PropTypes.bool.isRequired,
|
||||
errorType: PropTypes.string,
|
||||
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
||||
uploadFileModalVisible: PropTypes.bool.isRequired
|
||||
uploadFileModalVisible: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
editorAccessibility: PropTypes.shape({
|
||||
lintMessages: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
lintMessages: PropTypes.array.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
project: PropTypes.shape({
|
||||
|
@ -184,9 +213,9 @@ MobileIDEView.propTypes = {
|
|||
name: PropTypes.string.isRequired,
|
||||
owner: PropTypes.shape({
|
||||
username: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
id: PropTypes.string,
|
||||
}),
|
||||
updatedAt: PropTypes.string
|
||||
updatedAt: PropTypes.string,
|
||||
}).isRequired,
|
||||
|
||||
startSketch: PropTypes.func.isRequired,
|
||||
|
@ -198,7 +227,7 @@ MobileIDEView.propTypes = {
|
|||
selectedFile: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
name: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
updateFileContent: PropTypes.func.isRequired,
|
||||
|
@ -206,7 +235,7 @@ MobileIDEView.propTypes = {
|
|||
files: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired
|
||||
content: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
|
||||
closeEditorOptions: PropTypes.func.isRequired,
|
||||
|
@ -225,7 +254,7 @@ MobileIDEView.propTypes = {
|
|||
|
||||
console: PropTypes.arrayOf(PropTypes.shape({
|
||||
method: PropTypes.string.isRequired,
|
||||
args: PropTypes.arrayOf(PropTypes.string)
|
||||
args: PropTypes.arrayOf(PropTypes.string),
|
||||
})).isRequired,
|
||||
|
||||
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||
|
@ -235,15 +264,22 @@ MobileIDEView.propTypes = {
|
|||
user: PropTypes.shape({
|
||||
authenticated: PropTypes.bool.isRequired,
|
||||
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
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
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 !== 'root'),
|
||||
htmlFile: getHTMLFile(state.files),
|
||||
|
@ -253,7 +289,7 @@ function mapStateToProps(state) {
|
|||
user: state.user,
|
||||
project: state.project,
|
||||
toast: state.toast,
|
||||
console: state.console
|
||||
console: state.console,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -274,5 +310,4 @@ function mapDispatchToProps(dispatch) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView));
|
||||
|
|
229
client/modules/Mobile/MobileDashboardView.jsx
Normal file
229
client/modules/Mobile/MobileDashboardView.jsx
Normal 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);
|
|
@ -18,11 +18,7 @@ import { getHTMLFile } from '../IDE/reducers/files';
|
|||
import { ExitIcon } from '../../common/icons';
|
||||
import { remSize } from '../../theme';
|
||||
import Footer from '../../components/mobile/Footer';
|
||||
|
||||
const Content = styled.div`
|
||||
z-index: 0;
|
||||
margin-top: ${remSize(68)};
|
||||
`;
|
||||
import Content from './MobileViewContent';
|
||||
|
||||
const MobileSketchView = () => {
|
||||
const { files, ide, preferences } = useSelector(state => state);
|
||||
|
|
10
client/modules/Mobile/MobileViewContent.jsx
Normal file
10
client/modules/Mobile/MobileViewContent.jsx
Normal 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)};
|
||||
`;
|
|
@ -7,8 +7,8 @@ import CopyableInput from '../../IDE/components/CopyableInput';
|
|||
import APIKeyList from './APIKeyList';
|
||||
|
||||
export const APIKeyPropType = PropTypes.shape({
|
||||
id: PropTypes.object.isRequired,
|
||||
token: PropTypes.object,
|
||||
id: PropTypes.object.isRequired, // eslint-disable-line
|
||||
token: PropTypes.object, // eslint-disable-line
|
||||
label: PropTypes.string.isRequired,
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
lastUsedAt: PropTypes.string
|
||||
|
@ -29,7 +29,7 @@ class APIKeyForm extends React.Component {
|
|||
const { keyLabel } = this.state;
|
||||
|
||||
this.setState({
|
||||
keyLabel: ''
|
||||
keyLabel: '',
|
||||
});
|
||||
|
||||
this.props.createApiKey(keyLabel);
|
||||
|
|
|
@ -34,7 +34,9 @@ function AccountForm(props) {
|
|||
id="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>
|
||||
{
|
||||
user.verified !== 'verified' &&
|
||||
|
@ -66,7 +68,9 @@ function AccountForm(props) {
|
|||
defaultValue={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 className="form__field">
|
||||
<label htmlFor="current password" className="form__label">{t('AccountForm.CurrentPassword')}</label>
|
||||
|
@ -77,11 +81,9 @@ function AccountForm(props) {
|
|||
id="currentPassword"
|
||||
{...domOnlyProps(currentPassword)}
|
||||
/>
|
||||
{
|
||||
currentPassword.touched &&
|
||||
currentPassword.error &&
|
||||
{currentPassword.touched && currentPassword.error && (
|
||||
<span className="form-error">{currentPassword.error}</span>
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<p className="form__field">
|
||||
<label htmlFor="new password" className="form__label">{t('AccountForm.NewPassword')}</label>
|
||||
|
@ -92,7 +94,9 @@ function AccountForm(props) {
|
|||
id="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>
|
||||
<Button
|
||||
type="submit"
|
||||
|
@ -105,10 +109,10 @@ function AccountForm(props) {
|
|||
|
||||
AccountForm.propTypes = {
|
||||
fields: PropTypes.shape({
|
||||
username: PropTypes.object.isRequired,
|
||||
email: PropTypes.object.isRequired,
|
||||
currentPassword: PropTypes.object.isRequired,
|
||||
newPassword: PropTypes.object.isRequired,
|
||||
username: PropTypes.object.isRequired, // eslint-disable-line
|
||||
email: PropTypes.object.isRequired, // eslint-disable-line
|
||||
currentPassword: PropTypes.object.isRequired, // eslint-disable-line
|
||||
newPassword: PropTypes.object.isRequired, // eslint-disable-line
|
||||
}).isRequired,
|
||||
user: PropTypes.shape({
|
||||
verified: PropTypes.number.isRequired,
|
||||
|
@ -126,7 +130,7 @@ AccountForm.propTypes = {
|
|||
AccountForm.defaultProps = {
|
||||
submitting: false,
|
||||
pristine: true,
|
||||
invalid: false
|
||||
invalid: false,
|
||||
};
|
||||
|
||||
export default withTranslation()(AccountForm);
|
||||
|
|
|
@ -8,10 +8,16 @@ import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
|||
|
||||
function LoginForm(props) {
|
||||
const {
|
||||
fields: { email, password }, handleSubmit, submitting, pristine
|
||||
fields: { email, password },
|
||||
handleSubmit,
|
||||
submitting,
|
||||
pristine,
|
||||
} = props;
|
||||
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">
|
||||
<label htmlFor="email" className="form__label">{props.t('LoginForm.UsernameOrEmail')}</label>
|
||||
<input
|
||||
|
@ -21,7 +27,9 @@ function LoginForm(props) {
|
|||
id="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 className="form__field">
|
||||
<label htmlFor="password" className="form__label">{props.t('LoginForm.Password')}</label>
|
||||
|
@ -32,7 +40,9 @@ function LoginForm(props) {
|
|||
id="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>
|
||||
<Button
|
||||
type="submit"
|
||||
|
@ -45,8 +55,8 @@ function LoginForm(props) {
|
|||
|
||||
LoginForm.propTypes = {
|
||||
fields: PropTypes.shape({
|
||||
email: PropTypes.object.isRequired,
|
||||
password: PropTypes.object.isRequired
|
||||
email: PropTypes.object.isRequired, // eslint-disable-line
|
||||
password: PropTypes.object.isRequired, // eslint-disable-line
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
validateAndLoginUser: PropTypes.func.isRequired,
|
||||
|
@ -60,7 +70,7 @@ LoginForm.propTypes = {
|
|||
LoginForm.defaultProps = {
|
||||
submitting: false,
|
||||
pristine: true,
|
||||
invalid: false
|
||||
invalid: false,
|
||||
};
|
||||
|
||||
export default withTranslation()(LoginForm);
|
||||
|
|
|
@ -10,7 +10,10 @@ function NewPasswordForm(props) {
|
|||
t
|
||||
} = props;
|
||||
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">
|
||||
<label htmlFor="password" className="form__label">{t('NewPasswordForm.Title')}</label>
|
||||
<input
|
||||
|
@ -20,7 +23,9 @@ function NewPasswordForm(props) {
|
|||
id="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 className="form__field">
|
||||
<label htmlFor="confirm password" className="form__label">{t('NewPasswordForm.ConfirmPassword')}</label>
|
||||
|
@ -31,11 +36,9 @@ function NewPasswordForm(props) {
|
|||
id="confirm password"
|
||||
{...domOnlyProps(confirmPassword)}
|
||||
/>
|
||||
{
|
||||
confirmPassword.touched &&
|
||||
confirmPassword.error &&
|
||||
{confirmPassword.touched && confirmPassword.error && (
|
||||
<span className="form-error">{confirmPassword.error}</span>
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<Button type="submit" disabled={submitting || invalid || pristine}>{t('NewPasswordForm.SubmitSetNewPassword')}</Button>
|
||||
</form>
|
||||
|
@ -44,8 +47,8 @@ function NewPasswordForm(props) {
|
|||
|
||||
NewPasswordForm.propTypes = {
|
||||
fields: PropTypes.shape({
|
||||
password: PropTypes.object.isRequired,
|
||||
confirmPassword: PropTypes.object.isRequired
|
||||
password: PropTypes.object.isRequired, // eslint-disable-line
|
||||
confirmPassword: PropTypes.object.isRequired, // eslint-disable-line
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
updatePassword: PropTypes.func.isRequired,
|
||||
|
@ -61,7 +64,7 @@ NewPasswordForm.propTypes = {
|
|||
NewPasswordForm.defaultProps = {
|
||||
invalid: false,
|
||||
pristine: true,
|
||||
submitting: false
|
||||
submitting: false,
|
||||
};
|
||||
|
||||
export default withTranslation()(NewPasswordForm);
|
||||
|
|
|
@ -9,7 +9,10 @@ function ResetPasswordForm(props) {
|
|||
fields: { email }, handleSubmit, submitting, invalid, pristine, t
|
||||
} = props;
|
||||
return (
|
||||
<form className="form" onSubmit={handleSubmit(props.initiateResetPassword.bind(this))}>
|
||||
<form
|
||||
className="form"
|
||||
onSubmit={handleSubmit(props.initiateResetPassword.bind(this))}
|
||||
>
|
||||
<p className="form__field">
|
||||
<label htmlFor="email" className="form__label">{t('ResetPasswordForm.Email')}</label>
|
||||
<input
|
||||
|
@ -19,7 +22,9 @@ function ResetPasswordForm(props) {
|
|||
id="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>
|
||||
<Button
|
||||
type="submit"
|
||||
|
@ -48,7 +53,7 @@ ResetPasswordForm.propTypes = {
|
|||
ResetPasswordForm.defaultProps = {
|
||||
submitting: false,
|
||||
pristine: true,
|
||||
invalid: false
|
||||
invalid: false,
|
||||
};
|
||||
|
||||
export default withTranslation()(ResetPasswordForm);
|
||||
|
|
|
@ -9,10 +9,17 @@ function SignupForm(props) {
|
|||
const {
|
||||
fields: {
|
||||
username, email, password, confirmPassword
|
||||
}, handleSubmit, submitting, invalid, pristine
|
||||
},
|
||||
handleSubmit,
|
||||
submitting,
|
||||
invalid,
|
||||
pristine,
|
||||
} = props;
|
||||
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">
|
||||
<label htmlFor="username" className="form__label">{props.t('SignupForm.Title')}</label>
|
||||
<input
|
||||
|
@ -22,7 +29,9 @@ function SignupForm(props) {
|
|||
id="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 className="form__field">
|
||||
<label htmlFor="email" className="form__label">{props.t('SignupForm.Email')}</label>
|
||||
|
@ -33,7 +42,9 @@ function SignupForm(props) {
|
|||
id="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 className="form__field">
|
||||
<label htmlFor="password" className="form__label">{props.t('SignupForm.Password')}</label>
|
||||
|
@ -44,7 +55,9 @@ function SignupForm(props) {
|
|||
id="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 className="form__field">
|
||||
<label htmlFor="confirm password" className="form__label">{props.t('SignupForm.ConfirmPassword')}</label>
|
||||
|
@ -55,11 +68,9 @@ function SignupForm(props) {
|
|||
id="confirm password"
|
||||
{...domOnlyProps(confirmPassword)}
|
||||
/>
|
||||
{
|
||||
confirmPassword.touched &&
|
||||
confirmPassword.error &&
|
||||
{confirmPassword.touched && confirmPassword.error && (
|
||||
<span className="form-error">{confirmPassword.error}</span>
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<Button
|
||||
type="submit"
|
||||
|
@ -72,10 +83,10 @@ function SignupForm(props) {
|
|||
|
||||
SignupForm.propTypes = {
|
||||
fields: PropTypes.shape({
|
||||
username: PropTypes.object.isRequired,
|
||||
email: PropTypes.object.isRequired,
|
||||
password: PropTypes.object.isRequired,
|
||||
confirmPassword: PropTypes.object.isRequired
|
||||
username: PropTypes.object.isRequired, // eslint-disable-line
|
||||
email: PropTypes.object.isRequired, // eslint-disable-line
|
||||
password: PropTypes.object.isRequired, // eslint-disable-line
|
||||
confirmPassword: PropTypes.object.isRequired, // eslint-disable-line
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
signUpUser: PropTypes.func.isRequired,
|
||||
|
@ -89,7 +100,7 @@ SignupForm.propTypes = {
|
|||
SignupForm.defaultProps = {
|
||||
submitting: false,
|
||||
pristine: true,
|
||||
invalid: false
|
||||
invalid: false,
|
||||
};
|
||||
|
||||
export default withTranslation()(SignupForm);
|
||||
|
|
|
@ -15,6 +15,7 @@ import AccountView from './modules/User/pages/AccountView';
|
|||
import CollectionView from './modules/User/pages/CollectionView';
|
||||
import DashboardView from './modules/User/pages/DashboardView';
|
||||
import createRedirectWithUsername from './components/createRedirectWithUsername';
|
||||
import MobileDashboardView from './modules/Mobile/MobileDashboardView';
|
||||
import { getUser } from './modules/User/actions';
|
||||
import { stopSketch } from './modules/IDE/actions/ide';
|
||||
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
|
||||
const onRouteChange = (store) => {
|
||||
const path = window.location.pathname;
|
||||
if (path.includes('/mobile')) return;
|
||||
if (path.includes('/mobile/preview')) return;
|
||||
|
||||
store.dispatch(stopSketch());
|
||||
};
|
||||
|
@ -57,9 +58,17 @@ const routes = store => (
|
|||
<Route path="/:username/collections/:collection_id" component={CollectionView} />
|
||||
<Route path="/about" component={IDEView} />
|
||||
|
||||
<Route path="/mobile" component={MobileIDEView} />
|
||||
|
||||
<Route path="/mobile/preview" component={MobileSketchView} />
|
||||
<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>
|
||||
);
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ export const prop = key => (props) => {
|
|||
return value;
|
||||
};
|
||||
|
||||
|
||||
export default {
|
||||
[Theme.light]: {
|
||||
colors,
|
||||
|
@ -103,6 +104,14 @@ export default {
|
|||
border: grays.middleLight
|
||||
},
|
||||
Separator: grays.middleLight,
|
||||
|
||||
TabHighlight: colors.p5jsPink,
|
||||
SketchList: {
|
||||
background: grays.lighter,
|
||||
card: {
|
||||
background: grays.lighter
|
||||
}
|
||||
}
|
||||
},
|
||||
[Theme.dark]: {
|
||||
colors,
|
||||
|
@ -148,6 +157,14 @@ export default {
|
|||
border: grays.middleDark
|
||||
},
|
||||
Separator: grays.middleDark,
|
||||
|
||||
TabHighlight: colors.p5jsPink,
|
||||
SketchList: {
|
||||
background: grays.darker,
|
||||
card: {
|
||||
background: grays.dark
|
||||
}
|
||||
}
|
||||
},
|
||||
[Theme.contrast]: {
|
||||
colors,
|
||||
|
@ -193,5 +210,13 @@ export default {
|
|||
border: grays.middleDark
|
||||
},
|
||||
Separator: grays.middleDark,
|
||||
|
||||
TabHighlight: grays.darker,
|
||||
SketchList: {
|
||||
background: colors.yellow,
|
||||
card: {
|
||||
background: grays.dark
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -7,6 +7,8 @@ import { collectionForUserExists } from '../controllers/collection.controller';
|
|||
|
||||
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
|
||||
// until i figure out isomorphic rendering
|
||||
|
||||
|
@ -114,20 +116,6 @@ router.get('/about', (req, res) => {
|
|||
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) => {
|
||||
userExists(req.params.username, (exists) => {
|
||||
const isLoggedInUser = req.user && req.user.username === req.params.username;
|
||||
|
@ -156,4 +144,5 @@ router.get('/:username/collections', (req, res) => {
|
|||
));
|
||||
});
|
||||
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -132,6 +132,10 @@ app.get(
|
|||
// isomorphic rendering
|
||||
app.use('/', serverRoutes);
|
||||
|
||||
if (process.env.MOBILE_ENABLED) {
|
||||
app.use('/mobile', serverRoutes);
|
||||
}
|
||||
|
||||
app.use(assetRoutes);
|
||||
|
||||
app.use('/', embedRoutes);
|
||||
|
|
Loading…
Reference in a new issue