diff --git a/client/common/icons.jsx b/client/common/icons.jsx index 215083a6..c92fa2e7 100644 --- a/client/common/icons.jsx +++ b/client/common/icons.jsx @@ -16,6 +16,12 @@ import More from '../images/more.svg'; import Code from '../images/code.svg'; import Terminal from '../images/terminal.svg'; +import Folder from '../images/folder-padded.svg'; + +import CircleTerminal from '../images/circle-terminal.svg'; +import CircleFolder from '../images/circle-folder.svg'; +import CircleInfo from '../images/circle-info.svg'; + // HOC that adds the right web accessibility props // https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html @@ -81,3 +87,9 @@ export const PlayIcon = withLabel(Play); export const MoreIcon = withLabel(More); export const TerminalIcon = withLabel(Terminal); export const CodeIcon = withLabel(Code); + +export const FolderIcon = withLabel(Folder); + +export const CircleTerminalIcon = withLabel(CircleTerminal); +export const CircleFolderIcon = withLabel(CircleFolder); +export const CircleInfoIcon = withLabel(CircleInfo); diff --git a/client/components/Dropdown.jsx b/client/components/Dropdown.jsx index e29da90a..885cdc18 100644 --- a/client/components/Dropdown.jsx +++ b/client/components/Dropdown.jsx @@ -25,7 +25,7 @@ const DropdownWrapper = styled.ul` display: flex; flex-direction: column; height: auto; - z-index: 9999; + z-index: 2; border-radius: ${remSize(6)}; & li:first-child { border-radius: ${remSize(5)} ${remSize(5)} 0 0; } diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 4f315bb3..4056607f 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -9,7 +9,7 @@ import i18next from 'i18next'; import * as IDEActions from '../modules/IDE/actions/ide'; import * as toastActions from '../modules/IDE/actions/toast'; import * as projectActions from '../modules/IDE/actions/project'; -import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences'; +import { setAllAccessibleOutput, setLanguage } from '../modules/IDE/actions/preferences'; import { logoutUser } from '../modules/User/actions'; import getConfig from '../utils/getConfig'; @@ -72,7 +72,6 @@ class Nav extends React.PureComponent { document.removeEventListener('mousedown', this.handleClick, false); document.removeEventListener('keydown', this.closeDropDown, false); } - setDropdown(dropdown) { this.setState({ dropdownOpen: dropdown @@ -170,7 +169,7 @@ class Nav extends React.PureComponent { } handleLangSelection(event) { - i18next.changeLanguage(event.target.value); + this.props.setLanguage(event.target.value); this.props.showToast(1500); this.props.setToastText('Toast.LangChange'); this.setDropdown('none'); @@ -808,8 +807,8 @@ Nav.propTypes = { params: PropTypes.shape({ username: PropTypes.string }), - t: PropTypes.func.isRequired - + t: PropTypes.func.isRequired, + setLanguage: PropTypes.func.isRequired, }; Nav.defaultProps = { @@ -839,7 +838,8 @@ const mapDispatchToProps = { ...projectActions, ...toastActions, logoutUser, - setAllAccessibleOutput + setAllAccessibleOutput, + setLanguage }; export default withTranslation()(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav))); diff --git a/client/components/__test__/Nav.test.jsx b/client/components/__test__/Nav.test.jsx index ba7d3fd6..ee175fe7 100644 --- a/client/components/__test__/Nav.test.jsx +++ b/client/components/__test__/Nav.test.jsx @@ -45,7 +45,8 @@ describe('Nav', () => { rootFile: { id: 'root-file' }, - t: jest.fn() + t: jest.fn(), + setLanguage: jest.fn() }; it('renders correctly', () => { diff --git a/client/components/mobile/ActionStrip.jsx b/client/components/mobile/ActionStrip.jsx index 0d75a579..6f72b34b 100644 --- a/client/components/mobile/ActionStrip.jsx +++ b/client/components/mobile/ActionStrip.jsx @@ -1,35 +1,66 @@ import React from 'react'; import styled from 'styled-components'; +import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { useDispatch, useSelector } from 'react-redux'; -import { remSize } from '../../theme'; +import { remSize, prop } from '../../theme'; import IconButton from './IconButton'; -import { TerminalIcon } from '../../common/icons'; +import { TerminalIcon, FolderIcon } from '../../common/icons'; import * as IDEActions from '../../modules/IDE/actions/ide'; -const BottomBarContent = styled.h2` +const BottomBarContent = styled.div` padding: ${remSize(8)}; - + display: flex; + svg { max-height: ${remSize(32)}; + + } + + path { fill: ${prop('primaryTextColor')} !important } + + .inverted { + path { fill: ${prop('backgroundColor')} !important } + rect { fill: ${prop('primaryTextColor')} !important } } `; -export default () => { +// Maybe this component shouldn't be connected, and instead just receive the `actions` prop +const ActionStrip = ({ toggleExplorer }) => { const { expandConsole, collapseConsole } = bindActionCreators(IDEActions, useDispatch()); const { consoleIsExpanded } = useSelector(state => state.ide); - const actions = [{ icon: TerminalIcon, aria: 'Say Something', action: consoleIsExpanded ? collapseConsole : expandConsole }]; + const actions = [ + { + icon: TerminalIcon, inverted: true, aria: 'Open terminal console', action: consoleIsExpanded ? collapseConsole : expandConsole + }, + { icon: FolderIcon, aria: 'Open files explorer', action: toggleExplorer } + ]; return ( - {actions.map(({ icon, aria, action }) => - ( action()} - />))} + {actions.map(({ + icon, aria, action, inverted + }) => + ( + action()} + />))} ); }; + +ActionStrip.propTypes = { + toggleExplorer: PropTypes.func +}; + +ActionStrip.defaultProps = { + toggleExplorer: () => {} +}; + +export default ActionStrip; diff --git a/client/components/mobile/Explorer.jsx b/client/components/mobile/Explorer.jsx new file mode 100644 index 00000000..40455e24 --- /dev/null +++ b/client/components/mobile/Explorer.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import styled from 'styled-components'; +import PropTypes from 'prop-types'; +import Sidebar from './Sidebar'; +import ConnectedFileNode from '../../modules/IDE/components/FileNode'; + + +const Explorer = ({ id, canEdit, onPressClose }) => ( + + onPressClose()} /> + +); + +Explorer.propTypes = { + id: PropTypes.number.isRequired, + onPressClose: PropTypes.func, + canEdit: PropTypes.bool +}; +Explorer.defaultProps = { + canEdit: false, + onPressClose: () => {} +}; + +export default Explorer; diff --git a/client/components/mobile/FloatingNav.jsx b/client/components/mobile/FloatingNav.jsx new file mode 100644 index 00000000..de19c4ff --- /dev/null +++ b/client/components/mobile/FloatingNav.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { remSize, prop } from '../../theme'; +import Button from '../../common/Button'; +import IconButton from './IconButton'; + +const FloatingContainer = styled.div` + position: fixed; + right: ${remSize(16)}; + top: ${remSize(80)}; + + text-align: right; + z-index: 3; + + svg { width: ${remSize(32)}; }; + svg > path { fill: ${prop('Button.default.background')} !important }; +`; + +const FloatingNav = ({ items }) => ( + + { items.map(({ icon, onPress }) => + ( + + ))} + +); + +FloatingNav.propTypes = { + items: PropTypes.arrayOf(PropTypes.shape({ + icon: PropTypes.element, + onPress: PropTypes.func + })) +}; + +FloatingNav.defaultProps = { + items: [] +}; + +export default FloatingNav; diff --git a/client/components/mobile/Header.jsx b/client/components/mobile/Header.jsx index cb6702ed..7b3fbae1 100644 --- a/client/components/mobile/Header.jsx +++ b/client/components/mobile/Header.jsx @@ -14,7 +14,7 @@ const textColor = ({ transparent, inverted }) => prop((transparent === false && const HeaderDiv = styled.div` - position: fixed; + ${props => props.fixed && 'position: fixed;'} width: 100%; background: ${props => background(props)}; color: ${textColor}; @@ -57,9 +57,9 @@ const TitleContainer = styled.div` const Header = ({ title, subtitle, leftButton, children, - transparent, inverted, slim + transparent, inverted, slim, fixed }) => ( - + {leftButton} {title &&

