Merge branch 'develop' into llk-release-2019-08-29

This commit is contained in:
Ben Wheeler 2019-08-28 21:49:51 -04:00
commit 5893ccfef1
61 changed files with 1090 additions and 233 deletions

20
package-lock.json generated
View file

@ -17654,14 +17654,14 @@
"dev": true
},
"url-loader": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.0.1.tgz",
"integrity": "sha512-nd+jtHG6VgYx/NnXxXSWCJ7FtHIhuyk6Pe48HKhq29Avq3r5FSdIrenvzlbb67A3SNFaQyLk0/lMZfubj0+5ww==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.1.0.tgz",
"integrity": "sha512-kVrp/8VfEm5fUt+fl2E0FQyrpmOYgMEkBsv8+UDP1wFhszECq5JyGF33I7cajlVY90zRZ6MyfgKXngLvHYZX8A==",
"dev": true,
"requires": {
"loader-utils": "^1.1.0",
"loader-utils": "^1.2.3",
"mime": "^2.4.4",
"schema-utils": "^1.0.0"
"schema-utils": "^2.0.0"
},
"dependencies": {
"mime": {
@ -17669,6 +17669,16 @@
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
"integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==",
"dev": true
},
"schema-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz",
"integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==",
"dev": true,
"requires": {
"ajv": "^6.1.0",
"ajv-keywords": "^3.1.0"
}
}
}
},

View file

@ -131,7 +131,7 @@
"source-map-support": "0.3.2",
"style-loader": "0.12.3",
"tap": "14.2.0",
"url-loader": "2.0.1",
"url-loader": "2.1.0",
"watch": "0.16.0",
"webpack": "2.7.0",
"webpack-dev-middleware": "2.0.4",

View file

@ -256,6 +256,10 @@
}
}
div.cards + div.faq {
padding-top: 2rem;
}
.faq {
p {
margin-bottom: 1.25rem;

View file

@ -8,19 +8,16 @@ require('./formik-forms.scss');
require('../forms/row.scss');
const FormikCheckboxSubComponent = ({
className,
field,
id,
label,
labelClassName,
...props
}) => (
<div className="checkbox">
<input
checked={field.value}
className={classNames(
'formik-checkbox',
className
)}
className="formik-checkbox"
id={id}
name={field.name}
type="checkbox"
@ -32,8 +29,9 @@ const FormikCheckboxSubComponent = ({
{label && (
<label
className={classNames(
'formik-checkbox-label',
'formik-label',
'formik-checkbox-label'
labelClassName
)}
htmlFor={id}
>
@ -44,7 +42,6 @@ const FormikCheckboxSubComponent = ({
);
FormikCheckboxSubComponent.propTypes = {
className: PropTypes.string,
field: PropTypes.shape({
name: PropTypes.string,
onBlur: PropTypes.function,
@ -52,31 +49,32 @@ FormikCheckboxSubComponent.propTypes = {
value: PropTypes.bool
}),
id: PropTypes.string,
label: PropTypes.string
label: PropTypes.string,
labelClassName: PropTypes.string
};
const FormikCheckbox = ({
className,
id,
label,
labelClassName,
name,
...props
}) => (
<Field
className={className}
component={FormikCheckboxSubComponent}
id={id}
label={label}
labelClassName={labelClassName}
name={name}
{...props}
/>
);
FormikCheckbox.propTypes = {
className: PropTypes.string,
id: PropTypes.string,
label: PropTypes.string,
labelClassName: PropTypes.string,
name: PropTypes.string
};

View file

@ -1,9 +1,5 @@
@import "../../colors";
.formik-checkbox-label {
font-weight: 300;
}
input[type="checkbox"].formik-checkbox {
display: block;
float: left;
@ -14,26 +10,19 @@ input[type="checkbox"].formik-checkbox {
height: 1.25rem;
appearance: none;
&:focus:checked {
transition: all .5s ease;
&:focus {
transition: all .25s 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";
}
background-image: url("/svgs/forms/checkmark.svg");
background-position: center;
}
}

View file

@ -11,6 +11,8 @@ require('./formik-input.scss');
const FormikInput = ({
className,
error,
onSetRef,
toolTip,
validationClassName,
wrapperClassName,
...props
@ -29,21 +31,32 @@ const FormikInput = ({
{fail: error},
className
)}
/* formik uses "innerRef" to return the actual input element */
innerRef={onSetRef}
{...props}
/>
{error && (
{error ? (
<ValidationMessage
className={validationClassName}
message={error}
mode="error"
/>
) : toolTip && (
<ValidationMessage
className={validationClassName}
message={toolTip}
mode="info"
/>
)}
</div>
);
FormikInput.propTypes = {
className: PropTypes.string,
error: PropTypes.string,
// error and toolTip can be false, in which case we ignore them
error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
onSetRef: PropTypes.func,
toolTip: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
type: PropTypes.string,
validationClassName: PropTypes.string,
wrapperClassName: PropTypes.string

View file

@ -5,7 +5,7 @@
border-radius: .5rem;
background-color: $ui-white;
margin-bottom: .5rem;
transition: all .5s ease;
transition: all .5s ease, font-size 0s;
border: 1px solid $active-gray;
padding: 0 1rem;
color: $type-gray;
@ -15,6 +15,7 @@
box-shadow: 0 0 0 .25rem $ui-blue-25percent;
outline: none;
border: 1px solid $ui-blue;
transition: all .5s ease, font-size 0s;
}
&.fail {
@ -25,4 +26,9 @@
outline: none;
}
}
&::placeholder {
font-style: italic;
color: $type-gray-75percent;
}
}

View file

@ -10,24 +10,26 @@ require('./formik-radio-button.scss');
require('../forms/row.scss');
const FormikRadioButtonSubComponent = ({
buttonValue,
children,
className,
field,
field, // field.value is the current selected value of the entire radio group
id,
label,
labelClassName,
value,
...props
}) => (
<React.Fragment>
<input
checked={buttonValue === field.value}
checked={value === field.value}
className={classNames(
'formik-radio-button',
className
)}
id={id}
name={field.name}
type="radio"
value={buttonValue}
value={value}
onBlur={field.onBlur} /* eslint-disable-line react/jsx-handler-names */
onChange={field.onChange} /* eslint-disable-line react/jsx-handler-names */
{...props}
@ -39,7 +41,7 @@ const FormikRadioButtonSubComponent = ({
'formik-radio-label',
labelClassName
)}
htmlFor={buttonValue}
htmlFor={id}
>
{label}
</label>
@ -49,7 +51,6 @@ const FormikRadioButtonSubComponent = ({
);
FormikRadioButtonSubComponent.propTypes = {
buttonValue: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
field: PropTypes.shape({
@ -58,6 +59,7 @@ FormikRadioButtonSubComponent.propTypes = {
onChange: PropTypes.function,
value: PropTypes.string
}),
id: PropTypes.string,
label: PropTypes.string,
labelClassName: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
@ -65,21 +67,24 @@ FormikRadioButtonSubComponent.propTypes = {
const FormikRadioButton = ({
buttonValue,
className,
id,
isCustomInput,
label,
name,
onSetCustom,
onSetCustomRef,
value,
...props
}) => (
<Field
buttonValue={buttonValue}
className={className}
component={FormikRadioButtonSubComponent}
id={id}
label={label}
labelClassName={isCustomInput ? 'formik-radio-label-other' : ''}
name={name}
value={value}
{...props}
>
{isCustomInput && (
@ -91,18 +96,20 @@ const FormikRadioButton = ({
onChange={event => onSetCustom(event.target.value)}
onFocus={event => onSetCustom(event.target.value)}
/* eslint-enable react/jsx-no-bind */
onSetRef={onSetCustomRef}
/>
)}
</Field>
);
FormikRadioButton.propTypes = {
buttonValue: PropTypes.string,
className: PropTypes.string,
id: PropTypes.string,
isCustomInput: PropTypes.bool,
label: PropTypes.string,
name: PropTypes.string,
onSetCustom: PropTypes.func,
onSetCustomRef: PropTypes.func,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
};

View file

@ -1,4 +1,3 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
import {Field} from 'formik';
@ -7,6 +6,7 @@ const ValidationMessage = require('../forms/validation-message.jsx');
require('../forms/select.scss');
require('../forms/row.scss');
require('./formik-select.scss');
const FormikSelect = ({
className,
@ -27,9 +27,7 @@ const FormikSelect = ({
return (
<div className="select row-with-tooltip">
<Field
className={classNames(
className
)}
className={className}
component="select"
{...props}
>
@ -39,6 +37,7 @@ const FormikSelect = ({
<ValidationMessage
className={validationClassName}
message={error}
mode="error"
/>
)}
</div>

View file

@ -0,0 +1,12 @@
@import "../../colors";
.select {
.fail {
border: 1px solid $ui-orange;
&:focus {
box-shadow: 0 0 0 .25rem $ui-orange-25percent;
outline: none;
}
}
}

View file

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

View file

@ -11,7 +11,7 @@
margin-bottom: .75rem;
border: 1px solid $active-gray;
border-radius: 5px;
background: $ui-light-gray url("../../../static/svgs/forms/carot.svg") no-repeat right center;
background: $ui-light-gray url("../../../static/svgs/forms/caret.svg") no-repeat right center;
padding-right: 4rem;
padding-left: 1rem;
width: 100%;
@ -42,7 +42,7 @@
&:focus,
&:hover {
background: $ui-light-gray url("../../../static/svgs/forms/carot-hover.svg") no-repeat right center;
background: $ui-light-gray url("../../../static/svgs/forms/caret-hover.svg") no-repeat right center;
}
> option {

View file

@ -5,14 +5,24 @@ const React = require('react');
require('./validation-message.scss');
const ValidationMessage = props => (
<div className={classNames(['validation-message', props.className])}>
<div
className={classNames(
'validation-message',
{
'validation-error': props.mode === 'error',
'validation-info': props.mode === 'info'
},
props.className
)}
>
{props.message}
</div>
);
ValidationMessage.propTypes = {
className: PropTypes.string,
message: PropTypes.string
message: PropTypes.string,
mode: PropTypes.string
};
module.exports = ValidationMessage;

View file

@ -39,6 +39,24 @@
}
}
.validation-left {
$arrow-border-width: 1rem;
left: unset;
right: 0;
margin-left: unset;
margin-right: $arrow-border-width;
transform: translate(-16rem, 0);
&:before {
left: unset;
right: -$arrow-border-width / 2;
border-top: 1px solid $active-gray;
border-right: 1px solid $active-gray;
border-bottom: none;
border-left: none;
}
}
@media #{$intermediate-and-smaller} {
.validation-message {
position: relative;
@ -52,3 +70,21 @@
}
}
}
.validation-error {
background-color: $ui-orange;
&:before {
background-color: $ui-orange;
}
}
.validation-info {
background-color: $ui-blue;
box-shadow: 0 0 4px 2px rgba(0, 0, 0, .15);
font-weight: 500;
&:before {
background-color: $ui-blue;
}
}

View file

@ -1,6 +1,9 @@
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
const MediaQuery = require('react-responsive').default;
const frameless = require('../../lib/frameless');
require('./info-button.scss');
@ -22,25 +25,38 @@ class InfoButton extends React.Component {
this.setState({visible: true});
}
render () {
const messageJsx = this.state.visible && (
<div className="info-button-message">
{this.props.message}
</div>
);
return (
<React.Fragment>
<div
className="info-button"
onClick={this.handleShowMessage}
onMouseOut={this.handleHideMessage}
onMouseOver={this.handleShowMessage}
>
{this.state.visible && (
<div className="info-button-message">
{this.props.message}
<MediaQuery minWidth={frameless.desktop}>
{messageJsx}
</MediaQuery>
</div>
)}
{/* for small screens, add additional position: relative element,
so info message can position itself relative to the width which
encloses info-button -- rather than relative to info-button itself */}
<MediaQuery maxWidth={frameless.desktop - 1}>
<div style={{position: 'relative'}}>
{messageJsx}
</div>
</MediaQuery>
</React.Fragment>
);
}
}
InfoButton.propTypes = {
message: PropTypes.string
message: PropTypes.string.isRequired
};
module.exports = InfoButton;

View file

@ -9,17 +9,9 @@
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;
}
background-image: url("/svgs/info-button/info-button.svg");
background-size: cover;
top: .125rem;
}
.info-button-message {
@ -41,7 +33,7 @@
line-height: 1.25rem;
text-align: left;
font-size: .875rem;
z-index: 1;
z-index: 2;
&:before {
display: block;
@ -65,11 +57,12 @@
@media #{$intermediate-and-smaller} {
.info-button-message {
position: relative;
position: absolute;
transform: none;
margin: inherit;
width: 100%;
height: inherit;
/* since we're positioning message relative to info-button's parent,
we need to center this element within its width. */
margin: 0 calc((100% - 16.5rem) / 2);;
top: .125rem;
&:before {
display: none;

View file

@ -87,8 +87,10 @@ class BirthDateStep extends React.Component {
return (
<JoinFlowStep
description={this.props.intl.formatMessage({id: 'registration.private'})}
headerImgSrc="/images/hoc/getting-started.jpg"
descriptionClassName="join-flow-birthdate-description"
headerImgSrc="/images/join-flow/birthdate-header.png"
infoMessage={this.props.intl.formatMessage({id: 'registration.birthDateStepInfo'})}
innerClassName="join-flow-inner-birthdate-step"
title={this.props.intl.formatMessage({id: 'registration.birthDateStepTitle'})}
waiting={isSubmitting}
onSubmit={handleSubmit}
@ -106,16 +108,21 @@ class BirthDateStep extends React.Component {
'join-flow-select-month',
{fail: errors.birth_month}
)}
error={errors.birth_month}
/* hide month (left side) error, if year (right side) error exists */
error={errors.birth_year ? null : errors.birth_month}
id="birth_month"
name="birth_month"
options={birthMonthOptions}
validate={this.validateSelect}
validationClassName="validation-birthdate-input"
validationClassName={classNames(
'validation-birthdate-month',
'validation-left'
)}
/>
<FormikSelect
className={classNames(
'join-flow-select',
'join-flow-select-year',
{fail: errors.birth_year}
)}
error={errors.birth_year}
@ -123,7 +130,7 @@ class BirthDateStep extends React.Component {
name="birth_year"
options={birthYearOptions}
validate={this.validateSelect}
validationClassName="validation-birthdate-input"
validationClassName="validation-birthdate-year"
/>
</div>
</JoinFlowStep>

View file

@ -29,7 +29,7 @@ class CountryStep extends React.Component {
this.countryOptions = [...countryData.registrationCountryOptions];
this.countryOptions.unshift({
disabled: true,
label: this.props.intl.formatMessage({id: 'registration.selectCountry'}),
label: this.props.intl.formatMessage({id: 'general.country'}),
value: 'null'
});
}
@ -68,7 +68,9 @@ class CountryStep extends React.Component {
return (
<JoinFlowStep
description={this.props.intl.formatMessage({id: 'registration.countryStepDescription'})}
headerImgSrc="/images/hoc/getting-started.jpg"
descriptionClassName="join-flow-country-description"
headerImgSrc="/images/join-flow/country-header.png"
innerClassName="join-flow-inner-country-step"
title={this.props.intl.formatMessage({id: 'registration.countryStepTitle'})}
waiting={isSubmitting}
onSubmit={handleSubmit}

View file

@ -9,6 +9,7 @@ const FormattedMessage = require('react-intl').FormattedMessage;
const JoinFlowStep = require('./join-flow-step.jsx');
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx');
require('./join-flow-steps.scss');
@ -16,13 +17,21 @@ class EmailStep extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleSetEmailRef',
'handleValidSubmit',
'validateEmailIfPresent',
'validateEmail',
'validateForm'
]);
}
validateEmailIfPresent (email) {
if (!email) return null; // skip validation if email is blank; null indicates valid
componentDidMount () {
// automatically start with focus on username field
if (this.emailInput) this.emailInput.focus();
}
handleSetEmailRef (emailInputRef) {
this.emailInput = emailInputRef;
}
validateEmail (email) {
if (!email) return this.props.intl.formatMessage({id: 'general.required'});
const isValidLocally = emailValidator.validate(email);
if (isValidLocally) {
return null; // TODO: validate email address remotely
@ -40,6 +49,8 @@ class EmailStep extends React.Component {
return (
<Formik
initialValues={{
email: '',
subscribe: false
}}
validate={this.validateForm}
validateOnBlur={false}
@ -51,6 +62,7 @@ class EmailStep extends React.Component {
errors,
handleSubmit,
isSubmitting,
setFieldError,
validateField
} = props;
return (
@ -72,8 +84,8 @@ class EmailStep extends React.Component {
}}
/>
)}
headerImgSrc="/images/hoc/getting-started.jpg"
innerContentClassName="modal-inner-content-email"
headerImgSrc="/images/join-flow/email-header.png"
innerClassName="join-flow-inner-email-step"
nextButton={this.props.intl.formatMessage({id: 'registration.createAccount'})}
title={this.props.intl.formatMessage({id: 'registration.emailStepTitle'})}
waiting={isSubmitting}
@ -89,10 +101,21 @@ class EmailStep extends React.Component {
id="email"
name="email"
placeholder={this.props.intl.formatMessage({id: 'general.emailAddress'})}
validate={this.validateEmailIfPresent}
validate={this.validateEmail}
validationClassName="validation-full-width-input"
onBlur={() => validateField('email')} // eslint-disable-line react/jsx-no-bind
/* eslint-disable react/jsx-no-bind */
onBlur={() => validateField('email')}
onFocus={() => setFieldError('email', null)}
/* eslint-enable react/jsx-no-bind */
onSetRef={this.handleSetEmailRef}
/>
<div className="join-flow-email-checkbox-row">
<FormikCheckbox
id="subscribeCheckbox"
label={this.props.intl.formatMessage({id: 'registration.receiveEmails'})}
name="subscribe"
/>
</div>
</JoinFlowStep>
);
}}

View file

@ -11,10 +11,12 @@ const JoinFlowStep = require('./join-flow-step.jsx');
require('./join-flow-steps.scss');
const GenderOption = ({
id,
label,
onSetFieldValue,
selectedValue,
value
value,
...props
}) => (
<div
className={classNames(
@ -29,17 +31,20 @@ const GenderOption = ({
/* eslint-enable react/jsx-no-bind */
>
<FormikRadioButton
buttonValue={value}
className={classNames(
'join-flow-radio'
)}
id={id}
label={label}
name="gender"
value={value}
{...props}
/>
</div>
);
GenderOption.propTypes = {
id: PropTypes.string,
label: PropTypes.string,
onSetFieldValue: PropTypes.func,
selectedValue: PropTypes.string,
@ -50,9 +55,13 @@ class GenderStep extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleSetCustomRef',
'handleValidSubmit'
]);
}
handleSetCustomRef (customInputRef) {
this.customInput = customInputRef;
}
handleValidSubmit (formData, formikBag) {
formikBag.setSubmitting(false);
if (!formData.gender || formData.gender === 'null') {
@ -80,25 +89,34 @@ class GenderStep extends React.Component {
} = props;
return (
<JoinFlowStep
className="join-flow-gender-step"
description={this.props.intl.formatMessage({id: 'registration.genderStepDescription'})}
descriptionClassName="join-flow-gender-description"
infoMessage={this.props.intl.formatMessage({id: 'registration.genderStepInfo'})}
innerClassName="join-flow-inner-gender-step"
title={this.props.intl.formatMessage({id: 'registration.genderStepTitle'})}
waiting={isSubmitting}
onSubmit={handleSubmit}
>
<GenderOption
id="GenderRadioOptionFemale"
label={this.props.intl.formatMessage({id: 'general.female'})}
selectedValue={values.gender}
value="Female"
onSetFieldValue={setFieldValue}
/>
<GenderOption
id="GenderRadioOptionMale"
label={this.props.intl.formatMessage({id: 'general.male'})}
selectedValue={values.gender}
value="Male"
onSetFieldValue={setFieldValue}
/>
<GenderOption
label={this.props.intl.formatMessage({id: 'general.nonBinary'})}
selectedValue={values.gender}
value="Non-binary"
onSetFieldValue={setFieldValue}
/>
<div
className={classNames(
'col-sm-9',
@ -108,26 +126,32 @@ class GenderStep extends React.Component {
{'gender-radio-row-selected': (values.gender === values.custom)}
)}
/* eslint-disable react/jsx-no-bind */
onClick={() => setFieldValue('gender', values.custom, false)}
onClick={() => {
setFieldValue('gender', values.custom, false);
if (this.customInput) this.customInput.focus();
}}
/* eslint-enable react/jsx-no-bind */
>
<FormikRadioButton
isCustomInput
buttonValue={values.custom}
className={classNames(
'join-flow-radio'
)}
id="GenderRadioOptionCustom"
label={this.props.intl.formatMessage({id: 'registration.genderOptionAnother'})}
name="gender"
value={values.custom}
/* eslint-disable react/jsx-no-bind */
onSetCustom={newCustomVal => setValues({
gender: newCustomVal,
custom: newCustomVal
})}
onSetCustomRef={this.handleSetCustomRef}
/* eslint-enable react/jsx-no-bind */
/>
</div>
<GenderOption
id="GenderRadioOptionPreferNot"
label={this.props.intl.formatMessage({id: 'registration.genderOptionPreferNotToSay'})}
selectedValue={values.gender}
value="Prefer not to say"

View file

@ -11,29 +11,32 @@ require('./join-flow-step.scss');
const JoinFlowStep = ({
children,
className,
innerClassName,
description,
descriptionClassName,
footerContent,
headerImgSrc,
infoMessage,
innerContentClassName,
nextButton,
onSubmit,
title,
waiting
}) => (
<form onSubmit={onSubmit}>
<div className="join-flow-outer-content">
{headerImgSrc && (
<div className="join-flow-header-image">
<img src={headerImgSrc} />
<div className="join-flow-header-image-wrapper">
<img
className="join-flow-header-image"
src={headerImgSrc}
/>
</div>
)}
<div>
<ModalInnerContent
className={classNames(
'join-flow-inner-content',
className,
innerContentClassName
innerClassName
)}
>
{title && (
@ -43,7 +46,12 @@ const JoinFlowStep = ({
/>
)}
{description && (
<div className="join-flow-description">
<div
className={classNames(
'join-flow-description',
descriptionClassName
)}
>
{description}
{infoMessage && (
<InfoButton message={infoMessage} />
@ -62,17 +70,18 @@ const JoinFlowStep = ({
content={nextButton}
waiting={waiting}
/>
</div>
</form>
);
JoinFlowStep.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
description: PropTypes.string,
descriptionClassName: PropTypes.string,
footerContent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
headerImgSrc: PropTypes.string,
infoMessage: PropTypes.string,
innerContentClassName: PropTypes.string,
innerClassName: PropTypes.string,
nextButton: PropTypes.node,
onSubmit: PropTypes.func,
title: PropTypes.string,

View file

@ -1,6 +1,24 @@
@import "../../colors";
@import "../../frameless";
.join-flow-outer-content {
/* hopefully this lets text expand the height of the modal, if need be */
min-height: 32.5rem;
display: flex;
justify-content: space-between;
flex-direction: column;
overflow-wrap: break-word;
}
.join-flow-inner-content {
box-shadow: none;
width: calc(100% - 5.875rem);
/* must use padding for top, rather than margin, because margins will collapse */
margin: 0 auto;
padding: 2.3125rem 0 2.5rem;
font-size: .875rem;
}
.join-flow-title {
color: $type-gray;
font-size: 1.875rem;
@ -15,25 +33,21 @@
text-align: center;
}
.join-flow-inner-content {
box-shadow: none;
width: calc(100% - 5.875rem);
/* must use padding for top, rather than margin, because margins will collapse */
margin: 0 auto;
padding: 2.3125rem 0 2.5rem;
font-size: .875rem;
}
/* overflow will only work if this class is set on parent of img, not img itself */
.join-flow-header-image {
.join-flow-header-image-wrapper {
width: 100%;
height: 7.5rem;
min-height: 7.5rem;
max-height: 8.75rem;
overflow: hidden;
margin: 0;
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
}
.join-flow-header-image {
width: 27.5rem;
}
.join-flow-footer-message {
width: 100%;
padding: 1.125rem 1.5rem 1.125rem;

View file

@ -13,6 +13,10 @@
}
}
.join-flow-input-password {
font-size: 1.5rem;
}
.join-flow-password-confirm {
margin-bottom: .6875rem;
}
@ -38,8 +42,13 @@
transform: translate(21.5625rem, 0);
}
.validation-birthdate-input {
transform: translate(8.75rem, .25rem);
.validation-birthdate-month {
transform: translate(-9.25rem, 0);
width: 7.25rem;
}
.validation-birthdate-year {
transform: translate(8.75rem, 0);
width: 7.25rem;
}
@ -55,9 +64,22 @@
}
}
.select .join-flow-select {
height: 3.5rem;
background-color: white;
border-color: $box-shadow-light-gray;
font-size: 1rem;
font-weight: 500;
padding-right: 3.25rem;
}
.select .join-flow-select-month {
width: 9.125rem;
margin-right: .5rem;
width: 9.125rem;
}
.select .join-flow-select-year {
width: 9.125rem;
}
.select .join-flow-select-country {
@ -74,24 +96,68 @@
margin: 0 auto;
}
.join-flow-gender-step {
height: 27.375rem;
.join-flow-inner-username-step {
padding-top: 2.75rem;
}
.join-flow-inner-birthdate-step {
padding-top: 1rem;
padding-bottom: 2.25rem;
}
.join-flow-inner-gender-step {
/* need height so that flex will adjust children proportionately */
height: 27.25rem;
padding-top: 2.625rem;
padding-bottom: 1rem;
}
.join-flow-inner-country-step {
padding-top: 1rem;
padding-bottom: 2rem;
}
.join-flow-inner-email-step {
padding-top: 3rem;
}
.join-flow-inner-welcome-step {
padding-top: 3rem;
}
.join-flow-birthdate-description {
margin-top: 1.25rem;
margin-right: -.5rem;
margin-bottom: 2rem;
margin-left: -.5rem;
}
.join-flow-gender-description {
margin-top: .625rem;
margin-bottom: 1.25rem;
}
.join-flow-country-description {
margin-top: 1rem;
}
.gender-radio-row {
transition: all .125s ease;
width: 20.875rem;
height: 2.85rem;
background-color: $ui-gray;
border-radius: .5rem;
margin-bottom: 0.375rem;
margin: 0 auto 0.375rem;
padding-left: 0.8125rem;
display: flex;
align-items: center;
}
.gender-radio-row-selected {
.gender-radio-row:hover {
background-color: $ui-blue-10percent;
}
.gender-radio-row-selected, .gender-radio-row-selected:hover {
transition: all .125s ease;
background-color: $ui-blue-25percent;
}
@ -106,6 +172,11 @@
padding-top: 2.9rem;
}
.join-flow-email-checkbox-row {
font-size: .75rem;
margin: .25rem .125rem;
}
a.join-flow-link:link, a.join-flow-link:visited, a.join-flow-link:active {
text-decoration: underline;
}

View file

@ -21,16 +21,34 @@ class UsernameStep extends React.Component {
super(props);
bindAll(this, [
'handleChangeShowPassword',
'handleFocused',
'handleSetUsernameRef',
'handleValidSubmit',
'validatePasswordIfPresent',
'validatePasswordConfirmIfPresent',
'validateUsernameIfPresent',
'validateForm'
]);
this.state = {
focused: null,
showPassword: false
};
}
componentDidMount () {
// automatically start with focus on username field
if (this.usernameInput) this.usernameInput.focus();
}
handleChangeShowPassword () {
this.setState({showPassword: !this.state.showPassword});
}
// track the currently focused input field, to determine whether each field should
// display a tooltip. (We only display it if a field is focused and has never been touched.)
handleFocused (fieldName) {
this.setState({focused: fieldName});
}
handleSetUsernameRef (usernameInputRef) {
this.usernameInput = usernameInputRef;
}
// we allow username to be empty on blur, since you might not have typed anything yet
validateUsernameIfPresent (username) {
if (!username) return null; // skip validation if username is blank; null indicates valid
@ -109,7 +127,9 @@ class UsernameStep extends React.Component {
handleSubmit,
isSubmitting,
setFieldError,
setFieldTouched,
setFieldValue,
touched,
validateField,
values
} = props;
@ -118,6 +138,7 @@ class UsernameStep extends React.Component {
description={this.props.intl.formatMessage({
id: 'registration.usernameStepDescriptionNonEducator'
})}
innerClassName="join-flow-inner-username-step"
title={this.props.intl.formatMessage({id: 'general.joinScratch'})}
waiting={isSubmitting}
onSubmit={handleSubmit}
@ -133,15 +154,21 @@ class UsernameStep extends React.Component {
error={errors.username}
id="username"
name="username"
placeholder={this.props.intl.formatMessage({id: 'general.username'})}
toolTip={this.state.focused === 'username' && !touched.username &&
this.props.intl.formatMessage({id: 'registration.usernameAdviceShort'})}
validate={this.validateUsernameIfPresent}
validationClassName="validation-full-width-input"
/* eslint-disable react/jsx-no-bind */
onBlur={() => validateField('username')}
onChange={e => {
setFieldValue('username', e.target.value);
setFieldTouched('username');
setFieldError('username', null);
}}
onFocus={() => this.handleFocused('username')}
/* eslint-enable react/jsx-no-bind */
onSetRef={this.handleSetUsernameRef}
/>
<div className="join-flow-password-section">
<div className="join-flow-input-title">
@ -149,11 +176,16 @@ class UsernameStep extends React.Component {
</div>
<FormikInput
className={classNames(
'join-flow-input'
'join-flow-input',
{'join-flow-input-password':
!values.showPassword && values.password.length > 0}
)}
error={errors.password}
id="password"
name="password"
placeholder={this.props.intl.formatMessage({id: 'general.password'})}
toolTip={this.state.focused === 'password' && !touched.password &&
this.props.intl.formatMessage({id: 'registration.passwordAdviceShort'})}
type={values.showPassword ? 'text' : 'password'}
/* eslint-disable react/jsx-no-bind */
validate={password => this.validatePasswordIfPresent(password, values.username)}
@ -161,19 +193,34 @@ class UsernameStep extends React.Component {
onBlur={() => validateField('password')}
onChange={e => {
setFieldValue('password', e.target.value);
setFieldTouched('password');
setFieldError('password', null);
}}
onFocus={() => this.handleFocused('password')}
/* eslint-enable react/jsx-no-bind */
/>
<FormikInput
className={classNames(
'join-flow-input',
'join-flow-password-confirm',
{fail: errors.passwordConfirm}
{
'join-flow-input-password':
!values.showPassword && values.passwordConfirm.length > 0,
'fail': errors.passwordConfirm
}
)}
error={errors.passwordConfirm}
id="passwordConfirm"
name="passwordConfirm"
placeholder={this.props.intl.formatMessage({
id: 'registration.confirmPasswordInstruction'
})}
toolTip={
this.state.focused === 'passwordConfirm' && !touched.passwordConfirm &&
this.props.intl.formatMessage({
id: 'registration.confirmPasswordInstruction'
})
}
type={values.showPassword ? 'text' : 'password'}
/* eslint-disable react/jsx-no-bind */
validate={() =>
@ -181,13 +228,13 @@ class UsernameStep extends React.Component {
values.passwordConfirm)
}
validationClassName="validation-full-width-input"
onBlur={() =>
validateField('passwordConfirm')
}
onBlur={() => validateField('passwordConfirm')}
onChange={e => {
setFieldValue('passwordConfirm', e.target.value);
setFieldTouched('passwordConfirm');
setFieldError('passwordConfirm', null);
}}
onFocus={() => this.handleFocused('passwordConfirm')}
/* eslint-enable react/jsx-no-bind */
/>
<div className="join-flow-input-title">

View file

@ -27,8 +27,6 @@ class WelcomeStep extends React.Component {
render () {
return (
<Formik
initialValues={{
}}
validate={this.validateForm}
validateOnBlur={false}
validateOnChange={false}
@ -44,7 +42,8 @@ class WelcomeStep extends React.Component {
description={this.props.intl.formatMessage({
id: 'registration.welcomeStepDescriptionNonEducator'
})}
headerImgSrc="/images/hoc/getting-started.jpg"
headerImgSrc="/images/join-flow/welcome-header.png"
innerClassName="join-flow-inner-welcome-step"
nextButton={
<React.Fragment>
<FormattedMessage id="registration.makeProject" />

View file

@ -6,15 +6,14 @@ const JoinFlow = require('../../join-flow/join-flow.jsx');
require('./modal.scss');
const JoinModal = ({
isOpen,
onCompleteRegistration, // eslint-disable-line no-unused-vars
onRequestClose,
...modalProps
}) => (
<Modal
isOpen
useStandardSizes
className="mod-join"
isOpen={isOpen}
onRequestClose={onRequestClose}
{...modalProps}
>
@ -25,7 +24,6 @@ const JoinModal = ({
);
JoinModal.propTypes = {
isOpen: PropTypes.bool,
onCompleteRegistration: PropTypes.func,
onRequestClose: PropTypes.func
};

View file

@ -59,7 +59,7 @@ const TTTModal = props => (
rel="noopener noreferrer"
target="_blank"
>
<FormattedMessage id="ideas.downloadPDF" />
<FormattedMessage id="general.downloadPDF" />
</a>
</div>
</div>
@ -76,7 +76,7 @@ const TTTModal = props => (
rel="noopener noreferrer"
target="_blank"
>
<FormattedMessage id="ideas.downloadPDF" />
<FormattedMessage id="general.downloadPDF" />
</a>
</div>
</div>

View file

@ -23,8 +23,6 @@ const AccountNav = require('./accountnav.jsx');
require('./navigation.scss');
const USE_SCRATCH3_REGISTRATION = false;
class Navigation extends React.Component {
constructor (props) {
super(props);
@ -48,7 +46,7 @@ class Navigation extends React.Component {
}
componentDidUpdate (prevProps) {
if (prevProps.user !== this.props.user) {
this.props.closeAccountMenus();
this.props.handleCloseAccountNav();
if (this.props.user) {
const intervalId = setInterval(() => {
this.props.getMessageCount(this.props.user.username);
@ -198,17 +196,6 @@ class Navigation extends React.Component {
<FormattedMessage id="general.joinScratch" />
</a>
</li>,
(
USE_SCRATCH3_REGISTRATION ? (
<Scratch3Registration
key="scratch3registration"
/>
) : (
<Registration
key="registration"
/>
)
),
<li
className="link right login-item"
key="login"
@ -225,7 +212,20 @@ class Navigation extends React.Component {
key="login-dropdown"
/>
</li>
]) : []}
]) : []
}
{this.props.registrationOpen && (
this.props.useScratch3Registration ? (
<Scratch3Registration
isOpen
key="scratch3registration"
/>
) : (
<Registration
key="registration"
/>
)
)}
</ul>
<CanceledDeletionModal />
</NavigationBox>
@ -235,7 +235,6 @@ class Navigation extends React.Component {
Navigation.propTypes = {
accountNavOpen: PropTypes.bool,
closeAccountMenus: PropTypes.func,
getMessageCount: PropTypes.func,
handleCloseAccountNav: PropTypes.func,
handleLogOut: PropTypes.func,
@ -250,12 +249,14 @@ Navigation.propTypes = {
educator_invitee: PropTypes.bool,
student: PropTypes.bool
}),
registrationOpen: PropTypes.bool,
searchTerm: PropTypes.string,
session: PropTypes.shape({
status: PropTypes.string
}),
setMessageCount: PropTypes.func,
unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
useScratch3Registration: PropTypes.bool,
user: PropTypes.shape({
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
thumbnailUrl: PropTypes.string,
@ -273,15 +274,14 @@ const mapStateToProps = state => ({
accountNavOpen: state.navigation && state.navigation.accountNavOpen,
session: state.session,
permissions: state.permissions,
registrationOpen: state.navigation.registrationOpen,
searchTerm: state.navigation.searchTerm,
unreadMessageCount: state.messageCount.messageCount,
user: state.session && state.session.session && state.session.session.user
user: state.session && state.session.session && state.session.session.user,
useScratch3Registration: state.navigation.useScratch3Registration
});
const mapDispatchToProps = dispatch => ({
closeAccountMenus: () => {
dispatch(navigationActions.closeAccountMenus());
},
getMessageCount: username => {
dispatch(messageCountActions.getCount(username));
},

View file

@ -28,10 +28,6 @@ Registration.propTypes = {
isOpen: PropTypes.bool
};
const mapStateToProps = state => ({
isOpen: state.navigation.registrationOpen
});
const mapDispatchToProps = dispatch => ({
handleCloseRegistration: () => {
dispatch(navigationActions.setRegistrationOpen(false));
@ -42,6 +38,6 @@ const mapDispatchToProps = dispatch => ({
});
module.exports = connect(
mapStateToProps,
() => ({}),
mapDispatchToProps
)(Registration);

View file

@ -13,6 +13,7 @@
"general.confirmEmail": "Confirm Email",
"general.contactUs": "Contact Us",
"general.contact": "Contact",
"general.downloadPDF": "Download PDF",
"general.emailUs": "Email Us",
"general.conferences": "Conferences",
"general.copyright": "Scratch is a project of the Lifelong Kindergarten Group at the MIT Media Lab",
@ -62,6 +63,7 @@
"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 didnt request for your account to be deleted, you should {resetLink} to make sure your account is secure.",
"general.noDeletionLink": "change your password",
"general.nonBinary": "Non-binary",
"general.notRequired": "Not Required",
"general.okay": "Okay",
"general.other": "Other",
@ -156,9 +158,10 @@
"registration.generalError": "Sorry, an unexpected error occurred.",
"registration.classroomInviteExistingStudentStepDescription": "you have been invited to join the class:",
"registration.classroomInviteNewStudentStepDescription": "Your teacher has invited you to join a class:",
"registration.confirmPasswordInstruction": "Type password again",
"registration.confirmYourEmail": "Confirm Your Email",
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
"registration.createAccount": "Create Account",
"registration.createAccount": "Create Your Account",
"registration.createUsername": "Create a username",
"registration.genderStepTitle": "What's your gender?",
"registration.genderStepDescription": "Scratch welcomes people of all genders. We will always keep this information private.",
@ -178,20 +181,23 @@
"registration.nextStep": "Next Step",
"registration.notYou": "Not you? Log in as another user",
"registration.optIn": "Send me updates on using Scratch in educational settings",
"registration.passwordAdviceShort": "Write it down so you remember. Dont share it with others!",
"registration.personalStepTitle": "Personal Information",
"registration.personalStepDescription": "Your individual responses will not be displayed publicly, and will be kept confidential and secure",
"registration.private": "Scratch will always keep this information private.",
"registration.receiveEmails": "I'd like to receive emails from the Scratch Team about project ideas, events, and more.",
"registration.selectCountry": "select country",
"registration.studentPersonalStepDescription": "This information will not appear on the Scratch website.",
"registration.showPassword": "Show password",
"registration.usernameStepDescription": "Fill in the following forms to request an account. The approval process may take up to one day.",
"registration.usernameStepDescriptionNonEducator": "Create projects, share ideas, make friends. Its free!",
"registration.usernameStepRealName": "Please do not use any portion of your real name in your username.",
"registration.usernameAdviceShort": "Don't use your real name",
"registration.studentUsernameStepDescription": "You can make games, animations, and stories using Scratch. Setting up an account is easy and it's free. Fill in the form below to get started.",
"registration.studentUsernameStepHelpText": "Already have a Scratch account?",
"registration.studentUsernameStepTooltip": "You'll need to create a new Scratch account to join this class.",
"registration.studentUsernameFieldHelpText": "For safety, don't use your real name!",
"registration.acceptTermsOfUse": "By creating an account, I accept and agree to the {touLink}.",
"registration.acceptTermsOfUse": "By creating an account, you accept and agree to the {touLink}.",
"registration.usernameStepTitle": "Request a Teacher Account",
"registration.usernameStepTitleScratcher": "Create a Scratch Account",
"registration.validationMaxLength": "Sorry, you have exceeded the maximum character limit.",

View file

@ -20,7 +20,7 @@ module.exports.validateUsernameRemotely = username => (
uri: `/accounts/checkusername/${username}/`
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
resolve({valid: false, errMsgId: 'general.apiError'});
resolve({valid: false, errMsgId: 'general.error'});
}
switch (body.msg) {
case 'valid username':

View file

@ -18,6 +18,7 @@ const Types = keyMirror({
});
module.exports.getInitialState = () => ({
useScratch3Registration: false,
accountNavOpen: false,
canceledDeletionOpen: false,
loginError: null,
@ -96,11 +97,6 @@ module.exports.handleCompleteRegistration = () => (dispatch => {
dispatch(module.exports.setRegistrationOpen(false));
});
module.exports.closeAccountMenus = () => (dispatch => {
dispatch(module.exports.setAccountNavOpen(false));
dispatch(module.exports.setRegistrationOpen(false));
});
module.exports.handleLogIn = (formData, callback) => (dispatch => {
dispatch(module.exports.setLoginError(null));
formData.useMessages = true; // NOTE: this may or may not be being used anywhere else

View file

@ -141,7 +141,7 @@ const ConferenceSplash = () => (
<FormattedMessage id="conference-2019.website" />
</a>
</section>
<section className="conf2019-panel mod-uk">
<section className="conf2019-panel">
<FlexRow className="conf2019-panel-title">
<img
alt="EU Flag"
@ -237,7 +237,7 @@ const ConferenceSplash = () => (
<FormattedMessage id="conference-2019.website" />
</a>
</section>
<section className="conf2019-panel mod-kenya mod-last">
<section className="conf2019-panel mod-last">
<FlexRow className="conf2019-panel-title">
<img
alt="Kenya Flag"
@ -249,8 +249,89 @@ const ConferenceSplash = () => (
</div>
</FlexRow>
<p className="conf2019-panel-desc">
<FormattedMessage id="conference-2019.kenyaPostpone" />
<FormattedMessage id="conference-2019.kenyaDesc" />
</p>
<table className="conf2019-panel-details">
<tbody>
<tr className="conf2019-panel-row">
<td className="conf2019-panel-row-icon">
<img
alt="Calendar Icon"
className="conf2019-panel-row-icon-image"
src="/svgs/conference/index/calendar-icon-solid.svg"
/>
</td>
<td><FormattedMessage id="conference-2019.date" /></td>
<td>
<FormattedDate
day="2-digit"
month="long"
value={new Date(2019, 9, 16)}
year="numeric"
/>
{' - '}
<FormattedDate
day="2-digit"
month="long"
value={new Date(2019, 9, 18)}
year="numeric"
/>
</td>
</tr>
<tr className="conf2019-panel-row">
<td className="conf2019-panel-row-icon">
<img
alt="Map Icon"
className="conf2019-panel-row-icon-image"
src="/svgs/conference/index/map-icon-solid.svg"
/>
</td>
<td><FormattedMessage id="conference-2019.location" /></td>
<td>{'Nairobi, Kenya'}</td>
</tr>
<tr className="conf2019-panel-row">
<td className="conf2019-panel-row-icon">
<img
alt="Audience Icon"
className="conf2019-panel-row-icon-image"
src="/svgs/conference/index/audience-icon-solid.svg"
/>
</td>
<td><FormattedMessage id="conference-2019.audience" /></td>
<td><FormattedMessage id="conference-2019.kenyaAudience" /></td>
</tr>
<tr className="conf2019-panel-row">
<td className="conf2019-panel-row-icon">
<img
alt="Language Icon"
className="conf2019-panel-row-icon-image"
src="/svgs/conference/index/language-icon-solid.svg"
/>
</td>
<td><FormattedMessage id="conference-2019.language" /></td>
<td><FormattedMessage id="general.english" /></td>
</tr>
<tr className="conf2019-panel-row">
<td className="conf2019-panel-row-icon">
<img
alt="Language Icon"
className="conf2019-panel-row-icon-image"
src="/svgs/conference/index/hashtag-icon-solid.svg"
/>
</td>
<td><FormattedMessage id="conference-2019.hashtag" /></td>
<td>#scratchafrica #scratch2019nbo</td>
</tr>
</tbody>
</table>
<a
className="button mod-2019-conf mod-2019-conf-website-button"
href="https://www.scratchafrica.com/"
rel="noopener noreferrer"
target="_blank"
>
<FormattedMessage id="conference-2019.website" />
</a>
</section>
</div>
<div className="conf2019-title-band conf2019-mailing-list">

View file

@ -119,12 +119,6 @@ h1.title-banner-h1.mod-2019 {
margin: 2rem 0;
}
.mod-kenya {
.conf2019-panel-desc {
font-style: italic;
}
}
.conf2019-mailing-list {
font-weight: normal;
}

View file

@ -20,11 +20,11 @@
"conference-2019.ukDesc": "Hosted by Raspberry Pi, the 2019 Scratch Conference Europe will take place in Cambridge, UK, from Friday 23 August to Sunday 25 August. The schedule is full of exciting participatory activities led by members of the Scratch community. Participants can look forward to workshops, talks, and keynotes across a range of topics, including the new Scratch 3.0, as well as plenty of informal opportunities to chat and connect!",
"conference-2019.ukAudience": "Education professionals and volunteers",
"conference-2019.kenyaTitle": "Scratch2019NBO",
"conference-2019.kenyaTitle": "Scratch Conference Africa: Scratch2019NBO",
"conference-2019.kenyaSubTitle": "Waves of Innovation",
"conference-2019.kenyaDesc": "In recognition of Africa's technological contributions to the world and the potential of the youth of Africa, Scratch2019NBO will be held in Nairobi, Kenya. Join educators from around the world to share lessons, empower young people, and celebrate accomplishments in creative coding.",
"conference-2019.kenyaPostpone": "The Scratch2019NBO conference, originally planned for Nairobi, Kenya in July 2019, has been postponed. Information about future plans will be available later this year.",
"conference-2019.kenyaAudience": "Educators, students, and enthusiasts",
"conference-2019.kenyaAudience": "Educators",
"conference-2019.chileDesc": "Scratch al Sur Conferencia Chile 2019 is an event aimed at teachers of all educational areas and levels, who seek to innovate in the classroom through creative learning, thus supporting public policies that are promoted through the National Plan of Digital Languages, launched by the Chilean government as of 2019. Various workshops, panels, experiences, stands, a presentation of the new Scratch 3.0, Makey-Makey, and much more will be offered.",
"conference-2019.chileAudience": "Teachers and policy makers",

View file

@ -140,7 +140,7 @@ const Credits = () => (
{' '}
Ben Berg, Amos Blanton, Karen Brennan, Juanita Buitrago, Leo Burd,
Gaia Carini, Kasia Chmielinski, Michelle Chung, Shane Clements,
Hannah Cole, Sayamindu Dasgupta, Margarita Dekoli, Evelyn Eastmond,
Hannah Cole, Sayamindu Dasgupta, Margarita Dekoli,
Dave Feinberg, Chris Graves, Megan Haddadi, Connor Hudson,
Christina Huang, Tony Hwang, Abdulrahman Idlbi, Randy Jou, Lily Kim,
Tauntaun Kim, Saskia Leggett, Tim Mickel, Amon Millner, Ricarose Roque,

View file

@ -21,7 +21,6 @@
"ideas.seeAllTutorials": "See All Tutorials",
"ideas.cardsTitle": "Get the Entire Collection of Coding Cards",
"ideas.cardsText": "With the Scratch Coding Cards, you can learn to create interactive games, stories, music, animations, and more!",
"ideas.downloadPDF": "Download PDF",
"ideas.starterProjectsTitle": "Starter Projects",
"ideas.starterProjectsText": "You can play with Starter Projects and remix them to make your own creations.",
"ideas.starterProjectsButton": "Explore Starter Projects",

View file

@ -0,0 +1,3 @@
{
"cards.microbit-cardsLink": "https://resources.scratch.mit.edu/www/cards/en/microbit-cards.pdf"
}

View file

@ -2,7 +2,9 @@
"microbit.headerText": "{microbitLink} is a tiny circuit board designed to help kids learn to code and create with technology. It has many features including an LED display, buttons, and a motion sensor. You can connect it to Scratch and build creative projects that combine the magic of the digital and physical worlds.",
"microbit.gettingStarted": "Getting Started",
"microbit.installMicrobitHex": "Install Scratch micro:bit HEX",
"microbit.cardsDescription": "These cards show how to start making projects with micro:bit and Scratch.",
"microbit.connectUSB": "Connect a micro:bit to your computer with a USB cable",
"microbit.downloadCardsTitle": "Download micro:bit Cards",
"microbit.downloadHex": "Download the Scratch micro:bit HEX file",
"microbit.dragDropHex": "Drag and drop the HEX file onto your micro:bit",
"microbit.connectingMicrobit": "Connecting micro:bit to Scratch",

View file

@ -17,6 +17,7 @@ const ExtensionRequirements = require('../../components/extension-landing/extens
const ExtensionSection = require('../../components/extension-landing/extension-section.jsx');
const InstallScratchLink = require('../../components/extension-landing/install-scratch-link.jsx');
const ProjectCard = require('../../components/extension-landing/project-card.jsx');
const Button = require('../../components/forms/button.jsx');
const Steps = require('../../components/steps/steps.jsx');
const Step = require('../../components/steps/step.jsx');
@ -269,6 +270,35 @@ class MicroBit extends ExtensionLanding {
/>
</Steps>
</ExtensionSection>
<ExtensionSection className="cards">
<FlexRow
as="section"
className="cards-row"
>
<div className="cards-image-column">
<img
className="cards-image"
src="/images/microbit/microbit-with-scratch.png"
/>
</div>
<div className="cards-description-column">
<h2>
<FormattedMessage id="microbit.downloadCardsTitle" />
</h2>
<p>
<FormattedMessage id="microbit.cardsDescription" />
</p>
<p>
<a href={this.props.intl.formatMessage({id: 'cards.microbit-cardsLink'})}>
<Button className="download-cards-button large">
<FormattedMessage id="general.downloadPDF" />
</Button>
</a>
</p>
</div>
</FlexRow>
<hr />
</ExtensionSection>
<ExtensionSection className="faq">
<h2><FormattedMessage id="microbit.troubleshootingTitle" /></h2>
<h3 className="faq-title"><FormattedMessage id="microbit.checkOSVersionTitle" /></h3>

View file

@ -14,4 +14,60 @@
}
}
}
.cards {
/* ends with <hr>, so no need for extra padding */
padding-bottom: 0;
/* slightly more padding on top, since <hr> at bottom has its own extra padding */
padding-top: 4.5rem;
}
.cards-row {
flex-wrap: nowrap;
}
.cards-image-column {
width: 50%;
}
.cards-image {
width: calc(100% - 4rem);
margin-top: 1rem;
margin-right: 2rem;
margin-bottom: 1rem;
}
.cards-description-column {
width: 50%;
p {
font-size: 1.2rem;
}
}
.download-cards-button {
min-width: 10rem;
&:before {
display: inline-block;
background-image: url("/svgs/extensions/download-white.svg");
background-repeat: no-repeat;
width: 1.5rem;
height: 1.5rem;
margin-right: .75rem;
vertical-align: text-top;
content: "";
}
}
}
@media #{$medium-and-smaller} {
.microbit {
.cards-image-column {
width: calc(100% - 2rem);
}
.cards-description-column {
width: calc(100% - 2rem);
}
}
}

View file

@ -18,6 +18,7 @@ const ProjectInfo = require('../../lib/project-info');
const PreviewPresentation = require('./presentation.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
const Registration = require('../../components/registration/registration.jsx');
const Scratch3Registration = require('../../components/registration/scratch3-registration.jsx');
const ConnectedLogin = require('../../components/login/connected-login.jsx');
const CanceledDeletionModal = require('../../components/login/canceled-deletion-modal.jsx');
const NotAvailable = require('../../components/not-available/not-available.jsx');
@ -751,7 +752,15 @@ class Preview extends React.Component {
onUpdateProjectThumbnail={this.props.handleUpdateProjectThumbnail}
onUpdateProjectTitle={this.handleUpdateProjectTitle}
/>
{this.props.registrationOpen && (
this.props.useScratch3Registration ? (
<Scratch3Registration
isOpen
/>
) : (
<Registration />
)
)}
<CanceledDeletionModal />
</React.Fragment>
}
@ -822,6 +831,7 @@ Preview.propTypes = {
projectInfo: projectShape,
projectNotAvailable: PropTypes.bool,
projectStudios: PropTypes.arrayOf(PropTypes.object),
registrationOpen: PropTypes.bool,
remixProject: PropTypes.func,
remixes: PropTypes.arrayOf(PropTypes.object),
replies: PropTypes.objectOf(PropTypes.array),
@ -835,6 +845,7 @@ Preview.propTypes = {
shareProject: PropTypes.func.isRequired,
toggleStudio: PropTypes.func.isRequired,
updateProject: PropTypes.func.isRequired,
useScratch3Registration: PropTypes.bool,
user: PropTypes.shape({
id: PropTypes.number,
banned: PropTypes.bool,
@ -927,9 +938,11 @@ const mapStateToProps = state => {
projectInfo: state.preview.projectInfo,
projectNotAvailable: state.preview.projectNotAvailable,
projectStudios: state.preview.projectStudios,
registrationOpen: state.navigation.registrationOpen,
remixes: state.preview.remixes,
replies: state.preview.replies,
sessionStatus: state.session.status, // check if used
useScratch3Registration: state.navigation.useScratch3Registration,
user: state.session.session.user,
userOwnsProject: userOwnsProject,
userPresent: userPresent,

View file

@ -6,7 +6,6 @@
"tips.tttBody": "What do you want to make with Scratch? For each activity, you can try the <strong>Tutorial</strong>, download a set of <strong>Activity Cards</strong>, or view the <strong>Educator Guide</strong>.",
"tips.cardsHeader": "Get the Entire Collection of Activity Cards",
"tips.cardsBody": "With the Scratch Activity Cards, you can learn to create interactive games, stories, music, animations, and more!",
"tips.cardsDownload": "Download PDF",
"tips.cardsPurchase": "Purchase Printed Set",
"tips.starterProjectsHeader": "Starter Projects",
"tips.starterProjectsBody": "You can play with Starter Projects to get ideas for making your own projects.",

View file

@ -134,7 +134,7 @@ class Tips extends React.Component {
})}
>
<Button className="tips-button">
<FormattedMessage id="tips.cardsDownload" />
<FormattedMessage id="general.downloadPDF" />
</Button>
</a>
<a

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View file

@ -0,0 +1 @@
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="64"><path style="isolation:isolate" fill="#231f20" opacity=".1" d="M.01 0h48v64h-48z"/><path d="M24 37.58a1.88 1.88 0 0 1-1.33-.58l-5.11-5.11a1.89 1.89 0 0 1 0-2.65c.73-.73 12.14-.73 12.87 0a1.87 1.87 0 0 1 0 2.64L25.32 37a1.86 1.86 0 0 1-1.32.58z" fill="#b3b3b3"/></svg>

After

Width:  |  Height:  |  Size: 350 B

View file

@ -0,0 +1 @@
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="64"><path d="M24 37.43a1.88 1.88 0 0 1-1.33-.55l-5.11-5.11a1.87 1.87 0 0 1 0-2.64c.73-.73 12.14-.73 12.87 0a1.87 1.87 0 0 1 0 2.64l-5.11 5.11a1.86 1.86 0 0 1-1.32.55z" fill="#b3b3b3"/><path style="isolation:isolate" fill="#231f20" opacity=".1" d="M.01 0h1v64h-1z"/></svg>

After

Width:  |  Height:  |  Size: 350 B

View file

@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><title>carot</title><rect x="0.01" width="48" height="48" fill="#231f20" opacity="0.1"/><path d="M24,29.43a1.87,1.87,0,0,1-1.33-.55l-5.11-5.11a1.87,1.87,0,0,1,0-2.65c0.73-.73,12.14-0.73,12.87,0a1.87,1.87,0,0,1,0,2.65l-5.11,5.11A1.87,1.87,0,0,1,24,29.43Z" fill="#b3b3b3"/></svg>

Before

Width:  |  Height:  |  Size: 393 B

View file

@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><title>carot-hover</title><path d="M24,29.43a1.87,1.87,0,0,1-1.33-.55l-5.11-5.11a1.87,1.87,0,0,1,0-2.65c0.73-.73,12.14-0.73,12.87,0a1.87,1.87,0,0,1,0,2.65l-5.11,5.11A1.87,1.87,0,0,1,24,29.43Z" fill="#b3b3b3"/><rect x="0.01" width="1" height="48" fill="#231f20" opacity="0.1"/></svg>

Before

Width:  |  Height:  |  Size: 398 B

View file

@ -0,0 +1 @@
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M7.861 15.403a1.66 1.66 0 0 1-1.177-.488L3.488 11.72a1.663 1.663 0 0 1 0-2.354 1.663 1.663 0 0 1 2.354 0l2.02 2.02 6.297-6.297a1.665 1.665 0 0 1 2.354 2.354l-7.475 7.473a1.66 1.66 0 0 1-1.177.488" id="a"/></defs><use fill="#FFF" xlink:href="#a" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 393 B

View file

@ -0,0 +1 @@
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M10 0C4.477 0 0 4.477 0 10s4.477 10 10 10 10-4.477 10-10S15.523 0 10 0" fill="#4794FF"/><path d="M9.988 13.388a1.33 1.33 0 1 1-.001 2.662 1.33 1.33 0 0 1 .001-2.662zm2.68-8.768c.83.53 1.335 1.347 1.418 2.305.084.97-.28 1.95-.949 2.56-.596.543-1.148.852-1.59 1.101-.698.39-.698.41-.698.772 0 .553-.014 1.002-1.002 1.002-.918 0-1.002-.449-1.002-1.002 0-1.559.956-2.092 1.722-2.52.391-.22.795-.445 1.22-.833.176-.16.335-.523.302-.906-.029-.332-.196-.597-.496-.788-.845-.537-2.063-.325-2.869.195-.985.637-.819 1.547-1.755 1.291-1.062-.29-.915-1-.529-1.697.418-.756 1.117-1.317 1.898-1.662 1.471-.65 3.13-.58 4.33.182z" fill="#FFF"/></g></svg>

After

Width:  |  Height:  |  Size: 745 B

5
test/__mocks__/react-responsive.js vendored Normal file
View file

@ -0,0 +1,5 @@
// __mocks__/react-responsive.js
const MediaQuery = ({children}) => children;
export default MediaQuery;

View file

@ -0,0 +1,85 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import FormikInput from '../../../src/components/formik-forms/formik-input.jsx';
import {Formik} from 'formik';
describe('FormikInput', () => {
test('No validation message with empty error, empty toolTip', () => {
const component = mountWithIntl(
<Formik>
<FormikInput
error=""
toolTip=""
/>
</Formik>
);
expect(component.find('ValidationMessage').exists()).toEqual(false);
expect(component.find('div.validation-error').exists()).toEqual(false);
expect(component.find('div.validation-info').exists()).toEqual(false);
});
test('No validation message with false error, false toolTip', () => {
const component = mountWithIntl(
<Formik>
<FormikInput
error={false}
toolTip={false}
/>
</Formik>
);
expect(component.find('ValidationMessage').exists()).toEqual(false);
expect(component.find('div.validation-error').exists()).toEqual(false);
expect(component.find('div.validation-info').exists()).toEqual(false);
});
test('No validation message with nonexistent error or toolTip', () => {
const component = mountWithIntl(
<Formik>
<FormikInput />
</Formik>
);
expect(component.find('ValidationMessage').exists()).toEqual(false);
expect(component.find('div.validation-error').exists()).toEqual(false);
expect(component.find('div.validation-info').exists()).toEqual(false);
});
test('Validation message shown when error given', () => {
const component = mountWithIntl(
<Formik>
<FormikInput
error="There was an error"
/>
</Formik>
);
expect(component.find('ValidationMessage').exists()).toEqual(true);
expect(component.find('div.validation-error').exists()).toEqual(true);
expect(component.find('div.validation-info').exists()).toEqual(false);
});
test('Tooltip shown when toolTip given', () => {
const component = mountWithIntl(
<Formik>
<FormikInput
toolTip="Have fun out there!"
/>
</Formik>
);
expect(component.find('ValidationMessage').exists()).toEqual(true);
expect(component.find('div.validation-error').exists()).toEqual(false);
expect(component.find('div.validation-info').exists()).toEqual(true);
});
test('If both error and toolTip messages, error takes precedence', () => {
const component = mountWithIntl(
<Formik>
<FormikInput
error="There was an error"
toolTip="Have fun out there!"
/>
</Formik>
);
expect(component.find('ValidationMessage').exists()).toEqual(true);
expect(component.find('div.validation-error').exists()).toEqual(true);
expect(component.find('div.validation-info').exists()).toEqual(false);
});
});

View file

@ -0,0 +1,60 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import FormikSelect from '../../../src/components/formik-forms/formik-select.jsx';
import {Formik} from 'formik';
import {Field} from 'formik';
describe('FormikSelect', () => {
test('No validation message without an error', () => {
const component = mountWithIntl(
<Formik>
<FormikSelect
error=""
options={[]}
/>
</Formik>
);
expect(component.find('ValidationMessage').exists()).toEqual(false);
expect(component.find(Field).exists()).toEqual(true);
});
test('Validation message shown when error present', () => {
const component = mountWithIntl(
<Formik>
<FormikSelect
error="uh oh. error"
options={[]}
/>
</Formik>
);
expect(component.find('ValidationMessage').exists()).toEqual(true);
expect(component.find(Field).exists()).toEqual(true);
});
test('list of options passed to formik', () => {
const optionList = [
{
disabled: false,
label: 'option1',
value: 'value1'
},
{
disabled: false,
label: 'option2',
value: 'value2'
}
];
const component = mountWithIntl(
<Formik>
<FormikSelect
error=""
options={optionList}
/>
</Formik>
);
expect(component.find(Field).exists()).toEqual(true);
expect(component.find(Field).prop('children').length).toEqual(2);
});
});

View file

@ -11,6 +11,15 @@ describe('InfoButton', () => {
);
expect(component.find('div.info-button-message').exists()).toEqual(false);
});
test('mouseOver on info button makes info message visible', () => {
const component = mountWithIntl(
<InfoButton
message="Here is some info about something!"
/>
);
component.find('div.info-button').simulate('mouseOver');
expect(component.find('div.info-button-message').exists()).toEqual(true);
});
test('clicking on info button makes info message visible', () => {
const component = mountWithIntl(
<InfoButton
@ -26,7 +35,7 @@ describe('InfoButton', () => {
message="Here is some info about something!"
/>
);
component.find('div.info-button').simulate('click');
component.find('div.info-button').simulate('mouseOver');
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

@ -20,7 +20,7 @@ describe('JoinFlowStep', () => {
{...props}
/>
);
expect(component.find('div.join-flow-header-image').exists()).toEqual(true);
expect(component.find('img.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);
@ -39,7 +39,7 @@ describe('JoinFlowStep', () => {
<JoinFlowStep />
);
expect(component.find('div.join-flow-header-image').exists()).toEqual(false);
expect(component.find('img.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);

View file

@ -0,0 +1,241 @@
const {
handleToggleAccountNav,
navigationReducer,
setAccountNavOpen,
setCanceledDeletionOpen,
setLoginError,
setLoginOpen,
setRegistrationOpen,
setSearchTerm,
toggleLoginOpen
} = require('../../../src/redux/navigation');
describe('unit test lib/validate.js', () => {
test('initialState', () => {
let defaultState;
/* navigationReducer(state, action) */
expect(navigationReducer(defaultState, {type: 'anything'})).toBeDefined();
expect(navigationReducer(defaultState, {type: 'anything'}).accountNavOpen).toBe(false);
expect(navigationReducer(defaultState, {type: 'anything'}).canceledDeletionOpen).toBe(false);
expect(navigationReducer(defaultState, {type: 'anything'}).loginError).toBe(null);
expect(navigationReducer(defaultState, {type: 'anything'}).loginOpen).toBe(false);
expect(navigationReducer(defaultState, {type: 'anything'}).registrationOpen).toBe(false);
expect(navigationReducer(defaultState, {type: 'anything'}).searchTerm).toBe('');
expect(navigationReducer(defaultState, {type: 'anything'}).useScratch3Registration).toBe(false);
});
// handleToggleAccountNav
test('handleToggleAccountNav can toggle on', () => {
const initialState = {
accountNavOpen: false
};
const action = handleToggleAccountNav();
const resultState = navigationReducer(initialState, action);
expect(resultState.accountNavOpen).toBe(true);
});
test('handleToggleAccountNav can toggle off', () => {
const initialState = {
accountNavOpen: true
};
const action = handleToggleAccountNav();
const resultState = navigationReducer(initialState, action);
expect(resultState.accountNavOpen).toBe(false);
});
// setAccountNavOpen
test('setAccountNavOpen opens account nav, if it is closed', () => {
const initialState = {
accountNavOpen: false
};
const action = setAccountNavOpen(true);
const resultState = navigationReducer(initialState, action);
expect(resultState.accountNavOpen).toBe(true);
});
test('setAccountNavOpen leaves account nav open, if it is already open', () => {
const initialState = {
accountNavOpen: true
};
const action = setAccountNavOpen(true);
const resultState = navigationReducer(initialState, action);
expect(resultState.accountNavOpen).toBe(true);
});
test('setAccountNavOpen closes account nav, if it is open', () => {
const initialState = {
accountNavOpen: true
};
const action = setAccountNavOpen(false);
const resultState = navigationReducer(initialState, action);
expect(resultState.accountNavOpen).toBe(false);
});
test('setAccountNavOpen leaves account nav closed, if it is already closed', () => {
const initialState = {
accountNavOpen: false
};
const action = setAccountNavOpen(false);
const resultState = navigationReducer(initialState, action);
expect(resultState.accountNavOpen).toBe(false);
});
// setCanceledDeletionOpen
test('setCanceledDeletionOpen opens account nav, if it is closed', () => {
const initialState = {
canceledDeletionOpen: false
};
const action = setCanceledDeletionOpen(true);
const resultState = navigationReducer(initialState, action);
expect(resultState.canceledDeletionOpen).toBe(true);
});
test('setCanceledDeletionOpen leaves account nav open, if it is already open', () => {
const initialState = {
canceledDeletionOpen: true
};
const action = setCanceledDeletionOpen(true);
const resultState = navigationReducer(initialState, action);
expect(resultState.canceledDeletionOpen).toBe(true);
});
test('setCanceledDeletionOpen closes account nav, if it is open', () => {
const initialState = {
canceledDeletionOpen: true
};
const action = setCanceledDeletionOpen(false);
const resultState = navigationReducer(initialState, action);
expect(resultState.canceledDeletionOpen).toBe(false);
});
test('setCanceledDeletionOpen leaves account nav closed, if it is already closed', () => {
const initialState = {
canceledDeletionOpen: false
};
const action = setCanceledDeletionOpen(false);
const resultState = navigationReducer(initialState, action);
expect(resultState.canceledDeletionOpen).toBe(false);
});
// setLoginError
test('setLoginError sets login error', () => {
const initialState = {
loginError: null
};
const action = setLoginError('Danger! Error! Mistake!');
const resultState = navigationReducer(initialState, action);
expect(resultState.loginError).toBe('Danger! Error! Mistake!');
});
// setLoginOpen
test('setLoginOpen opens account nav, if it is closed', () => {
const initialState = {
loginOpen: false
};
const action = setLoginOpen(true);
const resultState = navigationReducer(initialState, action);
expect(resultState.loginOpen).toBe(true);
});
test('setLoginOpen leaves account nav open, if it is already open', () => {
const initialState = {
loginOpen: true
};
const action = setLoginOpen(true);
const resultState = navigationReducer(initialState, action);
expect(resultState.loginOpen).toBe(true);
});
test('setLoginOpen closes account nav, if it is open', () => {
const initialState = {
loginOpen: true
};
const action = setLoginOpen(false);
const resultState = navigationReducer(initialState, action);
expect(resultState.loginOpen).toBe(false);
});
test('setLoginOpen leaves account nav closed, if it is already closed', () => {
const initialState = {
loginOpen: false
};
const action = setLoginOpen(false);
const resultState = navigationReducer(initialState, action);
expect(resultState.loginOpen).toBe(false);
});
// setRegistrationOpen
test('setRegistrationOpen opens account nav, if it is closed', () => {
const initialState = {
registrationOpen: false
};
const action = setRegistrationOpen(true);
const resultState = navigationReducer(initialState, action);
expect(resultState.registrationOpen).toBe(true);
});
test('setRegistrationOpen leaves account nav open, if it is already open', () => {
const initialState = {
registrationOpen: true
};
const action = setRegistrationOpen(true);
const resultState = navigationReducer(initialState, action);
expect(resultState.registrationOpen).toBe(true);
});
test('setRegistrationOpen closes account nav, if it is open', () => {
const initialState = {
registrationOpen: true
};
const action = setRegistrationOpen(false);
const resultState = navigationReducer(initialState, action);
expect(resultState.registrationOpen).toBe(false);
});
test('setRegistrationOpen leaves account nav closed, if it is already closed', () => {
const initialState = {
registrationOpen: false
};
const action = setRegistrationOpen(false);
const resultState = navigationReducer(initialState, action);
expect(resultState.registrationOpen).toBe(false);
});
// setSearchTerm
test('setSearchTerm sets search term', () => {
const initialState = {
searchTerm: null
};
const action = setSearchTerm('outer space');
const resultState = navigationReducer(initialState, action);
expect(resultState.searchTerm).toBe('outer space');
});
// toggleLoginOpen
test('toggleLoginOpen can toggle on', () => {
const initialState = {
loginOpen: false
};
const action = toggleLoginOpen();
const resultState = navigationReducer(initialState, action);
expect(resultState.loginOpen).toBe(true);
});
test('toggleLoginOpen can toggle off', () => {
const initialState = {
loginOpen: true
};
const action = toggleLoginOpen();
const resultState = navigationReducer(initialState, action);
expect(resultState.loginOpen).toBe(false);
});
});