🔀 pull from develop
This commit is contained in:
commit
f5c7a31fbd
44 changed files with 759 additions and 265 deletions
|
@ -16,6 +16,12 @@ import More from '../images/more.svg';
|
||||||
import Code from '../images/code.svg';
|
import Code from '../images/code.svg';
|
||||||
import Terminal from '../images/terminal.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
|
// HOC that adds the right web accessibility props
|
||||||
// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
|
// 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 MoreIcon = withLabel(More);
|
||||||
export const TerminalIcon = withLabel(Terminal);
|
export const TerminalIcon = withLabel(Terminal);
|
||||||
export const CodeIcon = withLabel(Code);
|
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);
|
||||||
|
|
|
@ -25,7 +25,7 @@ const DropdownWrapper = styled.ul`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: auto;
|
height: auto;
|
||||||
z-index: 9999;
|
z-index: 2;
|
||||||
border-radius: ${remSize(6)};
|
border-radius: ${remSize(6)};
|
||||||
|
|
||||||
& li:first-child { border-radius: ${remSize(5)} ${remSize(5)} 0 0; }
|
& li:first-child { border-radius: ${remSize(5)} ${remSize(5)} 0 0; }
|
||||||
|
|
|
@ -9,7 +9,7 @@ import i18next from 'i18next';
|
||||||
import * as IDEActions from '../modules/IDE/actions/ide';
|
import * as IDEActions from '../modules/IDE/actions/ide';
|
||||||
import * as toastActions from '../modules/IDE/actions/toast';
|
import * as toastActions from '../modules/IDE/actions/toast';
|
||||||
import * as projectActions from '../modules/IDE/actions/project';
|
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 { logoutUser } from '../modules/User/actions';
|
||||||
|
|
||||||
import getConfig from '../utils/getConfig';
|
import getConfig from '../utils/getConfig';
|
||||||
|
@ -72,7 +72,6 @@ class Nav extends React.PureComponent {
|
||||||
document.removeEventListener('mousedown', this.handleClick, false);
|
document.removeEventListener('mousedown', this.handleClick, false);
|
||||||
document.removeEventListener('keydown', this.closeDropDown, false);
|
document.removeEventListener('keydown', this.closeDropDown, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDropdown(dropdown) {
|
setDropdown(dropdown) {
|
||||||
this.setState({
|
this.setState({
|
||||||
dropdownOpen: dropdown
|
dropdownOpen: dropdown
|
||||||
|
@ -170,7 +169,7 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLangSelection(event) {
|
handleLangSelection(event) {
|
||||||
i18next.changeLanguage(event.target.value);
|
this.props.setLanguage(event.target.value);
|
||||||
this.props.showToast(1500);
|
this.props.showToast(1500);
|
||||||
this.props.setToastText('Toast.LangChange');
|
this.props.setToastText('Toast.LangChange');
|
||||||
this.setDropdown('none');
|
this.setDropdown('none');
|
||||||
|
@ -808,8 +807,8 @@ Nav.propTypes = {
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
}),
|
}),
|
||||||
t: PropTypes.func.isRequired
|
t: PropTypes.func.isRequired,
|
||||||
|
setLanguage: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
Nav.defaultProps = {
|
Nav.defaultProps = {
|
||||||
|
@ -839,7 +838,8 @@ const mapDispatchToProps = {
|
||||||
...projectActions,
|
...projectActions,
|
||||||
...toastActions,
|
...toastActions,
|
||||||
logoutUser,
|
logoutUser,
|
||||||
setAllAccessibleOutput
|
setAllAccessibleOutput,
|
||||||
|
setLanguage
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withTranslation()(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav)));
|
export default withTranslation()(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav)));
|
||||||
|
|
|
@ -45,7 +45,8 @@ describe('Nav', () => {
|
||||||
rootFile: {
|
rootFile: {
|
||||||
id: 'root-file'
|
id: 'root-file'
|
||||||
},
|
},
|
||||||
t: jest.fn()
|
t: jest.fn(),
|
||||||
|
setLanguage: jest.fn()
|
||||||
};
|
};
|
||||||
|
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
|
|
|
@ -1,30 +1,51 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { remSize } from '../../theme';
|
import { remSize, prop } from '../../theme';
|
||||||
import IconButton from './IconButton';
|
import IconButton from './IconButton';
|
||||||
import { TerminalIcon } from '../../common/icons';
|
import { TerminalIcon, FolderIcon } from '../../common/icons';
|
||||||
import * as IDEActions from '../../modules/IDE/actions/ide';
|
import * as IDEActions from '../../modules/IDE/actions/ide';
|
||||||
|
|
||||||
const BottomBarContent = styled.h2`
|
const BottomBarContent = styled.div`
|
||||||
padding: ${remSize(8)};
|
padding: ${remSize(8)};
|
||||||
|
display: flex;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
max-height: ${remSize(32)};
|
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 { expandConsole, collapseConsole } = bindActionCreators(IDEActions, useDispatch());
|
||||||
const { consoleIsExpanded } = useSelector(state => state.ide);
|
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 (
|
return (
|
||||||
<BottomBarContent>
|
<BottomBarContent>
|
||||||
{actions.map(({ icon, aria, action }) =>
|
{actions.map(({
|
||||||
(<IconButton
|
icon, aria, action, inverted
|
||||||
|
}) =>
|
||||||
|
(
|
||||||
|
<IconButton
|
||||||
|
inverted={inverted}
|
||||||
|
className={inverted && 'inverted'}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
aria-label={aria}
|
aria-label={aria}
|
||||||
key={`bottom-bar-${aria}`}
|
key={`bottom-bar-${aria}`}
|
||||||
|
@ -33,3 +54,13 @@ export default () => {
|
||||||
</BottomBarContent>
|
</BottomBarContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ActionStrip.propTypes = {
|
||||||
|
toggleExplorer: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
ActionStrip.defaultProps = {
|
||||||
|
toggleExplorer: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActionStrip;
|
||||||
|
|
24
client/components/mobile/Explorer.jsx
Normal file
24
client/components/mobile/Explorer.jsx
Normal file
|
@ -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 }) => (
|
||||||
|
<Sidebar title="Files" onPressClose={onPressClose}>
|
||||||
|
<ConnectedFileNode id={id} canEdit={canEdit} onClickFile={() => onPressClose()} />
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
|
||||||
|
Explorer.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
onPressClose: PropTypes.func,
|
||||||
|
canEdit: PropTypes.bool
|
||||||
|
};
|
||||||
|
Explorer.defaultProps = {
|
||||||
|
canEdit: false,
|
||||||
|
onPressClose: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Explorer;
|
43
client/components/mobile/FloatingNav.jsx
Normal file
43
client/components/mobile/FloatingNav.jsx
Normal file
|
@ -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 }) => (
|
||||||
|
<FloatingContainer>
|
||||||
|
{ items.map(({ icon, onPress }) =>
|
||||||
|
(
|
||||||
|
<IconButton
|
||||||
|
onClick={onPress}
|
||||||
|
icon={icon}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FloatingContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
FloatingNav.propTypes = {
|
||||||
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
icon: PropTypes.element,
|
||||||
|
onPress: PropTypes.func
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
FloatingNav.defaultProps = {
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FloatingNav;
|
|
@ -14,7 +14,7 @@ const textColor = ({ transparent, inverted }) => prop((transparent === false &&
|
||||||
|
|
||||||
|
|
||||||
const HeaderDiv = styled.div`
|
const HeaderDiv = styled.div`
|
||||||
position: fixed;
|
${props => props.fixed && 'position: fixed;'}
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: ${props => background(props)};
|
background: ${props => background(props)};
|
||||||
color: ${textColor};
|
color: ${textColor};
|
||||||
|
@ -57,9 +57,9 @@ const TitleContainer = styled.div`
|
||||||
|
|
||||||
const Header = ({
|
const Header = ({
|
||||||
title, subtitle, leftButton, children,
|
title, subtitle, leftButton, children,
|
||||||
transparent, inverted, slim
|
transparent, inverted, slim, fixed
|
||||||
}) => (
|
}) => (
|
||||||
<HeaderDiv transparent={transparent} slim={slim} inverted={inverted}>
|
<HeaderDiv transparent={transparent} slim={slim} inverted={inverted} fixed={fixed}>
|
||||||
{leftButton}
|
{leftButton}
|
||||||
<TitleContainer padded={subtitle === null}>
|
<TitleContainer padded={subtitle === null}>
|
||||||
{title && <h2>{title}</h2>}
|
{title && <h2>{title}</h2>}
|
||||||
|
@ -79,6 +79,7 @@ Header.propTypes = {
|
||||||
transparent: PropTypes.bool,
|
transparent: PropTypes.bool,
|
||||||
inverted: PropTypes.bool,
|
inverted: PropTypes.bool,
|
||||||
slim: PropTypes.bool,
|
slim: PropTypes.bool,
|
||||||
|
fixed: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
Header.defaultProps = {
|
Header.defaultProps = {
|
||||||
|
@ -88,7 +89,8 @@ Header.defaultProps = {
|
||||||
children: [],
|
children: [],
|
||||||
transparent: false,
|
transparent: false,
|
||||||
inverted: false,
|
inverted: false,
|
||||||
slim: false
|
slim: false,
|
||||||
|
fixed: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|
|
@ -2,7 +2,10 @@ import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { remSize } from '../../theme';
|
import { remSize } from '../../theme';
|
||||||
|
|
||||||
|
// Applies padding to top and bottom so editor content is always visible
|
||||||
|
|
||||||
export default styled.div`
|
export default styled.div`
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
margin-top: ${remSize(16)};
|
margin-top: ${remSize(16)};
|
||||||
|
.CodeMirror-sizer > * { padding-bottom: ${remSize(320)}; };
|
||||||
`;
|
`;
|
||||||
|
|
46
client/components/mobile/Sidebar.jsx
Normal file
46
client/components/mobile/Sidebar.jsx
Normal file
|
@ -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 }) => (
|
||||||
|
<SidebarWrapper>
|
||||||
|
{title &&
|
||||||
|
<Header slim title={title} fixed={false}>
|
||||||
|
<IconButton onClick={onPressClose} icon={ExitIcon} aria-label="Return to ide view" />
|
||||||
|
</Header>}
|
||||||
|
{children}
|
||||||
|
</SidebarWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
|
@ -1,10 +1,29 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { useModalBehavior } from '../utils/custom-hooks';
|
import { useModalBehavior } from '../utils/custom-hooks';
|
||||||
|
|
||||||
export default (component) => {
|
const BackgroundOverlay = styled.div`
|
||||||
const [visible, trigger, setRef] = useModalBehavior();
|
position: fixed;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
|
||||||
const wrapper = () => <div ref={setRef}> {visible && component} </div>; // eslint-disable-line
|
background: black;
|
||||||
|
opacity: 0.3;
|
||||||
|
`;
|
||||||
|
|
||||||
return [trigger, wrapper];
|
export default (Element, hasOverlay = false) => {
|
||||||
|
const [visible, toggle, setRef] = useModalBehavior();
|
||||||
|
|
||||||
|
const wrapper = () => (visible &&
|
||||||
|
<div>
|
||||||
|
{hasOverlay && <BackgroundOverlay />}
|
||||||
|
<div ref={setRef}>
|
||||||
|
{ (typeof (Element) === 'function')
|
||||||
|
? Element(toggle)
|
||||||
|
: Element}
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
return [toggle, wrapper];
|
||||||
};
|
};
|
||||||
|
|
|
@ -93,6 +93,7 @@ export const SHOW_TOAST = 'SHOW_TOAST';
|
||||||
export const HIDE_TOAST = 'HIDE_TOAST';
|
export const HIDE_TOAST = 'HIDE_TOAST';
|
||||||
export const SET_TOAST_TEXT = 'SET_TOAST_TEXT';
|
export const SET_TOAST_TEXT = 'SET_TOAST_TEXT';
|
||||||
export const SET_THEME = 'SET_THEME';
|
export const SET_THEME = 'SET_THEME';
|
||||||
|
export const SET_LANGUAGE = 'SET_LANGUAGE';
|
||||||
|
|
||||||
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
|
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
|
||||||
export const SET_AUTOREFRESH = 'SET_AUTOREFRESH';
|
export const SET_AUTOREFRESH = 'SET_AUTOREFRESH';
|
||||||
|
|
5
client/images/circle-folder.svg
Normal file
5
client/images/circle-folder.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#333333"/>
|
||||||
|
<path d="M25.144 12.0961V22.4321C25.144 22.8801 24.792 23.2321 24.344 23.2321H7.768C7.32 23.2321 7 22.8801 7 22.4321V12.0961C7 11.2321 7.704 10.5281 8.568 10.5281H23.576C24.44 10.5281 25.144 11.2321 25.144 12.0961Z" fill="#F0F0F0"/>
|
||||||
|
<path d="M9.24023 9.6C9.24023 9.6 9.24023 8 10.5842 8H15.1282C16.4402 8 16.4402 9.6 16.4402 9.6H9.24023Z" fill="#F0F0F0"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 507 B |
4
client/images/circle-info.svg
Normal file
4
client/images/circle-info.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#333333"/>
|
||||||
|
<path d="M16 7C11.0154 7 7 11.0154 7 16C7 20.95 11.0154 25 16 25C20.95 25 25 20.95 25 16C25 11.0154 20.95 7 16 7ZM17.3846 21.5038H14.6846V13.7154H17.3846V21.5038ZM16 12.9538C15.1 12.9538 14.4077 12.2615 14.4077 11.3615C14.4077 10.4962 15.1 9.80385 16 9.80385C16.9 9.80385 17.5923 10.4962 17.5923 11.3615C17.5923 12.2615 16.9 12.9538 16 12.9538Z" fill="#F0F0F0"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 514 B |
6
client/images/circle-terminal.svg
Normal file
6
client/images/circle-terminal.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#333333"/>
|
||||||
|
<rect x="5" y="8" width="22" height="16" rx="2" fill="#F0F0F0"/>
|
||||||
|
<path d="M24 21H14V20H24V21Z" fill="#333333"/>
|
||||||
|
<path d="M10.4081 16.0231L8.3676 18.0637C8.27757 18.1537 8.15754 18.1537 8.06752 18.0637C7.97749 17.9736 7.97749 17.8536 8.06752 17.7636L9.95802 15.8731L8.06752 13.9826C7.97749 13.8926 7.97749 13.7725 8.06752 13.6675C8.15754 13.5775 8.27757 13.5775 8.3676 13.6675L10.4081 15.723C10.4532 15.753 10.4832 15.8131 10.4832 15.8731C10.4832 15.9181 10.4532 15.9781 10.4081 16.0231Z" fill="#333333"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 656 B |
4
client/images/folder-padded.svg
Normal file
4
client/images/folder-padded.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M25.144 12.0961V22.4321C25.144 22.8801 24.792 23.2321 24.344 23.2321H7.768C7.32 23.2321 7 22.8801 7 22.4321V12.0961C7 11.2321 7.704 10.5281 8.568 10.5281H23.576C24.44 10.5281 25.144 11.2321 25.144 12.0961Z" fill="#F0F0F0"/>
|
||||||
|
<path d="M9.24023 9.6C9.24023 9.6 9.24023 8 10.5842 8H15.1282C16.4402 8 16.4402 9.6 16.4402 9.6H9.24023Z" fill="#F0F0F0"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 459 B |
|
@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||||
import getConfig from '../../utils/getConfig';
|
import getConfig from '../../utils/getConfig';
|
||||||
import DevTools from './components/DevTools';
|
import DevTools from './components/DevTools';
|
||||||
import { setPreviousPath } from '../IDE/actions/ide';
|
import { setPreviousPath } from '../IDE/actions/ide';
|
||||||
|
import { setLanguage } from '../IDE/actions/preferences';
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
|
@ -18,11 +19,17 @@ class App extends React.Component {
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
const locationWillChange = nextProps.location !== this.props.location;
|
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) {
|
if (locationWillChange && !shouldSkipRemembering) {
|
||||||
this.props.setPreviousPath(this.props.location.pathname);
|
this.props.setPreviousPath(this.props.location.pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.language !== nextProps.language) {
|
||||||
|
this.props.setLanguage(nextProps.language, { persistPreference: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
@ -50,18 +57,22 @@ App.propTypes = {
|
||||||
}),
|
}),
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
setPreviousPath: PropTypes.func.isRequired,
|
setPreviousPath: PropTypes.func.isRequired,
|
||||||
|
setLanguage: PropTypes.func.isRequired,
|
||||||
|
language: PropTypes.string,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
App.defaultProps = {
|
App.defaultProps = {
|
||||||
children: null,
|
children: null,
|
||||||
|
language: null,
|
||||||
theme: 'light'
|
theme: 'light'
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
theme: state.preferences.theme,
|
theme: state.preferences.theme,
|
||||||
|
language: state.preferences.language,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = { setPreviousPath };
|
const mapDispatchToProps = { setPreviousPath, setLanguage };
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import i18next from 'i18next';
|
||||||
import apiClient from '../../../utils/apiClient';
|
import apiClient from '../../../utils/apiClient';
|
||||||
import * as ActionTypes from '../../../constants';
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -143,9 +143,9 @@ class CollectionList extends React.Component {
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{this._renderFieldHeader('name', 'Name')}
|
{this._renderFieldHeader('name', 'Name')}
|
||||||
{(!mobile) && this._renderFieldHeader('createdAt', 'Date Created')}
|
{this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)}
|
||||||
{this._renderFieldHeader('updatedAt', 'Date Updated')}
|
{this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)}
|
||||||
{this._renderFieldHeader('numItems', '# sketches')}
|
{this._renderFieldHeader('numItems', mobile ? 'Sketches' : '# sketches')}
|
||||||
<th scope="col"></th>
|
<th scope="col"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
|
@ -213,7 +213,7 @@ class CollectionListRowBase extends React.Component {
|
||||||
{this.renderCollectionName()}
|
{this.renderCollectionName()}
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
{(!mobile) && <td>{format(new Date(collection.createdAt), 'MMM D, YYYY')}</td>}
|
<td>{mobile && 'Created: '}{format(new Date(collection.createdAt), 'MMM D, YYYY')}</td>
|
||||||
<td>{mobile && 'Updated: '}{formatDateCell(collection.updatedAt)}</td>
|
<td>{mobile && 'Updated: '}{formatDateCell(collection.updatedAt)}</td>
|
||||||
<td>{mobile && '# sketches: '}{(collection.items || []).length}</td>
|
<td>{mobile && '# sketches: '}{(collection.items || []).length}</td>
|
||||||
<td className="sketch-list__dropdown-column">
|
<td className="sketch-list__dropdown-column">
|
||||||
|
|
|
@ -88,6 +88,8 @@ const Console = () => {
|
||||||
|
|
||||||
const cm = useRef({});
|
const cm = useRef({});
|
||||||
|
|
||||||
|
useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; });
|
||||||
|
|
||||||
const consoleClass = classNames({
|
const consoleClass = classNames({
|
||||||
'preview-console': true,
|
'preview-console': true,
|
||||||
'preview-console--collapsed': !isExpanded
|
'preview-console--collapsed': !isExpanded
|
||||||
|
|
|
@ -108,10 +108,15 @@ export class FileNode extends React.Component {
|
||||||
handleFileClick = (event) => {
|
handleFileClick = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const { isDeleting } = this.state;
|
const { isDeleting } = this.state;
|
||||||
const { id, setSelectedFile, name } = this.props;
|
const {
|
||||||
|
id, setSelectedFile, name, onClickFile
|
||||||
|
} = this.props;
|
||||||
if (name !== 'root' && !isDeleting) {
|
if (name !== 'root' && !isDeleting) {
|
||||||
setSelectedFile(id);
|
setSelectedFile(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// debugger; // eslint-disable-line
|
||||||
|
if (onClickFile) { onClickFile(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFileNameChange = (event) => {
|
handleFileNameChange = (event) => {
|
||||||
|
@ -214,7 +219,7 @@ export class FileNode extends React.Component {
|
||||||
|
|
||||||
renderChild = childId => (
|
renderChild = childId => (
|
||||||
<li key={childId}>
|
<li key={childId}>
|
||||||
<ConnectedFileNode id={childId} parentId={this.props.id} canEdit={this.props.canEdit} />
|
<ConnectedFileNode id={childId} parentId={this.props.id} canEdit={this.props.canEdit} onClickFile={this.props.onClickFile} />
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -233,7 +238,7 @@ export class FileNode extends React.Component {
|
||||||
const isRoot = this.props.name === 'root';
|
const isRoot = this.props.name === 'root';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={itemClass}>
|
<div className={itemClass} >
|
||||||
{ !isRoot &&
|
{ !isRoot &&
|
||||||
<div className="file-item__content" onContextMenu={this.toggleFileOptions}>
|
<div className="file-item__content" onContextMenu={this.toggleFileOptions}>
|
||||||
<span className="file-item__spacer"></span>
|
<span className="file-item__spacer"></span>
|
||||||
|
@ -382,10 +387,12 @@ FileNode.propTypes = {
|
||||||
hideFolderChildren: PropTypes.func.isRequired,
|
hideFolderChildren: PropTypes.func.isRequired,
|
||||||
canEdit: PropTypes.bool.isRequired,
|
canEdit: PropTypes.bool.isRequired,
|
||||||
openUploadFileModal: PropTypes.func.isRequired,
|
openUploadFileModal: PropTypes.func.isRequired,
|
||||||
authenticated: PropTypes.bool.isRequired
|
authenticated: PropTypes.bool.isRequired,
|
||||||
|
onClickFile: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
FileNode.defaultProps = {
|
FileNode.defaultProps = {
|
||||||
|
onClickFile: null,
|
||||||
parentId: '0',
|
parentId: '0',
|
||||||
isSelectedFile: false,
|
isSelectedFile: false,
|
||||||
isFolderClosed: false,
|
isFolderClosed: false,
|
||||||
|
|
|
@ -437,8 +437,8 @@ class SketchList extends React.Component {
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{this._renderFieldHeader('name', 'Sketch')}
|
{this._renderFieldHeader('name', 'Sketch')}
|
||||||
{this._renderFieldHeader('createdAt', 'Date Created')}
|
{this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)}
|
||||||
{this._renderFieldHeader('updatedAt', 'Date Updated')}
|
{this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)}
|
||||||
<th scope="col"></th>
|
<th scope="col"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
|
@ -35,6 +35,7 @@ import AddToCollectionList from '../components/AddToCollectionList';
|
||||||
import Feedback from '../components/Feedback';
|
import Feedback from '../components/Feedback';
|
||||||
import { CollectionSearchbar } from '../components/Searchbar';
|
import { CollectionSearchbar } from '../components/Searchbar';
|
||||||
|
|
||||||
|
|
||||||
function getTitle(props) {
|
function getTitle(props) {
|
||||||
const { id } = props.project;
|
const { id } = props.project;
|
||||||
return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor';
|
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));
|
warnIfUnsavedChanges(this.props));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('keydown', this.handleGlobalKeydown, false);
|
document.removeEventListener('keydown', this.handleGlobalKeydown, false);
|
||||||
clearTimeout(this.autosaveInterval);
|
clearTimeout(this.autosaveInterval);
|
||||||
this.autosaveInterval = null;
|
this.autosaveInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleGlobalKeydown(e) {
|
handleGlobalKeydown(e) {
|
||||||
// 83 === s
|
// 83 === s
|
||||||
if (
|
if (
|
||||||
|
@ -428,6 +427,7 @@ class IDEView extends React.Component {
|
||||||
expandConsole={this.props.expandConsole}
|
expandConsole={this.props.expandConsole}
|
||||||
clearConsole={this.props.clearConsole}
|
clearConsole={this.props.clearConsole}
|
||||||
cmController={this.cmController}
|
cmController={this.cmController}
|
||||||
|
language={this.props.preferences.language}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -585,6 +585,7 @@ IDEView.propTypes = {
|
||||||
soundOutput: PropTypes.bool.isRequired,
|
soundOutput: PropTypes.bool.isRequired,
|
||||||
theme: PropTypes.string.isRequired,
|
theme: PropTypes.string.isRequired,
|
||||||
autorefresh: PropTypes.bool.isRequired,
|
autorefresh: PropTypes.bool.isRequired,
|
||||||
|
language: PropTypes.string.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
closePreferences: PropTypes.func.isRequired,
|
closePreferences: PropTypes.func.isRequired,
|
||||||
setFontSize: PropTypes.func.isRequired,
|
setFontSize: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -27,17 +27,34 @@ import Header from '../../../components/mobile/Header';
|
||||||
import Screen from '../../../components/mobile/MobileScreen';
|
import Screen from '../../../components/mobile/MobileScreen';
|
||||||
import Footer from '../../../components/mobile/Footer';
|
import Footer from '../../../components/mobile/Footer';
|
||||||
import IDEWrapper from '../../../components/mobile/IDEWrapper';
|
import IDEWrapper from '../../../components/mobile/IDEWrapper';
|
||||||
|
import MobileExplorer from '../../../components/mobile/Explorer';
|
||||||
import Console from '../components/Console';
|
import Console from '../components/Console';
|
||||||
import { remSize } from '../../../theme';
|
import { remSize } from '../../../theme';
|
||||||
// import OverlayManager from '../../../components/OverlayManager';
|
|
||||||
import ActionStrip from '../../../components/mobile/ActionStrip';
|
import ActionStrip from '../../../components/mobile/ActionStrip';
|
||||||
import useAsModal from '../../../components/useAsModal';
|
import useAsModal from '../../../components/useAsModal';
|
||||||
import { PreferencesIcon } from '../../../common/icons';
|
import { PreferencesIcon } from '../../../common/icons';
|
||||||
import Dropdown from '../../../components/Dropdown';
|
import Dropdown from '../../../components/Dropdown';
|
||||||
|
|
||||||
|
const getRootFile = files => files && files.filter(file => file.name === 'root')[0];
|
||||||
|
const getRootFileID = files => (root => root && root.id)(getRootFile(files));
|
||||||
|
|
||||||
const isUserOwner = ({ project, user }) =>
|
const isUserOwner = ({ project, user }) =>
|
||||||
project.owner && project.owner.id === user.id;
|
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`
|
const Expander = styled.div`
|
||||||
height: ${props => (props.expanded ? remSize(160) : remSize(27))};
|
height: ${props => (props.expanded ? remSize(160) : remSize(27))};
|
||||||
`;
|
`;
|
||||||
|
@ -69,17 +86,13 @@ const MobileIDEView = (props) => {
|
||||||
selectedFile, updateFileContent, files, user, params,
|
selectedFile, updateFileContent, files, user, params,
|
||||||
closeEditorOptions, showEditorOptions, logoutUser,
|
closeEditorOptions, showEditorOptions, logoutUser,
|
||||||
startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
|
startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
|
||||||
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch, getProject, clearPersistedState
|
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch, getProject, clearPersistedState, setUnsavedChanges
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [tmController, setTmController] = useState(null); // eslint-disable-line
|
const [tmController, setTmController] = useState(null); // eslint-disable-line
|
||||||
|
|
||||||
const { username } = user;
|
const { username } = user;
|
||||||
|
|
||||||
const [triggerNavDropdown, NavDropDown] = useAsModal(<Dropdown
|
|
||||||
items={getNavOptions(username, logoutUser)}
|
|
||||||
align="right"
|
|
||||||
/>);
|
|
||||||
|
|
||||||
// Force state reset
|
// Force state reset
|
||||||
useEffect(clearPersistedState, []);
|
useEffect(clearPersistedState, []);
|
||||||
|
@ -97,16 +110,29 @@ const MobileIDEView = (props) => {
|
||||||
setCurrentProjectID(params.project_id);
|
setCurrentProjectID(params.project_id);
|
||||||
}, [params, project, username]);
|
}, [params, project, username]);
|
||||||
|
|
||||||
|
// Screen Modals
|
||||||
|
const [toggleNavDropdown, NavDropDown] = useAsModal(<Dropdown
|
||||||
|
items={getNavOptions(username, logoutUser)}
|
||||||
|
align="right"
|
||||||
|
/>);
|
||||||
|
|
||||||
|
const [toggleExplorer, Explorer] = useAsModal(toggle =>
|
||||||
|
(<MobileExplorer
|
||||||
|
id={getRootFileID(files)}
|
||||||
|
canEdit={false}
|
||||||
|
onPressClose={toggle}
|
||||||
|
/>), true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Screen fullscreen>
|
<Screen fullscreen>
|
||||||
|
<Explorer />
|
||||||
<Header
|
<Header
|
||||||
title={project.name}
|
title={project.name}
|
||||||
subtitle={selectedFile.name}
|
subtitle={selectedFile.name}
|
||||||
>
|
>
|
||||||
<NavItem>
|
<NavItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={triggerNavDropdown}
|
onClick={toggleNavDropdown}
|
||||||
icon={MoreIcon}
|
icon={MoreIcon}
|
||||||
aria-label="Options"
|
aria-label="Options"
|
||||||
/>
|
/>
|
||||||
|
@ -149,6 +175,7 @@ const MobileIDEView = (props) => {
|
||||||
hideRuntimeErrorWarning={hideRuntimeErrorWarning}
|
hideRuntimeErrorWarning={hideRuntimeErrorWarning}
|
||||||
runtimeErrorWarningVisible={ide.runtimeErrorWarningVisible}
|
runtimeErrorWarningVisible={ide.runtimeErrorWarningVisible}
|
||||||
provideController={setTmController}
|
provideController={setTmController}
|
||||||
|
setUnsavedChanges={setUnsavedChanges}
|
||||||
/>
|
/>
|
||||||
</IDEWrapper>
|
</IDEWrapper>
|
||||||
|
|
||||||
|
@ -158,7 +185,7 @@ const MobileIDEView = (props) => {
|
||||||
<Console />
|
<Console />
|
||||||
</Expander>
|
</Expander>
|
||||||
)}
|
)}
|
||||||
<ActionStrip />
|
<ActionStrip toggleExplorer={toggleExplorer} />
|
||||||
</Footer>
|
</Footer>
|
||||||
</Screen>
|
</Screen>
|
||||||
);
|
);
|
||||||
|
@ -271,6 +298,7 @@ MobileIDEView.propTypes = {
|
||||||
|
|
||||||
logoutUser: PropTypes.func.isRequired,
|
logoutUser: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
setUnsavedChanges: PropTypes.func.isRequired,
|
||||||
getProject: PropTypes.func.isRequired,
|
getProject: PropTypes.func.isRequired,
|
||||||
clearPersistedState: PropTypes.func.isRequired,
|
clearPersistedState: PropTypes.func.isRequired,
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import i18next from 'i18next';
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
autosave: true,
|
autosave: true,
|
||||||
|
@ -10,7 +12,8 @@ const initialState = {
|
||||||
gridOutput: false,
|
gridOutput: false,
|
||||||
soundOutput: false,
|
soundOutput: false,
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
autorefresh: false
|
autorefresh: false,
|
||||||
|
language: 'en-US'
|
||||||
};
|
};
|
||||||
|
|
||||||
const preferences = (state = initialState, action) => {
|
const preferences = (state = initialState, action) => {
|
||||||
|
@ -37,6 +40,8 @@ const preferences = (state = initialState, action) => {
|
||||||
return Object.assign({}, state, { autorefresh: action.value });
|
return Object.assign({}, state, { autorefresh: action.value });
|
||||||
case ActionTypes.SET_LINE_NUMBERS:
|
case ActionTypes.SET_LINE_NUMBERS:
|
||||||
return Object.assign({}, state, { lineNumbers: action.value });
|
return Object.assign({}, state, { lineNumbers: action.value });
|
||||||
|
case ActionTypes.SET_LANGUAGE:
|
||||||
|
return Object.assign({}, state, { language: action.language });
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,9 +24,11 @@ import Loader from '../App/components/loader';
|
||||||
|
|
||||||
const EXAMPLE_USERNAME = 'p5';
|
const EXAMPLE_USERNAME = 'p5';
|
||||||
|
|
||||||
|
// @ghalestrilo 08/13/2020: I'm sorry
|
||||||
const ContentWrapper = styled(Content)`
|
const ContentWrapper = styled(Content)`
|
||||||
table {
|
table {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
|
margin-bottom: ${remSize(120)}
|
||||||
}
|
}
|
||||||
|
|
||||||
td ,thead button {
|
td ,thead button {
|
||||||
|
@ -55,14 +57,18 @@ const ContentWrapper = styled(Content)`
|
||||||
|
|
||||||
tbody td { justify-self: start; text-align: start; padding: 0 }
|
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: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; };
|
.sketch-list__dropdown-column { width: auto; };
|
||||||
|
|
||||||
tbody { height: ${remSize(48)}; }
|
tbody { height: ${remSize(48)}; }
|
||||||
|
|
||||||
.sketches-table-container {
|
.sketches-table-container {
|
||||||
padding-bottom: ${remSize(160)};
|
|
||||||
background: ${prop('SketchList.background')};
|
background: ${prop('SketchList.background')};
|
||||||
}
|
}
|
||||||
.sketches-table__row {
|
.sketches-table__row {
|
||||||
|
@ -79,18 +85,33 @@ const ContentWrapper = styled(Content)`
|
||||||
};
|
};
|
||||||
|
|
||||||
thead tr {
|
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 {
|
tbody tr {
|
||||||
padding: ${remSize(8)};
|
padding: ${remSize(8)};
|
||||||
border-radius: ${remSize(4)};
|
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-template-areas: "name name name" "content content content";
|
||||||
|
grid-row-gap: ${remSize(12)}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader-container { position: fixed ; padding-bottom: 32% }
|
.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`
|
const Subheader = styled.div`
|
||||||
|
@ -168,7 +189,7 @@ const MobileDashboard = ({ params, location }) => {
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
|
|
||||||
<ContentWrapper slimheader>
|
<ContentWrapper slimheader fieldcount={panel === Tabs[1] ? 4 : 3} noheader={panel === Tabs[2]}>
|
||||||
<Subheader>
|
<Subheader>
|
||||||
{panel === Tabs[0] && <SketchSearchbar />}
|
{panel === Tabs[0] && <SketchSearchbar />}
|
||||||
{panel === Tabs[1] && <CollectionSearchbar />}
|
{panel === Tabs[1] && <CollectionSearchbar />}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { browserHistory } from 'react-router';
|
||||||
import * as ActionTypes from '../../constants';
|
import * as ActionTypes from '../../constants';
|
||||||
import apiClient from '../../utils/apiClient';
|
import apiClient from '../../utils/apiClient';
|
||||||
import { showErrorModal, justOpenedProject } from '../IDE/actions/ide';
|
import { showErrorModal, justOpenedProject } from '../IDE/actions/ide';
|
||||||
|
import { setLanguage } from '../IDE/actions/preferences';
|
||||||
import { showToast, setToastText } from '../IDE/actions/toast';
|
import { showToast, setToastText } from '../IDE/actions/toast';
|
||||||
|
|
||||||
export function authError(error) {
|
export function authError(error) {
|
||||||
|
@ -59,6 +60,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) {
|
||||||
type: ActionTypes.SET_PREFERENCES,
|
type: ActionTypes.SET_PREFERENCES,
|
||||||
preferences: response.data.preferences
|
preferences: response.data.preferences
|
||||||
});
|
});
|
||||||
|
setLanguage(response.data.preferences.language, { persistPreference: false });
|
||||||
dispatch(justOpenedProject());
|
dispatch(justOpenedProject());
|
||||||
browserHistory.push(previousPath);
|
browserHistory.push(previousPath);
|
||||||
resolve();
|
resolve();
|
||||||
|
@ -80,8 +82,8 @@ export function getUser() {
|
||||||
type: ActionTypes.SET_PREFERENCES,
|
type: ActionTypes.SET_PREFERENCES,
|
||||||
preferences: response.data.preferences
|
preferences: response.data.preferences
|
||||||
});
|
});
|
||||||
})
|
setLanguage(response.data.preferences.language, { persistPreference: false });
|
||||||
.catch((error) => {
|
}).catch((error) => {
|
||||||
const { response } = error;
|
const { response } = error;
|
||||||
const message = response.message || response.data.error;
|
const message = response.message || response.data.error;
|
||||||
dispatch(authError(message));
|
dispatch(authError(message));
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Button from '../../../common/Button';
|
import Button from '../../../common/Button';
|
||||||
import { PlusIcon } from '../../../common/icons';
|
import { PlusIcon } from '../../../common/icons';
|
||||||
import CopyableInput from '../../IDE/components/CopyableInput';
|
import CopyableInput from '../../IDE/components/CopyableInput';
|
||||||
|
@ -12,7 +11,7 @@ export const APIKeyPropType = PropTypes.shape({
|
||||||
token: PropTypes.object, // eslint-disable-line
|
token: PropTypes.object, // eslint-disable-line
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
createdAt: PropTypes.string.isRequired,
|
createdAt: PropTypes.string.isRequired,
|
||||||
lastUsedAt: PropTypes.string,
|
lastUsedAt: PropTypes.string
|
||||||
});
|
});
|
||||||
|
|
||||||
class APIKeyForm extends React.Component {
|
class APIKeyForm extends React.Component {
|
||||||
|
@ -39,7 +38,7 @@ class APIKeyForm extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeKey(key) {
|
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)) {
|
if (window.confirm(message)) {
|
||||||
this.props.removeApiKey(key.id);
|
this.props.removeApiKey(key.id);
|
||||||
|
@ -51,10 +50,10 @@ class APIKeyForm extends React.Component {
|
||||||
|
|
||||||
if (hasApiKeys) {
|
if (hasApiKeys) {
|
||||||
return (
|
return (
|
||||||
<APIKeyList apiKeys={this.props.apiKeys} onRemove={this.removeKey} />
|
<APIKeyList apiKeys={this.props.apiKeys} onRemove={this.removeKey} t={this.props.t} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <p>You have no exsiting tokens.</p>;
|
return <p>{this.props.t('APIKeyForm.NoTokens')}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -63,27 +62,18 @@ class APIKeyForm extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="api-key-form">
|
<div className="api-key-form">
|
||||||
<p className="api-key-form__summary">
|
<p className="api-key-form__summary">
|
||||||
Personal Access Tokens act like your password to allow automated
|
{this.props.t('APIKeyForm.Summary')}
|
||||||
scripts to access the Editor API. Create a token for each script that
|
|
||||||
needs access.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="api-key-form__section">
|
<div className="api-key-form__section">
|
||||||
<h3 className="api-key-form__title">Create new token</h3>
|
<h3 className="api-key-form__title">{this.props.t('APIKeyForm.CreateToken')}</h3>
|
||||||
<form className="form form--inline" onSubmit={this.addKey}>
|
<form className="form form--inline" onSubmit={this.addKey}>
|
||||||
<label
|
<label htmlFor="keyLabel" className="form__label form__label--hidden ">{this.props.t('APIKeyForm.TokenLabel')}</label>
|
||||||
htmlFor="keyLabel"
|
|
||||||
className="form__label form__label--hidden "
|
|
||||||
>
|
|
||||||
What is this token for?
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
id="keyLabel"
|
id="keyLabel"
|
||||||
onChange={(event) => {
|
onChange={(event) => { this.setState({ keyLabel: event.target.value }); }}
|
||||||
this.setState({ keyLabel: event.target.value });
|
placeholder={this.props.t('APIKeyForm.TokenPlaceholder')}
|
||||||
}}
|
|
||||||
placeholder="What is this token for? e.g. Example import script"
|
|
||||||
type="text"
|
type="text"
|
||||||
value={this.state.keyLabel}
|
value={this.state.keyLabel}
|
||||||
/>
|
/>
|
||||||
|
@ -93,29 +83,25 @@ class APIKeyForm extends React.Component {
|
||||||
label="Create new key"
|
label="Create new key"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
{this.props.t('APIKeyForm.CreateTokenSubmit')}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{keyWithToken && (
|
{
|
||||||
|
keyWithToken && (
|
||||||
<div className="api-key-form__new-token">
|
<div className="api-key-form__new-token">
|
||||||
<h4 className="api-key-form__new-token__title">
|
<h4 className="api-key-form__new-token__title">{this.props.t('APIKeyForm.NewTokenTitle')}</h4>
|
||||||
Your new access token
|
|
||||||
</h4>
|
|
||||||
<p className="api-key-form__new-token__info">
|
<p className="api-key-form__new-token__info">
|
||||||
Make sure to copy your new personal access token now. You won’t
|
{this.props.t('APIKeyForm.NewTokenInfo')}
|
||||||
be able to see it again!
|
|
||||||
</p>
|
</p>
|
||||||
<CopyableInput
|
<CopyableInput label={keyWithToken.label} value={keyWithToken.token} />
|
||||||
label={keyWithToken.label}
|
|
||||||
value={keyWithToken.token}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="api-key-form__section">
|
<div className="api-key-form__section">
|
||||||
<h3 className="api-key-form__title">Existing tokens</h3>
|
<h3 className="api-key-form__title">{this.props.t('APIKeyForm.ExistingTokensTitle')}</h3>
|
||||||
{this.renderApiKeys()}
|
{this.renderApiKeys()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -127,6 +113,7 @@ APIKeyForm.propTypes = {
|
||||||
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
|
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
|
||||||
createApiKey: PropTypes.func.isRequired,
|
createApiKey: PropTypes.func.isRequired,
|
||||||
removeApiKey: PropTypes.func.isRequired,
|
removeApiKey: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default APIKeyForm;
|
export default APIKeyForm;
|
||||||
|
|
|
@ -8,22 +8,22 @@ import { APIKeyPropType } from './APIKeyForm';
|
||||||
|
|
||||||
import TrashCanIcon from '../../../images/trash-can.svg';
|
import TrashCanIcon from '../../../images/trash-can.svg';
|
||||||
|
|
||||||
function APIKeyList({ apiKeys, onRemove }) {
|
function APIKeyList({ apiKeys, onRemove, t }) {
|
||||||
return (
|
return (
|
||||||
<table className="api-key-list">
|
<table className="api-key-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>{t('APIKeyList.Name')}</th>
|
||||||
<th>Created on</th>
|
<th>{t('APIKeyList.Created')}</th>
|
||||||
<th>Last used</th>
|
<th>{t('APIKeyList.LastUsed')}</th>
|
||||||
<th>Actions</th>
|
<th>{t('APIKeyList.Actions')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{orderBy(apiKeys, ['createdAt'], ['desc']).map((key) => {
|
{orderBy(apiKeys, ['createdAt'], ['desc']).map((key) => {
|
||||||
const lastUsed = key.lastUsedAt ?
|
const lastUsed = key.lastUsedAt ?
|
||||||
distanceInWordsToNow(new Date(key.lastUsedAt), { addSuffix: true }) :
|
distanceInWordsToNow(new Date(key.lastUsedAt), { addSuffix: true }) :
|
||||||
'Never';
|
t('APIKeyList.Never');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={key.id}>
|
<tr key={key.id}>
|
||||||
|
@ -34,7 +34,7 @@ function APIKeyList({ apiKeys, onRemove }) {
|
||||||
<button
|
<button
|
||||||
className="api-key-list__delete-button"
|
className="api-key-list__delete-button"
|
||||||
onClick={() => onRemove(key)}
|
onClick={() => onRemove(key)}
|
||||||
aria-label="Delete API Key"
|
aria-label={t('APIKeyList.DeleteARIA')}
|
||||||
>
|
>
|
||||||
<TrashCanIcon focusable="false" aria-hidden="true" />
|
<TrashCanIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -50,6 +50,7 @@ function APIKeyList({ apiKeys, onRemove }) {
|
||||||
APIKeyList.propTypes = {
|
APIKeyList.propTypes = {
|
||||||
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
|
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
|
||||||
onRemove: PropTypes.func.isRequired,
|
onRemove: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default APIKeyList;
|
export default APIKeyList;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||||
import Button from '../../../common/Button';
|
import Button from '../../../common/Button';
|
||||||
|
|
||||||
|
@ -14,6 +15,7 @@ function AccountForm(props) {
|
||||||
submitting,
|
submitting,
|
||||||
invalid,
|
invalid,
|
||||||
pristine,
|
pristine,
|
||||||
|
t
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const handleInitiateVerification = (evt) => {
|
const handleInitiateVerification = (evt) => {
|
||||||
|
@ -24,12 +26,10 @@ function AccountForm(props) {
|
||||||
return (
|
return (
|
||||||
<form className="form" onSubmit={handleSubmit(props.updateSettings)}>
|
<form className="form" onSubmit={handleSubmit(props.updateSettings)}>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="email" className="form__label">
|
<label htmlFor="email" className="form__label">{t('AccountForm.Email')}</label>
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
aria-label="email"
|
aria-label={t('AccountForm.EmailARIA')}
|
||||||
type="text"
|
type="text"
|
||||||
id="email"
|
id="email"
|
||||||
{...domOnlyProps(email)}
|
{...domOnlyProps(email)}
|
||||||
|
@ -38,28 +38,31 @@ function AccountForm(props) {
|
||||||
<span className="form-error">{email.error}</span>
|
<span className="form-error">{email.error}</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{user.verified !== 'verified' && (
|
{
|
||||||
|
user.verified !== 'verified' &&
|
||||||
|
(
|
||||||
<p className="form__context">
|
<p className="form__context">
|
||||||
<span className="form__status">Unconfirmed.</span>
|
<span className="form__status">{t('AccountForm.Unconfirmed')}</span>
|
||||||
{user.emailVerificationInitiate === true ? (
|
{
|
||||||
<span className="form__status">
|
user.emailVerificationInitiate === true ?
|
||||||
{' '}
|
(
|
||||||
Confirmation sent, check your email.
|
<span className="form__status"> {t('AccountForm.EmailSent')}</span>
|
||||||
</span>
|
) :
|
||||||
) : (
|
(
|
||||||
<Button onClick={handleInitiateVerification}>
|
<Button
|
||||||
Resend confirmation email
|
onClick={handleInitiateVerification}
|
||||||
|
>{t('AccountForm.Resend')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="username" className="form__label">
|
<label htmlFor="username" className="form__label">{t('AccountForm.UserName')}</label>
|
||||||
User Name
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
aria-label="username"
|
aria-label={t('AccountForm.UserNameARIA')}
|
||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
defaultValue={username}
|
defaultValue={username}
|
||||||
|
@ -70,12 +73,10 @@ function AccountForm(props) {
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="current password" className="form__label">
|
<label htmlFor="current password" className="form__label">{t('AccountForm.CurrentPassword')}</label>
|
||||||
Current Password
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
aria-label="currentPassword"
|
aria-label={t('AccountForm.CurrentPasswordARIA')}
|
||||||
type="password"
|
type="password"
|
||||||
id="currentPassword"
|
id="currentPassword"
|
||||||
{...domOnlyProps(currentPassword)}
|
{...domOnlyProps(currentPassword)}
|
||||||
|
@ -85,12 +86,10 @@ function AccountForm(props) {
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="new password" className="form__label">
|
<label htmlFor="new password" className="form__label">{t('AccountForm.NewPassword')}</label>
|
||||||
New Password
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
aria-label="newPassword"
|
aria-label={t('AccountForm.NewPasswordARIA')}
|
||||||
type="password"
|
type="password"
|
||||||
id="newPassword"
|
id="newPassword"
|
||||||
{...domOnlyProps(newPassword)}
|
{...domOnlyProps(newPassword)}
|
||||||
|
@ -99,8 +98,10 @@ function AccountForm(props) {
|
||||||
<span className="form-error">{newPassword.error}</span>
|
<span className="form-error">{newPassword.error}</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<Button type="submit" disabled={submitting || invalid || pristine}>
|
<Button
|
||||||
Save All Settings
|
type="submit"
|
||||||
|
disabled={submitting || invalid || pristine}
|
||||||
|
>{t('AccountForm.SubmitSaveAllSettings')}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -123,6 +124,7 @@ AccountForm.propTypes = {
|
||||||
submitting: PropTypes.bool,
|
submitting: PropTypes.bool,
|
||||||
invalid: PropTypes.bool,
|
invalid: PropTypes.bool,
|
||||||
pristine: PropTypes.bool,
|
pristine: PropTypes.bool,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
AccountForm.defaultProps = {
|
AccountForm.defaultProps = {
|
||||||
|
@ -131,4 +133,4 @@ AccountForm.defaultProps = {
|
||||||
invalid: false,
|
invalid: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AccountForm;
|
export default withTranslation()(AccountForm);
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||||
import Button from '../../../common/Button';
|
import Button from '../../../common/Button';
|
||||||
|
|
||||||
function NewPasswordForm(props) {
|
function NewPasswordForm(props) {
|
||||||
const {
|
const {
|
||||||
fields: { password, confirmPassword },
|
fields: { password, confirmPassword }, handleSubmit, submitting, invalid, pristine,
|
||||||
handleSubmit,
|
t
|
||||||
submitting,
|
|
||||||
invalid,
|
|
||||||
pristine,
|
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
|
@ -18,12 +15,10 @@ function NewPasswordForm(props) {
|
||||||
onSubmit={handleSubmit(props.updatePassword.bind(this, props.params.reset_password_token))}
|
onSubmit={handleSubmit(props.updatePassword.bind(this, props.params.reset_password_token))}
|
||||||
>
|
>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="password" className="form__label">
|
<label htmlFor="password" className="form__label">{t('NewPasswordForm.Title')}</label>
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
aria-label="password"
|
aria-label={t('NewPasswordForm.TitleARIA')}
|
||||||
type="password"
|
type="password"
|
||||||
id="Password"
|
id="Password"
|
||||||
{...domOnlyProps(password)}
|
{...domOnlyProps(password)}
|
||||||
|
@ -33,13 +28,11 @@ function NewPasswordForm(props) {
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="confirm password" className="form__label">
|
<label htmlFor="confirm password" className="form__label">{t('NewPasswordForm.ConfirmPassword')}</label>
|
||||||
Confirm Password
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
type="password"
|
type="password"
|
||||||
aria-label="confirm password"
|
aria-label={t('NewPasswordForm.ConfirmPasswordARIA')}
|
||||||
id="confirm password"
|
id="confirm password"
|
||||||
{...domOnlyProps(confirmPassword)}
|
{...domOnlyProps(confirmPassword)}
|
||||||
/>
|
/>
|
||||||
|
@ -47,9 +40,7 @@ function NewPasswordForm(props) {
|
||||||
<span className="form-error">{confirmPassword.error}</span>
|
<span className="form-error">{confirmPassword.error}</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<Button type="submit" disabled={submitting || invalid || pristine}>
|
<Button type="submit" disabled={submitting || invalid || pristine}>{t('NewPasswordForm.SubmitSetNewPassword')}</Button>
|
||||||
Set New Password
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -67,6 +58,7 @@ NewPasswordForm.propTypes = {
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
reset_password_token: PropTypes.string,
|
reset_password_token: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
NewPasswordForm.defaultProps = {
|
NewPasswordForm.defaultProps = {
|
||||||
|
@ -75,4 +67,4 @@ NewPasswordForm.defaultProps = {
|
||||||
submitting: false,
|
submitting: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NewPasswordForm;
|
export default withTranslation()(NewPasswordForm);
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||||
import Button from '../../../common/Button';
|
import Button from '../../../common/Button';
|
||||||
|
|
||||||
function ResetPasswordForm(props) {
|
function ResetPasswordForm(props) {
|
||||||
const {
|
const {
|
||||||
fields: { email },
|
fields: { email }, handleSubmit, submitting, invalid, pristine, t
|
||||||
handleSubmit,
|
|
||||||
submitting,
|
|
||||||
invalid,
|
|
||||||
pristine,
|
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
|
@ -18,12 +14,10 @@ function ResetPasswordForm(props) {
|
||||||
onSubmit={handleSubmit(props.initiateResetPassword.bind(this))}
|
onSubmit={handleSubmit(props.initiateResetPassword.bind(this))}
|
||||||
>
|
>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="email" className="form__label">
|
<label htmlFor="email" className="form__label">{t('ResetPasswordForm.Email')}</label>
|
||||||
Email used for registration
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
aria-label="email"
|
aria-label={t('ResetPasswordForm.EmailARIA')}
|
||||||
type="text"
|
type="text"
|
||||||
id="email"
|
id="email"
|
||||||
{...domOnlyProps(email)}
|
{...domOnlyProps(email)}
|
||||||
|
@ -34,11 +28,8 @@ function ResetPasswordForm(props) {
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={
|
disabled={submitting || invalid || pristine || props.user.resetPasswordInitiate}
|
||||||
submitting || invalid || pristine || props.user.resetPasswordInitiate
|
>{t('ResetPasswordForm.Submit')}
|
||||||
}
|
|
||||||
>
|
|
||||||
Send Password Reset Email
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -54,8 +45,9 @@ ResetPasswordForm.propTypes = {
|
||||||
invalid: PropTypes.bool,
|
invalid: PropTypes.bool,
|
||||||
pristine: PropTypes.bool,
|
pristine: PropTypes.bool,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
resetPasswordInitiate: PropTypes.bool,
|
resetPasswordInitiate: PropTypes.bool
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
ResetPasswordForm.defaultProps = {
|
ResetPasswordForm.defaultProps = {
|
||||||
|
@ -64,4 +56,4 @@ ResetPasswordForm.defaultProps = {
|
||||||
invalid: false,
|
invalid: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ResetPasswordForm;
|
export default withTranslation()(ResetPasswordForm);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||||
import Button from '../../../common/Button';
|
import Button from '../../../common/Button';
|
||||||
|
@ -20,12 +21,10 @@ function SignupForm(props) {
|
||||||
onSubmit={handleSubmit(props.signUpUser.bind(this, props.previousPath))}
|
onSubmit={handleSubmit(props.signUpUser.bind(this, props.previousPath))}
|
||||||
>
|
>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="username" className="form__label">
|
<label htmlFor="username" className="form__label">{props.t('SignupForm.Title')}</label>
|
||||||
User Name
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
aria-label="username"
|
aria-label={props.t('SignupForm.TitleARIA')}
|
||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
{...domOnlyProps(username)}
|
{...domOnlyProps(username)}
|
||||||
|
@ -35,12 +34,10 @@ function SignupForm(props) {
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="email" className="form__label">
|
<label htmlFor="email" className="form__label">{props.t('SignupForm.Email')}</label>
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
aria-label="email"
|
aria-label={props.t('SignupForm.EmailARIA')}
|
||||||
type="text"
|
type="text"
|
||||||
id="email"
|
id="email"
|
||||||
{...domOnlyProps(email)}
|
{...domOnlyProps(email)}
|
||||||
|
@ -50,12 +47,10 @@ function SignupForm(props) {
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="password" className="form__label">
|
<label htmlFor="password" className="form__label">{props.t('SignupForm.Password')}</label>
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
aria-label="password"
|
aria-label={props.t('SignupForm.PasswordARIA')}
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
{...domOnlyProps(password)}
|
{...domOnlyProps(password)}
|
||||||
|
@ -65,13 +60,11 @@ function SignupForm(props) {
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="form__field">
|
<p className="form__field">
|
||||||
<label htmlFor="confirm password" className="form__label">
|
<label htmlFor="confirm password" className="form__label">{props.t('SignupForm.ConfirmPassword')}</label>
|
||||||
Confirm Password
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="form__input"
|
className="form__input"
|
||||||
type="password"
|
type="password"
|
||||||
aria-label="confirm password"
|
aria-label={props.t('SignupForm.ConfirmPasswordARIA')}
|
||||||
id="confirm password"
|
id="confirm password"
|
||||||
{...domOnlyProps(confirmPassword)}
|
{...domOnlyProps(confirmPassword)}
|
||||||
/>
|
/>
|
||||||
|
@ -79,8 +72,10 @@ function SignupForm(props) {
|
||||||
<span className="form-error">{confirmPassword.error}</span>
|
<span className="form-error">{confirmPassword.error}</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<Button type="submit" disabled={submitting || invalid || pristine}>
|
<Button
|
||||||
Sign Up
|
type="submit"
|
||||||
|
disabled={submitting || invalid || pristine}
|
||||||
|
>{props.t('SignupForm.SubmitSignup')}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -99,6 +94,7 @@ SignupForm.propTypes = {
|
||||||
invalid: PropTypes.bool,
|
invalid: PropTypes.bool,
|
||||||
pristine: PropTypes.bool,
|
pristine: PropTypes.bool,
|
||||||
previousPath: PropTypes.string.isRequired,
|
previousPath: PropTypes.string.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
SignupForm.defaultProps = {
|
SignupForm.defaultProps = {
|
||||||
|
@ -107,4 +103,4 @@ SignupForm.defaultProps = {
|
||||||
invalid: false,
|
invalid: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SignupForm;
|
export default withTranslation()(SignupForm);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { reduxForm } from 'redux-form';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
|
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
|
||||||
import AccountForm from '../components/AccountForm';
|
import AccountForm from '../components/AccountForm';
|
||||||
import apiClient from '../../../utils/apiClient';
|
import apiClient from '../../../utils/apiClient';
|
||||||
|
@ -16,9 +17,11 @@ function SocialLoginPanel(props) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<AccountForm {...props} />
|
<AccountForm {...props} />
|
||||||
<h2 className="form-container__divider">Social Login</h2>
|
{/* eslint-disable-next-line react/prop-types */}
|
||||||
|
<h2 className="form-container__divider">{props.t('AccountView.SocialLogin')}</h2>
|
||||||
<p className="account__social-text">
|
<p className="account__social-text">
|
||||||
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')}
|
||||||
</p>
|
</p>
|
||||||
<div className="account__social-stack">
|
<div className="account__social-stack">
|
||||||
<SocialAuthButton service={SocialAuthButton.services.github} />
|
<SocialAuthButton service={SocialAuthButton.services.github} />
|
||||||
|
@ -39,21 +42,21 @@ class AccountView extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="account-settings__container">
|
<div className="account-settings__container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Account Settings</title>
|
<title>{this.props.t('AccountView.Title')}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<Nav layout="dashboard" />
|
<Nav layout="dashboard" />
|
||||||
|
|
||||||
<main className="account-settings">
|
<main className="account-settings">
|
||||||
<header className="account-settings__header">
|
<header className="account-settings__header">
|
||||||
<h1 className="account-settings__title">Account Settings</h1>
|
<h1 className="account-settings__title">{this.props.t('AccountView.Settings')}</h1>
|
||||||
</header>
|
</header>
|
||||||
{accessTokensUIEnabled &&
|
{accessTokensUIEnabled &&
|
||||||
<Tabs className="account__tabs">
|
<Tabs className="account__tabs">
|
||||||
<TabList>
|
<TabList>
|
||||||
<div className="tabs__titles">
|
<div className="tabs__titles">
|
||||||
<Tab><h4 className="tabs__title">Account</h4></Tab>
|
<Tab><h4 className="tabs__title">{this.props.t('AccountView.AccountTab')}</h4></Tab>
|
||||||
{accessTokensUIEnabled && <Tab><h4 className="tabs__title">Access Tokens</h4></Tab>}
|
{accessTokensUIEnabled && <Tab><h4 className="tabs__title">{this.props.t('AccountView.AccessTokensTab')}</h4></Tab>}
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
|
@ -107,13 +110,14 @@ function asyncValidate(formProps, dispatch, props) {
|
||||||
|
|
||||||
AccountView.propTypes = {
|
AccountView.propTypes = {
|
||||||
previousPath: PropTypes.string.isRequired,
|
previousPath: PropTypes.string.isRequired,
|
||||||
theme: PropTypes.string.isRequired
|
theme: PropTypes.string.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm({
|
export default withTranslation()(reduxForm({
|
||||||
form: 'updateAllSettings',
|
form: 'updateAllSettings',
|
||||||
fields: ['username', 'email', 'currentPassword', 'newPassword'],
|
fields: ['username', 'email', 'currentPassword', 'newPassword'],
|
||||||
validate: validateSettings,
|
validate: validateSettings,
|
||||||
asyncValidate,
|
asyncValidate,
|
||||||
asyncBlurFields: ['username', 'email', 'currentPassword']
|
asyncBlurFields: ['username', 'email', 'currentPassword']
|
||||||
}, mapStateToProps, mapDispatchToProps)(AccountView);
|
}, mapStateToProps, mapDispatchToProps)(AccountView));
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { verifyEmailConfirmation } from '../actions';
|
import { verifyEmailConfirmation } from '../actions';
|
||||||
|
@ -31,20 +32,20 @@ class EmailVerificationView extends React.Component {
|
||||||
|
|
||||||
if (this.verificationToken() == null) {
|
if (this.verificationToken() == null) {
|
||||||
status = (
|
status = (
|
||||||
<p>That link is invalid.</p>
|
<p>{this.props.t('EmailVerificationView.InvalidTokenNull')}</p>
|
||||||
);
|
);
|
||||||
} else if (emailVerificationTokenState === 'checking') {
|
} else if (emailVerificationTokenState === 'checking') {
|
||||||
status = (
|
status = (
|
||||||
<p>Validating token, please wait...</p>
|
<p>{this.props.t('EmailVerificationView.Checking')}</p>
|
||||||
);
|
);
|
||||||
} else if (emailVerificationTokenState === 'verified') {
|
} else if (emailVerificationTokenState === 'verified') {
|
||||||
status = (
|
status = (
|
||||||
<p>All done, your email address has been verified.</p>
|
<p>{this.props.t('EmailVerificationView.Verified')}</p>
|
||||||
);
|
);
|
||||||
setTimeout(() => browserHistory.push('/'), 1000);
|
setTimeout(() => browserHistory.push('/'), 1000);
|
||||||
} else if (emailVerificationTokenState === 'invalid') {
|
} else if (emailVerificationTokenState === 'invalid') {
|
||||||
status = (
|
status = (
|
||||||
<p>Something went wrong.</p>
|
<p>{this.props.t('EmailVerificationView.InvalidState')}</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,10 +54,10 @@ class EmailVerificationView extends React.Component {
|
||||||
<Nav layout="dashboard" />
|
<Nav layout="dashboard" />
|
||||||
<div className="form-container">
|
<div className="form-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Email Verification</title>
|
<title>{this.props.t('EmailVerificationView.Title')}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Verify your email</h2>
|
<h2 className="form-container__title">{this.props.t('EmailVerificationView.Verify')}</h2>
|
||||||
{status}
|
{status}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -83,6 +84,7 @@ EmailVerificationView.propTypes = {
|
||||||
'checking', 'verified', 'invalid'
|
'checking', 'verified', 'invalid'
|
||||||
]),
|
]),
|
||||||
verifyEmailConfirmation: PropTypes.func.isRequired,
|
verifyEmailConfirmation: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(EmailVerificationView);
|
export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(EmailVerificationView));
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { reduxForm } from 'redux-form';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import i18next from 'i18next';
|
||||||
import NewPasswordForm from '../components/NewPasswordForm';
|
import NewPasswordForm from '../components/NewPasswordForm';
|
||||||
import * as UserActions from '../actions';
|
import * as UserActions from '../actions';
|
||||||
import Nav from '../../../components/Nav';
|
import Nav from '../../../components/Nav';
|
||||||
|
@ -20,13 +22,13 @@ function NewPasswordView(props) {
|
||||||
<Nav layout="dashboard" />
|
<Nav layout="dashboard" />
|
||||||
<div className={newPasswordClass}>
|
<div className={newPasswordClass}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | New Password</title>
|
<title>{props.t('NewPasswordView.Title')}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Set a New Password</h2>
|
<h2 className="form-container__title">{props.t('NewPasswordView.Description')}</h2>
|
||||||
<NewPasswordForm {...props} />
|
<NewPasswordForm {...props} />
|
||||||
<p className="new-password__invalid">
|
<p className="new-password__invalid">
|
||||||
The password reset token is invalid or has expired.
|
{props.t('NewPasswordView.TokenInvalidOrExpired')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,21 +43,22 @@ NewPasswordView.propTypes = {
|
||||||
validateResetPasswordToken: PropTypes.func.isRequired,
|
validateResetPasswordToken: PropTypes.func.isRequired,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
resetPasswordInvalid: PropTypes.bool
|
resetPasswordInvalid: PropTypes.bool
|
||||||
}).isRequired
|
}).isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
function validate(formProps) {
|
function validate(formProps) {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
|
|
||||||
if (!formProps.password) {
|
if (!formProps.password) {
|
||||||
errors.password = 'Please enter a password';
|
errors.password = i18next.t('NewPasswordView.EmptyPassword');
|
||||||
}
|
}
|
||||||
if (!formProps.confirmPassword) {
|
if (!formProps.confirmPassword) {
|
||||||
errors.confirmPassword = 'Please enter a password confirmation';
|
errors.confirmPassword = i18next.t('NewPasswordView.PasswordConfirmation');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formProps.password !== formProps.confirmPassword) {
|
if (formProps.password !== formProps.confirmPassword) {
|
||||||
errors.password = 'Passwords must match';
|
errors.password = i18next.t('NewPasswordView.PasswordMismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
|
@ -71,8 +74,8 @@ function mapDispatchToProps(dispatch) {
|
||||||
return bindActionCreators(UserActions, dispatch);
|
return bindActionCreators(UserActions, dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default reduxForm({
|
export default withTranslation()(reduxForm({
|
||||||
form: 'new-password',
|
form: 'new-password',
|
||||||
fields: ['password', 'confirmPassword'],
|
fields: ['password', 'confirmPassword'],
|
||||||
validate
|
validate
|
||||||
}, mapStateToProps, mapDispatchToProps)(NewPasswordView);
|
}, mapStateToProps, mapDispatchToProps)(NewPasswordView));
|
||||||
|
|
|
@ -6,6 +6,7 @@ import classNames from 'classnames';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import * as UserActions from '../actions';
|
import * as UserActions from '../actions';
|
||||||
import ResetPasswordForm from '../components/ResetPasswordForm';
|
import ResetPasswordForm from '../components/ResetPasswordForm';
|
||||||
import { validateResetPassword } from '../../../utils/reduxFormUtils';
|
import { validateResetPassword } from '../../../utils/reduxFormUtils';
|
||||||
|
@ -23,19 +24,18 @@ function ResetPasswordView(props) {
|
||||||
<Nav layout="dashboard" />
|
<Nav layout="dashboard" />
|
||||||
<div className={resetPasswordClass}>
|
<div className={resetPasswordClass}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Reset Password</title>
|
<title>{props.t('ResetPasswordView.Title')}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Reset Your Password</h2>
|
<h2 className="form-container__title">{props.t('ResetPasswordView.Reset')}</h2>
|
||||||
<ResetPasswordForm {...props} />
|
<ResetPasswordForm {...props} />
|
||||||
<p className="reset-password__submitted">
|
<p className="reset-password__submitted">
|
||||||
Your password reset email should arrive shortly. If you don't see it, check
|
{props.t('ResetPasswordView.Submitted')}
|
||||||
in your spam folder as sometimes it can end up there.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="form__navigation-options">
|
<p className="form__navigation-options">
|
||||||
<Link className="form__login-button" to="/login">Log In</Link>
|
<Link className="form__login-button" to="/login">{props.t('ResetPasswordView.Login')}</Link>
|
||||||
or
|
{props.t('ResetPasswordView.LoginOr')}
|
||||||
<Link className="form__signup-button" to="/signup">Sign Up</Link>
|
<Link className="form__signup-button" to="/signup">{props.t('ResetPasswordView.SignUp')}</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,6 +48,7 @@ ResetPasswordView.propTypes = {
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
resetPasswordInitiate: PropTypes.bool
|
resetPasswordInitiate: PropTypes.bool
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
|
@ -60,8 +61,8 @@ function mapDispatchToProps(dispatch) {
|
||||||
return bindActionCreators(UserActions, dispatch);
|
return bindActionCreators(UserActions, dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default reduxForm({
|
export default withTranslation()(reduxForm({
|
||||||
form: 'reset-password',
|
form: 'reset-password',
|
||||||
fields: ['email'],
|
fields: ['email'],
|
||||||
validate: validateResetPassword
|
validate: validateResetPassword
|
||||||
}, mapStateToProps, mapDispatchToProps)(ResetPasswordView);
|
}, mapStateToProps, mapDispatchToProps)(ResetPasswordView));
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { bindActionCreators } from 'redux';
|
||||||
import { Link, browserHistory } from 'react-router';
|
import { Link, browserHistory } from 'react-router';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import * as UserActions from '../actions';
|
import * as UserActions from '../actions';
|
||||||
import SignupForm from '../components/SignupForm';
|
import SignupForm from '../components/SignupForm';
|
||||||
import apiClient from '../../../utils/apiClient';
|
import apiClient from '../../../utils/apiClient';
|
||||||
|
@ -26,19 +27,19 @@ class SignupView extends React.Component {
|
||||||
<Nav layout="dashboard" />
|
<Nav layout="dashboard" />
|
||||||
<main className="form-container">
|
<main className="form-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Signup</title>
|
<title>{this.props.t('SignupView.Title')}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Sign Up</h2>
|
<h2 className="form-container__title">{this.props.t('SignupView.Description')}</h2>
|
||||||
<SignupForm {...this.props} />
|
<SignupForm {...this.props} />
|
||||||
<h2 className="form-container__divider">Or</h2>
|
<h2 className="form-container__divider">{this.props.t('SignupView.Or')}</h2>
|
||||||
<div className="form-container__stack">
|
<div className="form-container__stack">
|
||||||
<SocialAuthButton service={SocialAuthButton.services.github} />
|
<SocialAuthButton service={SocialAuthButton.services.github} />
|
||||||
<SocialAuthButton service={SocialAuthButton.services.google} />
|
<SocialAuthButton service={SocialAuthButton.services.google} />
|
||||||
</div>
|
</div>
|
||||||
<p className="form__navigation-options">
|
<p className="form__navigation-options">
|
||||||
Already have an account?
|
{this.props.t('SignupView.AlreadyHave')}
|
||||||
<Link className="form__login-button" to="/login">Log In</Link>
|
<Link className="form__login-button" to="/login">{this.props.t('SignupView.Login')}</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
@ -108,7 +109,8 @@ SignupView.propTypes = {
|
||||||
previousPath: PropTypes.string.isRequired,
|
previousPath: PropTypes.string.isRequired,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
authenticated: PropTypes.bool
|
authenticated: PropTypes.bool
|
||||||
})
|
}),
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
SignupView.defaultProps = {
|
SignupView.defaultProps = {
|
||||||
|
@ -117,11 +119,11 @@ SignupView.defaultProps = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm({
|
export default withTranslation()(reduxForm({
|
||||||
form: 'signup',
|
form: 'signup',
|
||||||
fields: ['username', 'email', 'password', 'confirmPassword'],
|
fields: ['username', 'email', 'password', 'confirmPassword'],
|
||||||
onSubmitFail,
|
onSubmitFail,
|
||||||
validate: validateSignup,
|
validate: validateSignup,
|
||||||
asyncValidate,
|
asyncValidate,
|
||||||
asyncBlurFields: ['username', 'email']
|
asyncBlurFields: ['username', 'email']
|
||||||
}, mapStateToProps, mapDispatchToProps)(SignupView);
|
}, mapStateToProps, mapDispatchToProps)(SignupView));
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const useModalBehavior = (hideOverlay) => {
|
||||||
|
|
||||||
|
|
||||||
const handleClickOutside = ({ target }) => {
|
const handleClickOutside = ({ target }) => {
|
||||||
if (ref && ref.current && !ref.current.contains(target)) {
|
if (ref && ref.current && !(ref.current.contains && ref.current.contains(target))) {
|
||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
import i18n from 'i18next';
|
||||||
export const domOnlyProps = ({
|
export const domOnlyProps = ({
|
||||||
initialValue,
|
initialValue,
|
||||||
autofill,
|
autofill,
|
||||||
|
@ -20,19 +21,19 @@ const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))
|
||||||
|
|
||||||
function validateNameEmail(formProps, errors) {
|
function validateNameEmail(formProps, errors) {
|
||||||
if (!formProps.username) {
|
if (!formProps.username) {
|
||||||
errors.username = 'Please enter a username.';
|
errors.username = i18n.t('ReduxFormUtils.errorEmptyUsername');
|
||||||
} else if (!formProps.username.match(/^.{1,20}$/)) {
|
} else if (!formProps.username.match(/^.{1,20}$/)) {
|
||||||
errors.username = 'Username must be less than 20 characters.';
|
errors.username = i18n.t('ReduxFormUtils.errorLongUsername');
|
||||||
} else if (!formProps.username.match(/^[a-zA-Z0-9._-]{1,20}$/)) {
|
} else if (!formProps.username.match(/^[a-zA-Z0-9._-]{1,20}$/)) {
|
||||||
errors.username = 'Username must only consist of numbers, letters, periods, dashes, and underscores.';
|
errors.username = i18n.t('ReduxFormUtils.errorValidUsername');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formProps.email) {
|
if (!formProps.email) {
|
||||||
errors.email = 'Please enter an email.';
|
errors.email = i18n.t('ReduxFormUtils.errorEmptyEmail');
|
||||||
} else if (
|
} else if (
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
!formProps.email.match(EMAIL_REGEX)) {
|
!formProps.email.match(EMAIL_REGEX)) {
|
||||||
errors.email = 'Please enter a valid email address.';
|
errors.email = i18n.t('ReduxFormUtils.errorInvalidEmail');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,10 +43,10 @@ export function validateSettings(formProps) {
|
||||||
validateNameEmail(formProps, errors);
|
validateNameEmail(formProps, errors);
|
||||||
|
|
||||||
if (formProps.currentPassword && !formProps.newPassword) {
|
if (formProps.currentPassword && !formProps.newPassword) {
|
||||||
errors.newPassword = 'Please enter a new password or leave the current password empty.';
|
errors.newPassword = i18n.t('ReduxFormUtils.errorNewPassword');
|
||||||
}
|
}
|
||||||
if (formProps.newPassword && formProps.newPassword.length < 6) {
|
if (formProps.newPassword && formProps.newPassword.length < 6) {
|
||||||
errors.newPassword = 'Password must be at least 6 characters';
|
errors.newPassword = i18n.t('ReduxFormUtils.errorShortPassword');
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
@ -53,10 +54,10 @@ export function validateSettings(formProps) {
|
||||||
export function validateLogin(formProps) {
|
export function validateLogin(formProps) {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
if (!formProps.email) {
|
if (!formProps.email) {
|
||||||
errors.email = 'Please enter an email';
|
errors.email = i18n.t('ReduxFormUtils.errorEmptyEmail');
|
||||||
}
|
}
|
||||||
if (!formProps.password) {
|
if (!formProps.password) {
|
||||||
errors.password = 'Please enter a password';
|
errors.password = i18n.t('ReduxFormUtils.errorEmptyPassword');
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
@ -67,17 +68,17 @@ export function validateSignup(formProps) {
|
||||||
validateNameEmail(formProps, errors);
|
validateNameEmail(formProps, errors);
|
||||||
|
|
||||||
if (!formProps.password) {
|
if (!formProps.password) {
|
||||||
errors.password = 'Please enter a password';
|
errors.password = i18n.t('ReduxFormUtils.errorEmptyPassword');
|
||||||
}
|
}
|
||||||
if (formProps.password && formProps.password.length < 6) {
|
if (formProps.password && formProps.password.length < 6) {
|
||||||
errors.password = 'Password must be at least 6 characters';
|
errors.password = i18n.t('ReduxFormUtils.errorShortPassword');
|
||||||
}
|
}
|
||||||
if (!formProps.confirmPassword) {
|
if (!formProps.confirmPassword) {
|
||||||
errors.confirmPassword = 'Please enter a password confirmation';
|
errors.confirmPassword = i18n.t('ReduxFormUtils.errorConfirmPassword');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formProps.password !== formProps.confirmPassword && formProps.confirmPassword) {
|
if (formProps.password !== formProps.confirmPassword && formProps.confirmPassword) {
|
||||||
errors.confirmPassword = 'Passwords must match';
|
errors.confirmPassword = i18n.t('ReduxFormUtils.errorPasswordMismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
|
@ -85,11 +86,11 @@ export function validateSignup(formProps) {
|
||||||
export function validateResetPassword(formProps) {
|
export function validateResetPassword(formProps) {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
if (!formProps.email) {
|
if (!formProps.email) {
|
||||||
errors.email = 'Please enter an email.';
|
errors.email = i18n.t('ReduxFormUtils.errorEmptyEmail');
|
||||||
} else if (
|
} else if (
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
!formProps.email.match(EMAIL_REGEX)) {
|
!formProps.email.match(EMAIL_REGEX)) {
|
||||||
errors.email = 'Please enter a valid email address.';
|
errors.email = i18n.t('ReduxFormUtils.errorInvalidEmail');
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,8 @@ const userSchema = new Schema({
|
||||||
gridOutput: { type: Boolean, default: false },
|
gridOutput: { type: Boolean, default: false },
|
||||||
soundOutput: { type: Boolean, default: false },
|
soundOutput: { type: Boolean, default: false },
|
||||||
theme: { type: String, default: 'light' },
|
theme: { type: String, default: 'light' },
|
||||||
autorefresh: { type: Boolean, default: false }
|
autorefresh: { type: Boolean, default: false },
|
||||||
|
language: { type: String, default: 'en-US' }
|
||||||
},
|
},
|
||||||
totalSize: { type: Number, default: 0 }
|
totalSize: { type: Number, default: 0 }
|
||||||
}, { timestamps: true, usePushEach: true });
|
}, { timestamps: true, usePushEach: true });
|
||||||
|
|
|
@ -183,7 +183,6 @@
|
||||||
"Error": "Error",
|
"Error": "Error",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"p5logoARIA": "p5.js Logo"
|
"p5logoARIA": "p5.js Logo"
|
||||||
|
|
||||||
},
|
},
|
||||||
"IDEView": {
|
"IDEView": {
|
||||||
"SubmitFeedback": "Submit Feedback"
|
"SubmitFeedback": "Submit Feedback"
|
||||||
|
@ -197,7 +196,7 @@
|
||||||
"NewFileForm": {
|
"NewFileForm": {
|
||||||
"AddFileSubmit": "Add File",
|
"AddFileSubmit": "Add File",
|
||||||
"Placeholder": "Name"
|
"Placeholder": "Name"
|
||||||
},
|
},
|
||||||
"NewFolderModal": {
|
"NewFolderModal": {
|
||||||
"Title": "Create Folder",
|
"Title": "Create Folder",
|
||||||
"CloseButtonARIA": "Close New Folder Modal",
|
"CloseButtonARIA": "Close New Folder Modal",
|
||||||
|
@ -208,5 +207,114 @@
|
||||||
"NewFolderForm": {
|
"NewFolderForm": {
|
||||||
"AddFolderSubmit": "Add Folder",
|
"AddFolderSubmit": "Add Folder",
|
||||||
"Placeholder": "Name"
|
"Placeholder": "Name"
|
||||||
|
},
|
||||||
|
"ResetPasswordForm": {
|
||||||
|
"Email": "Email used for registration",
|
||||||
|
"EmailARIA": "email",
|
||||||
|
"Submit": "Send Password Reset Email"
|
||||||
|
},
|
||||||
|
"ResetPasswordView": {
|
||||||
|
"Title": "p5.js Web Editor | Reset Password",
|
||||||
|
"Reset": "Reset Your Password",
|
||||||
|
"Submitted": "Your password reset email should arrive shortly. If you don't see it, check\n in your spam folder as sometimes it can end up there.",
|
||||||
|
"Login": "Log In",
|
||||||
|
"LoginOr": "or",
|
||||||
|
"SignUp": "Sign Up"
|
||||||
|
},
|
||||||
|
"ReduxFormUtils": {
|
||||||
|
"errorInvalidEmail": "Please enter a valid email address",
|
||||||
|
"errorEmptyEmail": "Please enter an email",
|
||||||
|
"errorPasswordMismatch": "Passwords must match",
|
||||||
|
"errorEmptyPassword": "Please enter a password",
|
||||||
|
"errorShortPassword": "Password must be at least 6 characters",
|
||||||
|
"errorConfirmPassword": "Please enter a password confirmation",
|
||||||
|
"errorNewPassword": "Please enter a new password or leave the current password empty.",
|
||||||
|
"errorEmptyUsername": "Please enter a username.",
|
||||||
|
"errorLongUsername": "Username must be less than 20 characters.",
|
||||||
|
"errorValidUsername": "Username must only consist of numbers, letters, periods, dashes, and underscores."
|
||||||
|
},
|
||||||
|
"NewPasswordView": {
|
||||||
|
"Title": "p5.js Web Editor | New Password",
|
||||||
|
"Description": "Set a New Password",
|
||||||
|
"TokenInvalidOrExpired": "The password reset token is invalid or has expired.",
|
||||||
|
"EmptyPassword": "Please enter a password",
|
||||||
|
"PasswordConfirmation": "Please enter a password confirmation",
|
||||||
|
"PasswordMismatch": "Passwords must match"
|
||||||
|
},
|
||||||
|
"AccountForm": {
|
||||||
|
"Email": "Email",
|
||||||
|
"EmailARIA": "email",
|
||||||
|
"Unconfirmed": "Unconfirmed.",
|
||||||
|
"EmailSent": "Confirmation sent, check your email.",
|
||||||
|
"Resend": "Resend confirmation email",
|
||||||
|
"UserName": "User Name",
|
||||||
|
"UserNameARIA": "Username",
|
||||||
|
"CurrentPassword": "Current Password",
|
||||||
|
"CurrentPasswordARIA": "Current Password",
|
||||||
|
"NewPassword": "New Password",
|
||||||
|
"NewPasswordARIA": "New Password",
|
||||||
|
"SubmitSaveAllSettings": "Save All Settings"
|
||||||
|
},
|
||||||
|
"AccountView": {
|
||||||
|
"SocialLogin": "Social Login",
|
||||||
|
"SocialLoginDescription": "Use your GitHub or Google account to log into the p5.js Web Editor.",
|
||||||
|
"Title": "p5.js Web Editor | Account Settings",
|
||||||
|
"Settings": "Account Settings",
|
||||||
|
"AccountTab": "Account",
|
||||||
|
"AccessTokensTab": "Access Tokens"
|
||||||
|
},
|
||||||
|
"APIKeyForm": {
|
||||||
|
"ConfirmDelete": "Are you sure you want to delete {{key_label}}?",
|
||||||
|
"Summary": "Personal Access Tokens act like your password to allow automated\n scripts to access the Editor API. Create a token for each script\n that needs access.",
|
||||||
|
"CreateToken": "Create new token",
|
||||||
|
"TokenLabel": "What is this token for?",
|
||||||
|
"TokenPlaceholder": "What is this token for? e.g. Example import script",
|
||||||
|
"CreateTokenSubmit": "Create",
|
||||||
|
"NoTokens": "You have no existing tokens.",
|
||||||
|
"NewTokenTitle": "Your new access token",
|
||||||
|
"NewTokenInfo": "Make sure to copy your new personal access token now.\n You won’t be able to see it again!",
|
||||||
|
"ExistingTokensTitle": "Existing tokens"
|
||||||
|
},
|
||||||
|
"APIKeyList": {
|
||||||
|
"Name": "Name",
|
||||||
|
"Created": "Created on",
|
||||||
|
"LastUsed": "Last used",
|
||||||
|
"Actions": "Actions",
|
||||||
|
"Never": "Never",
|
||||||
|
"DeleteARIA": "Delete API Key"
|
||||||
|
},
|
||||||
|
"NewPasswordForm": {
|
||||||
|
"Title": "Password",
|
||||||
|
"TitleARIA": "Password",
|
||||||
|
"ConfirmPassword": "Confirm Password",
|
||||||
|
"ConfirmPasswordARIA": "Confirm Password",
|
||||||
|
"SubmitSetNewPassword": "Set New Password"
|
||||||
|
},
|
||||||
|
"SignupForm": {
|
||||||
|
"Title": "User Name",
|
||||||
|
"TitleARIA": "username",
|
||||||
|
"Email": "Email",
|
||||||
|
"EmailARIA": "email",
|
||||||
|
"Password": "Password",
|
||||||
|
"PasswordARIA": "password",
|
||||||
|
"ConfirmPassword": "Confirm Password",
|
||||||
|
"ConfirmPasswordARIA": "Confirm password",
|
||||||
|
"SubmitSignup": "Sign Up"
|
||||||
|
},
|
||||||
|
"SignupView": {
|
||||||
|
"Title": "p5.js Web Editor | Signup",
|
||||||
|
"Description": "Sign Up",
|
||||||
|
"Or": "Or",
|
||||||
|
"AlreadyHave": "Already have an account?",
|
||||||
|
"Login": "Log In"
|
||||||
|
},
|
||||||
|
|
||||||
|
"EmailVerificationView": {
|
||||||
|
"Title": "p5.js Web Editor | Email Verification",
|
||||||
|
"Verify": "Verify your email",
|
||||||
|
"InvalidTokenNull": "That link is invalid.",
|
||||||
|
"Checking": "Validating token, please wait...",
|
||||||
|
"Verified": "All done, your email address has been verified.",
|
||||||
|
"InvalidState": "Something went wrong."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -203,13 +203,117 @@
|
||||||
"EnterName": "Por favor introduce un nombre",
|
"EnterName": "Por favor introduce un nombre",
|
||||||
"EmptyName": " El nombre del directorio no debe contener solo espacios vacíos",
|
"EmptyName": " El nombre del directorio no debe contener solo espacios vacíos",
|
||||||
"InvalidExtension": "El nombre del directorio no debe contener una extensión"
|
"InvalidExtension": "El nombre del directorio no debe contener una extensión"
|
||||||
},
|
},
|
||||||
"NewFolderForm": {
|
"NewFolderForm": {
|
||||||
"AddFolderSubmit": "Agregar Directorio",
|
"AddFolderSubmit": "Agregar Directorio",
|
||||||
"Placeholder": "Nombre"
|
"Placeholder": "Nombre"
|
||||||
|
},
|
||||||
|
"ResetPasswordForm": {
|
||||||
|
"Email": "Correo electrónico usado al registrarse",
|
||||||
|
"EmailARIA": "correo electrónico",
|
||||||
|
"Submit": "Enviar correo para regenerar contraseña"
|
||||||
|
},
|
||||||
|
"ResetPasswordView": {
|
||||||
|
"Title": "Editor Web p5.js | Regenerar Contraseña",
|
||||||
|
"Reset": "Regenerar Contraseña",
|
||||||
|
"Submitted": "Your password reset email should arrive shortly. If you don't see it, check\n in your spam folder as sometimes it can end up there.",
|
||||||
|
"Login": "Ingresa",
|
||||||
|
"LoginOr": "o",
|
||||||
|
"SignUp": "Registráte"
|
||||||
|
},
|
||||||
|
"ReduxFormUtils": {
|
||||||
|
"errorInvalidEmail": "Por favor introduce un correo electrónico válido",
|
||||||
|
"errorEmptyEmail": "Por favor introduce un correo electrónico",
|
||||||
|
"errorPasswordMismatch": "Las contraseñas deben coincidir",
|
||||||
|
"errorEmptyPassword": "Por favor introduce una contraseña",
|
||||||
|
"errorShortPassword": "La contraseña debe tener al menos 6 caracteres",
|
||||||
|
"errorConfirmPassword": "Por favor confirma una contraseña",
|
||||||
|
"errorNewPassword": "Por favor introduce una nueva contraseña o deja la actual contraseña vacía",
|
||||||
|
"errorEmptyUsername": "Por favor introduce tu identificación",
|
||||||
|
"errorLongUsername": "La identificación debe ser menor a 20 caracteres.",
|
||||||
|
"errorValidUsername": "La identificación debe consistir solamente de números, letras, puntos, guiones y guiones bajos."
|
||||||
|
},
|
||||||
|
"NewPasswordView": {
|
||||||
|
"Title": "Editor Web p5.js | Nueva Contraseña",
|
||||||
|
"Description": "Define una nueva contraseña",
|
||||||
|
"TokenInvalidOrExpired": "El token para regenerar la contraseña es inválido o ha expirado.",
|
||||||
|
"EmptyPassword": "Por favor introduce una contraseña",
|
||||||
|
"PasswordConfirmation": "Por favor confirma la contraseña",
|
||||||
|
"PasswordMismatch": "Las contraseña deben coincidir"
|
||||||
|
},
|
||||||
|
"AccountForm": {
|
||||||
|
"Email": "Correo Electrónico",
|
||||||
|
"EmailARIA": "correo electrónico",
|
||||||
|
"Unconfirmed": "Sin confirmar.",
|
||||||
|
"EmailSent": "Confirmación enviada, revisa tu correo electrónico.",
|
||||||
|
"Resend": "Reenviar correo de confirmación",
|
||||||
|
"UserName": "Nombre de Identificación",
|
||||||
|
"UserNameARIA": "Nombre de identificación",
|
||||||
|
"CurrentPassword": "Contraseña Actual",
|
||||||
|
"CurrentPasswordARIA": "Contraseña Actual",
|
||||||
|
"NewPassword": "Nueva Contraseña",
|
||||||
|
"NewPasswordARIA": "Nueva Contraseña",
|
||||||
|
"SubmitSaveAllSettings": "Guardar Todas Las Configuraciones"
|
||||||
|
},
|
||||||
|
"AccountView": {
|
||||||
|
"SocialLogin": "Identificacion usando redes sociales",
|
||||||
|
"SocialLoginDescription": "Usa tu cuenta de GitHub o Google para acceder al Editor Web de p5.js .",
|
||||||
|
"Title": "Editor Web p5.js | Configuración Cuenta",
|
||||||
|
"Settings": "Configuración de la Cuenta",
|
||||||
|
"AccountTab": "Cuenta",
|
||||||
|
"AccessTokensTab": "Tokens de acceso"
|
||||||
|
},
|
||||||
|
"APIKeyForm": {
|
||||||
|
"ConfirmDelete": "¿Estas seguro que quieres borrar {{key_label}}?",
|
||||||
|
"Summary": " Los Tokens de acceso personal actuan como tu contraseña para permitir\n a scripts automáticos acceder al API del Editor. Crea un token por cada script \n que necesite acceso.",
|
||||||
|
"CreateToken": "Crear nuevo token",
|
||||||
|
"TokenLabel": "¿Para que es este token?",
|
||||||
|
"TokenPlaceholder": "¿Para que es este token? p.e. Ejemplo para Importar un Archivo",
|
||||||
|
"CreateTokenSubmit": "Crear",
|
||||||
|
"NoTokens": "No tienes tokens.",
|
||||||
|
"NewTokenTitle": "Tu nuevo token de acceso",
|
||||||
|
"NewTokenInfo": "Asegurate de copiar tu token ahora mismo.\n ¡No podras verlo de nuevo!",
|
||||||
|
"ExistingTokensTitle": "Tokens existentes"
|
||||||
|
},
|
||||||
|
"APIKeyList": {
|
||||||
|
"Name": "Nombre",
|
||||||
|
"Created": "Creado en",
|
||||||
|
"LastUsed": "Usado por última vez",
|
||||||
|
"Actions": "Acciones",
|
||||||
|
"Never": "Nunca",
|
||||||
|
"DeleteARIA": "Borrar clave de API"
|
||||||
|
},
|
||||||
|
"NewPasswordForm": {
|
||||||
|
"Title": "Contraseña",
|
||||||
|
"TitleARIA": "Contraseña",
|
||||||
|
"ConfirmPassword": "Confirmar Contraseña",
|
||||||
|
"ConfirmPasswordARIA": "Confirmar contraseña",
|
||||||
|
"SubmitSetNewPassword": "Crear Nueva Contraseña"
|
||||||
|
},
|
||||||
|
"SignupForm": {
|
||||||
|
"Title": "Identificación",
|
||||||
|
"TitleARIA": "Identificación",
|
||||||
|
"Email": "Correo electrónico",
|
||||||
|
"EmailARIA": "correo electrónico",
|
||||||
|
"Password": "Contraseña",
|
||||||
|
"PasswordARIA": "contraseña",
|
||||||
|
"ConfirmPassword": "Confirma tu contraseña",
|
||||||
|
"ConfirmPasswordARIA": "Confirma tu contraseña",
|
||||||
|
"SubmitSignup": "Registráte"
|
||||||
|
},
|
||||||
|
"SignupView": {
|
||||||
|
"Title": " Editor Web p5.js | Registráte",
|
||||||
|
"Description": "Registráte",
|
||||||
|
"Or": "o",
|
||||||
|
"AlreadyHave": "¿Ya tienes cuenta? ",
|
||||||
|
"Login": "Ingresa"
|
||||||
|
},
|
||||||
|
"EmailVerificationView": {
|
||||||
|
"Title": "Editor Web p5.js | Correo de Verificación",
|
||||||
|
"Verify": "Verica tu correo",
|
||||||
|
"InvalidTokenNull": "La liga es inválida.",
|
||||||
|
"Checking": "Validando token, por favor espera...",
|
||||||
|
"Verified": "Concluido, tu correo electrónico ha sido verificado.",
|
||||||
|
"InvalidState": "Algo salió mal."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue