🔀 merge from mobile-examples

This commit is contained in:
ghalestrilo 2020-08-17 14:31:45 -03:00
commit 1503b1bdab
26 changed files with 459 additions and 223 deletions

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

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

@ -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) {
@ -23,6 +24,10 @@ class App extends React.Component {
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 +55,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

@ -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 (
@ -389,6 +388,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>
@ -538,7 +538,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

@ -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 && ( {
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 wont {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;

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' && ( {
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);

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

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

@ -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,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,105 @@
"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"
} }
} }

View file

@ -203,13 +203,109 @@
"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"
} }
} }