{title}

} @@ -79,6 +79,7 @@ Header.propTypes = { transparent: PropTypes.bool, inverted: PropTypes.bool, slim: PropTypes.bool, + fixed: PropTypes.bool, }; Header.defaultProps = { @@ -88,7 +89,8 @@ Header.defaultProps = { children: [], transparent: false, inverted: false, - slim: false + slim: false, + fixed: true }; export default Header; diff --git a/client/components/mobile/IDEWrapper.jsx b/client/components/mobile/IDEWrapper.jsx index 0982cf81..5e66ee01 100644 --- a/client/components/mobile/IDEWrapper.jsx +++ b/client/components/mobile/IDEWrapper.jsx @@ -2,7 +2,10 @@ import React from 'react'; import styled from 'styled-components'; import { remSize } from '../../theme'; +// Applies padding to top and bottom so editor content is always visible + export default styled.div` z-index: 0; margin-top: ${remSize(16)}; + .CodeMirror-sizer > * { padding-bottom: ${remSize(320)}; }; `; diff --git a/client/components/mobile/Sidebar.jsx b/client/components/mobile/Sidebar.jsx new file mode 100644 index 00000000..bc6c13ab --- /dev/null +++ b/client/components/mobile/Sidebar.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router'; +import styled from 'styled-components'; +import { remSize, prop, common } from '../../theme'; +import Header from './Header'; +import IconButton from './IconButton'; +import { ExitIcon } from '../../common/icons'; + + +const SidebarWrapper = styled.div` + height: 100%; + width: ${remSize(180)}; + + position: fixed; + z-index: 2; + left: 0; + + background: white; + box-shadow: 0 6px 6px 0 rgba(0,0,0,0.10); +`; + +const Sidebar = ({ title, onPressClose, children }) => ( + + {title && +
+ +
} + {children} +
+); + +Sidebar.propTypes = { + title: PropTypes.string, + onPressClose: PropTypes.func, + children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]), +}; + +Sidebar.defaultProps = { + title: null, + children: [], + onPressClose: () => {} +}; + + +export default Sidebar; diff --git a/client/components/useAsModal.jsx b/client/components/useAsModal.jsx index 1afa0d30..350d1de2 100644 --- a/client/components/useAsModal.jsx +++ b/client/components/useAsModal.jsx @@ -1,10 +1,29 @@ import React from 'react'; +import styled from 'styled-components'; import { useModalBehavior } from '../utils/custom-hooks'; -export default (component) => { - const [visible, trigger, setRef] = useModalBehavior(); +const BackgroundOverlay = styled.div` + position: fixed; + z-index: 2; + width: 100% !important; + height: 100% !important; + + background: black; + opacity: 0.3; +`; - const wrapper = () =>
{visible && component}
; // eslint-disable-line +export default (Element, hasOverlay = false) => { + const [visible, toggle, setRef] = useModalBehavior(); - return [trigger, wrapper]; + const wrapper = () => (visible && +
+ {hasOverlay && } +
+ { (typeof (Element) === 'function') + ? Element(toggle) + : Element} +
+
); + + return [toggle, wrapper]; }; diff --git a/client/constants.js b/client/constants.js index 477409fc..a23badd8 100644 --- a/client/constants.js +++ b/client/constants.js @@ -93,6 +93,7 @@ export const SHOW_TOAST = 'SHOW_TOAST'; export const HIDE_TOAST = 'HIDE_TOAST'; export const SET_TOAST_TEXT = 'SET_TOAST_TEXT'; export const SET_THEME = 'SET_THEME'; +export const SET_LANGUAGE = 'SET_LANGUAGE'; export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES'; export const SET_AUTOREFRESH = 'SET_AUTOREFRESH'; diff --git a/client/images/circle-folder.svg b/client/images/circle-folder.svg new file mode 100644 index 00000000..ab2076b9 --- /dev/null +++ b/client/images/circle-folder.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/images/circle-info.svg b/client/images/circle-info.svg new file mode 100644 index 00000000..ed75766b --- /dev/null +++ b/client/images/circle-info.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/images/circle-terminal.svg b/client/images/circle-terminal.svg new file mode 100644 index 00000000..168efd85 --- /dev/null +++ b/client/images/circle-terminal.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/images/folder-padded.svg b/client/images/folder-padded.svg new file mode 100644 index 00000000..67980f0a --- /dev/null +++ b/client/images/folder-padded.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx index af441a9d..3b74e1fc 100644 --- a/client/modules/App/App.jsx +++ b/client/modules/App/App.jsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import getConfig from '../../utils/getConfig'; import DevTools from './components/DevTools'; import { setPreviousPath } from '../IDE/actions/ide'; +import { setLanguage } from '../IDE/actions/preferences'; class App extends React.Component { constructor(props, context) { @@ -18,11 +19,17 @@ class App extends React.Component { componentWillReceiveProps(nextProps) { const locationWillChange = nextProps.location !== this.props.location; - const shouldSkipRemembering = nextProps.location.state && nextProps.location.state.skipSavingPath === true; + const shouldSkipRemembering = + nextProps.location.state && + nextProps.location.state.skipSavingPath === true; if (locationWillChange && !shouldSkipRemembering) { this.props.setPreviousPath(this.props.location.pathname); } + + if (this.props.language !== nextProps.language) { + this.props.setLanguage(nextProps.language, { persistPreference: false }); + } } componentDidUpdate(prevProps) { @@ -50,18 +57,22 @@ App.propTypes = { }), }).isRequired, setPreviousPath: PropTypes.func.isRequired, + setLanguage: PropTypes.func.isRequired, + language: PropTypes.string, theme: PropTypes.string, }; App.defaultProps = { children: null, + language: null, theme: 'light' }; const mapStateToProps = state => ({ theme: state.preferences.theme, + language: state.preferences.language, }); -const mapDispatchToProps = { setPreviousPath }; +const mapDispatchToProps = { setPreviousPath, setLanguage }; export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js index a182da76..af9a8d88 100644 --- a/client/modules/IDE/actions/preferences.js +++ b/client/modules/IDE/actions/preferences.js @@ -1,3 +1,4 @@ +import i18next from 'i18next'; import apiClient from '../../../utils/apiClient'; import * as ActionTypes from '../../../constants'; @@ -210,3 +211,22 @@ export function setAllAccessibleOutput(value) { }; } +export function setLanguage(value, { persistPreference = true } = {}) { + return (dispatch, getState) => { + i18next.changeLanguage(value); + dispatch({ + type: ActionTypes.SET_LANGUAGE, + language: value + }); + const state = getState(); + if (persistPreference && state.user.authenticated) { + const formParams = { + preferences: { + language: value + } + }; + updatePreferences(formParams, dispatch); + } + }; +} + diff --git a/client/modules/IDE/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx index c161d2bb..1007ca97 100644 --- a/client/modules/IDE/components/CollectionList/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx @@ -143,9 +143,9 @@ class CollectionList extends React.Component { {this._renderFieldHeader('name', 'Name')} - {(!mobile) && 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')} diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx index f3119632..de20cea9 100644 --- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx @@ -213,7 +213,7 @@ class CollectionListRowBase extends React.Component { {this.renderCollectionName()} - {(!mobile) && {format(new Date(collection.createdAt), 'MMM D, YYYY')}} + {mobile && 'Created: '}{format(new Date(collection.createdAt), 'MMM D, YYYY')} {mobile && 'Updated: '}{formatDateCell(collection.updatedAt)} {mobile && '# sketches: '}{(collection.items || []).length} diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index 749caaaa..ddb6d55e 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -88,6 +88,8 @@ const Console = () => { const cm = useRef({}); + useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; }); + const consoleClass = classNames({ 'preview-console': true, 'preview-console--collapsed': !isExpanded diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index e7869f26..d0a10ef8 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -108,10 +108,15 @@ export class FileNode extends React.Component { handleFileClick = (event) => { event.stopPropagation(); const { isDeleting } = this.state; - const { id, setSelectedFile, name } = this.props; + const { + id, setSelectedFile, name, onClickFile + } = this.props; if (name !== 'root' && !isDeleting) { setSelectedFile(id); } + + // debugger; // eslint-disable-line + if (onClickFile) { onClickFile(); } } handleFileNameChange = (event) => { @@ -214,7 +219,7 @@ export class FileNode extends React.Component { renderChild = childId => (
  • - +
  • ) @@ -233,7 +238,7 @@ export class FileNode extends React.Component { const isRoot = this.props.name === 'root'; return ( -
    +
    { !isRoot &&
    @@ -382,10 +387,12 @@ FileNode.propTypes = { hideFolderChildren: PropTypes.func.isRequired, canEdit: PropTypes.bool.isRequired, openUploadFileModal: PropTypes.func.isRequired, - authenticated: PropTypes.bool.isRequired + authenticated: PropTypes.bool.isRequired, + onClickFile: PropTypes.func }; FileNode.defaultProps = { + onClickFile: null, parentId: '0', isSelectedFile: false, isFolderClosed: false, diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 4aacd8c2..d5f9d71d 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -437,8 +437,8 @@ class SketchList extends React.Component { {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`)} diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 4979eaee..9f010c16 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -35,6 +35,7 @@ import AddToCollectionList from '../components/AddToCollectionList'; import Feedback from '../components/Feedback'; import { CollectionSearchbar } from '../components/Searchbar'; + function getTitle(props) { const { id } = props.project; return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor'; @@ -167,13 +168,11 @@ class IDEView extends React.Component { warnIfUnsavedChanges(this.props)); } } - componentWillUnmount() { document.removeEventListener('keydown', this.handleGlobalKeydown, false); clearTimeout(this.autosaveInterval); this.autosaveInterval = null; } - handleGlobalKeydown(e) { // 83 === s if ( @@ -428,6 +427,7 @@ class IDEView extends React.Component { expandConsole={this.props.expandConsole} clearConsole={this.props.clearConsole} cmController={this.cmController} + language={this.props.preferences.language} />
    @@ -585,6 +585,7 @@ IDEView.propTypes = { soundOutput: PropTypes.bool.isRequired, theme: PropTypes.string.isRequired, autorefresh: PropTypes.bool.isRequired, + language: PropTypes.string.isRequired }).isRequired, closePreferences: PropTypes.func.isRequired, setFontSize: PropTypes.func.isRequired, diff --git a/client/modules/IDE/pages/MobileIDEView.jsx b/client/modules/IDE/pages/MobileIDEView.jsx index c2a408cf..7826dca2 100644 --- a/client/modules/IDE/pages/MobileIDEView.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -27,17 +27,34 @@ 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 MobileExplorer from '../../../components/mobile/Explorer'; import Console from '../components/Console'; import { remSize } from '../../../theme'; -// import OverlayManager from '../../../components/OverlayManager'; + import ActionStrip from '../../../components/mobile/ActionStrip'; import useAsModal from '../../../components/useAsModal'; import { PreferencesIcon } from '../../../common/icons'; import Dropdown from '../../../components/Dropdown'; +const getRootFile = files => files && files.filter(file => file.name === 'root')[0]; +const getRootFileID = files => (root => root && root.id)(getRootFile(files)); + const isUserOwner = ({ project, user }) => project.owner && project.owner.id === user.id; + +// const userCanEditProject = (props) => { +// let canEdit; +// if (!props.owner) { +// canEdit = true; +// } else if (props.user.authenticated && props.owner.id === props.user.id) { +// canEdit = true; +// } else { +// canEdit = false; +// } +// return canEdit; +// }; + const Expander = styled.div` height: ${props => (props.expanded ? remSize(160) : remSize(27))}; `; @@ -69,17 +86,13 @@ const MobileIDEView = (props) => { selectedFile, updateFileContent, files, user, params, closeEditorOptions, showEditorOptions, logoutUser, startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console, - showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch, getProject, clearPersistedState + showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch, getProject, clearPersistedState, setUnsavedChanges } = props; const [tmController, setTmController] = useState(null); // eslint-disable-line const { username } = user; - const [triggerNavDropdown, NavDropDown] = useAsModal(); // Force state reset useEffect(clearPersistedState, []); @@ -97,16 +110,29 @@ const MobileIDEView = (props) => { setCurrentProjectID(params.project_id); }, [params, project, username]); + // Screen Modals + const [toggleNavDropdown, NavDropDown] = useAsModal(); + + const [toggleExplorer, Explorer] = useAsModal(toggle => + (), true); return ( +
    @@ -149,6 +175,7 @@ const MobileIDEView = (props) => { hideRuntimeErrorWarning={hideRuntimeErrorWarning} runtimeErrorWarningVisible={ide.runtimeErrorWarningVisible} provideController={setTmController} + setUnsavedChanges={setUnsavedChanges} /> @@ -158,7 +185,7 @@ const MobileIDEView = (props) => { )} - + ); @@ -271,6 +298,7 @@ MobileIDEView.propTypes = { logoutUser: PropTypes.func.isRequired, + setUnsavedChanges: PropTypes.func.isRequired, getProject: PropTypes.func.isRequired, clearPersistedState: PropTypes.func.isRequired, params: PropTypes.shape({ diff --git a/client/modules/IDE/reducers/preferences.js b/client/modules/IDE/reducers/preferences.js index 080a7343..3ed39bef 100644 --- a/client/modules/IDE/reducers/preferences.js +++ b/client/modules/IDE/reducers/preferences.js @@ -1,5 +1,7 @@ +import i18next from 'i18next'; import * as ActionTypes from '../../../constants'; + const initialState = { fontSize: 18, autosave: true, @@ -10,7 +12,8 @@ const initialState = { gridOutput: false, soundOutput: false, theme: 'light', - autorefresh: false + autorefresh: false, + language: 'en-US' }; const preferences = (state = initialState, action) => { @@ -37,6 +40,8 @@ const preferences = (state = initialState, action) => { return Object.assign({}, state, { autorefresh: action.value }); case ActionTypes.SET_LINE_NUMBERS: return Object.assign({}, state, { lineNumbers: action.value }); + case ActionTypes.SET_LANGUAGE: + return Object.assign({}, state, { language: action.language }); default: return state; } diff --git a/client/modules/Mobile/MobileDashboardView.jsx b/client/modules/Mobile/MobileDashboardView.jsx index 7bae38f4..270faee7 100644 --- a/client/modules/Mobile/MobileDashboardView.jsx +++ b/client/modules/Mobile/MobileDashboardView.jsx @@ -24,9 +24,11 @@ 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 { @@ -55,14 +57,18 @@ const ContentWrapper = styled(Content)` 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; }; + 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 { - padding-bottom: ${remSize(160)}; background: ${prop('SketchList.background')}; } .sketches-table__row { @@ -79,18 +85,33 @@ const ContentWrapper = styled(Content)` }; thead tr { - grid-template-columns: 1fr 1fr 1fr 0fr; + 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: 5fr 5fr 1fr; + 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` @@ -168,7 +189,7 @@ const MobileDashboard = ({ params, location }) => {
    - + {panel === Tabs[0] && } {panel === Tabs[1] && } diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index dd54224d..d0648be3 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -2,6 +2,7 @@ import { browserHistory } from 'react-router'; import * as ActionTypes from '../../constants'; import apiClient from '../../utils/apiClient'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; +import { setLanguage } from '../IDE/actions/preferences'; import { showToast, setToastText } from '../IDE/actions/toast'; export function authError(error) { @@ -59,6 +60,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) { type: ActionTypes.SET_PREFERENCES, preferences: response.data.preferences }); + setLanguage(response.data.preferences.language, { persistPreference: false }); dispatch(justOpenedProject()); browserHistory.push(previousPath); resolve(); @@ -80,8 +82,8 @@ export function getUser() { type: ActionTypes.SET_PREFERENCES, preferences: response.data.preferences }); - }) - .catch((error) => { + setLanguage(response.data.preferences.language, { persistPreference: false }); + }).catch((error) => { const { response } = error; const message = response.message || response.data.error; dispatch(authError(message)); diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index b3a1a4b4..e5c7577f 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React from 'react'; - import Button from '../../../common/Button'; import { PlusIcon } from '../../../common/icons'; import CopyableInput from '../../IDE/components/CopyableInput'; @@ -12,7 +11,7 @@ export const APIKeyPropType = PropTypes.shape({ token: PropTypes.object, // eslint-disable-line label: PropTypes.string.isRequired, createdAt: PropTypes.string.isRequired, - lastUsedAt: PropTypes.string, + lastUsedAt: PropTypes.string }); class APIKeyForm extends React.Component { @@ -39,7 +38,7 @@ class APIKeyForm extends React.Component { } removeKey(key) { - const message = `Are you sure you want to delete "${key.label}"?`; + const message = this.props.t('APIKeyForm.ConfirmDelete', { key_label: key.label }); if (window.confirm(message)) { this.props.removeApiKey(key.id); @@ -51,10 +50,10 @@ class APIKeyForm extends React.Component { if (hasApiKeys) { return ( - + ); } - return

    You have no exsiting tokens.

    ; + return

    {this.props.t('APIKeyForm.NoTokens')}

    ; } render() { @@ -63,27 +62,18 @@ class APIKeyForm extends React.Component { return (

    - Personal Access Tokens act like your password to allow automated - scripts to access the Editor API. Create a token for each script that - needs access. + {this.props.t('APIKeyForm.Summary')}

    -

    Create new token

    +

    {this.props.t('APIKeyForm.CreateToken')}

    - + { - this.setState({ keyLabel: event.target.value }); - }} - placeholder="What is this token for? e.g. Example import script" + onChange={(event) => { this.setState({ keyLabel: event.target.value }); }} + placeholder={this.props.t('APIKeyForm.TokenPlaceholder')} type="text" value={this.state.keyLabel} /> @@ -93,29 +83,25 @@ class APIKeyForm extends React.Component { label="Create new key" type="submit" > - Create + {this.props.t('APIKeyForm.CreateTokenSubmit')}
    - {keyWithToken && ( -
    -

    - Your new access token -

    -

    - Make sure to copy your new personal access token now. You won’t - be able to see it again! -

    - -
    - )} + { + keyWithToken && ( +
    +

    {this.props.t('APIKeyForm.NewTokenTitle')}

    +

    + {this.props.t('APIKeyForm.NewTokenInfo')} +

    + +
    + ) + }
    -

    Existing tokens

    +

    {this.props.t('APIKeyForm.ExistingTokensTitle')}

    {this.renderApiKeys()}
    @@ -127,6 +113,7 @@ APIKeyForm.propTypes = { apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired, createApiKey: PropTypes.func.isRequired, removeApiKey: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; export default APIKeyForm; diff --git a/client/modules/User/components/APIKeyList.jsx b/client/modules/User/components/APIKeyList.jsx index 9201aa4b..2c2e39ac 100644 --- a/client/modules/User/components/APIKeyList.jsx +++ b/client/modules/User/components/APIKeyList.jsx @@ -8,22 +8,22 @@ import { APIKeyPropType } from './APIKeyForm'; import TrashCanIcon from '../../../images/trash-can.svg'; -function APIKeyList({ apiKeys, onRemove }) { +function APIKeyList({ apiKeys, onRemove, t }) { return ( - - - - + + + + {orderBy(apiKeys, ['createdAt'], ['desc']).map((key) => { const lastUsed = key.lastUsedAt ? distanceInWordsToNow(new Date(key.lastUsedAt), { addSuffix: true }) : - 'Never'; + t('APIKeyList.Never'); return ( @@ -34,7 +34,7 @@ function APIKeyList({ apiKeys, onRemove }) { @@ -50,6 +50,7 @@ function APIKeyList({ apiKeys, onRemove }) { APIKeyList.propTypes = { apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired, onRemove: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; export default APIKeyList; diff --git a/client/modules/User/components/AccountForm.jsx b/client/modules/User/components/AccountForm.jsx index 90d5fc18..c1bfe6db 100644 --- a/client/modules/User/components/AccountForm.jsx +++ b/client/modules/User/components/AccountForm.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'; @@ -14,6 +15,7 @@ function AccountForm(props) { submitting, invalid, pristine, + t } = props; const handleInitiateVerification = (evt) => { @@ -24,12 +26,10 @@ function AccountForm(props) { return (

    - + {email.error} )}

    - {user.verified !== 'verified' && ( -

    - Unconfirmed. - {user.emailVerificationInitiate === true ? ( - - {' '} - Confirmation sent, check your email. - - ) : ( - - )} -

    - )} + { + user.verified !== 'verified' && + ( +

    + {t('AccountForm.Unconfirmed')} + { + user.emailVerificationInitiate === true ? + ( + {t('AccountForm.EmailSent')} + ) : + ( + + ) + } +

    + ) + }

    - +

    - +

    - + {newPassword.error} )}

    - ); @@ -123,6 +124,7 @@ AccountForm.propTypes = { submitting: PropTypes.bool, invalid: PropTypes.bool, pristine: PropTypes.bool, + t: PropTypes.func.isRequired }; AccountForm.defaultProps = { @@ -131,4 +133,4 @@ AccountForm.defaultProps = { invalid: false, }; -export default AccountForm; +export default withTranslation()(AccountForm); diff --git a/client/modules/User/components/NewPasswordForm.jsx b/client/modules/User/components/NewPasswordForm.jsx index c971eb49..cf8b3ad9 100644 --- a/client/modules/User/components/NewPasswordForm.jsx +++ b/client/modules/User/components/NewPasswordForm.jsx @@ -1,16 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; - +import { withTranslation } from 'react-i18next'; import { domOnlyProps } from '../../../utils/reduxFormUtils'; import Button from '../../../common/Button'; function NewPasswordForm(props) { const { - fields: { password, confirmPassword }, - handleSubmit, - submitting, - invalid, - pristine, + fields: { password, confirmPassword }, handleSubmit, submitting, invalid, pristine, + t } = props; return (

    - +

    - + @@ -47,9 +40,7 @@ function NewPasswordForm(props) { {confirmPassword.error} )}

    - + ); } @@ -67,6 +58,7 @@ NewPasswordForm.propTypes = { params: PropTypes.shape({ reset_password_token: PropTypes.string, }).isRequired, + t: PropTypes.func.isRequired }; NewPasswordForm.defaultProps = { @@ -75,4 +67,4 @@ NewPasswordForm.defaultProps = { submitting: false, }; -export default NewPasswordForm; +export default withTranslation()(NewPasswordForm); diff --git a/client/modules/User/components/ResetPasswordForm.jsx b/client/modules/User/components/ResetPasswordForm.jsx index dc48fe73..ccd05ecc 100644 --- a/client/modules/User/components/ResetPasswordForm.jsx +++ b/client/modules/User/components/ResetPasswordForm.jsx @@ -1,16 +1,12 @@ import PropTypes from 'prop-types'; import React from 'react'; - +import { withTranslation } from 'react-i18next'; import { domOnlyProps } from '../../../utils/reduxFormUtils'; import Button from '../../../common/Button'; function ResetPasswordForm(props) { const { - fields: { email }, - handleSubmit, - submitting, - invalid, - pristine, + fields: { email }, handleSubmit, submitting, invalid, pristine, t } = props; return (

    - + ); @@ -54,8 +45,9 @@ ResetPasswordForm.propTypes = { invalid: PropTypes.bool, pristine: PropTypes.bool, user: PropTypes.shape({ - resetPasswordInitiate: PropTypes.bool, + resetPasswordInitiate: PropTypes.bool }).isRequired, + t: PropTypes.func.isRequired }; ResetPasswordForm.defaultProps = { @@ -64,4 +56,4 @@ ResetPasswordForm.defaultProps = { invalid: false, }; -export default ResetPasswordForm; +export default withTranslation()(ResetPasswordForm); diff --git a/client/modules/User/components/SignupForm.jsx b/client/modules/User/components/SignupForm.jsx index a2d2bd56..918e7cb3 100644 --- a/client/modules/User/components/SignupForm.jsx +++ b/client/modules/User/components/SignupForm.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'; @@ -20,12 +21,10 @@ function SignupForm(props) { onSubmit={handleSubmit(props.signUpUser.bind(this, props.previousPath))} >

    - +

    - +

    - +

    - + @@ -79,8 +72,10 @@ function SignupForm(props) { {confirmPassword.error} )}

    - ); @@ -99,6 +94,7 @@ SignupForm.propTypes = { invalid: PropTypes.bool, pristine: PropTypes.bool, previousPath: PropTypes.string.isRequired, + t: PropTypes.func.isRequired }; SignupForm.defaultProps = { @@ -107,4 +103,4 @@ SignupForm.defaultProps = { invalid: false, }; -export default SignupForm; +export default withTranslation()(SignupForm); diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index fff8d331..f9f46cc8 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -4,6 +4,7 @@ import { reduxForm } from 'redux-form'; import { bindActionCreators } from 'redux'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import { Helmet } from 'react-helmet'; +import { withTranslation } from 'react-i18next'; import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import AccountForm from '../components/AccountForm'; import apiClient from '../../../utils/apiClient'; @@ -16,9 +17,11 @@ function SocialLoginPanel(props) { return ( -

    Social Login

    + {/* eslint-disable-next-line react/prop-types */} +

    {props.t('AccountView.SocialLogin')}

    - Use your GitHub or Google account to log into the p5.js Web Editor. + {/* eslint-disable-next-line react/prop-types */} + {props.t('AccountView.SocialLoginDescription')}

    @@ -39,21 +42,21 @@ class AccountView extends React.Component { return (
    - p5.js Web Editor | Account Settings + {this.props.t('AccountView.Title')}
    NameCreated onLast usedActions{t('APIKeyList.Name')}{t('APIKeyList.Created')}{t('APIKeyList.LastUsed')}{t('APIKeyList.Actions')}