🔀 pull from develop

This commit is contained in:
ghalestrilo 2020-08-18 19:33:15 -03:00
commit f5c7a31fbd
44 changed files with 759 additions and 265 deletions

View File

@ -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);

View File

@ -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; }

View File

@ -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)));

View File

@ -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', () => {

View File

@ -1,35 +1,66 @@
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
icon={icon} }) =>
aria-label={aria} (
key={`bottom-bar-${aria}`} <IconButton
onClick={() => action()} inverted={inverted}
/>))} className={inverted && 'inverted'}
icon={icon}
aria-label={aria}
key={`bottom-bar-${aria}`}
onClick={() => action()}
/>))}
</BottomBarContent> </BottomBarContent>
); );
}; };
ActionStrip.propTypes = {
toggleExplorer: PropTypes.func
};
ActionStrip.defaultProps = {
toggleExplorer: () => {}
};
export default ActionStrip;

View 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;

View 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;

View File

@ -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;

View File

@ -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)}; };
`; `;

View 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;

View File

@ -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;
background: black;
opacity: 0.3;
`;
const wrapper = () => <div ref={setRef}> {visible && component} </div>; // eslint-disable-line export default (Element, hasOverlay = false) => {
const [visible, toggle, setRef] = useModalBehavior();
return [trigger, wrapper]; const wrapper = () => (visible &&
<div>
{hasOverlay && <BackgroundOverlay />}
<div ref={setRef}>
{ (typeof (Element) === 'function')
? Element(toggle)
: Element}
</div>
</div>);
return [toggle, wrapper];
}; };

View File

@ -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';

View 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

View 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

View 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

View 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

View File

@ -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);

View File

@ -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);
}
};
}

View File

@ -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>

View File

@ -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">

View File

@ -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

View File

@ -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,

View File

@ -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>

View File

@ -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,

View File

@ -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({

View File

@ -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;
} }

View File

@ -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 />}

View File

@ -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));

View File

@ -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 && ( {
<div className="api-key-form__new-token"> keyWithToken && (
<h4 className="api-key-form__new-token__title"> <div className="api-key-form__new-token">
Your new access token <h4 className="api-key-form__new-token__title">{this.props.t('APIKeyForm.NewTokenTitle')}</h4>
</h4> <p className="api-key-form__new-token__info">
<p className="api-key-form__new-token__info"> {this.props.t('APIKeyForm.NewTokenInfo')}
Make sure to copy your new personal access token now. You wont </p>
be able to see it again! <CopyableInput label={keyWithToken.label} value={keyWithToken.token} />
</p> </div>
<CopyableInput )
label={keyWithToken.label} }
value={keyWithToken.token}
/>
</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;

View File

@ -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;

View File

@ -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' && ( {
<p className="form__context"> user.verified !== 'verified' &&
<span className="form__status">Unconfirmed.</span> (
{user.emailVerificationInitiate === true ? ( <p className="form__context">
<span className="form__status"> <span className="form__status">{t('AccountForm.Unconfirmed')}</span>
{' '} {
Confirmation sent, check your email. user.emailVerificationInitiate === true ?
</span> (
) : ( <span className="form__status"> {t('AccountForm.EmailSent')}</span>
<Button onClick={handleInitiateVerification}> ) :
Resend confirmation email (
</Button> <Button
)} onClick={handleInitiateVerification}
</p> >{t('AccountForm.Resend')}
)} </Button>
)
}
</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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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));

View File

@ -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));

View File

@ -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));

View File

@ -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&apos;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>
&nbsp;or&nbsp; &nbsp;{props.t('ResetPasswordView.LoginOr')}&nbsp;
<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));

View File

@ -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?&nbsp; {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));

View File

@ -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();
} }
}; };

View File

@ -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;
} }

View File

@ -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 });

View File

@ -183,21 +183,20 @@
"Error": "Error", "Error": "Error",
"Save": "Save", "Save": "Save",
"p5logoARIA": "p5.js Logo" "p5logoARIA": "p5.js Logo"
}, },
"IDEView": { "IDEView": {
"SubmitFeedback": "Submit Feedback" "SubmitFeedback": "Submit Feedback"
}, },
"NewFileModal": { "NewFileModal": {
"Title": "Create File", "Title": "Create File",
"CloseButtonARIA": "Close New File Modal", "CloseButtonARIA": "Close New File Modal",
"EnterName": "Please enter a name", "EnterName": "Please enter a name",
"InvalidType": "Invalid file type. Valid extensions are .js, .css, .json, .txt, .csv, .tsv, .frag, and .vert." "InvalidType": "Invalid file type. Valid extensions are .js, .css, .json, .txt, .csv, .tsv, .frag, and .vert."
}, },
"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 wont 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."
} }
} }

View File

@ -188,8 +188,8 @@
"SubmitFeedback": "Enviar retroalimentación" "SubmitFeedback": "Enviar retroalimentación"
}, },
"NewFileModal": { "NewFileModal": {
"Title": "Crear Archivo", "Title": "Crear Archivo",
"CloseButtonARIA": "Cerrar diálogo de crear archivo", "CloseButtonARIA": "Cerrar diálogo de crear archivo",
"EnterName": "Por favor introduce un nombre", "EnterName": "Por favor introduce un nombre",
"InvalidType": "Tipo de archivo inválido. Las extensiones válidas son .js, .css, .json, .txt, .csv, .tsv, .frag y .vert." "InvalidType": "Tipo de archivo inválido. Las extensiones válidas son .js, .css, .json, .txt, .csv, .tsv, .frag y .vert."
}, },
@ -198,18 +198,122 @@
"Placeholder": "Nombre" "Placeholder": "Nombre"
}, },
"NewFolderModal": { "NewFolderModal": {
"Title": "Crear Directorio", "Title": "Crear Directorio",
"CloseButtonARIA": "Cerrar Diálogo de Nuevo Directorio", "CloseButtonARIA": "Cerrar Diálogo de Nuevo Directorio",
"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."
} }
} }