mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 09:35:56 -05:00
Merge pull request #3258 from LLK/release/2019-08-15
[Master] Release 2019-08-15
This commit is contained in:
commit
6727cb133c
32 changed files with 791 additions and 105 deletions
91
package-lock.json
generated
91
package-lock.json
generated
|
@ -4035,6 +4035,12 @@
|
|||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"email-validator": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz",
|
||||
"integrity": "sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==",
|
||||
"dev": true
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
|
||||
|
@ -5474,7 +5480,8 @@
|
|||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
|
@ -5495,12 +5502,14 @@
|
|||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
|
@ -5515,17 +5524,20 @@
|
|||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
|
@ -5642,7 +5654,8 @@
|
|||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
|
@ -5654,6 +5667,7 @@
|
|||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
|
@ -5668,6 +5682,7 @@
|
|||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
|
@ -5675,12 +5690,14 @@
|
|||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
|
@ -5699,6 +5716,7 @@
|
|||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
|
@ -5779,7 +5797,8 @@
|
|||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
|
@ -5791,6 +5810,7 @@
|
|||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
|
@ -5876,7 +5896,8 @@
|
|||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
|
@ -5912,6 +5933,7 @@
|
|||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
@ -5931,6 +5953,7 @@
|
|||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
|
@ -5974,12 +5997,14 @@
|
|||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7501,12 +7526,6 @@
|
|||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"isemail": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz",
|
||||
"integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=",
|
||||
"dev": true
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
@ -9206,6 +9225,14 @@
|
|||
"isemail": "1.x.x",
|
||||
"moment": "2.x.x",
|
||||
"topo": "1.x.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"isemail": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz",
|
||||
"integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"jquery": {
|
||||
|
@ -14929,15 +14956,15 @@
|
|||
}
|
||||
},
|
||||
"scratch-gui": {
|
||||
"version": "0.1.0-prerelease.20190808151251",
|
||||
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20190808151251.tgz",
|
||||
"integrity": "sha512-KBYxva8dWl/XrWTPePveV8oq2RUkhyLoDFQnz2pJSBJOKvkY1SPVCABpc4ocLJ9nOBAtsWOCSJVFBAu24aZD6A==",
|
||||
"version": "0.1.0-prerelease.20190814215530",
|
||||
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20190814215530.tgz",
|
||||
"integrity": "sha512-6xCCphwTYaBXjlQ7CJjmAnrQOoTLke/da2JAdrSWmGM0WxE+kFAcNpr2DBi7wzPIPTbNuy1nw+ECLb3Bst9LEQ==",
|
||||
"dev": true
|
||||
},
|
||||
"scratch-l10n": {
|
||||
"version": "3.5.20190807144510",
|
||||
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.5.20190807144510.tgz",
|
||||
"integrity": "sha512-rq3G4NZvlFvb0bQFwsqK+anb7S74OSutxqabmaP5R4NRbmkpz0PvbT1zMGKEmEScY+8h5VLkNO6im9RoDsSm7g==",
|
||||
"version": "3.5.20190813223429",
|
||||
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.5.20190813223429.tgz",
|
||||
"integrity": "sha512-rSxUSwv0RgZTXUknAWuc7BFZWewiNhrgyPUMos/qAw4GgVMdY1ZRSIHBEIItpCXXYLOzw4ObcNafIim6Taq9NA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/cli": "^7.1.2",
|
||||
|
@ -14998,13 +15025,15 @@
|
|||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
|
||||
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"braces": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
|
||||
"integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"arr-flatten": "^1.1.0",
|
||||
"array-unique": "^0.3.2",
|
||||
|
@ -15023,6 +15052,7 @@
|
|||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-extendable": "^0.1.0"
|
||||
}
|
||||
|
@ -15205,6 +15235,7 @@
|
|||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
|
||||
"integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"extend-shallow": "^2.0.1",
|
||||
"is-number": "^3.0.0",
|
||||
|
@ -15217,6 +15248,7 @@
|
|||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-extendable": "^0.1.0"
|
||||
}
|
||||
|
@ -15296,7 +15328,8 @@
|
|||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.1",
|
||||
|
@ -15313,6 +15346,7 @@
|
|||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
||||
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"kind-of": "^3.0.2"
|
||||
},
|
||||
|
@ -15322,6 +15356,7 @@
|
|||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-buffer": "^1.1.5"
|
||||
}
|
||||
|
@ -15332,13 +15367,15 @@
|
|||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
||||
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "3.1.10",
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"test:unit:jest": "jest ./test/unit/ && jest ./test/localization/*.test.js",
|
||||
"test:unit:tap": "tap ./test/{unit-legacy,localization-legacy}/*.js --no-coverage -R classic",
|
||||
"test:coverage": "tap ./test/{unit-legacy,localization-legacy}/*.js --coverage --coverage-report=lcov",
|
||||
"build": "npm run clean && npm run translate && webpack --bail",
|
||||
"build": "npm run clean && npm run translate && NODE_OPTIONS=--max_old_space_size=8000 webpack --bail",
|
||||
"clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl",
|
||||
"deploy": "npm run deploy:s3 && npm run deploy:fastly",
|
||||
"deploy:fastly": "node ./bin/configure-fastly.js",
|
||||
|
@ -72,6 +72,7 @@
|
|||
"copy-webpack-plugin": "0.2.0",
|
||||
"create-react-class": "15.6.2",
|
||||
"css-loader": "0.23.1",
|
||||
"email-validator": "2.0.4",
|
||||
"enzyme": "3.10.0",
|
||||
"enzyme-adapter-react-16": "1.14.0",
|
||||
"eslint": "5.16.0",
|
||||
|
@ -123,7 +124,7 @@
|
|||
"redux": "3.5.2",
|
||||
"redux-thunk": "2.0.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"scratch-gui": "0.1.0-prerelease.20190808151251",
|
||||
"scratch-gui": "0.1.0-prerelease.20190814215530",
|
||||
"scratch-l10n": "latest",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"slick-carousel": "1.6.0",
|
||||
|
|
83
src/components/formik-forms/formik-checkbox.jsx
Normal file
83
src/components/formik-forms/formik-checkbox.jsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
const classNames = require('classnames');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
import {Field} from 'formik';
|
||||
|
||||
require('./formik-checkbox.scss');
|
||||
require('./formik-forms.scss');
|
||||
require('../forms/row.scss');
|
||||
|
||||
const FormikCheckboxSubComponent = ({
|
||||
className,
|
||||
field,
|
||||
id,
|
||||
label,
|
||||
...props
|
||||
}) => (
|
||||
<div className="checkbox">
|
||||
<input
|
||||
checked={field.value}
|
||||
className={classNames(
|
||||
'formik-checkbox',
|
||||
className
|
||||
)}
|
||||
id={id}
|
||||
name={field.name}
|
||||
type="checkbox"
|
||||
value={field.value}
|
||||
onBlur={field.onBlur} /* eslint-disable-line react/jsx-handler-names */
|
||||
onChange={field.onChange} /* eslint-disable-line react/jsx-handler-names */
|
||||
{...props}
|
||||
/>
|
||||
{label && (
|
||||
<label
|
||||
className={classNames(
|
||||
'formik-label',
|
||||
'formik-checkbox-label'
|
||||
)}
|
||||
htmlFor={id}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
FormikCheckboxSubComponent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
onBlur: PropTypes.function,
|
||||
onChange: PropTypes.function,
|
||||
value: PropTypes.bool
|
||||
}),
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string
|
||||
};
|
||||
|
||||
|
||||
const FormikCheckbox = ({
|
||||
className,
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
...props
|
||||
}) => (
|
||||
<Field
|
||||
className={className}
|
||||
component={FormikCheckboxSubComponent}
|
||||
id={id}
|
||||
label={label}
|
||||
name={name}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
FormikCheckbox.propTypes = {
|
||||
className: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
name: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = FormikCheckbox;
|
39
src/components/formik-forms/formik-checkbox.scss
Normal file
39
src/components/formik-forms/formik-checkbox.scss
Normal file
|
@ -0,0 +1,39 @@
|
|||
@import "../../colors";
|
||||
|
||||
.formik-checkbox-label {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
input[type="checkbox"].formik-checkbox {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: .625rem;
|
||||
border: 1px solid $active-dark-gray;
|
||||
border-radius: 3px;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
appearance: none;
|
||||
|
||||
&:focus:checked {
|
||||
transition: all .5s ease;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 .25rem $ui-blue-25percent;
|
||||
}
|
||||
|
||||
&:focus:not(:checked) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:checked {
|
||||
background-color: $ui-blue;
|
||||
text-align: center;
|
||||
text-indent: .125rem;
|
||||
line-height: 1.25rem;
|
||||
font-size: .75rem;
|
||||
|
||||
&:after {
|
||||
color: $type-white;
|
||||
content: "\2714";
|
||||
}
|
||||
}
|
||||
}
|
3
src/components/formik-forms/formik-forms.scss
Normal file
3
src/components/formik-forms/formik-forms.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.formik-label {
|
||||
font-weight: 500;
|
||||
}
|
|
@ -5,8 +5,8 @@ import {Field} from 'formik';
|
|||
|
||||
const ValidationMessage = require('../forms/validation-message.jsx');
|
||||
|
||||
require('../forms/input.scss');
|
||||
require('../forms/row.scss');
|
||||
require('./formik-input.scss');
|
||||
|
||||
const FormikInput = ({
|
||||
className,
|
||||
|
@ -26,6 +26,7 @@ const FormikInput = ({
|
|||
<Field
|
||||
className={classNames(
|
||||
'input',
|
||||
{fail: error},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
28
src/components/formik-forms/formik-input.scss
Normal file
28
src/components/formik-forms/formik-input.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
@import "../../colors";
|
||||
|
||||
.input {
|
||||
height: 2.75rem;
|
||||
border-radius: .5rem;
|
||||
background-color: $ui-white;
|
||||
margin-bottom: .5rem;
|
||||
transition: all .5s ease;
|
||||
border: 1px solid $active-gray;
|
||||
padding: 0 1rem;
|
||||
color: $type-gray;
|
||||
font-size: .875rem;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 .25rem $ui-blue-25percent;
|
||||
outline: none;
|
||||
border: 1px solid $ui-blue;
|
||||
}
|
||||
|
||||
&.fail {
|
||||
border: 1px solid $ui-orange;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 .25rem $ui-orange-25percent;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import {Field} from 'formik';
|
|||
|
||||
const FormikInput = require('./formik-input.jsx');
|
||||
|
||||
require('./formik-forms.scss');
|
||||
require('./formik-radio-button.scss');
|
||||
require('../forms/row.scss');
|
||||
|
||||
|
@ -34,6 +35,7 @@ const FormikRadioButtonSubComponent = ({
|
|||
{label && (
|
||||
<label
|
||||
className={classNames(
|
||||
'formik-label',
|
||||
'formik-radio-label',
|
||||
labelClassName
|
||||
)}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@import "../../colors";
|
||||
|
||||
.formik-radio-label {
|
||||
font-weight: 300;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ const FormikSelect = ({
|
|||
}) => {
|
||||
const optionsList = options.map((item, index) => (
|
||||
<option
|
||||
disabled={item.disabled}
|
||||
key={index}
|
||||
value={item.value}
|
||||
>
|
||||
|
|
7
src/components/formik-forms/input.scss
Normal file
7
src/components/formik-forms/input.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
.input::placeholder {
|
||||
font-style: italic;
|
||||
color: $type-gray-75percent;
|
||||
}
|
46
src/components/info-button/info-button.jsx
Normal file
46
src/components/info-button/info-button.jsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
const bindAll = require('lodash.bindall');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
|
||||
require('./info-button.scss');
|
||||
|
||||
class InfoButton extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleHideMessage',
|
||||
'handleShowMessage'
|
||||
]);
|
||||
this.state = {
|
||||
visible: false
|
||||
};
|
||||
}
|
||||
handleHideMessage () {
|
||||
this.setState({visible: false});
|
||||
}
|
||||
handleShowMessage () {
|
||||
this.setState({visible: true});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div
|
||||
className="info-button"
|
||||
onClick={this.handleShowMessage}
|
||||
onMouseOut={this.handleHideMessage}
|
||||
onMouseOver={this.handleShowMessage}
|
||||
>
|
||||
{this.state.visible && (
|
||||
<div className="info-button-message">
|
||||
{this.props.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InfoButton.propTypes = {
|
||||
message: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = InfoButton;
|
78
src/components/info-button/info-button.scss
Normal file
78
src/components/info-button/info-button.scss
Normal file
|
@ -0,0 +1,78 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
.info-button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: .375rem;
|
||||
border-radius: 50%;
|
||||
background-color: $ui-blue;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: "?";
|
||||
color: $ui-white;
|
||||
font-family: verdana;
|
||||
font-weight: 400;
|
||||
top: -.125rem;
|
||||
left: .325rem;
|
||||
font-size: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.info-button-message {
|
||||
$arrow-border-width: 1rem;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(1rem, -1rem);
|
||||
width: 16.5rem;
|
||||
min-height: 1rem;
|
||||
margin-left: $arrow-border-width;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
padding: .75rem;
|
||||
overflow: visible;
|
||||
background-color: $ui-blue;
|
||||
color: $type-white;
|
||||
line-height: 1.25rem;
|
||||
text-align: left;
|
||||
font-size: .875rem;
|
||||
z-index: 1;
|
||||
|
||||
&:before {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: -$arrow-border-width / 2;
|
||||
|
||||
transform: rotate(45deg);
|
||||
|
||||
border-bottom: 1px solid $active-gray;
|
||||
border-left: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
|
||||
background-color: $ui-blue;
|
||||
width: $arrow-border-width;
|
||||
height: $arrow-border-width;
|
||||
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.info-button-message {
|
||||
position: relative;
|
||||
transform: none;
|
||||
margin: inherit;
|
||||
width: 100%;
|
||||
height: inherit;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -53,7 +53,7 @@ class BirthDateStep extends React.Component {
|
|||
}
|
||||
validateSelect (selection) {
|
||||
if (selection === 'null') {
|
||||
return this.props.intl.formatMessage({id: 'form.validationRequired'});
|
||||
return this.props.intl.formatMessage({id: 'general.required'});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -88,6 +88,7 @@ class BirthDateStep extends React.Component {
|
|||
<JoinFlowStep
|
||||
description={this.props.intl.formatMessage({id: 'registration.private'})}
|
||||
headerImgSrc="/images/hoc/getting-started.jpg"
|
||||
infoMessage={this.props.intl.formatMessage({id: 'registration.birthDateStepInfo'})}
|
||||
title={this.props.intl.formatMessage({id: 'registration.birthDateStepTitle'})}
|
||||
waiting={isSubmitting}
|
||||
onSubmit={handleSubmit}
|
||||
|
|
111
src/components/join-flow/country-step.jsx
Normal file
111
src/components/join-flow/country-step.jsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
const bindAll = require('lodash.bindall');
|
||||
const classNames = require('classnames');
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
import {Formik} from 'formik';
|
||||
const {injectIntl, intlShape} = require('react-intl');
|
||||
|
||||
const countryData = require('../../lib/country-data');
|
||||
const FormikSelect = require('../../components/formik-forms/formik-select.jsx');
|
||||
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||
|
||||
require('./join-flow-steps.scss');
|
||||
|
||||
class CountryStep extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleValidSubmit',
|
||||
'validateForm',
|
||||
'validateSelect'
|
||||
]);
|
||||
this.countryOptions = [];
|
||||
}
|
||||
componentDidMount () {
|
||||
this.setCountryOptions();
|
||||
}
|
||||
setCountryOptions () {
|
||||
if (this.countryOptions.length === 0) {
|
||||
this.countryOptions = [...countryData.registrationCountryOptions];
|
||||
this.countryOptions.unshift({
|
||||
disabled: true,
|
||||
label: this.props.intl.formatMessage({id: 'registration.selectCountry'}),
|
||||
value: 'null'
|
||||
});
|
||||
}
|
||||
}
|
||||
validateSelect (selection) {
|
||||
if (selection === 'null') {
|
||||
return this.props.intl.formatMessage({id: 'general.required'});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
validateForm () {
|
||||
return {};
|
||||
}
|
||||
handleValidSubmit (formData, formikBag) {
|
||||
formikBag.setSubmitting(false);
|
||||
this.props.onNextStep(formData);
|
||||
}
|
||||
render () {
|
||||
this.setCountryOptions();
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
country: 'null'
|
||||
}}
|
||||
validate={this.validateForm}
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
onSubmit={this.handleValidSubmit}
|
||||
>
|
||||
{props => {
|
||||
const {
|
||||
errors,
|
||||
handleSubmit,
|
||||
isSubmitting
|
||||
} = props;
|
||||
return (
|
||||
<JoinFlowStep
|
||||
description={this.props.intl.formatMessage({id: 'registration.countryStepDescription'})}
|
||||
headerImgSrc="/images/hoc/getting-started.jpg"
|
||||
title={this.props.intl.formatMessage({id: 'registration.countryStepTitle'})}
|
||||
waiting={isSubmitting}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'col-sm-9',
|
||||
'row'
|
||||
)}
|
||||
>
|
||||
<FormikSelect
|
||||
className={classNames(
|
||||
'join-flow-select',
|
||||
'join-flow-select-country',
|
||||
{fail: errors.country}
|
||||
)}
|
||||
error={errors.country}
|
||||
id="country"
|
||||
name="country"
|
||||
options={this.countryOptions}
|
||||
validate={this.validateSelect}
|
||||
validationClassName="validation-full-width-input"
|
||||
/>
|
||||
</div>
|
||||
</JoinFlowStep>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CountryStep.propTypes = {
|
||||
intl: intlShape,
|
||||
onNextStep: PropTypes.func
|
||||
};
|
||||
|
||||
const IntlCountryStep = injectIntl(CountryStep);
|
||||
|
||||
module.exports = IntlCountryStep;
|
|
@ -1,10 +1,14 @@
|
|||
const bindAll = require('lodash.bindall');
|
||||
const classNames = require('classnames');
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
import {Formik} from 'formik';
|
||||
const {injectIntl, intlShape} = require('react-intl');
|
||||
const emailValidator = require('email-validator');
|
||||
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||
|
||||
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
|
||||
|
||||
require('./join-flow-steps.scss');
|
||||
|
||||
|
@ -13,9 +17,18 @@ class EmailStep extends React.Component {
|
|||
super(props);
|
||||
bindAll(this, [
|
||||
'handleValidSubmit',
|
||||
'validateEmailIfPresent',
|
||||
'validateForm'
|
||||
]);
|
||||
}
|
||||
validateEmailIfPresent (email) {
|
||||
if (!email) return null; // skip validation if email is blank; null indicates valid
|
||||
const isValidLocally = emailValidator.validate(email);
|
||||
if (isValidLocally) {
|
||||
return null; // TODO: validate email address remotely
|
||||
}
|
||||
return this.props.intl.formatMessage({id: 'registration.validationEmailInvalid'});
|
||||
}
|
||||
validateForm () {
|
||||
return {};
|
||||
}
|
||||
|
@ -35,17 +48,52 @@ class EmailStep extends React.Component {
|
|||
>
|
||||
{props => {
|
||||
const {
|
||||
errors,
|
||||
handleSubmit,
|
||||
isSubmitting
|
||||
isSubmitting,
|
||||
validateField
|
||||
} = props;
|
||||
return (
|
||||
<JoinFlowStep
|
||||
description={this.props.intl.formatMessage({id: 'registration.emailStepDescription'})}
|
||||
footerContent={(
|
||||
<FormattedMessage
|
||||
id="registration.acceptTermsOfUse"
|
||||
values={{
|
||||
touLink: (
|
||||
<a
|
||||
className="join-flow-link"
|
||||
href="/terms_of_use"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage id="general.termsOfUse" />
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
headerImgSrc="/images/hoc/getting-started.jpg"
|
||||
innerContentClassName="modal-inner-content-email"
|
||||
nextButton={this.props.intl.formatMessage({id: 'registration.createAccount'})}
|
||||
title={this.props.intl.formatMessage({id: 'registration.emailStepTitle'})}
|
||||
waiting={isSubmitting}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<FormikInput
|
||||
className={classNames(
|
||||
'join-flow-input',
|
||||
'join-flow-input-tall',
|
||||
{fail: errors.email}
|
||||
)}
|
||||
error={errors.email}
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder={this.props.intl.formatMessage({id: 'general.emailAddress'})}
|
||||
validate={this.validateEmailIfPresent}
|
||||
validationClassName="validation-full-width-input"
|
||||
onBlur={() => validateField('email')} // eslint-disable-line react/jsx-no-bind
|
||||
/>
|
||||
</JoinFlowStep>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
|
@ -58,4 +106,5 @@ EmailStep.propTypes = {
|
|||
onNextStep: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
module.exports = injectIntl(EmailStep);
|
||||
|
|
|
@ -82,6 +82,7 @@ class GenderStep extends React.Component {
|
|||
<JoinFlowStep
|
||||
className="join-flow-gender-step"
|
||||
description={this.props.intl.formatMessage({id: 'registration.genderStepDescription'})}
|
||||
infoMessage={this.props.intl.formatMessage({id: 'registration.genderStepInfo'})}
|
||||
title={this.props.intl.formatMessage({id: 'registration.genderStepTitle'})}
|
||||
waiting={isSubmitting}
|
||||
onSubmit={handleSubmit}
|
||||
|
|
|
@ -5,6 +5,7 @@ const PropTypes = require('prop-types');
|
|||
const NextStepButton = require('./next-step-button.jsx');
|
||||
const ModalTitle = require('../modal/base/modal-title.jsx');
|
||||
const ModalInnerContent = require('../modal/base/modal-inner-content.jsx');
|
||||
const InfoButton = require('../info-button/info-button.jsx');
|
||||
|
||||
require('./join-flow-step.scss');
|
||||
|
||||
|
@ -12,7 +13,10 @@ const JoinFlowStep = ({
|
|||
children,
|
||||
className,
|
||||
description,
|
||||
footerContent,
|
||||
headerImgSrc,
|
||||
infoMessage,
|
||||
innerContentClassName,
|
||||
nextButton,
|
||||
onSubmit,
|
||||
title,
|
||||
|
@ -28,7 +32,8 @@ const JoinFlowStep = ({
|
|||
<ModalInnerContent
|
||||
className={classNames(
|
||||
'join-flow-inner-content',
|
||||
className
|
||||
className,
|
||||
innerContentClassName
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
|
@ -40,11 +45,19 @@ const JoinFlowStep = ({
|
|||
{description && (
|
||||
<div className="join-flow-description">
|
||||
{description}
|
||||
{infoMessage && (
|
||||
<InfoButton message={infoMessage} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</ModalInnerContent>
|
||||
</div>
|
||||
{footerContent && (
|
||||
<div className="join-flow-footer-message">
|
||||
{footerContent}
|
||||
</div>
|
||||
)}
|
||||
<NextStepButton
|
||||
content={nextButton}
|
||||
waiting={waiting}
|
||||
|
@ -56,7 +69,10 @@ JoinFlowStep.propTypes = {
|
|||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
footerContent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
headerImgSrc: PropTypes.string,
|
||||
infoMessage: PropTypes.string,
|
||||
innerContentClassName: PropTypes.string,
|
||||
nextButton: PropTypes.node,
|
||||
onSubmit: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
|
|
|
@ -33,3 +33,13 @@
|
|||
border-top-left-radius: 1rem;
|
||||
border-top-right-radius: 1rem;
|
||||
}
|
||||
|
||||
.join-flow-footer-message {
|
||||
width: 100%;
|
||||
padding: 1.125rem 1.5rem 1.125rem;
|
||||
background-color: $ui-blue-25percent;
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: $ui-blue;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.join-flow-password-confirm {
|
||||
margin-bottom: .6875rem;
|
||||
}
|
||||
|
||||
.join-flow-input-tall {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.join-flow-input-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: .5rem;
|
||||
|
@ -47,12 +55,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.select .join-flow-select {
|
||||
.select .join-flow-select-month {
|
||||
width: 9.125rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.join-flow-select-month {
|
||||
margin-right: .5rem;
|
||||
.select .join-flow-select-country {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.join-flow-password-section {
|
||||
|
@ -91,3 +101,11 @@
|
|||
height: 2rem;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.modal-inner-content-email {
|
||||
padding-top: 2.9rem;
|
||||
}
|
||||
|
||||
a.join-flow-link:link, a.join-flow-link:visited, a.join-flow-link:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ const Progression = require('../progression/progression.jsx');
|
|||
const UsernameStep = require('./username-step.jsx');
|
||||
const BirthDateStep = require('./birthdate-step.jsx');
|
||||
const GenderStep = require('./gender-step.jsx');
|
||||
const CountryStep = require('./country-step.jsx');
|
||||
const EmailStep = require('./email-step.jsx');
|
||||
const WelcomeStep = require('./welcome-step.jsx');
|
||||
|
||||
|
@ -42,6 +43,7 @@ class JoinFlow extends React.Component {
|
|||
<UsernameStep onNextStep={this.handleAdvanceStep} />
|
||||
<BirthDateStep onNextStep={this.handleAdvanceStep} />
|
||||
<GenderStep onNextStep={this.handleAdvanceStep} />
|
||||
<CountryStep onNextStep={this.handleAdvanceStep} />
|
||||
<EmailStep onNextStep={this.handleAdvanceStep} />
|
||||
<WelcomeStep
|
||||
email={this.state.formData.email}
|
||||
|
|
|
@ -7,6 +7,7 @@ const {injectIntl, intlShape} = require('react-intl');
|
|||
|
||||
const validate = require('../../lib/validate');
|
||||
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
|
||||
const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx');
|
||||
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||
|
||||
require('./join-flow-steps.scss');
|
||||
|
@ -26,9 +27,6 @@ class UsernameStep extends React.Component {
|
|||
'validateUsernameIfPresent',
|
||||
'validateForm'
|
||||
]);
|
||||
this.state = {
|
||||
showPassword: false
|
||||
};
|
||||
}
|
||||
handleChangeShowPassword () {
|
||||
this.setState({showPassword: !this.state.showPassword});
|
||||
|
@ -36,20 +34,26 @@ class UsernameStep extends React.Component {
|
|||
// we allow username to be empty on blur, since you might not have typed anything yet
|
||||
validateUsernameIfPresent (username) {
|
||||
if (!username) return null; // skip validation if username is blank; null indicates valid
|
||||
// if username is not blank, run both local and remote validations
|
||||
const localResult = validate.validateUsernameLocally(username);
|
||||
if (localResult.valid) {
|
||||
return validate.validateUsernameRemotely(username).then(
|
||||
remoteResult => {
|
||||
if (remoteResult.valid) return null;
|
||||
// there may be multiple validation errors. Prioritize vulgarity, then
|
||||
// length, then having invalid chars, then all other remote reports
|
||||
if (remoteResult.valid === false && remoteResult.errMsgId === 'registration.validationUsernameVulgar') {
|
||||
return this.props.intl.formatMessage({id: remoteResult.errMsgId});
|
||||
} else if (localResult.valid === false) {
|
||||
return this.props.intl.formatMessage({id: localResult.errMsgId});
|
||||
} else if (remoteResult.valid === false) {
|
||||
return this.props.intl.formatMessage({id: remoteResult.errMsgId});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
return this.props.intl.formatMessage({id: localResult.errMsgId});
|
||||
}
|
||||
validatePasswordIfPresent (password) {
|
||||
validatePasswordIfPresent (password, username) {
|
||||
if (!password) return null; // skip validation if password is blank; null indicates valid
|
||||
const localResult = validate.validatePassword(password);
|
||||
const localResult = validate.validatePassword(password, username);
|
||||
if (localResult.valid) return null;
|
||||
return this.props.intl.formatMessage({id: localResult.errMsgId});
|
||||
}
|
||||
|
@ -69,13 +73,10 @@ class UsernameStep extends React.Component {
|
|||
if (!usernameResult.valid) {
|
||||
errors.username = this.props.intl.formatMessage({id: usernameResult.errMsgId});
|
||||
}
|
||||
const passwordResult = validate.validatePassword(values.password);
|
||||
const passwordResult = validate.validatePassword(values.password, values.username);
|
||||
if (!passwordResult.valid) {
|
||||
errors.password = this.props.intl.formatMessage({id: passwordResult.errMsgId});
|
||||
}
|
||||
if (values.password === values.username) {
|
||||
errors.password = this.props.intl.formatMessage({id: 'registration.validationPasswordNotUsername'});
|
||||
}
|
||||
const passwordConfirmResult = validate.validatePasswordConfirm(values.password, values.passwordConfirm);
|
||||
if (!passwordConfirmResult.valid) {
|
||||
errors.passwordConfirm = this.props.intl.formatMessage({id: passwordConfirmResult.errMsgId});
|
||||
|
@ -85,6 +86,7 @@ class UsernameStep extends React.Component {
|
|||
// called after all validations pass with no errors
|
||||
handleValidSubmit (formData, formikBag) {
|
||||
formikBag.setSubmitting(false); // formik makes us do this ourselves
|
||||
delete formData.showPassword;
|
||||
this.props.onNextStep(formData);
|
||||
}
|
||||
render () {
|
||||
|
@ -93,7 +95,8 @@ class UsernameStep extends React.Component {
|
|||
initialValues={{
|
||||
username: '',
|
||||
password: '',
|
||||
passwordConfirm: ''
|
||||
passwordConfirm: '',
|
||||
showPassword: false
|
||||
}}
|
||||
validate={this.validateForm}
|
||||
validateOnBlur={false}
|
||||
|
@ -105,6 +108,8 @@ class UsernameStep extends React.Component {
|
|||
errors,
|
||||
handleSubmit,
|
||||
isSubmitting,
|
||||
setFieldError,
|
||||
setFieldValue,
|
||||
validateField,
|
||||
values
|
||||
} = props;
|
||||
|
@ -123,15 +128,20 @@ class UsernameStep extends React.Component {
|
|||
</div>
|
||||
<FormikInput
|
||||
className={classNames(
|
||||
'join-flow-input',
|
||||
{fail: errors.username}
|
||||
'join-flow-input'
|
||||
)}
|
||||
error={errors.username}
|
||||
id="username"
|
||||
name="username"
|
||||
validate={this.validateUsernameIfPresent}
|
||||
validationClassName="validation-full-width-input"
|
||||
onBlur={() => validateField('username')} // eslint-disable-line react/jsx-no-bind
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
onBlur={() => validateField('username')}
|
||||
onChange={e => {
|
||||
setFieldValue('username', e.target.value);
|
||||
setFieldError('username', null);
|
||||
}}
|
||||
/* eslint-enable react/jsx-no-bind */
|
||||
/>
|
||||
<div className="join-flow-password-section">
|
||||
<div className="join-flow-input-title">
|
||||
|
@ -139,28 +149,32 @@ class UsernameStep extends React.Component {
|
|||
</div>
|
||||
<FormikInput
|
||||
className={classNames(
|
||||
'join-flow-input',
|
||||
{fail: errors.password}
|
||||
'join-flow-input'
|
||||
)}
|
||||
error={errors.password}
|
||||
id="password"
|
||||
name="password"
|
||||
type={this.state.showPassword ? 'text' : 'password'}
|
||||
validate={this.validatePasswordIfPresent}
|
||||
validationClassName="validation-full-width-input"
|
||||
type={values.showPassword ? 'text' : 'password'}
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
validate={password => this.validatePasswordIfPresent(password, values.username)}
|
||||
validationClassName="validation-full-width-input"
|
||||
onBlur={() => validateField('password')}
|
||||
onChange={e => {
|
||||
setFieldValue('password', e.target.value);
|
||||
setFieldError('password', null);
|
||||
}}
|
||||
/* eslint-enable react/jsx-no-bind */
|
||||
/>
|
||||
<FormikInput
|
||||
className={classNames(
|
||||
'join-flow-input',
|
||||
'join-flow-password-confirm',
|
||||
{fail: errors.passwordConfirm}
|
||||
)}
|
||||
error={errors.passwordConfirm}
|
||||
id="passwordConfirm"
|
||||
name="passwordConfirm"
|
||||
type={this.state.showPassword ? 'text' : 'password'}
|
||||
type={values.showPassword ? 'text' : 'password'}
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
validate={() =>
|
||||
this.validatePasswordConfirmIfPresent(values.password,
|
||||
|
@ -170,17 +184,18 @@ class UsernameStep extends React.Component {
|
|||
onBlur={() =>
|
||||
validateField('passwordConfirm')
|
||||
}
|
||||
onChange={e => {
|
||||
setFieldValue('passwordConfirm', e.target.value);
|
||||
setFieldError('passwordConfirm', null);
|
||||
}}
|
||||
/* eslint-enable react/jsx-no-bind */
|
||||
/>
|
||||
<div className="join-flow-input-title">
|
||||
<div
|
||||
onClick={this.handleChangeShowPassword}
|
||||
>
|
||||
{/* TODO: should localize 'Hide password' if we use that */}
|
||||
{this.state.showPassword ? 'Hide password' : (
|
||||
this.props.intl.formatMessage({id: 'registration.showPassword'})
|
||||
)}
|
||||
</div>
|
||||
<FormikCheckbox
|
||||
id="showPassword"
|
||||
label={this.props.intl.formatMessage({id: 'registration.showPassword'})}
|
||||
name="showPassword"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -139,7 +139,7 @@ class UsernameStep extends React.Component {
|
|||
default:
|
||||
this.form.formsy.updateInputsWithError({
|
||||
'user.username': this.props.intl.formatMessage({
|
||||
id: 'registration.validationUsernameInvalid'
|
||||
id: 'registration.validationUsernameNotAllowed'
|
||||
})
|
||||
});
|
||||
return callback(false);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"general.accountSettings": "Account settings",
|
||||
"general.about": "About",
|
||||
"general.aboutScratch": "About Scratch",
|
||||
"general.apiError": "Whoops, Scratch had an error.",
|
||||
"general.back": "Back",
|
||||
"general.birthMonth": "Birth Month",
|
||||
"general.birthYear": "Birth Year",
|
||||
|
@ -20,7 +21,7 @@
|
|||
"general.create": "Create",
|
||||
"general.credits": "Credits",
|
||||
"general.dmca": "DMCA",
|
||||
"general.emailAddress": "Email Address",
|
||||
"general.emailAddress": "Email address",
|
||||
"general.english": "English",
|
||||
"general.error": "Oops! Something went wrong",
|
||||
"general.errorIdentifier": "Your error was logged with id {errorId}",
|
||||
|
@ -70,6 +71,7 @@
|
|||
"general.privacyPolicy": "Privacy Policy",
|
||||
"general.projects": "Projects",
|
||||
"general.profile": "Profile",
|
||||
"general.required": "Required",
|
||||
"general.resourcesTitle": "Educator Resources",
|
||||
"general.scratchConference": "Scratch Conference",
|
||||
"general.scratchEd": "ScratchEd",
|
||||
|
@ -141,6 +143,7 @@
|
|||
"parents.FaqResourcesQ": "What resources are available for learning Scratch?",
|
||||
"parents.introDescription": "Scratch is a programming language and an online community where children can program and share interactive media such as stories, games, and animation with people from all over the world. As children create with Scratch, they learn to think creatively, work collaboratively, and reason systematically. Scratch is designed and maintained by the Lifelong Kindergarten group at the MIT Media Lab.",
|
||||
|
||||
"registration.birthDateStepInfo": "This helps us understand the age range of people who use Scratch. We use this to confirm account ownership if you contact our team. This information will not be made public on your account.",
|
||||
"registration.birthDateStepTitle": "When were you born?",
|
||||
"registration.checkOutResources": "Get Started with Resources",
|
||||
"registration.checkOutResourcesDescription": "Explore materials for educators and facilitators written by the Scratch Team, including <a href='/educators#resources'>tips, tutorials, and guides</a>.",
|
||||
|
@ -148,14 +151,18 @@
|
|||
"registration.choosePasswordStepTitle": "Create a password",
|
||||
"registration.choosePasswordStepTooltip": "Don't use your name or anything that's easy for someone else to guess.",
|
||||
"registration.classroomApiGeneralError": "Sorry, we could not find the registration information for this class",
|
||||
"registration.countryStepDescription": "We’ll display your country on your profile.",
|
||||
"registration.countryStepTitle": "What country do you live in?",
|
||||
"registration.generalError": "Sorry, an unexpected error occurred.",
|
||||
"registration.classroomInviteExistingStudentStepDescription": "you have been invited to join the class:",
|
||||
"registration.classroomInviteNewStudentStepDescription": "Your teacher has invited you to join a class:",
|
||||
"registration.confirmYourEmail": "Confirm Your Email",
|
||||
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
|
||||
"registration.createAccount": "Create Account",
|
||||
"registration.createUsername": "Create a username",
|
||||
"registration.genderStepTitle": "What's your gender?",
|
||||
"registration.genderStepDescription": "Scratch welcomes people of all genders. We will always keep this information private.",
|
||||
"registration.genderStepInfo": "This helps us understand who uses Scratch, so that we can broaden participation. This information will not be made public on your account.",
|
||||
"registration.genderOptionAnother": "Another gender:",
|
||||
"registration.genderOptionPreferNotToSay": "Prefer not to say",
|
||||
"registration.emailStepTitle": "What's your email?",
|
||||
|
@ -184,18 +191,22 @@
|
|||
"registration.studentUsernameStepHelpText": "Already have a Scratch account?",
|
||||
"registration.studentUsernameStepTooltip": "You'll need to create a new Scratch account to join this class.",
|
||||
"registration.studentUsernameFieldHelpText": "For safety, don't use your real name!",
|
||||
"registration.acceptTermsOfUse": "By creating an account, I accept and agree to the {touLink}.",
|
||||
"registration.usernameStepTitle": "Request a Teacher Account",
|
||||
"registration.usernameStepTitleScratcher": "Create a Scratch Account",
|
||||
"registration.validationMaxLength": "Sorry, you have exceeded the maximum character limit.",
|
||||
"registration.validationPasswordLength": "Passwords must be at least six characters",
|
||||
"registration.validationPasswordNotEquals": "Your password may not be \"password\"",
|
||||
"registration.validationPasswordNotUsername": "Your password may not be your username",
|
||||
"registration.validationUsernameRegexp": "Your username may only contain letters, numbers, \"-\", and \"_\"",
|
||||
"registration.validationUsernameMinLength": "Usernames must be at least 3 characters",
|
||||
"registration.validationUsernameMaxLength": "Usernames must be at most 20 characters",
|
||||
"registration.validationUsernameExists": "Sorry, that username already exists",
|
||||
"registration.validationPasswordConfirmNotEquals": "Passwords don’t match",
|
||||
"registration.validationPasswordLength": "Must be 6 letters or longer",
|
||||
"registration.validationPasswordNotEquals": "Password is too easy to guess. Try something else?",
|
||||
"registration.validationPasswordNotUsername": "Password can’t match your username",
|
||||
"registration.validationUsernameRegexp": "Usernames can only use letters, numbers, - and _",
|
||||
"registration.validationUsernameMinLength": "Must be 3 letters or longer",
|
||||
"registration.validationUsernameMaxLength": "Must be 20 letters or shorter",
|
||||
"registration.validationUsernameExists": "Username taken. Try another?",
|
||||
"registration.validationUsernameNotAllowed": "Username not allowed",
|
||||
"registration.validationUsernameVulgar": "Hmm, that looks inappropriate",
|
||||
"registration.validationUsernameInvalid": "Invalid username",
|
||||
"registration.validationEmailInvalid": "Email doesn’t look right. Try another?",
|
||||
"registration.waitForApproval": "Wait for Approval",
|
||||
"registration.waitForApprovalDescription": "You can log into your Scratch Account now, but the features specific to Teachers are not yet available. Your information is being reviewed. Please be patient, the approval process can take up to one day. You will receive an email indicating your account has been upgraded once your account has been approved.",
|
||||
"registration.welcomeStepDescription": "You have successfully set up a Scratch account! You are now a member of the class:",
|
||||
|
|
|
@ -3,13 +3,13 @@ const api = require('./api');
|
|||
|
||||
module.exports.validateUsernameLocally = username => {
|
||||
if (!username || username === '') {
|
||||
return {valid: false, errMsgId: 'form.validationRequired'};
|
||||
return {valid: false, errMsgId: 'general.required'};
|
||||
} else if (username.length < 3) {
|
||||
return {valid: false, errMsgId: 'form.validationUsernameMinLength'};
|
||||
return {valid: false, errMsgId: 'registration.validationUsernameMinLength'};
|
||||
} else if (username.length > 20) {
|
||||
return {valid: false, errMsgId: 'form.validationUsernameMaxLength'};
|
||||
return {valid: false, errMsgId: 'registration.validationUsernameMaxLength'};
|
||||
} else if (!/^[\w-]+$/i.test(username)) {
|
||||
return {valid: false, errMsgId: 'form.validationUsernameRegexp'};
|
||||
return {valid: false, errMsgId: 'registration.validationUsernameRegexp'};
|
||||
}
|
||||
return {valid: true};
|
||||
};
|
||||
|
@ -29,34 +29,41 @@ module.exports.validateUsernameRemotely = username => (
|
|||
case 'username exists':
|
||||
resolve({valid: false, errMsgId: 'registration.validationUsernameExists'});
|
||||
break;
|
||||
case 'bad username':
|
||||
resolve({valid: false, errMsgId: 'registration.validationUsernameVulgar'});
|
||||
case 'bad username': // i.e., vulgar
|
||||
resolve({valid: false, errMsgId: 'registration.validationUsernameNotAllowed'});
|
||||
break;
|
||||
case 'invalid username':
|
||||
default:
|
||||
resolve({valid: false, errMsgId: 'registration.validationUsernameInvalid'});
|
||||
resolve({valid: false, errMsgId: 'registration.validationUsernameNotAllowed'});
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
module.exports.validatePassword = password => {
|
||||
/**
|
||||
* Validate password value, optionally also considering username value
|
||||
* @param {string} password password value to validate
|
||||
* @param {string} username username value to compare
|
||||
* @return {object} {valid: boolean, errMsgId: string}
|
||||
*/
|
||||
module.exports.validatePassword = (password, username) => {
|
||||
if (!password) {
|
||||
return {valid: false, errMsgId: 'form.validationRequired'};
|
||||
return {valid: false, errMsgId: 'general.required'};
|
||||
} else if (password.length < 6) {
|
||||
return {valid: false, errMsgId: 'registration.validationPasswordLength'};
|
||||
} else if (password === 'password') {
|
||||
return {valid: false, errMsgId: 'registration.validationPasswordNotEquals'};
|
||||
} else if (username && password === username) {
|
||||
return {valid: false, errMsgId: 'registration.validationPasswordNotUsername'};
|
||||
}
|
||||
return {valid: true};
|
||||
};
|
||||
|
||||
module.exports.validatePasswordConfirm = (password, passwordConfirm) => {
|
||||
if (!passwordConfirm) {
|
||||
return {valid: false, errMsgId: 'form.validationRequired'};
|
||||
return {valid: false, errMsgId: 'general.required'};
|
||||
} else if (password !== passwordConfirm) {
|
||||
// TODO: add a new string for this case
|
||||
return {valid: false, errMsgId: 'general.error'};
|
||||
return {valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'};
|
||||
}
|
||||
return {valid: true};
|
||||
};
|
||||
|
|
|
@ -54,8 +54,12 @@ class Comment extends React.Component {
|
|||
}
|
||||
|
||||
handleDelete () {
|
||||
if (this.props.canDeleteWithoutConfirm) {
|
||||
this.props.onDelete(this.props.id);
|
||||
} else {
|
||||
this.setState({deleting: true});
|
||||
}
|
||||
}
|
||||
|
||||
handleConfirmDelete () {
|
||||
this.setState({deleting: false});
|
||||
|
@ -267,6 +271,7 @@ Comment.propTypes = {
|
|||
username: PropTypes.string
|
||||
}),
|
||||
canDelete: PropTypes.bool,
|
||||
canDeleteWithoutConfirm: PropTypes.bool,
|
||||
canReply: PropTypes.bool,
|
||||
canReport: PropTypes.bool,
|
||||
canRestore: PropTypes.bool,
|
||||
|
|
|
@ -83,6 +83,13 @@
|
|||
width: calc(100% + 1rem);
|
||||
height: 100%;
|
||||
content: "";
|
||||
|
||||
/*
|
||||
Because this :before is absolutely positioned, it will eat clicks
|
||||
from non-absolute elements after it (like the author link).
|
||||
Prevent this by explicitly disabling pointer events on this background element.
|
||||
*/
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.comment-top-row {
|
||||
|
|
|
@ -74,6 +74,7 @@ class TopLevelComment extends React.Component {
|
|||
const {
|
||||
author,
|
||||
canDelete,
|
||||
canDeleteWithoutConfirm,
|
||||
canReply,
|
||||
canReport,
|
||||
canRestore,
|
||||
|
@ -103,6 +104,7 @@ class TopLevelComment extends React.Component {
|
|||
content,
|
||||
datetimeCreated,
|
||||
canDelete,
|
||||
canDeleteWithoutConfirm,
|
||||
canReply,
|
||||
canReport,
|
||||
canRestore,
|
||||
|
@ -126,6 +128,7 @@ class TopLevelComment extends React.Component {
|
|||
<Comment
|
||||
author={reply.author}
|
||||
canDelete={canDelete}
|
||||
canDeleteWithoutConfirm={canDeleteWithoutConfirm}
|
||||
canReply={canReply}
|
||||
canReport={canReport}
|
||||
canRestore={canRestore && parentVisible}
|
||||
|
@ -168,6 +171,7 @@ TopLevelComment.propTypes = {
|
|||
username: PropTypes.string
|
||||
}),
|
||||
canDelete: PropTypes.bool,
|
||||
canDeleteWithoutConfirm: PropTypes.bool,
|
||||
canReply: PropTypes.bool,
|
||||
canReport: PropTypes.bool,
|
||||
canRestore: PropTypes.bool,
|
||||
|
@ -190,6 +194,7 @@ TopLevelComment.propTypes = {
|
|||
};
|
||||
|
||||
TopLevelComment.defaultProps = {
|
||||
canDeleteWithoutConfirm: false,
|
||||
defaultExpanded: false,
|
||||
moreRepliesToLoad: false
|
||||
};
|
||||
|
|
|
@ -599,6 +599,7 @@ const PreviewPresentation = ({
|
|||
<TopLevelComment
|
||||
author={comment.author}
|
||||
canDelete={canDeleteComments}
|
||||
canDeleteWithoutConfirm={isAdmin}
|
||||
canReply={isLoggedIn && projectInfo.comments_allowed && isShared}
|
||||
canReport={isLoggedIn}
|
||||
canRestore={canRestoreComments}
|
||||
|
|
34
test/unit/components/info-button.test.jsx
Normal file
34
test/unit/components/info-button.test.jsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||
import InfoButton from '../../../src/components/info-button/info-button';
|
||||
|
||||
describe('InfoButton', () => {
|
||||
test('Info button defaults to not visible', () => {
|
||||
const component = mountWithIntl(
|
||||
<InfoButton
|
||||
message="Here is some info about something!"
|
||||
/>
|
||||
);
|
||||
expect(component.find('div.info-button-message').exists()).toEqual(false);
|
||||
});
|
||||
test('clicking on info button makes info message visible', () => {
|
||||
const component = mountWithIntl(
|
||||
<InfoButton
|
||||
message="Here is some info about something!"
|
||||
/>
|
||||
);
|
||||
component.find('div.info-button').simulate('click');
|
||||
expect(component.find('div.info-button-message').exists()).toEqual(true);
|
||||
});
|
||||
test('after message is visible, mouseOut makes it vanish', () => {
|
||||
const component = mountWithIntl(
|
||||
<InfoButton
|
||||
message="Here is some info about something!"
|
||||
/>
|
||||
);
|
||||
component.find('div.info-button').simulate('click');
|
||||
expect(component.find('div.info-button-message').exists()).toEqual(true);
|
||||
component.find('div.info-button').simulate('mouseOut');
|
||||
expect(component.find('div.info-button-message').exists()).toEqual(false);
|
||||
});
|
||||
});
|
64
test/unit/components/join-flow-step.test.jsx
Normal file
64
test/unit/components/join-flow-step.test.jsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import React from 'react';
|
||||
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||
import JoinFlowStep from '../../../src/components/join-flow/join-flow-step';
|
||||
|
||||
describe('JoinFlowStep', () => {
|
||||
|
||||
test('components exist when props present', () => {
|
||||
const props = {
|
||||
children: null,
|
||||
className: 'join-flow-step-class',
|
||||
description: 'description text',
|
||||
headerImgSrc: '/image.png',
|
||||
onSubmit: jest.fn(),
|
||||
nextButton: 'some stuff',
|
||||
title: 'join flow step title',
|
||||
waiting: true
|
||||
};
|
||||
const component = mountWithIntl(
|
||||
<JoinFlowStep
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
expect(component.find('div.join-flow-header-image').exists()).toEqual(true);
|
||||
expect(component.find({src: props.headerImgSrc}).exists()).toEqual(true);
|
||||
expect(component.find('.join-flow-inner-content').exists()).toEqual(true);
|
||||
expect(component.find('.join-flow-title').exists()).toEqual(true);
|
||||
expect(component.find('.join-flow-title').first()
|
||||
.prop('title')).toEqual(props.title);
|
||||
expect(component.find('div.join-flow-description').exists()).toEqual(true);
|
||||
expect(component.find('div.join-flow-description').text()).toEqual(props.description);
|
||||
expect(component.find('NextStepButton').prop('waiting')).toEqual(true);
|
||||
expect(component.find('NextStepButton').prop('content')).toEqual(props.nextButton);
|
||||
|
||||
component.unmount();
|
||||
});
|
||||
|
||||
test('components do not exist when props not present', () => {
|
||||
const component = mountWithIntl(
|
||||
<JoinFlowStep />
|
||||
);
|
||||
|
||||
expect(component.find('div.join-flow-header-image').exists()).toEqual(false);
|
||||
expect(component.find('.join-flow-inner-content').exists()).toEqual(true);
|
||||
expect(component.find('.join-flow-title').exists()).toEqual(false);
|
||||
expect(component.find('div.join-flow-description').exists()).toEqual(false);
|
||||
expect(component.find('NextStepButton').prop('waiting')).toEqual(false);
|
||||
|
||||
component.unmount();
|
||||
});
|
||||
|
||||
test('clicking submit calls passed in function', () => {
|
||||
const props = {
|
||||
onSubmit: jest.fn()
|
||||
};
|
||||
const component = mountWithIntl(
|
||||
<JoinFlowStep
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
component.find('button[type="submit"]').simulate('submit');
|
||||
expect(props.onSubmit).toHaveBeenCalled();
|
||||
component.unmount();
|
||||
});
|
||||
});
|
|
@ -11,17 +11,17 @@ describe('unit test lib/validate.js', () => {
|
|||
response = validate.validateUsernameLocally('abc-def-ghi');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validateUsernameLocally('');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'});
|
||||
expect(response).toEqual({valid: false, errMsgId: 'general.required'});
|
||||
response = validate.validateUsernameLocally('ab');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameMinLength'});
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameMinLength'});
|
||||
response = validate.validateUsernameLocally('abcdefghijklmnopqrstu');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameMaxLength'});
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameMaxLength'});
|
||||
response = validate.validateUsernameLocally('abc def');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameRegexp'});
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameRegexp'});
|
||||
response = validate.validateUsernameLocally('abc!def');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameRegexp'});
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameRegexp'});
|
||||
response = validate.validateUsernameLocally('abc😄def');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameRegexp'});
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameRegexp'});
|
||||
});
|
||||
|
||||
test('validate password', () => {
|
||||
|
@ -34,11 +34,15 @@ describe('unit test lib/validate.js', () => {
|
|||
response = validate.validatePassword('passwo');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validatePassword('');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'});
|
||||
expect(response).toEqual({valid: false, errMsgId: 'general.required'});
|
||||
response = validate.validatePassword('abcde');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'});
|
||||
response = validate.validatePassword('password');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotEquals'});
|
||||
response = validate.validatePassword('abcdefg', 'abcdefg');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotUsername'});
|
||||
response = validate.validatePassword('abcdefg', 'abcdefG');
|
||||
expect(response).toEqual({valid: true});
|
||||
});
|
||||
|
||||
test('validate password confirm', () => {
|
||||
|
@ -51,12 +55,12 @@ describe('unit test lib/validate.js', () => {
|
|||
response = validate.validatePasswordConfirm('passwo', 'passwo');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validatePasswordConfirm('', '');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'});
|
||||
expect(response).toEqual({valid: false, errMsgId: 'general.required'});
|
||||
response = validate.validatePasswordConfirm('abcdef', 'abcdefg');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'general.error'});
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'});
|
||||
response = validate.validatePasswordConfirm('abcdef', '123456');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'general.error'});
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'});
|
||||
response = validate.validatePasswordConfirm('', 'abcdefg');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'general.error'});
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue