Merge branch 'feature/mobile-examples' of https://github.com/ghalestrilo/p5.js-web-editor into feature/mobile-files-tab
This commit is contained in:
		
						commit
						7c1e6f3cc2
					
				
					 15 changed files with 344 additions and 181 deletions
				
			
		client
modules
IDE/components
Mobile
User
utils
translations/locales
|  | @ -143,9 +143,9 @@ class CollectionList extends React.Component { | |||
|             <thead> | ||||
|               <tr> | ||||
|                 {this._renderFieldHeader('name', 'Name')} | ||||
|                 {(!mobile) && this._renderFieldHeader('createdAt', 'Date Created')} | ||||
|                 {this._renderFieldHeader('updatedAt', 'Date Updated')} | ||||
|                 {this._renderFieldHeader('numItems', '# sketches')} | ||||
|                 {this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)} | ||||
|                 {this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)} | ||||
|                 {this._renderFieldHeader('numItems', mobile ? 'Sketches' : '# sketches')} | ||||
|                 <th scope="col"></th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|  |  | |||
|  | @ -213,7 +213,7 @@ class CollectionListRowBase extends React.Component { | |||
|             {this.renderCollectionName()} | ||||
|           </span> | ||||
|         </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 && '# sketches: '}{(collection.items || []).length}</td> | ||||
|         <td className="sketch-list__dropdown-column"> | ||||
|  |  | |||
|  | @ -437,8 +437,8 @@ class SketchList extends React.Component { | |||
|             <thead> | ||||
|               <tr> | ||||
|                 {this._renderFieldHeader('name', 'Sketch')} | ||||
|                 {this._renderFieldHeader('createdAt', 'Date Created')} | ||||
|                 {this._renderFieldHeader('updatedAt', 'Date Updated')} | ||||
|                 {this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)} | ||||
|                 {this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)} | ||||
|                 <th scope="col"></th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|  |  | |||
|  | @ -24,9 +24,11 @@ import Loader from '../App/components/loader'; | |||
| 
 | ||||
| const EXAMPLE_USERNAME = 'p5'; | ||||
| 
 | ||||
| // @ghalestrilo 08/13/2020: I'm sorry | ||||
| const ContentWrapper = styled(Content)` | ||||
|   table { | ||||
|     table-layout: fixed; | ||||
|     margin-bottom: ${remSize(120)} | ||||
|   } | ||||
|    | ||||
|   td ,thead button { | ||||
|  | @ -55,14 +57,18 @@ const ContentWrapper = styled(Content)` | |||
|    | ||||
|   tbody td              { justify-self: start; text-align: start; padding: 0 } | ||||
|   tbody td:nth-child(2) { justify-self: start;  text-align: start; padding-left: ${remSize(12)}}; | ||||
|   tbody td:last-child   { justify-self: end;    text-align: end; }; | ||||
|   tbody td:last-child   { | ||||
|     justify-self: end; | ||||
|     text-align: end; | ||||
|     grid-row-start: 1; | ||||
|     grid-column-start: 3; | ||||
|   }; | ||||
| 
 | ||||
|   .sketch-list__dropdown-column { width: auto; }; | ||||
| 
 | ||||
|   tbody { height: ${remSize(48)}; } | ||||
| 
 | ||||
|   .sketches-table-container { | ||||
|     padding-bottom: ${remSize(160)}; | ||||
|     background: ${prop('SketchList.background')}; | ||||
|     } | ||||
|   .sketches-table__row { | ||||
|  | @ -79,18 +85,33 @@ const ContentWrapper = styled(Content)` | |||
|   }; | ||||
| 
 | ||||
|   thead tr { | ||||
|     grid-template-columns: 1fr 1fr 1fr 0fr; | ||||
|     grid-template-columns: repeat(${props => props.fieldcount}, 1fr) 0fr; | ||||
|     ${props => props.noheader && 'display: none;'} | ||||
|   } | ||||
| 
 | ||||
|   tbody tr { | ||||
|     padding: ${remSize(8)}; | ||||
|     border-radius: ${remSize(4)}; | ||||
|     grid-template-columns: 5fr 5fr 1fr; | ||||
|     grid-template-columns: repeat(${props => props.fieldcount - 1}) 1fr; | ||||
|     grid-template-areas: "name name name" "content content content"; | ||||
|     grid-row-gap: ${remSize(12)} | ||||
|   } | ||||
| 
 | ||||
|   .loader-container { position: fixed ; padding-bottom: 32% } | ||||
| 
 | ||||
|   .sketches-table thead th { | ||||
|     background-color: transparent; | ||||
|   } | ||||
| 
 | ||||
|   .asset-table thead th { | ||||
|     height: initial; | ||||
|     align-self: center; | ||||
|   } | ||||
| 
 | ||||
|   .asset-table thead tr { | ||||
|     height: ${remSize(32)} | ||||
|   } | ||||
| 
 | ||||
| `; | ||||
| 
 | ||||
| const Subheader = styled.div` | ||||
|  | @ -168,7 +189,7 @@ const MobileDashboard = ({ params, location }) => { | |||
|       </Header> | ||||
| 
 | ||||
| 
 | ||||
|       <ContentWrapper slimheader> | ||||
|       <ContentWrapper slimheader fieldcount={panel === Tabs[1] ? 4 : 3} noheader={panel === Tabs[2]}> | ||||
|         <Subheader> | ||||
|           {panel === Tabs[0] && <SketchSearchbar />} | ||||
|           {panel === Tabs[1] && <CollectionSearchbar />} | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import PropTypes from 'prop-types'; | ||||
| import React from 'react'; | ||||
| 
 | ||||
| import Button from '../../../common/Button'; | ||||
| import { PlusIcon } from '../../../common/icons'; | ||||
| import CopyableInput from '../../IDE/components/CopyableInput'; | ||||
|  | @ -12,7 +11,7 @@ export const APIKeyPropType = PropTypes.shape({ | |||
|   token: PropTypes.object, // eslint-disable-line | ||||
|   label: PropTypes.string.isRequired, | ||||
|   createdAt: PropTypes.string.isRequired, | ||||
|   lastUsedAt: PropTypes.string, | ||||
|   lastUsedAt: PropTypes.string | ||||
| }); | ||||
| 
 | ||||
| class APIKeyForm extends React.Component { | ||||
|  | @ -39,7 +38,7 @@ class APIKeyForm extends React.Component { | |||
|   } | ||||
| 
 | ||||
|   removeKey(key) { | ||||
|     const message = `Are you sure you want to delete "${key.label}"?`; | ||||
|     const message = this.props.t('APIKeyForm.ConfirmDelete', { key_label: key.label }); | ||||
| 
 | ||||
|     if (window.confirm(message)) { | ||||
|       this.props.removeApiKey(key.id); | ||||
|  | @ -51,10 +50,10 @@ class APIKeyForm extends React.Component { | |||
| 
 | ||||
|     if (hasApiKeys) { | ||||
|       return ( | ||||
|         <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() { | ||||
|  | @ -63,27 +62,18 @@ class APIKeyForm extends React.Component { | |||
|     return ( | ||||
|       <div className="api-key-form"> | ||||
|         <p className="api-key-form__summary"> | ||||
|           Personal Access Tokens act like your password to allow automated | ||||
|           scripts to access the Editor API. Create a token for each script that | ||||
|           needs access. | ||||
|           {this.props.t('APIKeyForm.Summary')} | ||||
|         </p> | ||||
| 
 | ||||
|         <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}> | ||||
|             <label | ||||
|               htmlFor="keyLabel" | ||||
|               className="form__label form__label--hidden " | ||||
|             > | ||||
|               What is this token for? | ||||
|             </label> | ||||
|             <label htmlFor="keyLabel" className="form__label form__label--hidden ">{this.props.t('APIKeyForm.TokenLabel')}</label> | ||||
|             <input | ||||
|               className="form__input" | ||||
|               id="keyLabel" | ||||
|               onChange={(event) => { | ||||
|                 this.setState({ keyLabel: event.target.value }); | ||||
|               }} | ||||
|               placeholder="What is this token for? e.g. Example import script" | ||||
|               onChange={(event) => { this.setState({ keyLabel: event.target.value }); }} | ||||
|               placeholder={this.props.t('APIKeyForm.TokenPlaceholder')} | ||||
|               type="text" | ||||
|               value={this.state.keyLabel} | ||||
|             /> | ||||
|  | @ -93,29 +83,25 @@ class APIKeyForm extends React.Component { | |||
|               label="Create new key" | ||||
|               type="submit" | ||||
|             > | ||||
|               Create | ||||
|               {this.props.t('APIKeyForm.CreateTokenSubmit')} | ||||
|             </Button> | ||||
|           </form> | ||||
| 
 | ||||
|           {keyWithToken && ( | ||||
|             <div className="api-key-form__new-token"> | ||||
|               <h4 className="api-key-form__new-token__title"> | ||||
|                 Your new access token | ||||
|               </h4> | ||||
|               <p className="api-key-form__new-token__info"> | ||||
|                 Make sure to copy your new personal access token now. You won’t | ||||
|                 be able to see it again! | ||||
|               </p> | ||||
|               <CopyableInput | ||||
|                 label={keyWithToken.label} | ||||
|                 value={keyWithToken.token} | ||||
|               /> | ||||
|             </div> | ||||
|           )} | ||||
|           { | ||||
|             keyWithToken && ( | ||||
|               <div className="api-key-form__new-token"> | ||||
|                 <h4 className="api-key-form__new-token__title">{this.props.t('APIKeyForm.NewTokenTitle')}</h4> | ||||
|                 <p className="api-key-form__new-token__info"> | ||||
|                   {this.props.t('APIKeyForm.NewTokenInfo')} | ||||
|                 </p> | ||||
|                 <CopyableInput label={keyWithToken.label} value={keyWithToken.token} /> | ||||
|               </div> | ||||
|             ) | ||||
|           } | ||||
|         </div> | ||||
| 
 | ||||
|         <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()} | ||||
|         </div> | ||||
|       </div> | ||||
|  | @ -127,6 +113,7 @@ APIKeyForm.propTypes = { | |||
|   apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired, | ||||
|   createApiKey: PropTypes.func.isRequired, | ||||
|   removeApiKey: PropTypes.func.isRequired, | ||||
|   t: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| export default APIKeyForm; | ||||
|  |  | |||
|  | @ -8,22 +8,22 @@ import { APIKeyPropType } from './APIKeyForm'; | |||
| 
 | ||||
| import TrashCanIcon from '../../../images/trash-can.svg'; | ||||
| 
 | ||||
| function APIKeyList({ apiKeys, onRemove }) { | ||||
| function APIKeyList({ apiKeys, onRemove, t }) { | ||||
|   return ( | ||||
|     <table className="api-key-list"> | ||||
|       <thead> | ||||
|         <tr> | ||||
|           <th>Name</th> | ||||
|           <th>Created on</th> | ||||
|           <th>Last used</th> | ||||
|           <th>Actions</th> | ||||
|           <th>{t('APIKeyList.Name')}</th> | ||||
|           <th>{t('APIKeyList.Created')}</th> | ||||
|           <th>{t('APIKeyList.LastUsed')}</th> | ||||
|           <th>{t('APIKeyList.Actions')}</th> | ||||
|         </tr> | ||||
|       </thead> | ||||
|       <tbody> | ||||
|         {orderBy(apiKeys, ['createdAt'], ['desc']).map((key) => { | ||||
|           const lastUsed = key.lastUsedAt ? | ||||
|             distanceInWordsToNow(new Date(key.lastUsedAt), { addSuffix: true }) : | ||||
|             'Never'; | ||||
|             t('APIKeyList.Never'); | ||||
| 
 | ||||
|           return ( | ||||
|             <tr key={key.id}> | ||||
|  | @ -34,7 +34,7 @@ function APIKeyList({ apiKeys, onRemove }) { | |||
|                 <button | ||||
|                   className="api-key-list__delete-button" | ||||
|                   onClick={() => onRemove(key)} | ||||
|                   aria-label="Delete API Key" | ||||
|                   aria-label={t('APIKeyList.DeleteARIA')} | ||||
|                 > | ||||
|                   <TrashCanIcon focusable="false" aria-hidden="true" /> | ||||
|                 </button> | ||||
|  | @ -50,6 +50,7 @@ function APIKeyList({ apiKeys, onRemove }) { | |||
| APIKeyList.propTypes = { | ||||
|   apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired, | ||||
|   onRemove: PropTypes.func.isRequired, | ||||
|   t: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| export default APIKeyList; | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import PropTypes from 'prop-types'; | ||||
| import React from 'react'; | ||||
| import { withTranslation } from 'react-i18next'; | ||||
| import { domOnlyProps } from '../../../utils/reduxFormUtils'; | ||||
| import Button from '../../../common/Button'; | ||||
| 
 | ||||
|  | @ -14,6 +15,7 @@ function AccountForm(props) { | |||
|     submitting, | ||||
|     invalid, | ||||
|     pristine, | ||||
|     t | ||||
|   } = props; | ||||
| 
 | ||||
|   const handleInitiateVerification = (evt) => { | ||||
|  | @ -24,12 +26,10 @@ function AccountForm(props) { | |||
|   return ( | ||||
|     <form className="form" onSubmit={handleSubmit(props.updateSettings)}> | ||||
|       <p className="form__field"> | ||||
|         <label htmlFor="email" className="form__label"> | ||||
|           Email | ||||
|         </label> | ||||
|         <label htmlFor="email" className="form__label">{t('AccountForm.Email')}</label> | ||||
|         <input | ||||
|           className="form__input" | ||||
|           aria-label="email" | ||||
|           aria-label={t('AccountForm.EmailARIA')} | ||||
|           type="text" | ||||
|           id="email" | ||||
|           {...domOnlyProps(email)} | ||||
|  | @ -38,28 +38,31 @@ function AccountForm(props) { | |||
|           <span className="form-error">{email.error}</span> | ||||
|         )} | ||||
|       </p> | ||||
|       {user.verified !== 'verified' && ( | ||||
|         <p className="form__context"> | ||||
|           <span className="form__status">Unconfirmed.</span> | ||||
|           {user.emailVerificationInitiate === true ? ( | ||||
|             <span className="form__status"> | ||||
|               {' '} | ||||
|               Confirmation sent, check your email. | ||||
|             </span> | ||||
|           ) : ( | ||||
|             <Button onClick={handleInitiateVerification}> | ||||
|               Resend confirmation email | ||||
|             </Button> | ||||
|           )} | ||||
|         </p> | ||||
|       )} | ||||
|       { | ||||
|         user.verified !== 'verified' && | ||||
|           ( | ||||
|             <p className="form__context"> | ||||
|               <span className="form__status">{t('AccountForm.Unconfirmed')}</span> | ||||
|               { | ||||
|                 user.emailVerificationInitiate === true ? | ||||
|                   ( | ||||
|                     <span className="form__status"> {t('AccountForm.EmailSent')}</span> | ||||
|                   ) : | ||||
|                   ( | ||||
|                     <Button | ||||
|                       onClick={handleInitiateVerification} | ||||
|                     >{t('AccountForm.Resend')} | ||||
|                     </Button> | ||||
|                   ) | ||||
|               } | ||||
|             </p> | ||||
|           ) | ||||
|       } | ||||
|       <p className="form__field"> | ||||
|         <label htmlFor="username" className="form__label"> | ||||
|           User Name | ||||
|         </label> | ||||
|         <label htmlFor="username" className="form__label">{t('AccountForm.UserName')}</label> | ||||
|         <input | ||||
|           className="form__input" | ||||
|           aria-label="username" | ||||
|           aria-label={t('AccountForm.UserNameARIA')} | ||||
|           type="text" | ||||
|           id="username" | ||||
|           defaultValue={username} | ||||
|  | @ -70,12 +73,10 @@ function AccountForm(props) { | |||
|         )} | ||||
|       </p> | ||||
|       <p className="form__field"> | ||||
|         <label htmlFor="current password" className="form__label"> | ||||
|           Current Password | ||||
|         </label> | ||||
|         <label htmlFor="current password" className="form__label">{t('AccountForm.CurrentPassword')}</label> | ||||
|         <input | ||||
|           className="form__input" | ||||
|           aria-label="currentPassword" | ||||
|           aria-label={t('AccountForm.CurrentPasswordARIA')} | ||||
|           type="password" | ||||
|           id="currentPassword" | ||||
|           {...domOnlyProps(currentPassword)} | ||||
|  | @ -85,12 +86,10 @@ function AccountForm(props) { | |||
|         )} | ||||
|       </p> | ||||
|       <p className="form__field"> | ||||
|         <label htmlFor="new password" className="form__label"> | ||||
|           New Password | ||||
|         </label> | ||||
|         <label htmlFor="new password" className="form__label">{t('AccountForm.NewPassword')}</label> | ||||
|         <input | ||||
|           className="form__input" | ||||
|           aria-label="newPassword" | ||||
|           aria-label={t('AccountForm.NewPasswordARIA')} | ||||
|           type="password" | ||||
|           id="newPassword" | ||||
|           {...domOnlyProps(newPassword)} | ||||
|  | @ -99,8 +98,10 @@ function AccountForm(props) { | |||
|           <span className="form-error">{newPassword.error}</span> | ||||
|         )} | ||||
|       </p> | ||||
|       <Button type="submit" disabled={submitting || invalid || pristine}> | ||||
|         Save All Settings | ||||
|       <Button | ||||
|         type="submit" | ||||
|         disabled={submitting || invalid || pristine} | ||||
|       >{t('AccountForm.SubmitSaveAllSettings')} | ||||
|       </Button> | ||||
|     </form> | ||||
|   ); | ||||
|  | @ -123,6 +124,7 @@ AccountForm.propTypes = { | |||
|   submitting: PropTypes.bool, | ||||
|   invalid: PropTypes.bool, | ||||
|   pristine: PropTypes.bool, | ||||
|   t: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| AccountForm.defaultProps = { | ||||
|  | @ -131,4 +133,4 @@ AccountForm.defaultProps = { | |||
|   invalid: false, | ||||
| }; | ||||
| 
 | ||||
| export default AccountForm; | ||||
| export default withTranslation()(AccountForm); | ||||
|  |  | |||
|  | @ -1,16 +1,13 @@ | |||
| import PropTypes from 'prop-types'; | ||||
| import React from 'react'; | ||||
| 
 | ||||
| import { withTranslation } from 'react-i18next'; | ||||
| import { domOnlyProps } from '../../../utils/reduxFormUtils'; | ||||
| import Button from '../../../common/Button'; | ||||
| 
 | ||||
| function NewPasswordForm(props) { | ||||
|   const { | ||||
|     fields: { password, confirmPassword }, | ||||
|     handleSubmit, | ||||
|     submitting, | ||||
|     invalid, | ||||
|     pristine, | ||||
|     fields: { password, confirmPassword }, handleSubmit, submitting, invalid, pristine, | ||||
|     t | ||||
|   } = props; | ||||
|   return ( | ||||
|     <form | ||||
|  | @ -18,12 +15,10 @@ function NewPasswordForm(props) { | |||
|       onSubmit={handleSubmit(props.updatePassword.bind(this, props.params.reset_password_token))} | ||||
|     > | ||||
|       <p className="form__field"> | ||||
|         <label htmlFor="password" className="form__label"> | ||||
|           Password | ||||
|         </label> | ||||
|         <label htmlFor="password" className="form__label">{t('NewPasswordForm.Title')}</label> | ||||
|         <input | ||||
|           className="form__input" | ||||
|           aria-label="password" | ||||
|           aria-label={t('NewPasswordForm.TitleARIA')} | ||||
|           type="password" | ||||
|           id="Password" | ||||
|           {...domOnlyProps(password)} | ||||
|  | @ -33,13 +28,11 @@ function NewPasswordForm(props) { | |||
|         )} | ||||
|       </p> | ||||
|       <p className="form__field"> | ||||
|         <label htmlFor="confirm password" className="form__label"> | ||||
|           Confirm Password | ||||
|         </label> | ||||
|         <label htmlFor="confirm password" className="form__label">{t('NewPasswordForm.ConfirmPassword')}</label> | ||||
|         <input | ||||
|           className="form__input" | ||||
|           type="password" | ||||
|           aria-label="confirm password" | ||||
|           aria-label={t('NewPasswordForm.ConfirmPasswordARIA')} | ||||
|           id="confirm password" | ||||
|           {...domOnlyProps(confirmPassword)} | ||||
|         /> | ||||
|  | @ -47,9 +40,7 @@ function NewPasswordForm(props) { | |||
|           <span className="form-error">{confirmPassword.error}</span> | ||||
|         )} | ||||
|       </p> | ||||
|       <Button type="submit" disabled={submitting || invalid || pristine}> | ||||
|         Set New Password | ||||
|       </Button> | ||||
|       <Button type="submit" disabled={submitting || invalid || pristine}>{t('NewPasswordForm.SubmitSetNewPassword')}</Button> | ||||
|     </form> | ||||
|   ); | ||||
| } | ||||
|  | @ -67,6 +58,7 @@ NewPasswordForm.propTypes = { | |||
|   params: PropTypes.shape({ | ||||
|     reset_password_token: PropTypes.string, | ||||
|   }).isRequired, | ||||
|   t: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| NewPasswordForm.defaultProps = { | ||||
|  | @ -75,4 +67,4 @@ NewPasswordForm.defaultProps = { | |||
|   submitting: false, | ||||
| }; | ||||
| 
 | ||||
| export default NewPasswordForm; | ||||
| export default withTranslation()(NewPasswordForm); | ||||
|  |  | |||
|  | @ -1,16 +1,12 @@ | |||
| import PropTypes from 'prop-types'; | ||||
| import React from 'react'; | ||||
| 
 | ||||
| import { withTranslation } from 'react-i18next'; | ||||
| import { domOnlyProps } from '../../../utils/reduxFormUtils'; | ||||
| import Button from '../../../common/Button'; | ||||
| 
 | ||||
| function ResetPasswordForm(props) { | ||||
|   const { | ||||
|     fields: { email }, | ||||
|     handleSubmit, | ||||
|     submitting, | ||||
|     invalid, | ||||
|     pristine, | ||||
|     fields: { email }, handleSubmit, submitting, invalid, pristine, t | ||||
|   } = props; | ||||
|   return ( | ||||
|     <form | ||||
|  | @ -18,12 +14,10 @@ function ResetPasswordForm(props) { | |||
|       onSubmit={handleSubmit(props.initiateResetPassword.bind(this))} | ||||
|     > | ||||
|       <p className="form__field"> | ||||
|         <label htmlFor="email" className="form__label"> | ||||
|           Email used for registration | ||||
|         </label> | ||||
|         <label htmlFor="email" className="form__label">{t('ResetPasswordForm.Email')}</label> | ||||
|         <input | ||||
|           className="form__input" | ||||
|           aria-label="email" | ||||
|           aria-label={t('ResetPasswordForm.EmailARIA')} | ||||
|           type="text" | ||||
|           id="email" | ||||
|           {...domOnlyProps(email)} | ||||
|  | @ -34,11 +28,8 @@ function ResetPasswordForm(props) { | |||
|       </p> | ||||
|       <Button | ||||
|         type="submit" | ||||
|         disabled={ | ||||
|           submitting || invalid || pristine || props.user.resetPasswordInitiate | ||||
|         } | ||||
|       > | ||||
|         Send Password Reset Email | ||||
|         disabled={submitting || invalid || pristine || props.user.resetPasswordInitiate} | ||||
|       >{t('ResetPasswordForm.Submit')} | ||||
|       </Button> | ||||
|     </form> | ||||
|   ); | ||||
|  | @ -54,8 +45,9 @@ ResetPasswordForm.propTypes = { | |||
|   invalid: PropTypes.bool, | ||||
|   pristine: PropTypes.bool, | ||||
|   user: PropTypes.shape({ | ||||
|     resetPasswordInitiate: PropTypes.bool, | ||||
|     resetPasswordInitiate: PropTypes.bool | ||||
|   }).isRequired, | ||||
|   t: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| ResetPasswordForm.defaultProps = { | ||||
|  | @ -64,4 +56,4 @@ ResetPasswordForm.defaultProps = { | |||
|   invalid: false, | ||||
| }; | ||||
| 
 | ||||
| export default ResetPasswordForm; | ||||
| export default withTranslation()(ResetPasswordForm); | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import { reduxForm } from 'redux-form'; | |||
| import { bindActionCreators } from 'redux'; | ||||
| import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; | ||||
| import { Helmet } from 'react-helmet'; | ||||
| import { withTranslation } from 'react-i18next'; | ||||
| import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; | ||||
| import AccountForm from '../components/AccountForm'; | ||||
| import apiClient from '../../../utils/apiClient'; | ||||
|  | @ -16,9 +17,11 @@ function SocialLoginPanel(props) { | |||
|   return ( | ||||
|     <React.Fragment> | ||||
|       <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"> | ||||
|         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> | ||||
|       <div className="account__social-stack"> | ||||
|         <SocialAuthButton service={SocialAuthButton.services.github} /> | ||||
|  | @ -39,21 +42,21 @@ class AccountView extends React.Component { | |||
|     return ( | ||||
|       <div className="account-settings__container"> | ||||
|         <Helmet> | ||||
|           <title>p5.js Web Editor | Account Settings</title> | ||||
|           <title>{this.props.t('AccountView.Title')}</title> | ||||
|         </Helmet> | ||||
| 
 | ||||
|         <Nav layout="dashboard" /> | ||||
| 
 | ||||
|         <main className="account-settings"> | ||||
|           <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> | ||||
|           {accessTokensUIEnabled && | ||||
|             <Tabs className="account__tabs"> | ||||
|               <TabList> | ||||
|                 <div className="tabs__titles"> | ||||
|                   <Tab><h4 className="tabs__title">Account</h4></Tab> | ||||
|                   {accessTokensUIEnabled && <Tab><h4 className="tabs__title">Access Tokens</h4></Tab>} | ||||
|                   <Tab><h4 className="tabs__title">{this.props.t('AccountView.AccountTab')}</h4></Tab> | ||||
|                   {accessTokensUIEnabled && <Tab><h4 className="tabs__title">{this.props.t('AccountView.AccessTokensTab')}</h4></Tab>} | ||||
|                 </div> | ||||
|               </TabList> | ||||
|               <TabPanel> | ||||
|  | @ -107,13 +110,14 @@ function asyncValidate(formProps, dispatch, props) { | |||
| 
 | ||||
| AccountView.propTypes = { | ||||
|   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', | ||||
|   fields: ['username', 'email', 'currentPassword', 'newPassword'], | ||||
|   validate: validateSettings, | ||||
|   asyncValidate, | ||||
|   asyncBlurFields: ['username', 'email', 'currentPassword'] | ||||
| }, mapStateToProps, mapDispatchToProps)(AccountView); | ||||
| }, mapStateToProps, mapDispatchToProps)(AccountView)); | ||||
|  |  | |||
|  | @ -4,6 +4,8 @@ import { reduxForm } from 'redux-form'; | |||
| import classNames from 'classnames'; | ||||
| import { bindActionCreators } from 'redux'; | ||||
| import { Helmet } from 'react-helmet'; | ||||
| import { withTranslation } from 'react-i18next'; | ||||
| import i18next from 'i18next'; | ||||
| import NewPasswordForm from '../components/NewPasswordForm'; | ||||
| import * as UserActions from '../actions'; | ||||
| import Nav from '../../../components/Nav'; | ||||
|  | @ -20,13 +22,13 @@ function NewPasswordView(props) { | |||
|       <Nav layout="dashboard" /> | ||||
|       <div className={newPasswordClass}> | ||||
|         <Helmet> | ||||
|           <title>p5.js Web Editor | New Password</title> | ||||
|           <title>{props.t('NewPasswordView.Title')}</title> | ||||
|         </Helmet> | ||||
|         <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} /> | ||||
|           <p className="new-password__invalid"> | ||||
|             The password reset token is invalid or has expired. | ||||
|             {props.t('NewPasswordView.TokenInvalidOrExpired')} | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|  | @ -41,21 +43,22 @@ NewPasswordView.propTypes = { | |||
|   validateResetPasswordToken: PropTypes.func.isRequired, | ||||
|   user: PropTypes.shape({ | ||||
|     resetPasswordInvalid: PropTypes.bool | ||||
|   }).isRequired | ||||
|   }).isRequired, | ||||
|   t: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| function validate(formProps) { | ||||
|   const errors = {}; | ||||
| 
 | ||||
|   if (!formProps.password) { | ||||
|     errors.password = 'Please enter a password'; | ||||
|     errors.password = i18next.t('NewPasswordView.EmptyPassword'); | ||||
|   } | ||||
|   if (!formProps.confirmPassword) { | ||||
|     errors.confirmPassword = 'Please enter a password confirmation'; | ||||
|     errors.confirmPassword = i18next.t('NewPasswordView.PasswordConfirmation'); | ||||
|   } | ||||
| 
 | ||||
|   if (formProps.password !== formProps.confirmPassword) { | ||||
|     errors.password = 'Passwords must match'; | ||||
|     errors.password = i18next.t('NewPasswordView.PasswordMismatch'); | ||||
|   } | ||||
| 
 | ||||
|   return errors; | ||||
|  | @ -71,8 +74,8 @@ function mapDispatchToProps(dispatch) { | |||
|   return bindActionCreators(UserActions, dispatch); | ||||
| } | ||||
| 
 | ||||
| export default reduxForm({ | ||||
| export default withTranslation()(reduxForm({ | ||||
|   form: 'new-password', | ||||
|   fields: ['password', 'confirmPassword'], | ||||
|   validate | ||||
| }, mapStateToProps, mapDispatchToProps)(NewPasswordView); | ||||
| }, mapStateToProps, mapDispatchToProps)(NewPasswordView)); | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import classNames from 'classnames'; | |||
| import { bindActionCreators } from 'redux'; | ||||
| import { reduxForm } from 'redux-form'; | ||||
| import { Helmet } from 'react-helmet'; | ||||
| import { withTranslation } from 'react-i18next'; | ||||
| import * as UserActions from '../actions'; | ||||
| import ResetPasswordForm from '../components/ResetPasswordForm'; | ||||
| import { validateResetPassword } from '../../../utils/reduxFormUtils'; | ||||
|  | @ -23,19 +24,18 @@ function ResetPasswordView(props) { | |||
|       <Nav layout="dashboard" /> | ||||
|       <div className={resetPasswordClass}> | ||||
|         <Helmet> | ||||
|           <title>p5.js Web Editor | Reset Password</title> | ||||
|           <title>{props.t('ResetPasswordView.Title')}</title> | ||||
|         </Helmet> | ||||
|         <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} /> | ||||
|           <p className="reset-password__submitted"> | ||||
|             Your password reset email should arrive shortly. If you don't see it, check | ||||
|             in your spam folder as sometimes it can end up there. | ||||
|             {props.t('ResetPasswordView.Submitted')} | ||||
|           </p> | ||||
|           <p className="form__navigation-options"> | ||||
|             <Link className="form__login-button" to="/login">Log In</Link> | ||||
|              or  | ||||
|             <Link className="form__signup-button" to="/signup">Sign Up</Link> | ||||
|             <Link className="form__login-button" to="/login">{props.t('ResetPasswordView.Login')}</Link> | ||||
|              {props.t('ResetPasswordView.LoginOr')}  | ||||
|             <Link className="form__signup-button" to="/signup">{props.t('ResetPasswordView.SignUp')}</Link> | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|  | @ -48,6 +48,7 @@ ResetPasswordView.propTypes = { | |||
|   user: PropTypes.shape({ | ||||
|     resetPasswordInitiate: PropTypes.bool | ||||
|   }).isRequired, | ||||
|   t: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| function mapStateToProps(state) { | ||||
|  | @ -60,8 +61,8 @@ function mapDispatchToProps(dispatch) { | |||
|   return bindActionCreators(UserActions, dispatch); | ||||
| } | ||||
| 
 | ||||
| export default reduxForm({ | ||||
| export default withTranslation()(reduxForm({ | ||||
|   form: 'reset-password', | ||||
|   fields: ['email'], | ||||
|   validate: validateResetPassword | ||||
| }, mapStateToProps, mapDispatchToProps)(ResetPasswordView); | ||||
| }, mapStateToProps, mapDispatchToProps)(ResetPasswordView)); | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| /* eslint-disable */ | ||||
| import i18n from 'i18next'; | ||||
| export const domOnlyProps = ({ | ||||
|   initialValue, | ||||
|   autofill, | ||||
|  | @ -20,19 +21,19 @@ const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+")) | |||
| 
 | ||||
| function validateNameEmail(formProps, errors) { | ||||
|   if (!formProps.username) { | ||||
|     errors.username = 'Please enter a username.'; | ||||
|     errors.username = i18n.t('ReduxFormUtils.errorEmptyUsername'); | ||||
|   } 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}$/)) { | ||||
|     errors.username = 'Username must only consist of numbers, letters, periods, dashes, and underscores.'; | ||||
|     errors.username = i18n.t('ReduxFormUtils.errorValidUsername'); | ||||
|   } | ||||
| 
 | ||||
|   if (!formProps.email) { | ||||
|     errors.email = 'Please enter an email.'; | ||||
|     errors.email = i18n.t('ReduxFormUtils.errorEmptyEmail'); | ||||
|   } else if ( | ||||
|     // eslint-disable-next-line max-len
 | ||||
|     !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); | ||||
| 
 | ||||
|   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) { | ||||
|     errors.newPassword = 'Password must be at least 6 characters'; | ||||
|     errors.newPassword = i18n.t('ReduxFormUtils.errorShortPassword'); | ||||
|   } | ||||
|   return errors; | ||||
| } | ||||
|  | @ -53,10 +54,10 @@ export function validateSettings(formProps) { | |||
| export function validateLogin(formProps) { | ||||
|   const errors = {}; | ||||
|   if (!formProps.email) { | ||||
|     errors.email = 'Please enter an email'; | ||||
|     errors.email = i18n.t('ReduxFormUtils.errorEmptyEmail'); | ||||
|   } | ||||
|   if (!formProps.password) { | ||||
|     errors.password = 'Please enter a password'; | ||||
|     errors.password = i18n.t('ReduxFormUtils.errorEmptyPassword'); | ||||
|   } | ||||
|   return errors; | ||||
| } | ||||
|  | @ -67,17 +68,17 @@ export function validateSignup(formProps) { | |||
|   validateNameEmail(formProps, errors); | ||||
| 
 | ||||
|   if (!formProps.password) { | ||||
|     errors.password = 'Please enter a password'; | ||||
|     errors.password = i18n.t('ReduxFormUtils.errorEmptyPassword'); | ||||
|   } | ||||
|   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) { | ||||
|     errors.confirmPassword = 'Please enter a password confirmation'; | ||||
|     errors.confirmPassword = i18n.t('ReduxFormUtils.errorConfirmPassword'); | ||||
|   } | ||||
| 
 | ||||
|   if (formProps.password !== formProps.confirmPassword && formProps.confirmPassword) { | ||||
|     errors.confirmPassword = 'Passwords must match'; | ||||
|     errors.confirmPassword = i18n.t('ReduxFormUtils.errorPasswordMismatch'); | ||||
|   } | ||||
| 
 | ||||
|   return errors; | ||||
|  | @ -85,11 +86,11 @@ export function validateSignup(formProps) { | |||
| export function validateResetPassword(formProps) { | ||||
|   const errors = {}; | ||||
|   if (!formProps.email) { | ||||
|     errors.email = 'Please enter an email.'; | ||||
|     errors.email = i18n.t('ReduxFormUtils.errorEmptyEmail'); | ||||
|   } else if ( | ||||
|     // eslint-disable-next-line max-len
 | ||||
|     !formProps.email.match(EMAIL_REGEX)) { | ||||
|     errors.email = 'Please enter a valid email address.'; | ||||
|     errors.email = i18n.t('ReduxFormUtils.errorInvalidEmail'); | ||||
|   } | ||||
|   return errors; | ||||
| } | ||||
|  |  | |||
|  | @ -183,21 +183,20 @@ | |||
|     "Error": "Error", | ||||
|     "Save": "Save", | ||||
|     "p5logoARIA": "p5.js Logo" | ||||
| 
 | ||||
|   }, | ||||
|   "IDEView": { | ||||
|     "SubmitFeedback": "Submit Feedback" | ||||
|   }, | ||||
|   "NewFileModal": { | ||||
|     "Title": "Create File", | ||||
|      "CloseButtonARIA": "Close New File Modal", | ||||
|     "CloseButtonARIA": "Close New File Modal", | ||||
|     "EnterName": "Please enter a name", | ||||
|     "InvalidType": "Invalid file type. Valid extensions are .js, .css, .json, .txt, .csv, .tsv, .frag, and .vert." | ||||
|   }, | ||||
|   "NewFileForm": { | ||||
|   "AddFileSubmit": "Add File", | ||||
|     "AddFileSubmit": "Add File", | ||||
|     "Placeholder": "Name" | ||||
| }, | ||||
|   }, | ||||
|   "NewFolderModal": { | ||||
|     "Title": "Create Folder", | ||||
|     "CloseButtonARIA": "Close New Folder Modal", | ||||
|  | @ -208,5 +207,87 @@ | |||
|   "NewFolderForm": { | ||||
|     "AddFolderSubmit": "Add Folder", | ||||
|     "Placeholder": "Name" | ||||
|   }, | ||||
|   "ResetPasswordForm": { | ||||
|     "Email": "Email used for registration", | ||||
|     "EmailARIA": "email", | ||||
|     "Submit": "Send Password Reset Email" | ||||
|   }, | ||||
|   "ResetPasswordView": { | ||||
|     "Title": "p5.js Web Editor | Reset Password", | ||||
|     "Reset": "Reset Your Password", | ||||
|     "Submitted": "Your password reset email should arrive shortly. If you don't see it, check\n            in your spam folder as sometimes it can end up there.", | ||||
|     "Login": "Log In", | ||||
|     "LoginOr": "or", | ||||
|     "SignUp": "Sign Up" | ||||
|   }, | ||||
|   "ReduxFormUtils": { | ||||
|     "errorInvalidEmail": "Please enter a valid email address", | ||||
|     "errorEmptyEmail": "Please enter an email", | ||||
|     "errorPasswordMismatch": "Passwords must match", | ||||
|     "errorEmptyPassword": "Please enter a password", | ||||
|     "errorShortPassword": "Password must be at least 6 characters", | ||||
|     "errorConfirmPassword": "Please enter a password confirmation", | ||||
|     "errorNewPassword": "Please enter a new password or leave the current password empty.", | ||||
|     "errorEmptyUsername": "Please enter a username.", | ||||
|     "errorLongUsername": "Username must be less than 20 characters.", | ||||
|     "errorValidUsername": "Username must only consist of numbers, letters, periods, dashes, and underscores." | ||||
|   }, | ||||
|   "NewPasswordView": { | ||||
|     "Title": "p5.js Web Editor | New Password", | ||||
|     "Description": "Set a New Password", | ||||
|     "TokenInvalidOrExpired": "The password reset token is invalid or has expired.", | ||||
|     "EmptyPassword": "Please enter a password", | ||||
|     "PasswordConfirmation": "Please enter a password confirmation", | ||||
|     "PasswordMismatch": "Passwords must match" | ||||
|   }, | ||||
|   "AccountForm": { | ||||
|     "Email": "Email", | ||||
|     "EmailARIA": "email", | ||||
|     "Unconfirmed": "Unconfirmed.", | ||||
|     "EmailSent": "Confirmation sent, check your email.", | ||||
|     "Resend": "Resend confirmation email", | ||||
|     "UserName": "User Name", | ||||
|     "UserNameARIA": "Username", | ||||
|     "CurrentPassword": "Current Password", | ||||
|     "CurrentPasswordARIA": "Current Password", | ||||
|     "NewPassword": "New Password", | ||||
|     "NewPasswordARIA": "New Password", | ||||
|     "SubmitSaveAllSettings": "Save All Settings" | ||||
|   }, | ||||
|   "AccountView": { | ||||
|     "SocialLogin": "Social Login", | ||||
|     "SocialLoginDescription": "Use your GitHub or Google account to log into the p5.js Web Editor.", | ||||
|     "Title": "p5.js Web Editor | Account Settings", | ||||
|     "Settings": "Account Settings", | ||||
|     "AccountTab": "Account", | ||||
|     "AccessTokensTab": "Access Tokens" | ||||
|   }, | ||||
|   "APIKeyForm": { | ||||
|     "ConfirmDelete": "Are you sure you want to delete {{key_label}}?", | ||||
|     "Summary": "Personal Access Tokens act like your password to allow automated\n          scripts to access the Editor API. Create a token for each script\n          that needs access.", | ||||
|     "CreateToken": "Create new token", | ||||
|     "TokenLabel": "What is this token for?", | ||||
|     "TokenPlaceholder": "What is this token for? e.g. Example import script", | ||||
|     "CreateTokenSubmit": "Create", | ||||
|     "NoTokens": "You have no existing tokens.", | ||||
|     "NewTokenTitle": "Your new access token", | ||||
|     "NewTokenInfo": "Make sure to copy your new personal access token now.\n                  You won’t be able to see it again!", | ||||
|     "ExistingTokensTitle": "Existing tokens" | ||||
|   }, | ||||
|   "APIKeyList": { | ||||
|     "Name": "Name", | ||||
|     "Created": "Created on", | ||||
|     "LastUsed": "Last used", | ||||
|     "Actions": "Actions", | ||||
|     "Never": "Never", | ||||
|     "DeleteARIA": "Delete API Key" | ||||
|   }, | ||||
|   "NewPasswordForm": { | ||||
|     "Title": "Password", | ||||
|     "TitleARIA": "Password", | ||||
|     "ConfirmPassword": "Confirm Password", | ||||
|     "ConfirmPasswordARIA": "Confirm Password", | ||||
|     "SubmitSetNewPassword": "Set New Password" | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -188,8 +188,8 @@ | |||
|     "SubmitFeedback": "Enviar retroalimentación" | ||||
|   }, | ||||
|   "NewFileModal": { | ||||
|   "Title": "Crear Archivo", | ||||
|   "CloseButtonARIA": "Cerrar diálogo de crear archivo", | ||||
|     "Title": "Crear Archivo", | ||||
|     "CloseButtonARIA": "Cerrar diálogo de crear archivo", | ||||
|     "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." | ||||
|   }, | ||||
|  | @ -198,18 +198,96 @@ | |||
|     "Placeholder": "Nombre" | ||||
|   }, | ||||
|   "NewFolderModal": { | ||||
|   "Title": "Crear Directorio", | ||||
|   "CloseButtonARIA": "Cerrar Diálogo de Nuevo Directorio", | ||||
|   "EnterName": "Por favor introduce un nombre", | ||||
|   "EmptyName": " El nombre del directorio no debe contener solo espacios vacíos", | ||||
|   "InvalidExtension": "El nombre del directorio no debe contener una extensión" | ||||
| }, | ||||
|     "Title": "Crear Directorio", | ||||
|     "CloseButtonARIA": "Cerrar Diálogo de Nuevo Directorio", | ||||
|     "EnterName": "Por favor introduce un nombre", | ||||
|     "EmptyName": " El nombre del directorio no debe contener solo espacios vacíos", | ||||
|     "InvalidExtension": "El nombre del directorio no debe contener una extensión" | ||||
|   }, | ||||
|   "NewFolderForm": { | ||||
|     "AddFolderSubmit": "Agregar Directorio", | ||||
|     "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" | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 ghalestrilo
						ghalestrilo