mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 01:25:52 -05:00
Merge pull request #3225 from LLK/develop
Develop → Merge → Release 2019-08-07
This commit is contained in:
commit
026292ad83
27 changed files with 5638 additions and 3615 deletions
3
.babelrc
3
.babelrc
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
"transform-object-rest-spread",
|
||||
"transform-require-context"
|
||||
],
|
||||
"presets": ["es2015", "react"],
|
||||
}
|
||||
|
|
|
@ -8,8 +8,6 @@ var routeJson = require('../src/routes.json');
|
|||
const FASTLY_SERVICE_ID = process.env.FASTLY_SERVICE_ID || '';
|
||||
const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME || '';
|
||||
|
||||
const FASTLY_CONCURRENCY = process.env.FASTLY_CONCURRENCY || 1;
|
||||
|
||||
var fastly = require('./lib/fastly-extended')(process.env.FASTLY_API_KEY, FASTLY_SERVICE_ID);
|
||||
|
||||
var extraAppRoutes = [
|
||||
|
@ -94,7 +92,7 @@ async.auto({
|
|||
}],
|
||||
appRouteRequestConditions: ['version', function (results, cb) {
|
||||
var conditions = {};
|
||||
async.eachOfLimit(routes, FASTLY_CONCURRENCY, function (route, id, cb2) {
|
||||
async.forEachOf(routes, function (route, id, cb2) {
|
||||
var condition = {
|
||||
name: fastlyConfig.getConditionNameForRoute(route, 'request'),
|
||||
statement: 'req.url ~ "' + route.pattern + '"',
|
||||
|
@ -114,7 +112,7 @@ async.auto({
|
|||
}],
|
||||
appRouteHeaders: ['version', 'appRouteRequestConditions', function (results, cb) {
|
||||
var headers = {};
|
||||
async.eachOfLimit(routes, FASTLY_CONCURRENCY, function (route, id, cb2) {
|
||||
async.forEachOf(routes, function (route, id, cb2) {
|
||||
if (route.redirect) {
|
||||
async.auto({
|
||||
responseCondition: function (cb3) {
|
||||
|
@ -147,7 +145,7 @@ async.auto({
|
|||
};
|
||||
fastly.setFastlyHeader(results.version, header, cb3);
|
||||
}]
|
||||
}, FASTLY_CONCURRENCY, function (err, redirectResults) {
|
||||
}, function (err, redirectResults) {
|
||||
if (err) return cb2(err);
|
||||
headers[id] = redirectResults.redirectHeader;
|
||||
cb2(null, redirectResults);
|
||||
|
@ -215,7 +213,7 @@ async.auto({
|
|||
};
|
||||
fastly.setFastlyHeader(results.version, header, cb2);
|
||||
}]
|
||||
}, FASTLY_CONCURRENCY, function (err, redirectResults) {
|
||||
}, function (err, redirectResults) {
|
||||
if (err) return cb(err);
|
||||
cb(null, redirectResults);
|
||||
});
|
||||
|
@ -261,12 +259,12 @@ async.auto({
|
|||
};
|
||||
fastly.setFastlyHeader(results.version, header, cb2);
|
||||
}]
|
||||
}, FASTLY_CONCURRENCY, function (err, redirectResults) {
|
||||
}, function (err, redirectResults) {
|
||||
if (err) return cb(err);
|
||||
cb(null, redirectResults);
|
||||
});
|
||||
}]
|
||||
}, FASTLY_CONCURRENCY, function (err, results) {
|
||||
}, function (err, results) {
|
||||
if (err) throw new Error(err);
|
||||
if (process.env.FASTLY_ACTIVATE_CHANGES) {
|
||||
fastly.activateVersion(results.version, function (e, resp) {
|
||||
|
|
7398
package-lock.json
generated
7398
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
@ -61,6 +61,7 @@
|
|||
"babel-eslint": "10.0.2",
|
||||
"babel-loader": "7.1.0",
|
||||
"babel-plugin-transform-object-rest-spread": "6.26.0",
|
||||
"babel-plugin-transform-require-context": "0.1.1",
|
||||
"babel-preset-es2015": "6.22.0",
|
||||
"babel-preset-react": "6.22.0",
|
||||
"bowser": "1.9.4",
|
||||
|
@ -71,6 +72,8 @@
|
|||
"copy-webpack-plugin": "0.2.0",
|
||||
"create-react-class": "15.6.2",
|
||||
"css-loader": "0.23.1",
|
||||
"enzyme": "3.10.0",
|
||||
"enzyme-adapter-react-16": "1.14.0",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-config-scratch": "5.0.0",
|
||||
"eslint-plugin-cypress": "^2.0.1",
|
||||
|
@ -120,7 +123,7 @@
|
|||
"redux": "3.5.2",
|
||||
"redux-thunk": "2.0.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"scratch-gui": "0.1.0-prerelease.20190731192641",
|
||||
"scratch-gui": "0.1.0-prerelease.20190807160203",
|
||||
"scratch-l10n": "latest",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"slick-carousel": "1.6.0",
|
||||
|
@ -133,6 +136,15 @@
|
|||
"webpack-dev-middleware": "2.0.4",
|
||||
"xhr": "2.2.0"
|
||||
},
|
||||
"jest": {
|
||||
"setupFiles": [
|
||||
"<rootDir>/test/helpers/enzyme-setup.js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
|
||||
"\\.(css|less|scss)$": "<rootDir>/test/__mocks__/styleMock.js"
|
||||
}
|
||||
},
|
||||
"nyc": {
|
||||
"include": [
|
||||
"bin/**/*.js",
|
||||
|
|
|
@ -12,9 +12,17 @@ const FormikInput = ({
|
|||
className,
|
||||
error,
|
||||
validationClassName,
|
||||
wrapperClassName,
|
||||
...props
|
||||
}) => (
|
||||
<div className="col-sm-9 row row-with-tooltip">
|
||||
<div
|
||||
className={classNames(
|
||||
'col-sm-9',
|
||||
'row',
|
||||
'row-with-tooltip',
|
||||
wrapperClassName
|
||||
)}
|
||||
>
|
||||
<Field
|
||||
className={classNames(
|
||||
'input',
|
||||
|
@ -36,7 +44,8 @@ FormikInput.propTypes = {
|
|||
className: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
validationClassName: PropTypes.string
|
||||
validationClassName: PropTypes.string,
|
||||
wrapperClassName: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = FormikInput;
|
||||
|
|
107
src/components/formik-forms/formik-radio-button.jsx
Normal file
107
src/components/formik-forms/formik-radio-button.jsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
const classNames = require('classnames');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
import {Field} from 'formik';
|
||||
|
||||
const FormikInput = require('./formik-input.jsx');
|
||||
|
||||
require('./formik-radio-button.scss');
|
||||
require('../forms/row.scss');
|
||||
|
||||
const FormikRadioButtonSubComponent = ({
|
||||
buttonValue,
|
||||
children,
|
||||
className,
|
||||
field,
|
||||
label,
|
||||
labelClassName,
|
||||
...props
|
||||
}) => (
|
||||
<React.Fragment>
|
||||
<input
|
||||
checked={buttonValue === field.value}
|
||||
className={classNames(
|
||||
'formik-radio-button',
|
||||
className
|
||||
)}
|
||||
name={field.name}
|
||||
type="radio"
|
||||
value={buttonValue}
|
||||
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-radio-label',
|
||||
labelClassName
|
||||
)}
|
||||
htmlFor={buttonValue}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
{children}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
FormikRadioButtonSubComponent.propTypes = {
|
||||
buttonValue: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
onBlur: PropTypes.function,
|
||||
onChange: PropTypes.function,
|
||||
value: PropTypes.string
|
||||
}),
|
||||
label: PropTypes.string,
|
||||
labelClassName: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
||||
};
|
||||
|
||||
|
||||
const FormikRadioButton = ({
|
||||
buttonValue,
|
||||
className,
|
||||
isCustomInput,
|
||||
label,
|
||||
name,
|
||||
onSetCustom,
|
||||
...props
|
||||
}) => (
|
||||
<Field
|
||||
buttonValue={buttonValue}
|
||||
className={className}
|
||||
component={FormikRadioButtonSubComponent}
|
||||
label={label}
|
||||
labelClassName={isCustomInput ? 'formik-radio-label-other' : ''}
|
||||
name={name}
|
||||
{...props}
|
||||
>
|
||||
{isCustomInput && (
|
||||
<FormikInput
|
||||
className="formik-radio-input"
|
||||
name="custom"
|
||||
wrapperClassName="formik-radio-input-wrapper"
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
onChange={event => onSetCustom(event.target.value)}
|
||||
onFocus={event => onSetCustom(event.target.value)}
|
||||
/* eslint-enable react/jsx-no-bind */
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
|
||||
FormikRadioButton.propTypes = {
|
||||
buttonValue: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
isCustomInput: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
onSetCustom: PropTypes.func,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
||||
};
|
||||
|
||||
module.exports = FormikRadioButton;
|
61
src/components/formik-forms/formik-radio-button.scss
Normal file
61
src/components/formik-forms/formik-radio-button.scss
Normal file
|
@ -0,0 +1,61 @@
|
|||
@import "../../colors";
|
||||
|
||||
.formik-radio-label {
|
||||
font-weight: 300;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
input[type="radio"].formik-radio-button {
|
||||
margin-top: 1px;
|
||||
border: 1px solid $box-shadow-light-gray;
|
||||
border-radius: 50%;
|
||||
width: 1rem;
|
||||
min-width: 1rem; /* necessary to prevent width from being too small in 'other' case */
|
||||
height: 1rem;
|
||||
appearance: none;
|
||||
background-color: $ui-white;
|
||||
|
||||
&:checked,
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:checked {
|
||||
transition: all .25s ease;
|
||||
box-shadow: 0 0 0 2px $ui-blue-25percent;
|
||||
border: 1px solid $ui-blue;
|
||||
background-color: $ui-white;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
transform: translate(.125rem, .125rem);
|
||||
border-radius: 50%;
|
||||
background-color: $ui-blue;
|
||||
width: .625rem;
|
||||
height: .625rem;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input.formik-radio-input, .formik-radio-input input {
|
||||
height: 2.1875rem;
|
||||
width: 10.25rem;
|
||||
margin-bottom: 0;
|
||||
border-radius: .5rem;
|
||||
background-color: $ui-white;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 .25rem $ui-blue-25percent;
|
||||
}
|
||||
}
|
||||
|
||||
.formik-radio-input-wrapper {
|
||||
margin-left: auto;
|
||||
margin-right: .25rem;
|
||||
}
|
||||
|
||||
.formik-radio-label-other {
|
||||
max-width: 7rem;
|
||||
margin-right: .25rem;
|
||||
}
|
|
@ -30,3 +30,13 @@
|
|||
margin-bottom: .75rem;
|
||||
line-height: 1.7rem;
|
||||
}
|
||||
|
||||
.row-inline {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* override margin-bottom so placing a label next to a radio button does not
|
||||
mess up vertical alignment */
|
||||
.row-inline label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
148
src/components/join-flow/gender-step.jsx
Normal file
148
src/components/join-flow/gender-step.jsx
Normal file
|
@ -0,0 +1,148 @@
|
|||
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 FormikRadioButton = require('../../components/formik-forms/formik-radio-button.jsx');
|
||||
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||
|
||||
require('./join-flow-steps.scss');
|
||||
|
||||
const GenderOption = ({
|
||||
label,
|
||||
onSetFieldValue,
|
||||
selectedValue,
|
||||
value
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(
|
||||
'col-sm-9',
|
||||
'row',
|
||||
'row-inline',
|
||||
'gender-radio-row',
|
||||
{'gender-radio-row-selected': (selectedValue === value)}
|
||||
)}
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
onClick={() => onSetFieldValue('gender', value, false)}
|
||||
/* eslint-enable react/jsx-no-bind */
|
||||
>
|
||||
<FormikRadioButton
|
||||
buttonValue={value}
|
||||
className={classNames(
|
||||
'join-flow-radio'
|
||||
)}
|
||||
label={label}
|
||||
name="gender"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
GenderOption.propTypes = {
|
||||
label: PropTypes.string,
|
||||
onSetFieldValue: PropTypes.func,
|
||||
selectedValue: PropTypes.string,
|
||||
value: PropTypes.string
|
||||
};
|
||||
|
||||
class GenderStep extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleValidSubmit'
|
||||
]);
|
||||
}
|
||||
handleValidSubmit (formData, formikBag) {
|
||||
formikBag.setSubmitting(false);
|
||||
if (!formData.gender || formData.gender === 'null') {
|
||||
formData.gender = 'Prefer not to say';
|
||||
}
|
||||
delete formData.custom;
|
||||
this.props.onNextStep(formData);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
gender: 'null',
|
||||
custom: ''
|
||||
}}
|
||||
onSubmit={this.handleValidSubmit}
|
||||
>
|
||||
{props => {
|
||||
const {
|
||||
handleSubmit,
|
||||
isSubmitting,
|
||||
setFieldValue,
|
||||
setValues,
|
||||
values
|
||||
} = props;
|
||||
return (
|
||||
<JoinFlowStep
|
||||
className="join-flow-gender-step"
|
||||
description={this.props.intl.formatMessage({id: 'registration.genderStepDescription'})}
|
||||
title={this.props.intl.formatMessage({id: 'registration.genderStepTitle'})}
|
||||
waiting={isSubmitting}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<GenderOption
|
||||
label={this.props.intl.formatMessage({id: 'general.female'})}
|
||||
selectedValue={values.gender}
|
||||
value="Female"
|
||||
onSetFieldValue={setFieldValue}
|
||||
/>
|
||||
<GenderOption
|
||||
label={this.props.intl.formatMessage({id: 'general.male'})}
|
||||
selectedValue={values.gender}
|
||||
value="Male"
|
||||
onSetFieldValue={setFieldValue}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'col-sm-9',
|
||||
'row',
|
||||
'row-inline',
|
||||
'gender-radio-row',
|
||||
{'gender-radio-row-selected': (values.gender === values.custom)}
|
||||
)}
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
onClick={() => setFieldValue('gender', values.custom, false)}
|
||||
/* eslint-enable react/jsx-no-bind */
|
||||
>
|
||||
<FormikRadioButton
|
||||
isCustomInput
|
||||
buttonValue={values.custom}
|
||||
className={classNames(
|
||||
'join-flow-radio'
|
||||
)}
|
||||
label={this.props.intl.formatMessage({id: 'registration.genderOptionAnother'})}
|
||||
name="gender"
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
onSetCustom={newCustomVal => setValues({
|
||||
gender: newCustomVal,
|
||||
custom: newCustomVal
|
||||
})}
|
||||
/* eslint-enable react/jsx-no-bind */
|
||||
/>
|
||||
</div>
|
||||
<GenderOption
|
||||
label={this.props.intl.formatMessage({id: 'registration.genderOptionPreferNotToSay'})}
|
||||
selectedValue={values.gender}
|
||||
value="Prefer not to say"
|
||||
onSetFieldValue={setFieldValue}
|
||||
/>
|
||||
</JoinFlowStep>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GenderStep.propTypes = {
|
||||
intl: intlShape,
|
||||
onNextStep: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = injectIntl(GenderStep);
|
|
@ -1,3 +1,4 @@
|
|||
const classNames = require('classnames');
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
|
@ -9,8 +10,10 @@ require('./join-flow-step.scss');
|
|||
|
||||
const JoinFlowStep = ({
|
||||
children,
|
||||
className,
|
||||
description,
|
||||
headerImgSrc,
|
||||
nextButton,
|
||||
onSubmit,
|
||||
title,
|
||||
waiting
|
||||
|
@ -22,7 +25,12 @@ const JoinFlowStep = ({
|
|||
</div>
|
||||
)}
|
||||
<div>
|
||||
<ModalInnerContent className="join-flow-inner-content">
|
||||
<ModalInnerContent
|
||||
className={classNames(
|
||||
'join-flow-inner-content',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<ModalTitle
|
||||
className="join-flow-title"
|
||||
|
@ -37,14 +45,19 @@ const JoinFlowStep = ({
|
|||
{children}
|
||||
</ModalInnerContent>
|
||||
</div>
|
||||
<NextStepButton waiting={waiting} />
|
||||
<NextStepButton
|
||||
content={nextButton}
|
||||
waiting={waiting}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
|
||||
JoinFlowStep.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
headerImgSrc: PropTypes.string,
|
||||
nextButton: PropTypes.node,
|
||||
onSubmit: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
waiting: PropTypes.bool
|
||||
|
|
|
@ -18,6 +18,14 @@
|
|||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.join-flow-instructions {
|
||||
font-size: .875rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.37500rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.validation-full-width-input {
|
||||
transform: translate(21.5625rem, 0);
|
||||
}
|
||||
|
@ -55,3 +63,31 @@
|
|||
display: flex;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.join-flow-gender-step {
|
||||
height: 27.375rem;
|
||||
padding-top: 3rem;
|
||||
}
|
||||
|
||||
.gender-radio-row {
|
||||
transition: all .125s ease;
|
||||
width: 20.875rem;
|
||||
height: 2.85rem;
|
||||
background-color: $ui-gray;
|
||||
border-radius: .5rem;
|
||||
margin-bottom: 0.375rem;
|
||||
padding-left: 0.8125rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gender-radio-row-selected {
|
||||
transition: all .125s ease;
|
||||
background-color: $ui-blue-25percent;
|
||||
}
|
||||
|
||||
.join-flow-next-button-arrow {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ const intlShape = require('../../lib/intl.jsx').intlShape;
|
|||
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 EmailStep = require('./email-step.jsx');
|
||||
const WelcomeStep = require('./welcome-step.jsx');
|
||||
|
||||
|
@ -40,8 +41,10 @@ class JoinFlow extends React.Component {
|
|||
<Progression step={this.state.step}>
|
||||
<UsernameStep onNextStep={this.handleAdvanceStep} />
|
||||
<BirthDateStep onNextStep={this.handleAdvanceStep} />
|
||||
<GenderStep onNextStep={this.handleAdvanceStep} />
|
||||
<EmailStep onNextStep={this.handleAdvanceStep} />
|
||||
<WelcomeStep
|
||||
email={this.state.formData.email}
|
||||
username={this.state.formData.username}
|
||||
onNextStep={this.handleAdvanceStep}
|
||||
/>
|
||||
|
|
|
@ -19,8 +19,8 @@ const NextStepButton = props => (
|
|||
{props.waiting ?
|
||||
<Spinner /> : (
|
||||
<ModalTitle
|
||||
className="next-step-title-large"
|
||||
title={props.text ? props.text : props.intl.formatMessage({id: 'registration.nextStep'})}
|
||||
className="next-step-title"
|
||||
title={props.content ? props.content : props.intl.formatMessage({id: 'general.next'})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -28,8 +28,8 @@ const NextStepButton = props => (
|
|||
);
|
||||
|
||||
NextStepButton.propTypes = {
|
||||
content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
intl: intl.intlShape,
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
waiting: PropTypes.bool
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
background-color: $ui-orange;
|
||||
}
|
||||
|
||||
.next-step-title-large {
|
||||
.next-step-title {
|
||||
font-size: 1.25rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ const bindAll = require('lodash.bindall');
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
import {Formik} from 'formik';
|
||||
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||
const {injectIntl, intlShape} = require('react-intl');
|
||||
|
||||
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||
|
@ -44,13 +45,31 @@ class WelcomeStep extends React.Component {
|
|||
id: 'registration.welcomeStepDescriptionNonEducator'
|
||||
})}
|
||||
headerImgSrc="/images/hoc/getting-started.jpg"
|
||||
nextButton={
|
||||
<React.Fragment>
|
||||
<FormattedMessage id="registration.makeProject" />
|
||||
<img
|
||||
className="join-flow-next-button-arrow"
|
||||
src="/svgs/project/r-arrow.svg"
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
title={`${this.props.intl.formatMessage(
|
||||
{id: 'registration.welcomeStepTitleNonEducator'},
|
||||
{username: this.props.username}
|
||||
)}`}
|
||||
waiting={isSubmitting}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="join-flow-instructions">
|
||||
<FormattedMessage
|
||||
id="registration.welcomeStepInstructions"
|
||||
values={{
|
||||
email: this.props.email
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</JoinFlowStep>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
|
@ -59,6 +78,7 @@ class WelcomeStep extends React.Component {
|
|||
}
|
||||
|
||||
WelcomeStep.propTypes = {
|
||||
email: PropTypes.string,
|
||||
intl: intlShape,
|
||||
onNextStep: PropTypes.func,
|
||||
username: PropTypes.string
|
||||
|
|
|
@ -20,7 +20,7 @@ const ModalTitle = ({
|
|||
|
||||
ModalTitle.propTypes = {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
};
|
||||
|
||||
module.exports = ModalTitle;
|
||||
|
|
|
@ -33,29 +33,20 @@ require('./steps.scss');
|
|||
const DEFAULT_COUNTRY = 'us';
|
||||
|
||||
/**
|
||||
* Return a list of options to give to frc select
|
||||
* Return a list of options to give to select
|
||||
* @param {object} reactIntl react-intl, used to localize strings
|
||||
* @param {string} defaultCountry optional string of default country to put at top of list
|
||||
* @return {object} ordered set of county options formatted for frc select
|
||||
* @return {object} ordered set of county options formatted for select
|
||||
*/
|
||||
const getCountryOptions = (reactIntl, defaultCountry) => {
|
||||
const options = countryData.countryOptions.concat({
|
||||
const getCountryOptions = reactIntl => (
|
||||
[
|
||||
{
|
||||
label: reactIntl.formatMessage({id: 'registration.selectCountry'}),
|
||||
disabled: true,
|
||||
value: ''
|
||||
});
|
||||
|
||||
if (typeof defaultCountry !== 'undefined') {
|
||||
return options.sort((a, b) => {
|
||||
if (a.disabled) return -1;
|
||||
if (b.disabled) return 1;
|
||||
if (a.value === defaultCountry) return -1;
|
||||
if (b.value === defaultCountry) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
return options;
|
||||
};
|
||||
},
|
||||
...countryData.registrationCountryOptions
|
||||
]
|
||||
);
|
||||
|
||||
const NextStepButton = props => (
|
||||
<Button
|
||||
|
@ -417,10 +408,14 @@ class DemographicsStep extends React.Component {
|
|||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'birthDateValidator',
|
||||
'countryValidator',
|
||||
'getCountryName',
|
||||
'getMonthOptions',
|
||||
'getYearOptions',
|
||||
'handleChooseGender',
|
||||
'handleValidSubmit'
|
||||
'handleValidSubmit',
|
||||
'isValidBirthdate'
|
||||
]);
|
||||
this.state = {
|
||||
otherDisabled: true
|
||||
|
@ -444,9 +439,24 @@ class DemographicsStep extends React.Component {
|
|||
handleChooseGender (name, gender) {
|
||||
this.setState({otherDisabled: gender !== 'other'});
|
||||
}
|
||||
// look up country name using user's country code selection
|
||||
getCountryName (values) {
|
||||
if (values.countryCode) {
|
||||
const countryInfo = countryData.lookupCountryInfo(values.countryCode);
|
||||
if (countryInfo) {
|
||||
return countryInfo.name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
handleValidSubmit (formData) {
|
||||
const countryName = this.getCountryName(formData);
|
||||
if (countryName && formData.user) {
|
||||
formData.user.country = countryName;
|
||||
return this.props.onNextStep(formData);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
isValidBirthdate (year, month) {
|
||||
const birthdate = new Date(
|
||||
year,
|
||||
|
@ -459,8 +469,13 @@ class DemographicsStep extends React.Component {
|
|||
const isValid = this.isValidBirthdate(values['user.birth.year'], values['user.birth.month']);
|
||||
return isValid ? true : this.props.intl.formatMessage({id: 'teacherRegistration.validationAge'});
|
||||
}
|
||||
countryValidator (values) {
|
||||
const countryName = this.getCountryName(values);
|
||||
if (countryName) return true;
|
||||
return this.props.intl.formatMessage({id: 'general.invalidSelection'});
|
||||
}
|
||||
render () {
|
||||
const countryOptions = getCountryOptions(this.props.intl, DEFAULT_COUNTRY);
|
||||
const countryOptions = getCountryOptions(this.props.intl);
|
||||
return (
|
||||
<Slide className="registration-step demographics-step">
|
||||
<h2>
|
||||
|
@ -539,8 +554,11 @@ class DemographicsStep extends React.Component {
|
|||
<Select
|
||||
required
|
||||
label={this.props.intl.formatMessage({id: 'general.country'})}
|
||||
name="user.country"
|
||||
name="countryCode"
|
||||
options={countryOptions}
|
||||
validations={{
|
||||
countryVal: values => this.countryValidator(values)
|
||||
}}
|
||||
value={countryOptions[0].value}
|
||||
/>
|
||||
<Checkbox
|
||||
|
@ -948,6 +966,7 @@ class AddressStep extends React.Component {
|
|||
render () {
|
||||
let stateOptions = countryData.subdivisionOptions[this.state.countryChoice];
|
||||
stateOptions = [{}].concat(stateOptions);
|
||||
const countryOptions = getCountryOptions(this.props.intl);
|
||||
return (
|
||||
<Slide className="registration-step address-step">
|
||||
<h2>
|
||||
|
@ -970,7 +989,7 @@ class AddressStep extends React.Component {
|
|||
this.props.intl.formatMessage({id: 'general.country'})
|
||||
}
|
||||
name="address.country"
|
||||
options={getCountryOptions(this.props.intl)}
|
||||
options={countryOptions}
|
||||
value={this.props.defaultCountry}
|
||||
onChange={this.handleChangeCountry}
|
||||
/>
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"general.getStarted": "Get Started",
|
||||
"general.gender": "Gender",
|
||||
"general.guidelines": "Community Guidelines",
|
||||
"general.invalidSelection": "Invalid selection",
|
||||
"general.jobs": "Jobs",
|
||||
"general.joinScratch": "Join Scratch",
|
||||
"general.legal": "Legal",
|
||||
|
@ -56,6 +57,7 @@
|
|||
"general.myClass": "My Class",
|
||||
"general.myClasses": "My Classes",
|
||||
"general.myStuff": "My Stuff",
|
||||
"general.next": "Next",
|
||||
"general.noDeletionTitle": "Your Account Will Not Be Deleted",
|
||||
"general.noDeletionDescription": "Your account was scheduled for deletion but you logged in. Your account has been reactivated. If you didn’t request for your account to be deleted, you should {resetLink} to make sure your account is secure.",
|
||||
"general.noDeletionLink": "change your password",
|
||||
|
@ -152,12 +154,17 @@
|
|||
"registration.confirmYourEmail": "Confirm Your Email",
|
||||
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
|
||||
"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.genderOptionAnother": "Another gender:",
|
||||
"registration.genderOptionPreferNotToSay": "Prefer not to say",
|
||||
"registration.emailStepTitle": "What's your email?",
|
||||
"registration.emailStepDescription": "We need this to finish creating your account. Scratch will always keep this information private.",
|
||||
"registration.goToClass": "Go to Class",
|
||||
"registration.invitedBy": "invited by",
|
||||
"registration.lastStepTitle": "Thank you for requesting a Scratch Teacher Account",
|
||||
"registration.lastStepDescription": "We are currently processing your application. ",
|
||||
"registration.makeProject": "Make a Project",
|
||||
"registration.mustBeNewStudent": "You must be a new student to complete your registration",
|
||||
"registration.nameStepTooltip": "This information is used for verification and to aggregate usage statistics.",
|
||||
"registration.newPassword": "New Password",
|
||||
|
@ -193,6 +200,7 @@
|
|||
"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.welcomeStepDescriptionNonEducator": "You’re now logged in! You can start exploring and creating projects.",
|
||||
"registration.welcomeStepInstructions": "Want to share and comment? Click the link on the email we sent to {email}.",
|
||||
"registration.welcomeStepPrompt": "To get started, click on the button below.",
|
||||
"registration.welcomeStepTitle": "Hurray! Welcome to Scratch!",
|
||||
"registration.welcomeStepTitleNonEducator": "Welcome to Scratch, {username}!",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -115,9 +115,7 @@ class TeacherRegistration extends React.Component {
|
|||
onNextStep={this.handleAdvanceStep}
|
||||
/>
|
||||
<Steps.PhoneNumberStep
|
||||
defaultCountry={
|
||||
this.state.formData.user && this.state.formData.user.country
|
||||
}
|
||||
defaultCountry={this.state.formData.countryCode}
|
||||
waiting={this.state.waiting}
|
||||
onNextStep={this.handleAdvanceStep}
|
||||
/>
|
||||
|
@ -126,9 +124,7 @@ class TeacherRegistration extends React.Component {
|
|||
onNextStep={this.handleAdvanceStep}
|
||||
/>
|
||||
<Steps.AddressStep
|
||||
defaultCountry={
|
||||
this.state.formData.user && this.state.formData.user.country
|
||||
}
|
||||
defaultCountry={this.state.formData.countryCode}
|
||||
waiting={this.state.waiting}
|
||||
onNextStep={this.handleAdvanceStep}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
module.exports = {
|
||||
extends: ['scratch/react'],
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
|
|
3
test/__mocks__/fileMock.js
Normal file
3
test/__mocks__/fileMock.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
// __mocks__/fileMock.js
|
||||
|
||||
module.exports = 'test-file-stub';
|
3
test/__mocks__/styleMock.js
Normal file
3
test/__mocks__/styleMock.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
// __mocks__/styleMock.js
|
||||
|
||||
module.exports = {};
|
4
test/helpers/enzyme-setup.js
Normal file
4
test/helpers/enzyme-setup.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
Enzyme.configure({adapter: new Adapter()});
|
39
test/helpers/intl-helpers.jsx
Normal file
39
test/helpers/intl-helpers.jsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Helpers for using enzyme and react-test-renderer with react-intl
|
||||
* Directly from https://github.com/yahoo/react-intl/wiki/Testing-with-React-Intl
|
||||
*/
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import {IntlProvider, intlShape} from 'react-intl';
|
||||
import {mount, shallow} from 'enzyme';
|
||||
|
||||
const intlProvider = new IntlProvider({locale: 'en'}, {});
|
||||
const {intl} = intlProvider.getChildContext();
|
||||
|
||||
const nodeWithIntlProp = node => React.cloneElement(node, {intl});
|
||||
|
||||
const shallowWithIntl = (node, {context} = {}) => shallow(
|
||||
nodeWithIntlProp(node),
|
||||
{
|
||||
context: Object.assign({}, context, {intl})
|
||||
}
|
||||
);
|
||||
|
||||
const mountWithIntl = (node, {context, childContextTypes} = {}) => mount(
|
||||
nodeWithIntlProp(node),
|
||||
{
|
||||
context: Object.assign({}, context, {intl}),
|
||||
childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes)
|
||||
}
|
||||
);
|
||||
|
||||
// react-test-renderer component for use with snapshot testing
|
||||
const componentWithIntl = (children, props = {locale: 'en'}) => renderer.create(
|
||||
<IntlProvider {...props}>{children}</IntlProvider>
|
||||
);
|
||||
|
||||
export {
|
||||
componentWithIntl,
|
||||
shallowWithIntl,
|
||||
mountWithIntl
|
||||
};
|
32
test/unit/components/next-step-button.test.jsx
Normal file
32
test/unit/components/next-step-button.test.jsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||
import NextStepButton from '../../../src/components/join-flow/next-step-button';
|
||||
import Spinner from '../../../src/components/spinner/spinner.jsx';
|
||||
|
||||
describe('NextStepButton', () => {
|
||||
const defaultProps = () => ({
|
||||
text: 'I am a button',
|
||||
waiting: false
|
||||
|
||||
});
|
||||
test('testing spinner does not show and button enabled', () => {
|
||||
const component = mountWithIntl(
|
||||
<NextStepButton
|
||||
{...defaultProps()}
|
||||
/>
|
||||
);
|
||||
expect(component.find(Spinner).exists()).toEqual(false);
|
||||
expect(component.find('button[type="submit"]').prop('disabled')).toBe(false);
|
||||
|
||||
});
|
||||
test('testing spinner does show and button disabled', () => {
|
||||
const component = mountWithIntl(
|
||||
<NextStepButton
|
||||
{...defaultProps()}
|
||||
/>
|
||||
);
|
||||
component.setProps({waiting: true});
|
||||
expect(component.find(Spinner).exists()).toEqual(true);
|
||||
expect(component.find('button[type="submit"]').prop('disabled')).toBe(true);
|
||||
});
|
||||
});
|
111
test/unit/lib/country-data.test.js
Normal file
111
test/unit/lib/country-data.test.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
const {
|
||||
countryInfo,
|
||||
countryOptions,
|
||||
lookupCountryInfo,
|
||||
dupeCommonCountries,
|
||||
registrationCountryOptions,
|
||||
subdivisionOptions
|
||||
} = require('../../../src/lib/country-data');
|
||||
|
||||
describe('unit test lib/country-data.js', () => {
|
||||
|
||||
test('countryInfo has the ballpark number of countries we expect', () => {
|
||||
expect(typeof countryInfo).toBe('object');
|
||||
expect(countryInfo.length > 200).toEqual(true);
|
||||
expect(countryInfo.length < 300).toEqual(true);
|
||||
});
|
||||
|
||||
test('countryOptions() maintains number of items', () => {
|
||||
expect(typeof countryOptions).toBe('function');
|
||||
const myCountryOptions = countryOptions(countryInfo, 'name');
|
||||
const numCountries = countryInfo.length;
|
||||
expect(myCountryOptions.length).toEqual(numCountries);
|
||||
});
|
||||
|
||||
test('countryOptions() called with value=name will use correct display strings and country name', () => {
|
||||
expect(typeof countryOptions).toBe('function');
|
||||
const myCountryOptions = countryOptions(countryInfo, 'name');
|
||||
|
||||
const eswatiniInfo = myCountryOptions.find(country => country.value === 'Swaziland');
|
||||
expect(eswatiniInfo).toBeTruthy();
|
||||
expect(eswatiniInfo.label).toEqual('Eswatini');
|
||||
|
||||
const swedenInfo = myCountryOptions.find(country => country.value === 'Sweden');
|
||||
expect(swedenInfo).toBeTruthy();
|
||||
expect(swedenInfo.label).toEqual('Sweden');
|
||||
});
|
||||
|
||||
test('countryOptions() called with value==code will use correct display strings and country code', () => {
|
||||
expect(typeof countryOptions).toBe('function');
|
||||
const myCountryOptions = countryOptions(countryInfo, 'code');
|
||||
const szInfo = myCountryOptions
|
||||
.find(country => country.value === 'sz');
|
||||
expect(szInfo).toBeTruthy();
|
||||
expect(szInfo.label).toEqual('Eswatini');
|
||||
});
|
||||
|
||||
test('lookupCountryInfo() will find country info', () => {
|
||||
expect(typeof lookupCountryInfo).toBe('function');
|
||||
const eswatiniInfo = lookupCountryInfo('sz');
|
||||
expect(eswatiniInfo.name).toEqual('Swaziland');
|
||||
expect(eswatiniInfo.display).toEqual('Eswatini');
|
||||
expect(eswatiniInfo.code).toEqual('sz');
|
||||
});
|
||||
|
||||
test('calling dupeCommonCountries() will duplicate the requested country info at start of array', () => {
|
||||
expect(typeof dupeCommonCountries).toBe('function');
|
||||
const countryInfoWithCommon = dupeCommonCountries(countryInfo, ['ca', 'gb']);
|
||||
|
||||
// test that the two entries have been added to the start of the array
|
||||
const numCountries = countryInfo.length;
|
||||
expect(countryInfoWithCommon.length).toEqual(numCountries + 2);
|
||||
expect(countryInfoWithCommon[0]).toEqual({code: 'ca', name: 'Canada'});
|
||||
expect(countryInfoWithCommon[1]).toEqual({code: 'gb', name: 'United Kingdom'});
|
||||
|
||||
// test that there are now two entries for Canada
|
||||
const canadaItems = countryInfoWithCommon.reduce((acc, thisCountry) => (
|
||||
thisCountry.code === 'ca' ? [...acc, thisCountry] : acc
|
||||
), []);
|
||||
expect(canadaItems.length).toEqual(2);
|
||||
// test that there are now two entries for UK
|
||||
const ukItems = countryInfoWithCommon.reduce((acc, thisCountry) => (
|
||||
thisCountry.code === 'gb' ? [...acc, thisCountry] : acc
|
||||
), []);
|
||||
expect(ukItems.length).toEqual(2);
|
||||
});
|
||||
|
||||
test('registrationCountryOptions object places USA and UK at start, with display name versions', () => {
|
||||
expect(typeof registrationCountryOptions).toBe('object');
|
||||
const numCountries = countryInfo.length;
|
||||
|
||||
// test that the two entries have been added to the start of the array, and that
|
||||
// the name of the USA includes "America"
|
||||
expect(registrationCountryOptions.length).toEqual(numCountries + 2);
|
||||
expect(registrationCountryOptions[0]).toEqual({value: 'us', label: 'United States of America'});
|
||||
expect(registrationCountryOptions[1]).toEqual({value: 'gb', label: 'United Kingdom'});
|
||||
|
||||
// test that there are now two entries for USA
|
||||
const usaOptions = registrationCountryOptions.reduce((acc, thisCountry) => (
|
||||
thisCountry.value === 'us' ? [...acc, thisCountry] : acc
|
||||
), []);
|
||||
expect(usaOptions.length).toEqual(2);
|
||||
// test that there are now two entries for UK
|
||||
const ukOptions = registrationCountryOptions.reduce((acc, thisCountry) => (
|
||||
thisCountry.value === 'gb' ? [...acc, thisCountry] : acc
|
||||
), []);
|
||||
expect(ukOptions.length).toEqual(2);
|
||||
});
|
||||
|
||||
test('subdivisionOptions object should include correct info for sample country', () => {
|
||||
expect(typeof subdivisionOptions).toBe('object');
|
||||
// 71 subdivisions in Bangladesh
|
||||
expect(subdivisionOptions.bd.length > 50).toEqual(true);
|
||||
expect(subdivisionOptions.bd.length < 100).toEqual(true);
|
||||
const nilphamari = subdivisionOptions.bd.find(item => item.label === 'Nilphamari');
|
||||
expect(nilphamari).toEqual({
|
||||
label: 'Nilphamari',
|
||||
value: 'bd-46',
|
||||
type: 'District'
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue