Merge pull request #3258 from LLK/release/2019-08-15

[Master] Release 2019-08-15
This commit is contained in:
Benjamin Wheeler 2019-08-15 10:51:19 -04:00 committed by GitHub
commit 6727cb133c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 791 additions and 105 deletions

91
package-lock.json generated
View file

@ -4035,6 +4035,12 @@
"minimalistic-crypto-utils": "^1.0.0" "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": { "emoji-regex": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
@ -5474,7 +5480,8 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -5495,12 +5502,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -5515,17 +5524,20 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -5642,7 +5654,8 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -5654,6 +5667,7 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -5668,6 +5682,7 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -5675,12 +5690,14 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -5699,6 +5716,7 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -5779,7 +5797,8 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -5791,6 +5810,7 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -5876,7 +5896,8 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -5912,6 +5933,7 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -5931,6 +5953,7 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -5974,12 +5997,14 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
} }
} }
}, },
@ -7501,12 +7526,6 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" "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": { "isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -9206,6 +9225,14 @@
"isemail": "1.x.x", "isemail": "1.x.x",
"moment": "2.x.x", "moment": "2.x.x",
"topo": "1.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": { "jquery": {
@ -14929,15 +14956,15 @@
} }
}, },
"scratch-gui": { "scratch-gui": {
"version": "0.1.0-prerelease.20190808151251", "version": "0.1.0-prerelease.20190814215530",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20190808151251.tgz", "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20190814215530.tgz",
"integrity": "sha512-KBYxva8dWl/XrWTPePveV8oq2RUkhyLoDFQnz2pJSBJOKvkY1SPVCABpc4ocLJ9nOBAtsWOCSJVFBAu24aZD6A==", "integrity": "sha512-6xCCphwTYaBXjlQ7CJjmAnrQOoTLke/da2JAdrSWmGM0WxE+kFAcNpr2DBi7wzPIPTbNuy1nw+ECLb3Bst9LEQ==",
"dev": true "dev": true
}, },
"scratch-l10n": { "scratch-l10n": {
"version": "3.5.20190807144510", "version": "3.5.20190813223429",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.5.20190807144510.tgz", "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.5.20190813223429.tgz",
"integrity": "sha512-rq3G4NZvlFvb0bQFwsqK+anb7S74OSutxqabmaP5R4NRbmkpz0PvbT1zMGKEmEScY+8h5VLkNO6im9RoDsSm7g==", "integrity": "sha512-rSxUSwv0RgZTXUknAWuc7BFZWewiNhrgyPUMos/qAw4GgVMdY1ZRSIHBEIItpCXXYLOzw4ObcNafIim6Taq9NA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/cli": "^7.1.2", "@babel/cli": "^7.1.2",
@ -14998,13 +15025,15 @@
"version": "0.3.2", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
"dev": true "dev": true,
"optional": true
}, },
"braces": { "braces": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
"integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"arr-flatten": "^1.1.0", "arr-flatten": "^1.1.0",
"array-unique": "^0.3.2", "array-unique": "^0.3.2",
@ -15023,6 +15052,7 @@
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"is-extendable": "^0.1.0" "is-extendable": "^0.1.0"
} }
@ -15205,6 +15235,7 @@
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
"integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"extend-shallow": "^2.0.1", "extend-shallow": "^2.0.1",
"is-number": "^3.0.0", "is-number": "^3.0.0",
@ -15217,6 +15248,7 @@
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"is-extendable": "^0.1.0" "is-extendable": "^0.1.0"
} }
@ -15296,7 +15328,8 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true "dev": true,
"optional": true
}, },
"is-glob": { "is-glob": {
"version": "4.0.1", "version": "4.0.1",
@ -15313,6 +15346,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"kind-of": "^3.0.2" "kind-of": "^3.0.2"
}, },
@ -15322,6 +15356,7 @@
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"is-buffer": "^1.1.5" "is-buffer": "^1.1.5"
} }
@ -15332,13 +15367,15 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true "dev": true,
"optional": true
}, },
"kind-of": { "kind-of": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
"dev": true "dev": true,
"optional": true
}, },
"micromatch": { "micromatch": {
"version": "3.1.10", "version": "3.1.10",

View file

@ -16,7 +16,7 @@
"test:unit:jest": "jest ./test/unit/ && jest ./test/localization/*.test.js", "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: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", "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", "clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl",
"deploy": "npm run deploy:s3 && npm run deploy:fastly", "deploy": "npm run deploy:s3 && npm run deploy:fastly",
"deploy:fastly": "node ./bin/configure-fastly.js", "deploy:fastly": "node ./bin/configure-fastly.js",
@ -72,6 +72,7 @@
"copy-webpack-plugin": "0.2.0", "copy-webpack-plugin": "0.2.0",
"create-react-class": "15.6.2", "create-react-class": "15.6.2",
"css-loader": "0.23.1", "css-loader": "0.23.1",
"email-validator": "2.0.4",
"enzyme": "3.10.0", "enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.14.0", "enzyme-adapter-react-16": "1.14.0",
"eslint": "5.16.0", "eslint": "5.16.0",
@ -123,7 +124,7 @@
"redux": "3.5.2", "redux": "3.5.2",
"redux-thunk": "2.0.1", "redux-thunk": "2.0.1",
"sass-loader": "6.0.6", "sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20190808151251", "scratch-gui": "0.1.0-prerelease.20190814215530",
"scratch-l10n": "latest", "scratch-l10n": "latest",
"selenium-webdriver": "3.6.0", "selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0", "slick-carousel": "1.6.0",

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

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

View file

@ -0,0 +1,3 @@
.formik-label {
font-weight: 500;
}

View file

@ -5,8 +5,8 @@ import {Field} from 'formik';
const ValidationMessage = require('../forms/validation-message.jsx'); const ValidationMessage = require('../forms/validation-message.jsx');
require('../forms/input.scss');
require('../forms/row.scss'); require('../forms/row.scss');
require('./formik-input.scss');
const FormikInput = ({ const FormikInput = ({
className, className,
@ -26,6 +26,7 @@ const FormikInput = ({
<Field <Field
className={classNames( className={classNames(
'input', 'input',
{fail: error},
className className
)} )}
{...props} {...props}

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

View file

@ -5,6 +5,7 @@ import {Field} from 'formik';
const FormikInput = require('./formik-input.jsx'); const FormikInput = require('./formik-input.jsx');
require('./formik-forms.scss');
require('./formik-radio-button.scss'); require('./formik-radio-button.scss');
require('../forms/row.scss'); require('../forms/row.scss');
@ -34,6 +35,7 @@ const FormikRadioButtonSubComponent = ({
{label && ( {label && (
<label <label
className={classNames( className={classNames(
'formik-label',
'formik-radio-label', 'formik-radio-label',
labelClassName labelClassName
)} )}

View file

@ -1,7 +1,6 @@
@import "../../colors"; @import "../../colors";
.formik-radio-label { .formik-radio-label {
font-weight: 300;
margin-left: 1rem; margin-left: 1rem;
} }

View file

@ -17,6 +17,7 @@ const FormikSelect = ({
}) => { }) => {
const optionsList = options.map((item, index) => ( const optionsList = options.map((item, index) => (
<option <option
disabled={item.disabled}
key={index} key={index}
value={item.value} value={item.value}
> >

View file

@ -0,0 +1,7 @@
@import "../../colors";
@import "../../frameless";
.input::placeholder {
font-style: italic;
color: $type-gray-75percent;
}

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

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

View file

@ -53,7 +53,7 @@ class BirthDateStep extends React.Component {
} }
validateSelect (selection) { validateSelect (selection) {
if (selection === 'null') { if (selection === 'null') {
return this.props.intl.formatMessage({id: 'form.validationRequired'}); return this.props.intl.formatMessage({id: 'general.required'});
} }
return null; return null;
} }
@ -88,6 +88,7 @@ class BirthDateStep extends React.Component {
<JoinFlowStep <JoinFlowStep
description={this.props.intl.formatMessage({id: 'registration.private'})} description={this.props.intl.formatMessage({id: 'registration.private'})}
headerImgSrc="/images/hoc/getting-started.jpg" headerImgSrc="/images/hoc/getting-started.jpg"
infoMessage={this.props.intl.formatMessage({id: 'registration.birthDateStepInfo'})}
title={this.props.intl.formatMessage({id: 'registration.birthDateStepTitle'})} title={this.props.intl.formatMessage({id: 'registration.birthDateStepTitle'})}
waiting={isSubmitting} waiting={isSubmitting}
onSubmit={handleSubmit} onSubmit={handleSubmit}

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

View file

@ -1,10 +1,14 @@
const bindAll = require('lodash.bindall'); const bindAll = require('lodash.bindall');
const classNames = require('classnames');
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
import {Formik} from 'formik'; import {Formik} from 'formik';
const {injectIntl, intlShape} = require('react-intl'); 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 JoinFlowStep = require('./join-flow-step.jsx');
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
require('./join-flow-steps.scss'); require('./join-flow-steps.scss');
@ -13,9 +17,18 @@ class EmailStep extends React.Component {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'handleValidSubmit', 'handleValidSubmit',
'validateEmailIfPresent',
'validateForm' '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 () { validateForm () {
return {}; return {};
} }
@ -35,17 +48,52 @@ class EmailStep extends React.Component {
> >
{props => { {props => {
const { const {
errors,
handleSubmit, handleSubmit,
isSubmitting isSubmitting,
validateField
} = props; } = props;
return ( return (
<JoinFlowStep <JoinFlowStep
description={this.props.intl.formatMessage({id: 'registration.emailStepDescription'})} 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" 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'})} title={this.props.intl.formatMessage({id: 'registration.emailStepTitle'})}
waiting={isSubmitting} waiting={isSubmitting}
onSubmit={handleSubmit} 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> </Formik>
@ -58,4 +106,5 @@ EmailStep.propTypes = {
onNextStep: PropTypes.func onNextStep: PropTypes.func
}; };
module.exports = injectIntl(EmailStep); module.exports = injectIntl(EmailStep);

View file

@ -82,6 +82,7 @@ class GenderStep extends React.Component {
<JoinFlowStep <JoinFlowStep
className="join-flow-gender-step" className="join-flow-gender-step"
description={this.props.intl.formatMessage({id: 'registration.genderStepDescription'})} description={this.props.intl.formatMessage({id: 'registration.genderStepDescription'})}
infoMessage={this.props.intl.formatMessage({id: 'registration.genderStepInfo'})}
title={this.props.intl.formatMessage({id: 'registration.genderStepTitle'})} title={this.props.intl.formatMessage({id: 'registration.genderStepTitle'})}
waiting={isSubmitting} waiting={isSubmitting}
onSubmit={handleSubmit} onSubmit={handleSubmit}

View file

@ -5,6 +5,7 @@ const PropTypes = require('prop-types');
const NextStepButton = require('./next-step-button.jsx'); const NextStepButton = require('./next-step-button.jsx');
const ModalTitle = require('../modal/base/modal-title.jsx'); const ModalTitle = require('../modal/base/modal-title.jsx');
const ModalInnerContent = require('../modal/base/modal-inner-content.jsx'); const ModalInnerContent = require('../modal/base/modal-inner-content.jsx');
const InfoButton = require('../info-button/info-button.jsx');
require('./join-flow-step.scss'); require('./join-flow-step.scss');
@ -12,7 +13,10 @@ const JoinFlowStep = ({
children, children,
className, className,
description, description,
footerContent,
headerImgSrc, headerImgSrc,
infoMessage,
innerContentClassName,
nextButton, nextButton,
onSubmit, onSubmit,
title, title,
@ -28,7 +32,8 @@ const JoinFlowStep = ({
<ModalInnerContent <ModalInnerContent
className={classNames( className={classNames(
'join-flow-inner-content', 'join-flow-inner-content',
className className,
innerContentClassName
)} )}
> >
{title && ( {title && (
@ -40,11 +45,19 @@ const JoinFlowStep = ({
{description && ( {description && (
<div className="join-flow-description"> <div className="join-flow-description">
{description} {description}
{infoMessage && (
<InfoButton message={infoMessage} />
)}
</div> </div>
)} )}
{children} {children}
</ModalInnerContent> </ModalInnerContent>
</div> </div>
{footerContent && (
<div className="join-flow-footer-message">
{footerContent}
</div>
)}
<NextStepButton <NextStepButton
content={nextButton} content={nextButton}
waiting={waiting} waiting={waiting}
@ -56,7 +69,10 @@ JoinFlowStep.propTypes = {
children: PropTypes.node, children: PropTypes.node,
className: PropTypes.string, className: PropTypes.string,
description: PropTypes.string, description: PropTypes.string,
footerContent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
headerImgSrc: PropTypes.string, headerImgSrc: PropTypes.string,
infoMessage: PropTypes.string,
innerContentClassName: PropTypes.string,
nextButton: PropTypes.node, nextButton: PropTypes.node,
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
title: PropTypes.string, title: PropTypes.string,

View file

@ -33,3 +33,13 @@
border-top-left-radius: 1rem; border-top-left-radius: 1rem;
border-top-right-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;
}

View file

@ -13,6 +13,14 @@
} }
} }
.join-flow-password-confirm {
margin-bottom: .6875rem;
}
.join-flow-input-tall {
height: 3rem;
}
.join-flow-input-title { .join-flow-input-title {
font-weight: bold; font-weight: bold;
margin-bottom: .5rem; margin-bottom: .5rem;
@ -47,12 +55,14 @@
} }
} }
.select .join-flow-select { .select .join-flow-select-month {
width: 9.125rem; width: 9.125rem;
margin-right: .5rem;
} }
.join-flow-select-month { .select .join-flow-select-country {
margin-right: .5rem; width: 100%;
margin: 0 auto;
} }
.join-flow-password-section { .join-flow-password-section {
@ -91,3 +101,11 @@
height: 2rem; height: 2rem;
margin-left: .5rem; 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;
}

View file

@ -10,6 +10,7 @@ const Progression = require('../progression/progression.jsx');
const UsernameStep = require('./username-step.jsx'); const UsernameStep = require('./username-step.jsx');
const BirthDateStep = require('./birthdate-step.jsx'); const BirthDateStep = require('./birthdate-step.jsx');
const GenderStep = require('./gender-step.jsx'); const GenderStep = require('./gender-step.jsx');
const CountryStep = require('./country-step.jsx');
const EmailStep = require('./email-step.jsx'); const EmailStep = require('./email-step.jsx');
const WelcomeStep = require('./welcome-step.jsx'); const WelcomeStep = require('./welcome-step.jsx');
@ -42,6 +43,7 @@ class JoinFlow extends React.Component {
<UsernameStep onNextStep={this.handleAdvanceStep} /> <UsernameStep onNextStep={this.handleAdvanceStep} />
<BirthDateStep onNextStep={this.handleAdvanceStep} /> <BirthDateStep onNextStep={this.handleAdvanceStep} />
<GenderStep onNextStep={this.handleAdvanceStep} /> <GenderStep onNextStep={this.handleAdvanceStep} />
<CountryStep onNextStep={this.handleAdvanceStep} />
<EmailStep onNextStep={this.handleAdvanceStep} /> <EmailStep onNextStep={this.handleAdvanceStep} />
<WelcomeStep <WelcomeStep
email={this.state.formData.email} email={this.state.formData.email}

View file

@ -7,6 +7,7 @@ const {injectIntl, intlShape} = require('react-intl');
const validate = require('../../lib/validate'); const validate = require('../../lib/validate');
const FormikInput = require('../../components/formik-forms/formik-input.jsx'); 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'); const JoinFlowStep = require('./join-flow-step.jsx');
require('./join-flow-steps.scss'); require('./join-flow-steps.scss');
@ -26,9 +27,6 @@ class UsernameStep extends React.Component {
'validateUsernameIfPresent', 'validateUsernameIfPresent',
'validateForm' 'validateForm'
]); ]);
this.state = {
showPassword: false
};
} }
handleChangeShowPassword () { handleChangeShowPassword () {
this.setState({showPassword: !this.state.showPassword}); 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 // we allow username to be empty on blur, since you might not have typed anything yet
validateUsernameIfPresent (username) { validateUsernameIfPresent (username) {
if (!username) return null; // skip validation if username is blank; null indicates valid 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); const localResult = validate.validateUsernameLocally(username);
if (localResult.valid) { return validate.validateUsernameRemotely(username).then(
return validate.validateUsernameRemotely(username).then( remoteResult => {
remoteResult => { // there may be multiple validation errors. Prioritize vulgarity, then
if (remoteResult.valid) return null; // 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 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 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; if (localResult.valid) return null;
return this.props.intl.formatMessage({id: localResult.errMsgId}); return this.props.intl.formatMessage({id: localResult.errMsgId});
} }
@ -69,13 +73,10 @@ class UsernameStep extends React.Component {
if (!usernameResult.valid) { if (!usernameResult.valid) {
errors.username = this.props.intl.formatMessage({id: usernameResult.errMsgId}); 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) { if (!passwordResult.valid) {
errors.password = this.props.intl.formatMessage({id: passwordResult.errMsgId}); 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); const passwordConfirmResult = validate.validatePasswordConfirm(values.password, values.passwordConfirm);
if (!passwordConfirmResult.valid) { if (!passwordConfirmResult.valid) {
errors.passwordConfirm = this.props.intl.formatMessage({id: passwordConfirmResult.errMsgId}); 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 // called after all validations pass with no errors
handleValidSubmit (formData, formikBag) { handleValidSubmit (formData, formikBag) {
formikBag.setSubmitting(false); // formik makes us do this ourselves formikBag.setSubmitting(false); // formik makes us do this ourselves
delete formData.showPassword;
this.props.onNextStep(formData); this.props.onNextStep(formData);
} }
render () { render () {
@ -93,7 +95,8 @@ class UsernameStep extends React.Component {
initialValues={{ initialValues={{
username: '', username: '',
password: '', password: '',
passwordConfirm: '' passwordConfirm: '',
showPassword: false
}} }}
validate={this.validateForm} validate={this.validateForm}
validateOnBlur={false} validateOnBlur={false}
@ -105,6 +108,8 @@ class UsernameStep extends React.Component {
errors, errors,
handleSubmit, handleSubmit,
isSubmitting, isSubmitting,
setFieldError,
setFieldValue,
validateField, validateField,
values values
} = props; } = props;
@ -123,15 +128,20 @@ class UsernameStep extends React.Component {
</div> </div>
<FormikInput <FormikInput
className={classNames( className={classNames(
'join-flow-input', 'join-flow-input'
{fail: errors.username}
)} )}
error={errors.username} error={errors.username}
id="username" id="username"
name="username" name="username"
validate={this.validateUsernameIfPresent} validate={this.validateUsernameIfPresent}
validationClassName="validation-full-width-input" 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-password-section">
<div className="join-flow-input-title"> <div className="join-flow-input-title">
@ -139,28 +149,32 @@ class UsernameStep extends React.Component {
</div> </div>
<FormikInput <FormikInput
className={classNames( className={classNames(
'join-flow-input', 'join-flow-input'
{fail: errors.password}
)} )}
error={errors.password} error={errors.password}
id="password" id="password"
name="password" name="password"
type={this.state.showPassword ? 'text' : 'password'} type={values.showPassword ? 'text' : 'password'}
validate={this.validatePasswordIfPresent}
validationClassName="validation-full-width-input"
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
validate={password => this.validatePasswordIfPresent(password, values.username)}
validationClassName="validation-full-width-input"
onBlur={() => validateField('password')} onBlur={() => validateField('password')}
onChange={e => {
setFieldValue('password', e.target.value);
setFieldError('password', null);
}}
/* eslint-enable react/jsx-no-bind */ /* eslint-enable react/jsx-no-bind */
/> />
<FormikInput <FormikInput
className={classNames( className={classNames(
'join-flow-input', 'join-flow-input',
'join-flow-password-confirm',
{fail: errors.passwordConfirm} {fail: errors.passwordConfirm}
)} )}
error={errors.passwordConfirm} error={errors.passwordConfirm}
id="passwordConfirm" id="passwordConfirm"
name="passwordConfirm" name="passwordConfirm"
type={this.state.showPassword ? 'text' : 'password'} type={values.showPassword ? 'text' : 'password'}
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
validate={() => validate={() =>
this.validatePasswordConfirmIfPresent(values.password, this.validatePasswordConfirmIfPresent(values.password,
@ -170,17 +184,18 @@ class UsernameStep extends React.Component {
onBlur={() => onBlur={() =>
validateField('passwordConfirm') validateField('passwordConfirm')
} }
onChange={e => {
setFieldValue('passwordConfirm', e.target.value);
setFieldError('passwordConfirm', null);
}}
/* eslint-enable react/jsx-no-bind */ /* eslint-enable react/jsx-no-bind */
/> />
<div className="join-flow-input-title"> <div className="join-flow-input-title">
<div <FormikCheckbox
onClick={this.handleChangeShowPassword} id="showPassword"
> label={this.props.intl.formatMessage({id: 'registration.showPassword'})}
{/* TODO: should localize 'Hide password' if we use that */} name="showPassword"
{this.state.showPassword ? 'Hide password' : ( />
this.props.intl.formatMessage({id: 'registration.showPassword'})
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -139,7 +139,7 @@ class UsernameStep extends React.Component {
default: default:
this.form.formsy.updateInputsWithError({ this.form.formsy.updateInputsWithError({
'user.username': this.props.intl.formatMessage({ 'user.username': this.props.intl.formatMessage({
id: 'registration.validationUsernameInvalid' id: 'registration.validationUsernameNotAllowed'
}) })
}); });
return callback(false); return callback(false);

View file

@ -2,6 +2,7 @@
"general.accountSettings": "Account settings", "general.accountSettings": "Account settings",
"general.about": "About", "general.about": "About",
"general.aboutScratch": "About Scratch", "general.aboutScratch": "About Scratch",
"general.apiError": "Whoops, Scratch had an error.",
"general.back": "Back", "general.back": "Back",
"general.birthMonth": "Birth Month", "general.birthMonth": "Birth Month",
"general.birthYear": "Birth Year", "general.birthYear": "Birth Year",
@ -20,7 +21,7 @@
"general.create": "Create", "general.create": "Create",
"general.credits": "Credits", "general.credits": "Credits",
"general.dmca": "DMCA", "general.dmca": "DMCA",
"general.emailAddress": "Email Address", "general.emailAddress": "Email address",
"general.english": "English", "general.english": "English",
"general.error": "Oops! Something went wrong", "general.error": "Oops! Something went wrong",
"general.errorIdentifier": "Your error was logged with id {errorId}", "general.errorIdentifier": "Your error was logged with id {errorId}",
@ -70,6 +71,7 @@
"general.privacyPolicy": "Privacy Policy", "general.privacyPolicy": "Privacy Policy",
"general.projects": "Projects", "general.projects": "Projects",
"general.profile": "Profile", "general.profile": "Profile",
"general.required": "Required",
"general.resourcesTitle": "Educator Resources", "general.resourcesTitle": "Educator Resources",
"general.scratchConference": "Scratch Conference", "general.scratchConference": "Scratch Conference",
"general.scratchEd": "ScratchEd", "general.scratchEd": "ScratchEd",
@ -141,6 +143,7 @@
"parents.FaqResourcesQ": "What resources are available for learning Scratch?", "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.", "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.birthDateStepTitle": "When were you born?",
"registration.checkOutResources": "Get Started with Resources", "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>.", "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.choosePasswordStepTitle": "Create a password",
"registration.choosePasswordStepTooltip": "Don't use your name or anything that's easy for someone else to guess.", "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.classroomApiGeneralError": "Sorry, we could not find the registration information for this class",
"registration.countryStepDescription": "Well display your country on your profile.",
"registration.countryStepTitle": "What country do you live in?",
"registration.generalError": "Sorry, an unexpected error occurred.", "registration.generalError": "Sorry, an unexpected error occurred.",
"registration.classroomInviteExistingStudentStepDescription": "you have been invited to join the class:", "registration.classroomInviteExistingStudentStepDescription": "you have been invited to join the class:",
"registration.classroomInviteNewStudentStepDescription": "Your teacher has invited you to join a class:", "registration.classroomInviteNewStudentStepDescription": "Your teacher has invited you to join a class:",
"registration.confirmYourEmail": "Confirm Your Email", "registration.confirmYourEmail": "Confirm Your Email",
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:", "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.createUsername": "Create a username",
"registration.genderStepTitle": "What's your gender?", "registration.genderStepTitle": "What's your gender?",
"registration.genderStepDescription": "Scratch welcomes people of all genders. We will always keep this information private.", "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.genderOptionAnother": "Another gender:",
"registration.genderOptionPreferNotToSay": "Prefer not to say", "registration.genderOptionPreferNotToSay": "Prefer not to say",
"registration.emailStepTitle": "What's your email?", "registration.emailStepTitle": "What's your email?",
@ -184,18 +191,22 @@
"registration.studentUsernameStepHelpText": "Already have a Scratch account?", "registration.studentUsernameStepHelpText": "Already have a Scratch account?",
"registration.studentUsernameStepTooltip": "You'll need to create a new Scratch account to join this class.", "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.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.usernameStepTitle": "Request a Teacher Account",
"registration.usernameStepTitleScratcher": "Create a Scratch Account", "registration.usernameStepTitleScratcher": "Create a Scratch Account",
"registration.validationMaxLength": "Sorry, you have exceeded the maximum character limit.", "registration.validationMaxLength": "Sorry, you have exceeded the maximum character limit.",
"registration.validationPasswordLength": "Passwords must be at least six characters", "registration.validationPasswordConfirmNotEquals": "Passwords dont match",
"registration.validationPasswordNotEquals": "Your password may not be \"password\"", "registration.validationPasswordLength": "Must be 6 letters or longer",
"registration.validationPasswordNotUsername": "Your password may not be your username", "registration.validationPasswordNotEquals": "Password is too easy to guess. Try something else?",
"registration.validationUsernameRegexp": "Your username may only contain letters, numbers, \"-\", and \"_\"", "registration.validationPasswordNotUsername": "Password cant match your username",
"registration.validationUsernameMinLength": "Usernames must be at least 3 characters", "registration.validationUsernameRegexp": "Usernames can only use letters, numbers, - and _",
"registration.validationUsernameMaxLength": "Usernames must be at most 20 characters", "registration.validationUsernameMinLength": "Must be 3 letters or longer",
"registration.validationUsernameExists": "Sorry, that username already exists", "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.validationUsernameVulgar": "Hmm, that looks inappropriate",
"registration.validationUsernameInvalid": "Invalid username", "registration.validationUsernameInvalid": "Invalid username",
"registration.validationEmailInvalid": "Email doesnt look right. Try another?",
"registration.waitForApproval": "Wait for Approval", "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.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:", "registration.welcomeStepDescription": "You have successfully set up a Scratch account! You are now a member of the class:",

View file

@ -3,13 +3,13 @@ const api = require('./api');
module.exports.validateUsernameLocally = username => { module.exports.validateUsernameLocally = username => {
if (!username || username === '') { if (!username || username === '') {
return {valid: false, errMsgId: 'form.validationRequired'}; return {valid: false, errMsgId: 'general.required'};
} else if (username.length < 3) { } else if (username.length < 3) {
return {valid: false, errMsgId: 'form.validationUsernameMinLength'}; return {valid: false, errMsgId: 'registration.validationUsernameMinLength'};
} else if (username.length > 20) { } else if (username.length > 20) {
return {valid: false, errMsgId: 'form.validationUsernameMaxLength'}; return {valid: false, errMsgId: 'registration.validationUsernameMaxLength'};
} else if (!/^[\w-]+$/i.test(username)) { } else if (!/^[\w-]+$/i.test(username)) {
return {valid: false, errMsgId: 'form.validationUsernameRegexp'}; return {valid: false, errMsgId: 'registration.validationUsernameRegexp'};
} }
return {valid: true}; return {valid: true};
}; };
@ -29,34 +29,41 @@ module.exports.validateUsernameRemotely = username => (
case 'username exists': case 'username exists':
resolve({valid: false, errMsgId: 'registration.validationUsernameExists'}); resolve({valid: false, errMsgId: 'registration.validationUsernameExists'});
break; break;
case 'bad username': case 'bad username': // i.e., vulgar
resolve({valid: false, errMsgId: 'registration.validationUsernameVulgar'}); resolve({valid: false, errMsgId: 'registration.validationUsernameNotAllowed'});
break; break;
case 'invalid username': case 'invalid username':
default: 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) { if (!password) {
return {valid: false, errMsgId: 'form.validationRequired'}; return {valid: false, errMsgId: 'general.required'};
} else if (password.length < 6) { } else if (password.length < 6) {
return {valid: false, errMsgId: 'registration.validationPasswordLength'}; return {valid: false, errMsgId: 'registration.validationPasswordLength'};
} else if (password === 'password') { } else if (password === 'password') {
return {valid: false, errMsgId: 'registration.validationPasswordNotEquals'}; return {valid: false, errMsgId: 'registration.validationPasswordNotEquals'};
} else if (username && password === username) {
return {valid: false, errMsgId: 'registration.validationPasswordNotUsername'};
} }
return {valid: true}; return {valid: true};
}; };
module.exports.validatePasswordConfirm = (password, passwordConfirm) => { module.exports.validatePasswordConfirm = (password, passwordConfirm) => {
if (!passwordConfirm) { if (!passwordConfirm) {
return {valid: false, errMsgId: 'form.validationRequired'}; return {valid: false, errMsgId: 'general.required'};
} else if (password !== passwordConfirm) { } else if (password !== passwordConfirm) {
// TODO: add a new string for this case return {valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'};
return {valid: false, errMsgId: 'general.error'};
} }
return {valid: true}; return {valid: true};
}; };

View file

@ -54,7 +54,11 @@ class Comment extends React.Component {
} }
handleDelete () { handleDelete () {
this.setState({deleting: true}); if (this.props.canDeleteWithoutConfirm) {
this.props.onDelete(this.props.id);
} else {
this.setState({deleting: true});
}
} }
handleConfirmDelete () { handleConfirmDelete () {
@ -267,6 +271,7 @@ Comment.propTypes = {
username: PropTypes.string username: PropTypes.string
}), }),
canDelete: PropTypes.bool, canDelete: PropTypes.bool,
canDeleteWithoutConfirm: PropTypes.bool,
canReply: PropTypes.bool, canReply: PropTypes.bool,
canReport: PropTypes.bool, canReport: PropTypes.bool,
canRestore: PropTypes.bool, canRestore: PropTypes.bool,

View file

@ -83,6 +83,13 @@
width: calc(100% + 1rem); width: calc(100% + 1rem);
height: 100%; height: 100%;
content: ""; 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 { .comment-top-row {

View file

@ -74,6 +74,7 @@ class TopLevelComment extends React.Component {
const { const {
author, author,
canDelete, canDelete,
canDeleteWithoutConfirm,
canReply, canReply,
canReport, canReport,
canRestore, canRestore,
@ -103,6 +104,7 @@ class TopLevelComment extends React.Component {
content, content,
datetimeCreated, datetimeCreated,
canDelete, canDelete,
canDeleteWithoutConfirm,
canReply, canReply,
canReport, canReport,
canRestore, canRestore,
@ -126,6 +128,7 @@ class TopLevelComment extends React.Component {
<Comment <Comment
author={reply.author} author={reply.author}
canDelete={canDelete} canDelete={canDelete}
canDeleteWithoutConfirm={canDeleteWithoutConfirm}
canReply={canReply} canReply={canReply}
canReport={canReport} canReport={canReport}
canRestore={canRestore && parentVisible} canRestore={canRestore && parentVisible}
@ -168,6 +171,7 @@ TopLevelComment.propTypes = {
username: PropTypes.string username: PropTypes.string
}), }),
canDelete: PropTypes.bool, canDelete: PropTypes.bool,
canDeleteWithoutConfirm: PropTypes.bool,
canReply: PropTypes.bool, canReply: PropTypes.bool,
canReport: PropTypes.bool, canReport: PropTypes.bool,
canRestore: PropTypes.bool, canRestore: PropTypes.bool,
@ -190,6 +194,7 @@ TopLevelComment.propTypes = {
}; };
TopLevelComment.defaultProps = { TopLevelComment.defaultProps = {
canDeleteWithoutConfirm: false,
defaultExpanded: false, defaultExpanded: false,
moreRepliesToLoad: false moreRepliesToLoad: false
}; };

View file

@ -599,6 +599,7 @@ const PreviewPresentation = ({
<TopLevelComment <TopLevelComment
author={comment.author} author={comment.author}
canDelete={canDeleteComments} canDelete={canDeleteComments}
canDeleteWithoutConfirm={isAdmin}
canReply={isLoggedIn && projectInfo.comments_allowed && isShared} canReply={isLoggedIn && projectInfo.comments_allowed && isShared}
canReport={isLoggedIn} canReport={isLoggedIn}
canRestore={canRestoreComments} canRestore={canRestoreComments}

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

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

View file

@ -11,17 +11,17 @@ describe('unit test lib/validate.js', () => {
response = validate.validateUsernameLocally('abc-def-ghi'); response = validate.validateUsernameLocally('abc-def-ghi');
expect(response).toEqual({valid: true}); expect(response).toEqual({valid: true});
response = validate.validateUsernameLocally(''); response = validate.validateUsernameLocally('');
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'}); expect(response).toEqual({valid: false, errMsgId: 'general.required'});
response = validate.validateUsernameLocally('ab'); response = validate.validateUsernameLocally('ab');
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameMinLength'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameMinLength'});
response = validate.validateUsernameLocally('abcdefghijklmnopqrstu'); 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'); 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'); 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'); response = validate.validateUsernameLocally('abc😄def');
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameRegexp'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameRegexp'});
}); });
test('validate password', () => { test('validate password', () => {
@ -34,11 +34,15 @@ describe('unit test lib/validate.js', () => {
response = validate.validatePassword('passwo'); response = validate.validatePassword('passwo');
expect(response).toEqual({valid: true}); expect(response).toEqual({valid: true});
response = validate.validatePassword(''); response = validate.validatePassword('');
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'}); expect(response).toEqual({valid: false, errMsgId: 'general.required'});
response = validate.validatePassword('abcde'); response = validate.validatePassword('abcde');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'});
response = validate.validatePassword('password'); response = validate.validatePassword('password');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotEquals'}); 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', () => { test('validate password confirm', () => {
@ -51,12 +55,12 @@ describe('unit test lib/validate.js', () => {
response = validate.validatePasswordConfirm('passwo', 'passwo'); response = validate.validatePasswordConfirm('passwo', 'passwo');
expect(response).toEqual({valid: true}); expect(response).toEqual({valid: true});
response = validate.validatePasswordConfirm('', ''); response = validate.validatePasswordConfirm('', '');
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'}); expect(response).toEqual({valid: false, errMsgId: 'general.required'});
response = validate.validatePasswordConfirm('abcdef', 'abcdefg'); 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'); 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'); response = validate.validatePasswordConfirm('', 'abcdefg');
expect(response).toEqual({valid: false, errMsgId: 'general.error'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'});
}); });
}); });