Merge pull request #1472 from ghalestrilo/feature/mobile-settings-screen
Mobile Preferences Screen Prototype
This commit is contained in:
commit
69bd6cb4a0
9 changed files with 387 additions and 46 deletions
|
@ -1,14 +1,16 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { prop, remSize } from '../../theme';
|
||||
|
||||
const background = prop('MobilePanel.default.background');
|
||||
const textColor = prop('primaryTextColor');
|
||||
|
||||
const Header = styled.div`
|
||||
|
||||
const HeaderDiv = styled.div`
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background: ${background};
|
||||
background: ${props => (props.transparent ? 'transparent' : background)};
|
||||
color: ${textColor};
|
||||
padding: ${remSize(12)};
|
||||
padding-left: ${remSize(16)};
|
||||
|
@ -23,8 +25,56 @@ const Header = styled.div`
|
|||
|
||||
// TODO:
|
||||
svg {
|
||||
height: 2rem;
|
||||
max-height: ${remSize(32)};
|
||||
padding: ${remSize(4)}
|
||||
}
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
margin-left: ${props => (props.leftButton ? remSize(32) : remSize(4))};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
margin-left: ${remSize(4)};
|
||||
margin-right: auto;
|
||||
|
||||
${props => props.padded && `h2{
|
||||
padding-top: ${remSize(10)};
|
||||
padding-bottom: ${remSize(10)};
|
||||
}`}
|
||||
`;
|
||||
|
||||
const Header = ({
|
||||
title, subtitle, leftButton, children, transparent
|
||||
}) => (
|
||||
<HeaderDiv transparent={transparent}>
|
||||
{leftButton}
|
||||
<TitleContainer padded={subtitle === null}>
|
||||
{title && <h2>{title}</h2>}
|
||||
{subtitle && <h3>{subtitle}</h3>}
|
||||
</TitleContainer>
|
||||
<IconContainer>
|
||||
{children}
|
||||
</IconContainer>
|
||||
</HeaderDiv>
|
||||
);
|
||||
|
||||
Header.propTypes = {
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
leftButton: PropTypes.element,
|
||||
children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]),
|
||||
transparent: PropTypes.bool
|
||||
};
|
||||
|
||||
Header.defaultProps = {
|
||||
title: null,
|
||||
subtitle: null,
|
||||
leftButton: null,
|
||||
children: [],
|
||||
transparent: false
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
|
|
@ -2,9 +2,10 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import Button from '../../common/Button';
|
||||
import { remSize } from '../../theme';
|
||||
|
||||
const ButtonWrapper = styled(Button)`
|
||||
width: 3rem;
|
||||
width: ${remSize(48)};
|
||||
> svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Screen = ({ children }) => (
|
||||
<div className="fullscreen-preview">
|
||||
const Screen = ({ children, fullscreen }) => (
|
||||
<div className={fullscreen && 'fullscreen-preview'}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
Screen.defaultProps = {
|
||||
fullscreen: false
|
||||
};
|
||||
|
||||
Screen.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
children: PropTypes.node.isRequired,
|
||||
fullscreen: PropTypes.bool
|
||||
};
|
||||
|
||||
export default Screen;
|
||||
|
|
67
client/components/mobile/PreferencePicker.jsx
Normal file
67
client/components/mobile/PreferencePicker.jsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { prop, remSize } from '../../theme';
|
||||
|
||||
|
||||
const PreferenceTitle = styled.h4.attrs(props => ({ ...props, className: 'preference__title' }))`
|
||||
color: ${prop('primaryTextColor')};
|
||||
`;
|
||||
|
||||
const Preference = styled.div.attrs(props => ({ ...props, className: 'preference' }))`
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: baseline !important;
|
||||
justify-items: space-between;
|
||||
`;
|
||||
|
||||
const OptionLabel = styled.label.attrs({ className: 'preference__option' })`
|
||||
font-size: ${remSize(14)} !important;
|
||||
`;
|
||||
|
||||
const PreferencePicker = ({
|
||||
title, value, onSelect, options,
|
||||
}) => (
|
||||
<Preference>
|
||||
<PreferenceTitle>{title}</PreferenceTitle>
|
||||
<div className="preference__options">
|
||||
{options.map(option => (
|
||||
<React.Fragment key={`${option.name}-${option.id}`} >
|
||||
<input
|
||||
type="radio"
|
||||
onChange={() => onSelect(option.value)}
|
||||
aria-label={option.ariaLabel}
|
||||
name={option.name}
|
||||
key={`${option.name}-${option.id}-input`}
|
||||
id={option.id}
|
||||
className="preference__radio-button"
|
||||
value={option.value}
|
||||
checked={value === option.value}
|
||||
/>
|
||||
<OptionLabel
|
||||
key={`${option.name}-${option.id}-label`}
|
||||
htmlFor={option.id}
|
||||
>
|
||||
{option.label}
|
||||
</OptionLabel>
|
||||
</React.Fragment>))}
|
||||
</div>
|
||||
</Preference>
|
||||
);
|
||||
|
||||
PreferencePicker.defaultProps = {
|
||||
options: []
|
||||
};
|
||||
|
||||
PreferencePicker.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
ariaLabel: PropTypes.string,
|
||||
})),
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default PreferencePicker;
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import { useState } from 'react';
|
||||
|
@ -27,16 +25,10 @@ import Header from '../../../components/mobile/Header';
|
|||
import Screen from '../../../components/mobile/MobileScreen';
|
||||
import Footer from '../../../components/mobile/Footer';
|
||||
import IDEWrapper from '../../../components/mobile/IDEWrapper';
|
||||
import { remSize } from '../../../theme';
|
||||
|
||||
const IconContainer = styled.div`
|
||||
margin-left: ${remSize(32)};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id);
|
||||
|
||||
const IDEViewMobile = (props) => {
|
||||
const MobileIDEView = (props) => {
|
||||
const {
|
||||
preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage,
|
||||
selectedFile, updateFileContent, files,
|
||||
|
@ -49,22 +41,21 @@ const IDEViewMobile = (props) => {
|
|||
const [overlay, setOverlay] = useState(null); // eslint-disable-line
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<Header>
|
||||
<Screen fullscreen>
|
||||
<Header
|
||||
title={project.name}
|
||||
subtitle={selectedFile.name}
|
||||
leftButton={
|
||||
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
|
||||
<div style={{ marginLeft: '1rem' }}>
|
||||
<h2>{project.name}</h2>
|
||||
<h3>{selectedFile.name}</h3>
|
||||
</div>
|
||||
|
||||
<IconContainer>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
to="/mobile/preferences"
|
||||
onClick={() => setOverlay('preferences')}
|
||||
icon={PreferencesIcon}
|
||||
aria-label="Open preferences menu"
|
||||
/>
|
||||
<IconButton to="/mobile/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" />
|
||||
</IconContainer>
|
||||
</Header>
|
||||
|
||||
<IDEWrapper>
|
||||
|
@ -109,7 +100,7 @@ const IDEViewMobile = (props) => {
|
|||
};
|
||||
|
||||
|
||||
IDEViewMobile.propTypes = {
|
||||
MobileIDEView.propTypes = {
|
||||
|
||||
preferences: PropTypes.shape({
|
||||
fontSize: PropTypes.number.isRequired,
|
||||
|
@ -256,4 +247,4 @@ function mapDispatchToProps(dispatch) {
|
|||
}
|
||||
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEViewMobile));
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView));
|
226
client/modules/Mobile/MobilePreferences.jsx
Normal file
226
client/modules/Mobile/MobilePreferences.jsx
Normal file
|
@ -0,0 +1,226 @@
|
|||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as PreferencesActions from '../IDE/actions/preferences';
|
||||
import * as IdeActions from '../IDE/actions/ide';
|
||||
|
||||
import IconButton from '../../components/mobile/IconButton';
|
||||
import Screen from '../../components/mobile/MobileScreen';
|
||||
import Header from '../../components/mobile/Header';
|
||||
import PreferencePicker from '../../components/mobile/PreferencePicker';
|
||||
import { ExitIcon } from '../../common/icons';
|
||||
import { remSize, prop } from '../../theme';
|
||||
|
||||
const Content = styled.div`
|
||||
z-index: 0;
|
||||
margin-top: ${remSize(68)};
|
||||
`;
|
||||
|
||||
|
||||
const SettingsHeader = styled(Header)`
|
||||
background: transparent;
|
||||
`;
|
||||
|
||||
const SectionHeader = styled.h2`
|
||||
color: ${prop('primaryTextColor')};
|
||||
padding-top: ${remSize(32)};
|
||||
`;
|
||||
|
||||
const SectionSubeader = styled.h3`
|
||||
color: ${prop('primaryTextColor')};
|
||||
`;
|
||||
|
||||
|
||||
const MobilePreferences = (props) => {
|
||||
const {
|
||||
setTheme, setAutosave, setLinewrap, setTextOutput, setGridOutput, setSoundOutput, lineNumbers, lintWarning
|
||||
} = props;
|
||||
const {
|
||||
theme, autosave, linewrap, textOutput, gridOutput, soundOutput, setLineNumbers, setLintWarning
|
||||
} = props;
|
||||
|
||||
const generalSettings = [
|
||||
{
|
||||
title: 'Theme',
|
||||
value: theme,
|
||||
options: [
|
||||
{
|
||||
value: 'light', label: 'light', ariaLabel: 'light theme on', name: 'light theme', id: 'light-theme-on'
|
||||
},
|
||||
{
|
||||
value: 'dark', label: 'dark', ariaLabel: 'dark theme on', name: 'dark theme', id: 'dark-theme-on'
|
||||
},
|
||||
{
|
||||
value: 'contrast',
|
||||
label: 'contrast',
|
||||
ariaLabel: 'contrast theme on',
|
||||
name: 'contrast theme',
|
||||
id: 'contrast-theme-on'
|
||||
}
|
||||
],
|
||||
onSelect: x => setTheme(x) // setTheme
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Autosave',
|
||||
value: autosave,
|
||||
options: [
|
||||
{
|
||||
value: true, label: 'On', ariaLabel: 'autosave on', name: 'autosave', id: 'autosave-on'
|
||||
},
|
||||
{
|
||||
value: false, label: 'Off', ariaLabel: 'autosave off', name: 'autosave', id: 'autosave-off'
|
||||
},
|
||||
],
|
||||
onSelect: x => setAutosave(x) // setAutosave
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Word Wrap',
|
||||
value: linewrap,
|
||||
options: [
|
||||
{
|
||||
value: true, label: 'On', ariaLabel: 'linewrap on', name: 'linewrap', id: 'linewrap-on'
|
||||
},
|
||||
{
|
||||
value: false, label: 'Off', ariaLabel: 'linewrap off', name: 'linewrap', id: 'linewrap-off'
|
||||
},
|
||||
],
|
||||
onSelect: x => setLinewrap(x)
|
||||
}
|
||||
];
|
||||
|
||||
const outputSettings = [
|
||||
{
|
||||
title: 'Plain-text',
|
||||
value: textOutput,
|
||||
options: [
|
||||
{
|
||||
value: true, label: 'On', ariaLabel: 'text output on', name: 'text output', id: 'text-output-on'
|
||||
},
|
||||
{
|
||||
value: false, label: 'Off', ariaLabel: 'text output off', name: 'text output', id: 'text-output-off'
|
||||
},
|
||||
],
|
||||
onSelect: x => setTextOutput(x)
|
||||
},
|
||||
{
|
||||
title: 'Table-text',
|
||||
value: gridOutput,
|
||||
options: [
|
||||
{
|
||||
value: true, label: 'On', ariaLabel: 'table output on', name: 'table output', id: 'table-output-on'
|
||||
},
|
||||
{
|
||||
value: false, label: 'Off', ariaLabel: 'table output off', name: 'table output', id: 'table-output-off'
|
||||
},
|
||||
],
|
||||
onSelect: x => setGridOutput(x)
|
||||
},
|
||||
{
|
||||
title: 'Sound',
|
||||
value: soundOutput,
|
||||
options: [
|
||||
{
|
||||
value: true, label: 'On', ariaLabel: 'sound output on', name: 'sound output', id: 'sound-output-on'
|
||||
},
|
||||
{
|
||||
value: false, label: 'Off', ariaLabel: 'sound output off', name: 'sound output', id: 'sound-output-off'
|
||||
},
|
||||
],
|
||||
onSelect: x => setSoundOutput(x)
|
||||
},
|
||||
];
|
||||
|
||||
const accessibilitySettings = [
|
||||
{
|
||||
title: 'Line Numbers',
|
||||
value: lineNumbers,
|
||||
options: [
|
||||
{
|
||||
value: true, label: 'On', ariaLabel: 'line numbers on', name: 'line numbers', id: 'line-numbers-on'
|
||||
},
|
||||
{
|
||||
value: false, label: 'Off', ariaLabel: 'line numbers off', name: 'line numbers', id: 'line-numbers-off'
|
||||
},
|
||||
],
|
||||
onSelect: x => setLineNumbers(x)
|
||||
},
|
||||
{
|
||||
title: 'Lint Warning Sound',
|
||||
value: lintWarning,
|
||||
options: [
|
||||
{
|
||||
value: true, label: 'On', ariaLabel: 'lint warning on', name: 'lint warning', id: 'lint-warning-on'
|
||||
},
|
||||
{
|
||||
value: false, label: 'Off', ariaLabel: 'lint warning off', name: 'lint warning', id: 'lint-warning-off'
|
||||
},
|
||||
],
|
||||
onSelect: x => setLintWarning(x)
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Screen fullscreen>
|
||||
<section>
|
||||
<SettingsHeader transparent title="Preferences">
|
||||
|
||||
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to ide view" />
|
||||
</SettingsHeader>
|
||||
<section className="preferences">
|
||||
<Content>
|
||||
<SectionHeader>General Settings</SectionHeader>
|
||||
{ generalSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||
|
||||
<SectionHeader>Accessibility</SectionHeader>
|
||||
{ accessibilitySettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||
|
||||
<SectionHeader>Accessible Output</SectionHeader>
|
||||
<SectionSubeader>Used with screen reader</SectionSubeader>
|
||||
{ outputSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||
|
||||
</Content>
|
||||
</section>
|
||||
</section>
|
||||
</Screen>);
|
||||
};
|
||||
|
||||
|
||||
MobilePreferences.propTypes = {
|
||||
fontSize: PropTypes.number.isRequired,
|
||||
lineNumbers: PropTypes.bool.isRequired,
|
||||
autosave: PropTypes.bool.isRequired,
|
||||
linewrap: PropTypes.bool.isRequired,
|
||||
textOutput: PropTypes.bool.isRequired,
|
||||
gridOutput: PropTypes.bool.isRequired,
|
||||
soundOutput: PropTypes.bool.isRequired,
|
||||
lintWarning: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.string.isRequired,
|
||||
|
||||
setLinewrap: PropTypes.func.isRequired,
|
||||
setLintWarning: PropTypes.func.isRequired,
|
||||
setTheme: PropTypes.func.isRequired,
|
||||
setFontSize: PropTypes.func.isRequired,
|
||||
setLineNumbers: PropTypes.func.isRequired,
|
||||
setAutosave: PropTypes.func.isRequired,
|
||||
setTextOutput: PropTypes.func.isRequired,
|
||||
setGridOutput: PropTypes.func.isRequired,
|
||||
setSoundOutput: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
...state.preferences,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({
|
||||
...PreferencesActions,
|
||||
...IdeActions
|
||||
}, dispatch);
|
||||
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobilePreferences));
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
@ -48,14 +47,13 @@ const MobileSketchView = (props) => {
|
|||
const { preferences, ide } = props;
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<Header>
|
||||
<Screen fullscreen>
|
||||
<Header
|
||||
leftButton={
|
||||
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
|
||||
<div style={{ marginLeft: '1rem' }}>
|
||||
<h2>{projectName}</h2>
|
||||
<h3><br /></h3>
|
||||
</div>
|
||||
</Header>
|
||||
}
|
||||
title={projectName}
|
||||
/>
|
||||
<Content>
|
||||
<PreviewFrame
|
||||
htmlFile={htmlFile}
|
||||
|
|
|
@ -2,8 +2,9 @@ import { Route, IndexRoute } from 'react-router';
|
|||
import React from 'react';
|
||||
import App from './modules/App/App';
|
||||
import IDEView from './modules/IDE/pages/IDEView';
|
||||
import IDEViewMobile from './modules/IDE/pages/IDEViewMobile';
|
||||
import MobileIDEView from './modules/IDE/pages/MobileIDEView';
|
||||
import MobileSketchView from './modules/Mobile/MobileSketchView';
|
||||
import MobilePreferences from './modules/Mobile/MobilePreferences';
|
||||
import FullView from './modules/IDE/pages/FullView';
|
||||
import LoginView from './modules/User/pages/LoginView';
|
||||
import SignupView from './modules/User/pages/SignupView';
|
||||
|
@ -56,8 +57,9 @@ const routes = store => (
|
|||
<Route path="/:username/collections/:collection_id" component={CollectionView} />
|
||||
<Route path="/about" component={IDEView} />
|
||||
|
||||
<Route path="/mobile" component={IDEViewMobile} />
|
||||
<Route path="/mobile" component={MobileIDEView} />
|
||||
<Route path="/mobile/preview" component={MobileSketchView} />
|
||||
<Route path="/mobile/preferences" component={MobilePreferences} />
|
||||
</Route>
|
||||
);
|
||||
|
||||
|
|
|
@ -123,7 +123,7 @@ if (process.env.MOBILE_ENABLED) {
|
|||
res.send(renderIndex());
|
||||
});
|
||||
|
||||
router.get('/mobile/*', (req, res) => {
|
||||
router.get('/mobile/preferences', (req, res) => {
|
||||
res.send(renderIndex());
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue