diff --git a/client/components/mobile/Tab.jsx b/client/components/mobile/Tab.jsx new file mode 100644 index 00000000..20a158b4 --- /dev/null +++ b/client/components/mobile/Tab.jsx @@ -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%; +`; diff --git a/client/components/mobile/TabSwitcher.jsx b/client/components/mobile/TabSwitcher.jsx new file mode 100644 index 00000000..15a3bb0e --- /dev/null +++ b/client/components/mobile/TabSwitcher.jsx @@ -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')}; +`; + diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx index f7a645e6..f3119632 100644 --- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx @@ -214,8 +214,8 @@ class CollectionListRowBase extends React.Component { {(!mobile) && {format(new Date(collection.createdAt), 'MMM D, YYYY')}} - {formatDateCell(collection.updatedAt)} - {(collection.items || []).length} + {mobile && 'Updated: '}{formatDateCell(collection.updatedAt)} + {mobile && '# sketches: '}{(collection.items || []).length} {this.renderActions()} diff --git a/client/modules/IDE/components/NewFileForm.jsx b/client/modules/IDE/components/NewFileForm.jsx index eb0740e1..40725879 100644 --- a/client/modules/IDE/components/NewFileForm.jsx +++ b/client/modules/IDE/components/NewFileForm.jsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; +import { withTranslation } from 'react-i18next'; import { domOnlyProps } from '../../../utils/reduxFormUtils'; import Button from '../../../common/Button'; @@ -35,14 +36,17 @@ class NewFileForm extends React.Component { className="new-file-form__name-input" id="name" type="text" - placeholder="Name" + placeholder={this.props.t('NewFileForm.Placeholder')} maxLength="128" {...domOnlyProps(name)} ref={(element) => { this.fileName = element; }} /> - + {name.touched && name.error && ( {name.error} @@ -59,6 +63,7 @@ NewFileForm.propTypes = { handleSubmit: PropTypes.func.isRequired, createFile: PropTypes.func.isRequired, focusOnModal: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; -export default NewFileForm; +export default withTranslation()(NewFileForm); diff --git a/client/modules/IDE/components/NewFileModal.jsx b/client/modules/IDE/components/NewFileModal.jsx index 412d9895..791cb0af 100644 --- a/client/modules/IDE/components/NewFileModal.jsx +++ b/client/modules/IDE/components/NewFileModal.jsx @@ -3,6 +3,8 @@ import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators, compose } from 'redux'; import { reduxForm } from 'redux-form'; +import { withTranslation } from 'react-i18next'; +import i18n from 'i18next'; import NewFileForm from './NewFileForm'; import { closeNewFileModal } from '../actions/ide'; import { createFile } from '../actions/files'; @@ -33,11 +35,11 @@ class NewFileModal extends React.Component {
{ this.modal = element; }}>
-

Create File

+

{this.props.t('NewFileModal.Title')}

@@ -54,16 +56,17 @@ class NewFileModal extends React.Component { NewFileModal.propTypes = { createFile: PropTypes.func.isRequired, - closeNewFileModal: PropTypes.func.isRequired + closeNewFileModal: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; function validate(formProps) { const errors = {}; if (!formProps.name) { - errors.name = 'Please enter a name'; + errors.name = i18n.t('NewFileModal.EnterName'); } else if (!formProps.name.match(CREATE_FILE_REGEX)) { - errors.name = 'Invalid file type. Valid extensions are .js, .css, .json, .txt, .csv, .tsv, .frag, and .vert.'; + errors.name = i18n.t('NewFileModal.InvalidType'); } return errors; @@ -77,11 +80,11 @@ function mapDispatchToProps(dispatch) { return bindActionCreators({ createFile, closeNewFileModal }, dispatch); } -export default compose( +export default withTranslation()(compose( connect(mapStateToProps, mapDispatchToProps), reduxForm({ form: 'new-file', fields: ['name'], validate }) -)(NewFileModal); +)(NewFileModal)); diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx index 04c590fe..db307861 100644 --- a/client/modules/IDE/components/NewFolderForm.jsx +++ b/client/modules/IDE/components/NewFolderForm.jsx @@ -1,9 +1,11 @@ import PropTypes from 'prop-types'; import React from 'react'; +import { withTranslation } from 'react-i18next'; import { domOnlyProps } from '../../../utils/reduxFormUtils'; import Button from '../../../common/Button'; + class NewFolderForm extends React.Component { constructor(props) { super(props); @@ -35,13 +37,14 @@ class NewFolderForm extends React.Component { id="name" type="text" maxLength="128" - placeholder="Name" - ref={(element) => { - this.fileName = element; - }} + placeholder={this.props.t('NewFolderForm.Placeholder')} + ref={(element) => { this.fileName = element; }} {...domOnlyProps(name)} /> - +
{name.touched && name.error && ( {name.error} @@ -60,9 +63,10 @@ NewFolderForm.propTypes = { closeModal: PropTypes.func.isRequired, submitting: PropTypes.bool, pristine: PropTypes.bool, + t: PropTypes.func.isRequired }; NewFolderForm.defaultProps = { submitting: false, pristine: true, }; -export default NewFolderForm; +export default withTranslation()(NewFolderForm); diff --git a/client/modules/IDE/components/NewFolderModal.jsx b/client/modules/IDE/components/NewFolderModal.jsx index 13029d6c..bf897afa 100644 --- a/client/modules/IDE/components/NewFolderModal.jsx +++ b/client/modules/IDE/components/NewFolderModal.jsx @@ -1,6 +1,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import { reduxForm } from 'redux-form'; +import { withTranslation } from 'react-i18next'; +import i18n from 'i18next'; import NewFolderForm from './NewFolderForm'; import ExitIcon from '../../../images/exit.svg'; @@ -15,11 +17,11 @@ class NewFolderModal extends React.Component {
{ this.newFolderModal = element; }} >
-

Create Folder

+

{this.props.t('NewFolderModal.Title')}

@@ -32,23 +34,24 @@ class NewFolderModal extends React.Component { } NewFolderModal.propTypes = { - closeModal: PropTypes.func.isRequired + closeModal: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; function validate(formProps) { const errors = {}; if (!formProps.name) { - errors.name = 'Please enter a name'; + errors.name = i18n.t('NewFolderModal.EnterName'); } else if (formProps.name.trim().length === 0) { - errors.name = 'Folder name cannot contain only spaces'; + errors.name = i18n.t('NewFolderModal.EmptyName'); } else if (formProps.name.match(/\.+/i)) { - errors.name = 'Folder name cannot contain an extension'; + errors.name = i18n.t('NewFolderModal.InvalidExtension'); } return errors; } -export default reduxForm({ +export default withTranslation()(reduxForm({ form: 'new-folder', fields: ['name'], validate -})(NewFolderModal); +})(NewFolderModal)); diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index f69d802e..4aacd8c2 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -293,8 +293,8 @@ class SketchListRowBase extends React.Component { {name} - {formatDateCell(sketch.createdAt, mobile)} - {formatDateCell(sketch.updatedAt, mobile)} + {mobile && 'Created: '}{formatDateCell(sketch.createdAt, mobile)} + {mobile && 'Updated: '}{formatDateCell(sketch.updatedAt, mobile)} {this.renderDropdown()} ); diff --git a/client/modules/Mobile/MobileDashboardView.jsx b/client/modules/Mobile/MobileDashboardView.jsx index ac4205d2..aeb66d5c 100644 --- a/client/modules/Mobile/MobileDashboardView.jsx +++ b/client/modules/Mobile/MobileDashboardView.jsx @@ -2,27 +2,31 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { useSelector } from 'react-redux'; -import { withRouter, Link } from 'react-router'; +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 } from '../../common/icons'; +import { ExitIcon, MoreIcon } from '../../common/icons'; import Footer from '../../components/mobile/Footer'; -import { prop, remSize } from '../../theme'; +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'; const ContentWrapper = styled(Content)` table { table-layout: fixed; - /* white-space: nowrap; */ } td ,thead button { @@ -31,38 +35,57 @@ const ContentWrapper = styled(Content)` text-align: left; }; - thead th { padding-left: 0; } - - thead th:not(:first-child), tbody td { - width: ${remSize(54)}; - } - - tbody th { font-weight: bold; }; tbody th { - font-size: ${remSize(12)}; + font-size: ${remSize(16)}; width: 100%; - padding-right: ${remSize(12)} + padding-right: ${remSize(12)}; + font-weight: bold; + display: flex; + grid-area: name; }; - tbody td { - text-align: center; + tbody td, thead th { + justify-self: stretch; + align-self: flex-end; + color: ${prop('primaryTextColor')} } - .sketch-list__sort-button { padding: 0 } - tbody { - height: ${remSize(48)}; + tbody td { justify-self: center; padding-left: ${remSize(12)}} + + thead th svg { margin-left: ${remSize(8)} } + + tbody td:last-child { justify-self: end; text-align: end; }; + .sketches-table .sketch-list__dropdown-column { min-width: unset }; + + tbody { height: ${remSize(48)}; } + + .sketches-table-container { + padding-bottom: ${remSize(160)}; + background: ${prop('SketchList.background')}; + } + .sketches-table__row { + background: ${prop('SketchList.card.background')} !important; height: auto } - .sketches-table-container { padding-bottom: ${remSize(160)} } -`; + tr { + align-self: start; + display: grid; + box-shadow: 0 0 18px 0 ${prop('shadowColor')}; + }; -const FooterTab = styled(Link)` - background: ${props => prop(props.selected ? 'backgroundColor' : 'MobilePanel.default.foreground')}; - color: ${props => prop(`MobilePanel.default.${props.selected ? 'foreground' : 'background'}`)}; - padding: ${remSize(8)} ${remSize(16)}; - width: 100%; - display: flex; + thead tr { + grid-template-columns: auto ${remSize(100)} ${remSize(100)} 0fr; + } + + tbody tr { + padding: ${remSize(8)}; + border-radius: ${remSize(4)}; + grid-template-columns: 5fr 5fr 2fr; + grid-template-areas: "name name name" "content content content"; + } + + .loader-container { position: fixed ; padding-bottom: 32% } `; const Subheader = styled.div` @@ -81,23 +104,17 @@ const SubheaderButton = styled(Button)` border-radius: 0px !important; `; - -const FooterTabSwitcher = styled.div` - display: flex; - - h3 { text-align: center; width: 100%; } -`; - const Panels = { sketches: SketchList, collections: CollectionList, assets: AssetList }; -const CreatePathname = { - sketches: '/mobile', - collections: '/mobile/:username/collections/create', -}; + +const navOptions = username => [ + { title: 'Create Sketch', href: '/mobile' }, + { title: 'Create Collection', href: `/mobile/${username}/collections/create` } +]; const getPanel = (pathname) => { @@ -106,7 +123,10 @@ const getPanel = (pathname) => { return matches && matches.length > 0 && matches[0]; }; -const getCreatePathname = (panel, username) => (CreatePathname[panel] || '#').replace(':username', username); +const NavItem = styled.li` + position: relative; +`; + const isOwner = (user, params) => user && params && user.username === params.username; @@ -114,27 +134,41 @@ const renderPanel = (name, props) => (Component => (Component && { const user = useSelector(state => state.user); - const { username } = params; + const { username: paramsUsername } = params; const { pathname } = location; const Tabs = Object.keys(Panels); - const isExamples = username === EXAMPLE_USERNAME; + const isExamples = paramsUsername === EXAMPLE_USERNAME; const panel = getPanel(pathname); + + const [toggleNavDropdown, NavDropdown] = useAsModal(); + return (
+ + + + +
- {isOwner(user, params) && (panel !== Tabs[2]) && new} {panel === Tabs[0] && } {panel === Tabs[1] && } - {renderPanel(panel, { username, key: pathname })} + {renderPanel(panel, { username: paramsUsername, key: pathname })}