Merge pull request #599 from mewtaylor/feature/teacher-registration-flow

[Updates] Teacher registration flow
This commit is contained in:
Matthew Taylor 2016-06-23 16:48:58 -04:00 committed by GitHub
commit 74bd2b3255
69 changed files with 2520 additions and 262 deletions

View file

@ -47,6 +47,8 @@ env:
- SENTRY_DSN_VAR=SENTRY_DSN_$TRAVIS_BRANCH
- SENTRY_DSN=${!SENTRY_DSN_VAR}
- SENTRY_DSN=${SENTRY_DSN:-$SENTRY_DSN_STAGING}
# SMARTY_STREETS_API_KEY
- secure: "uQKNgJaJEju8ErGUxIbLE0Y6ee4j6OFFbBpqyuKrNMk6apvvvXLp3lTdGZJq6j/ZwQeQ384m5bbfmhFwr7piPFj7W/zBXVKcifbF6ShfP7skMl834Kkefs3uEWU0VZw3nURgzNInSOPqqGLsISFywpwBXUWKfL0Q87KHNU0/I0EkwvImm3SAlNpR38m3yKcMF3h0zK8Fh2mO7iyHEIhtssdWabaRjf3t6Mr5vikACeXYJg+k4oEQZtsnSNnlLYWumdEDsxwonMozGKUBqlXwhHCdYNOJ1DUGuntbXOnylLt1/LA9I9B4hWQOrRDwqjyIOI+2dpADoCN040+Zr1VSrJhk7Wb7ogeaQLzZ4W/3dX54rbsnFHa+MuKqOsAxQ0Tjfk5xWq/pbLRsAyW6Pl7Q1v4yWOQ2COnM/tfJ6UaH9bxppOyKsX8n33rFjlvZU6CtY1GGa7fpB2zOKI5B5OovLjHeokIe/Tx+4coEDZqt44qkTGWr/eWDxrvkQqpQ29F9My3wBgB3gdou+3lWExS0a9M2wwp4EIduXEKNZXLGDuVefH5f3eFy09wH+nhctmMF8uhMbPefFubEi7fqXTkxntmDTy+/pD2A2w1jJhBwLhwlik335k+Wrbl3dclt7cjJ6fRVX9b+AuBCbGr633vM4xbk90whwXizSECIt5InGSw="
- SKIP_CLEANUP=true
- NODE_ENV=production
- WWW_VERSION=${TRAVIS_COMMIT:0:5}

View file

@ -43,8 +43,12 @@
"exenv": "1.2.0",
"fastly": "1.2.1",
"file-loader": "0.8.4",
"formsy-react": "0.18.0",
"formsy-react-components": "0.7.1",
"git-bundle-sha": "0.0.2",
"glob": "5.0.15",
"google-libphonenumber": "1.0.21",
"iso-3166-2": "0.4.0",
"json-loader": "0.5.2",
"json2po-stream": "1.0.3",
"jsx-loader": "0.13.2",
@ -67,6 +71,7 @@
"react-onclickoutside": "4.1.1",
"react-redux": "4.4.5",
"react-slick": "0.12.2",
"react-telephone-input": "3.4.5",
"redux": "3.5.2",
"redux-thunk": "2.0.1",
"sass-lint": "1.5.1",

View file

@ -34,6 +34,10 @@ $cols10: (10 * ($column + $gutter) - $gutter) / $em;
$cols11: (11 * ($column + $gutter) - $gutter) / $em;
$cols12: (12 * ($column + $gutter) - $gutter) / $em;
$desktop: 942px;
$tablet: 640px;
$mobile: 480px;
//
// Column-widths in a function, in ems
//
@ -42,50 +46,67 @@ $cols12: (12 * ($column + $gutter) - $gutter) / $em;
width: ($cols * ($column + $gutter) - $gutter) / $em;
}
$desktop: 942px;
$tablet: 640px;
$mobile: 480px;
//4 columns
@media only screen and (max-width: $mobile - 1) {
#view {
text-align: center;
}
@mixin submobile ($parent-selector, $child-selector) {
@media only screen and (max-width: $mobile - 1) {
#{$parent-selector} {
text-align: center;
}
.inner {
margin: 0 auto;
width: 100%;
#{$child-selector} {
margin: 0 auto;
width: 100%;
}
@content;
}
}
//6 columns
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
#view {
text-align: center;
}
@mixin mobile ($parent-selector, $child-selector) {
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
#{$parent-selector} {
text-align: center;
}
.inner {
margin: 0 auto;
width: $mobile;
#{$child-selector} {
margin: 0 auto;
width: $mobile;
}
@content;
}
}
//8 columns
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
#view {
text-align: center;
}
@mixin tablet ($parent-selector, $child-selector) {
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
#{$parent-selector} {
text-align: center;
}
.inner {
margin: 0 auto;
width: $tablet;
#{$child-selector} {
margin: 0 auto;
width: $tablet;
}
}
}
//12 columns
@media only screen and (min-width: $desktop) {
.inner {
margin: 0 auto;
width: $desktop;
@mixin desktop ($parent-selector, $child-selector) {
@media only screen and (min-width: $desktop) {
#{$child-selector} {
margin: 0 auto;
width: $desktop;
}
}
}
@mixin responsive-layout ($parent-selector, $child-selector) {
@include submobile($parent-selector, $child-selector);
@include mobile($parent-selector, $child-selector);
@include tablet($parent-selector, $child-selector);
@include desktop($parent-selector, $child-selector);
}
@include responsive-layout("#view", ".inner");

View file

@ -0,0 +1,17 @@
var classNames = require('classnames');
var React = require('react');
require('./card.scss');
var Card = React.createClass({
displayName: 'Card',
render: function () {
return (
<div className={classNames(['card', this.props.className])}>
{this.props.children}
</div>
);
}
});
module.exports = Card;

View file

@ -0,0 +1,8 @@
@import "../../colors";
@import "../../frameless";
.card {
border-radius: 8px / $em;
box-shadow: 0 0 0 .125rem $active-gray;
background-color: $ui-white;
}

View file

@ -0,0 +1,20 @@
var classNames = require('classnames');
var React = require('react');
require('./deck.scss');
var Deck = React.createClass({
displayName: 'Deck',
render: function () {
return (
<div className={classNames(['deck', this.props.className])}>
<div className="inner">
<img src="/images/logo_sm.png" />
{this.props.children}
</div>
</div>
);
}
});
module.exports = Deck;

View file

@ -0,0 +1,154 @@
@import "../../colors";
@import "../../frameless";
@include responsive-layout (".deck", ".slide");
.deck {
min-height: 100vh;
img {
margin-left: 2px;
padding: 12px 0;
width: 76px;
}
.step-navigation {
margin-top: 2rem;
}
}
.slide {
max-width: 28.75rem;
h2,
p {
text-align: center;
color: $type-white;
}
.description {
margin-top: 0;
margin-bottom: 2rem;
}
}
.card {
margin: 0 auto;
width: 23.75rem;
&,
h2,
p {
color: $type-gray;
}
}
.step-navigation {
text-align: center;
}
.form {
padding: 3rem 4rem;
.form-group {
margin-bottom: 1.2rem;
&.has-error {
.input {
border: 1px solid $ui-orange;
}
}
}
.button {
margin: 0 0 -3rem -4rem;
border-radius: .5rem;
box-shadow: none;
width: 23.75rem;
height: 4rem;
&.card-button {
display: block;
border-top-left-radius: 0;
border-top-right-radius: 0;
background-color: $ui-aqua;
}
&:hover {
box-shadow: none;
}
}
}
.input {
width: $cols5;
}
.help-block {
$arrow-border-width: 1rem;
display: block;
position: absolute;
margin-left: $arrow-border-width;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
padding: 1rem;
max-width: 18.75rem;
min-height: 1rem;
max-height: 3rem;
overflow: visible;
color: $type-white;
&:before {
display: block;
position: absolute;
top: 1rem;
left: -$arrow-border-width / 2;
transform: rotate(45deg);
border-bottom: 1px solid $active-gray;
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
width: $arrow-border-width;
height: $arrow-border-width;
content: "";
}
}
@media only screen and (max-width: $mobile - 1) {
.card {
width: 22.5rem;
}
.form {
text-align: left;
.button {
width: 22.5rem;
}
}
}
@media only screen and (max-width: $tablet - 1) {
.input {
width: 90%;
}
}
@media only screen and (max-width: $desktop - 1) {
.help-block {
position: relative;
transform: none;
margin: inherit;
width: 100%;
height: inherit;
&:before {
display: none;
}
}
}

View file

@ -12,7 +12,28 @@
justify-content: center;
}
&.uneven {
align-items: flex-start;
.short {
width: $cols3;
}
.long {
width: $cols8;
text-align: left;
}
}
@media only screen and (max-width: $tablet - 1) {
flex-direction: column;
&.uneven {
.short,
.long {
margin: auto;
width: 90%;
}
}
}
}

View file

@ -0,0 +1,28 @@
var classNames = require('classnames');
var React = require('react');
require('./charcount.scss');
var CharCount = React.createClass({
type: 'CharCount',
getDefaultProps: function () {
return {
maxCharacters: 0,
currentCharacters: 0
};
},
render: function () {
var classes = classNames(
'char-count',
this.props.className,
{overmax: (this.props.currentCharacters > this.props.maxCharacters)}
);
return (
<p className={classes}>
{this.props.currentCharacters}/{this.props.maxCharacters}
</p>
);
}
});
module.exports = CharCount;

View file

@ -0,0 +1,13 @@
@import "../../colors";
p {
&.char-count {
letter-spacing: 1px;
color: lighten($type-gray, 30%);
font-weight: 500;
&.overmax {
color: $ui-orange;
}
}
}

View file

@ -0,0 +1,25 @@
var classNames = require('classnames');
var FRCCheckboxGroup = require('formsy-react-components').CheckboxGroup;
var React = require('react');
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
var inputHOC = require('./input-hoc.jsx');
require('./row.scss');
require('./checkbox-group.scss');
var CheckboxGroup = React.createClass({
type: 'CheckboxGroup',
render: function () {
var classes = classNames(
'checkbox-group',
this.props.className
);
return (
<div className={classes}>
<FRCCheckboxGroup {... this.props} className={classes} />
</div>
);
}
});
module.exports = inputHOC(defaultValidationHOC(CheckboxGroup));

View file

@ -0,0 +1,9 @@
.checkbox-group {
.col-sm-9 {
flex-flow: column wrap;
.checkbox {
margin: .5rem 0;
}
}
}

View file

@ -0,0 +1,25 @@
var classNames = require('classnames');
var FRCCheckbox = require('formsy-react-components').Checkbox;
var React = require('react');
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
var inputHOC = require('./input-hoc.jsx');
require('./row.scss');
require('./checkbox.scss');
var Checkbox = React.createClass({
type: 'Checkbox',
render: function () {
var classes = classNames(
'checkbox-row',
this.props.className
);
return (
<div className={classes}>
<FRCCheckbox {... this.props} />
</div>
);
}
});
module.exports = inputHOC(defaultValidationHOC(Checkbox));

View file

@ -0,0 +1,43 @@
@import "../../colors";
@import "../../frameless";
.row {
.checkbox {
label {
font-weight: 300;
}
}
}
input {
&[type=checkbox] {
display: block;
float: left;
margin-right: 1rem;
border: 1px solid $active-gray;
border-radius: 3px;
width: 1.25rem;
height: 1.25rem;
appearance: none;
&:checked,
&:focus {
outline: none;
}
&:checked {
transition: all .5s ease;
box-shadow: 0 0 0 .25rem $active-gray;
background-color: $ui-blue;
text-align: center;
text-indent: .125rem;
line-height: 1.25rem;
font-size: .75rem;
&:after {
color: $type-white;
content: "\2714";
}
}
}
}

View file

@ -0,0 +1,31 @@
var classNames = require('classnames');
var Formsy = require('formsy-react');
var React = require('react');
var GeneralError = require('./general-error.jsx');
var validations = require('./validations.jsx').validations;
for (var validation in validations) {
Formsy.addValidationRule(validation, validations[validation]);
}
var Form = React.createClass({
getDefaultProps: function () {
return {
noValidate: true
};
},
render: function () {
var classes = classNames(
'form',
this.props.className
);
return (
<Formsy.Form {... this.props} className={classes}>
<GeneralError name="all" />
{this.props.children}
</Formsy.Form>
);
}
});
module.exports = Form;

View file

@ -0,0 +1,20 @@
var Formsy = require('formsy-react');
var React = require('react');
/*
* A special formsy-react component that only outputs
* error messages. If you want to display errors that
* don't apply to a specific field, insert one of these,
* give it a name, and apply your validation error to
* the name of the GeneralError component.
*/
module.exports = Formsy.HOC(React.createClass({
render: function () {
if (!this.props.showError()) return null;
return (
<p className="error">
{this.props.getErrorMessage()}
</p>
);
}
}));

View file

@ -0,0 +1,20 @@
var React = require('react');
module.exports = function InputComponentMixin (Component) {
var InputComponent = React.createClass({
getDefaultProps: function () {
return {
messages: {
'general.notRequired': 'Not Required'
}
};
},
render: function () {
return (
<Component help={this.props.required ? null : this.props.messages['general.notRequired']}
{...this.props} />
);
}
});
return InputComponent;
};

View file

@ -1,22 +1,46 @@
var React = require('react');
var classNames = require('classnames');
var FRCInput = require('formsy-react-components').Input;
var React = require('react');
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
var inputHOC = require('./input-hoc.jsx');
require('./input.scss');
require('./row.scss');
var Input = React.createClass({
type: 'Input',
propTypes: {
getDefaultProps: function () {
return {};
},
getInitialState: function () {
return {
status: ''
};
},
onValid: function () {
this.setState({
status: 'pass'
});
},
onInvalid: function () {
this.setState({
status: 'fail'
});
},
render: function () {
var classes = classNames(
'input',
this.state.status,
this.props.className
);
return (
<input {... this.props} className={classes} />
return (this.props.type === 'submit' || this.props.noformsy ?
<input {... this.props} className={classes} /> :
<FRCInput {... this.props}
className={classes}
onValid={this.onValid}
onInvalid={this.onInvalid} />
);
}
});
module.exports = Input;
module.exports = inputHOC(defaultValidationHOC(Input));

View file

@ -1,30 +1,34 @@
@import "../../colors";
@import "../../frameless";
$base-bg: $ui-white;
$focus-bg: lighten($ui-blue, 35%);
$fail-bg: lighten($ui-orange, 35%);
$base-bg: $ui-light-gray;
$pass-bg: lighten($ui-aqua, 35%);
.row {
label {
font-weight: 500;
}
}
.input {
transition: all 1s ease;
margin: .5em 0;
transition: all .5s ease;
margin: .75rem 0;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $base-bg;
padding: .75em 1em;
padding: 0 1rem;
height: 3rem;
color: $type-gray;
font-size: .8rem;
font-size: .875rem;
&:focus {
transition: all 1s ease;
transition: all .5s ease;
outline: none;
border: 1px solid $active-dark-gray;
background-color: $focus-bg;
border: 1px solid $ui-blue;
}
&.fail {
border: 1px solid $active-dark-gray;
background-color: $fail-bg;
border: 1px solid $ui-orange;
}
&.pass {

View file

@ -0,0 +1,68 @@
var classNames = require('classnames');
var React = require('react');
var FormsyMixin = require('formsy-react').Mixin;
var ReactPhoneInput = require('react-telephone-input/lib/withStyles');
var allCountries = require('react-telephone-input/lib/country_data').allCountries;
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
var validationHOCFactory = require('./validations.jsx').validationHOCFactory;
var Row = require('formsy-react-components').Row;
var ComponentMixin = require('formsy-react-components').ComponentMixin;
var inputHOC = require('./input-hoc.jsx');
var allIso2 = allCountries.map(function (country) {return country.iso2;});
require('./row.scss');
require('./phone-input.scss');
var PhoneInput = React.createClass({
displayName: 'PhoneInput',
mixins: [
FormsyMixin,
ComponentMixin
],
getDefaultProps: function () {
return {
validations: {
isPhone: true
},
flagsImagePath: '/images/flags.png',
defaultCountry: 'us'
};
},
onChangeInput: function (number, country) {
var value = {national_number: number, country_code: country};
this.setValue(value);
this.props.onChange(this.props.name, value);
},
render: function () {
var defaultCountry = PhoneInput.getDefaultProps().defaultCountry;
if (allIso2.indexOf(this.props.defaultCountry.toLowerCase()) !== -1) {
defaultCountry = this.props.defaultCountry.toLowerCase();
}
return (
<Row {... this.getRowProperties()}
htmlFor={this.getId()}
className={classNames('phone-input', this.props.className)}
>
<div className="input-group">
<ReactPhoneInput className="form-control"
{... this.props}
defaultCountry={defaultCountry}
onChange={this.onChangeInput}
id={this.getId()}
label={null}
disabled={this.isFormDisabled() || this.props.disabled}
/>
</div>
{this.renderHelp()}
{this.renderErrorMessage()}
</Row>
);
}
});
var phoneValidationHOC = validationHOCFactory({
isPhone: 'Please enter a valid phone number'
});
module.exports = inputHOC(defaultValidationHOC(phoneValidationHOC(PhoneInput)));

View file

@ -0,0 +1,42 @@
@import "../../colors";
.input-group {
margin: .75rem 0;
width: 100%;
}
.react-tel-input {
width: 100%;
input {
&[type=tel] {
background-color: $ui-light-gray;
height: 3rem;
&:focus {
outline: none;
}
}
}
.flag-dropdown {
background-color: $ui-light-gray;
height: 3rem;
.selected-flag {
background-color: $ui-light-gray;
height: 100%;
&:hover,
&:focus,
&:active {
background-color: $active-gray;
}
}
.country-list {
top: 3rem;
background-color: $ui-light-gray;
}
}
}

View file

@ -0,0 +1,23 @@
var classNames = require('classnames');
var FRCRadioGroup = require('formsy-react-components').RadioGroup;
var React = require('react');
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
var inputHOC = require('./input-hoc.jsx');
require('./row.scss');
require('./radio-group.scss');
var RadioGroup = React.createClass({
type: 'RadioGroup',
render: function () {
var classes = classNames(
'radio-group',
this.props.className
);
return (
<FRCRadioGroup {... this.props} className={classes} />
);
}
});
module.exports = inputHOC(defaultValidationHOC(RadioGroup));

View file

@ -0,0 +1,46 @@
@import "../../colors";
.row {
.radio {
label {
font-weight: 300;
}
}
}
.col-sm-9 {
display: flex;
flex-flow: row wrap;
input {
&[type="radio"] {
margin-top: 1px;
border: 1px solid $active-gray;
border-radius: 50%;
width: .875rem;
height: .875rem;
appearance: none;
&:checked,
&:focus {
outline: none;
}
&:checked {
transition: all .25s ease;
box-shadow: 0 0 0 .25rem $active-gray;
background-color: $ui-blue;
&:after {
display: block;
transform: translate(.25rem, .25rem);
border-radius: 50%;
background-color: $ui-white;
width: .25rem;
height: .25rem;
content: "";
}
}
}
}
}

View file

@ -0,0 +1,11 @@
/*
* Styles for the Row component used by formsy-react-components
* Should be imported for each component that extends one of
* the formsy-react-components
*/
.form-group {
.required-symbol {
display: none;
}
}

View file

@ -1,6 +1,11 @@
var React = require('react');
var classNames = require('classnames');
var defaults = require('lodash.defaultsdeep');
var FRCSelect = require('formsy-react-components').Select;
var React = require('react');
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
var inputHOC = require('./input-hoc.jsx');
require('./row.scss');
require('./select.scss');
var Select = React.createClass({
@ -13,12 +18,16 @@ var Select = React.createClass({
'select',
this.props.className
);
var props = this.props;
if (this.props.required && !this.props.value) {
props = defaults({}, this.props, {value: this.props.options[0].value});
}
return (
<select {... this.props} className={classes}>
{this.props.children}
</select>
<div className={classes}>
<FRCSelect {... props} />
</div>
);
}
});
module.exports = Select;
module.exports = inputHOC(defaultValidationHOC(Select));

View file

@ -1,9 +1,38 @@
@import "../../colors";
@import "../../frameless";
.select {
background-color: $ui-white;
width: 220px;
height: 28px;
line-height: 28px;
font-size: 1em;
label {
font-weight: 500;
}
}
select {
transition: all .5s ease;
margin: .75rem 0;
border: 1px solid $active-gray;
border-radius: 5px;
background: $ui-light-gray url("../../../static/svgs/forms/carot.svg") no-repeat right center;
padding-right: 4rem;
width: 100%;
height: 3rem;
text-indent: 1rem;
font-size: .875rem;
appearance: none;
&:focus {
transition: all .5s ease;
outline: none;
border: 1px solid $ui-blue;
}
&:focus,
&:hover {
background: $ui-light-gray url("../../../static/svgs/forms/carot-hover.svg") no-repeat right center;
}
> option {
background-color: $ui-white;
width: 100%;
}
}

View file

@ -0,0 +1,23 @@
var classNames = require('classnames');
var FRCTextarea = require('formsy-react-components').Textarea;
var React = require('react');
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
var inputHOC = require('./input-hoc.jsx');
require('./row.scss');
require('./textarea.scss');
var TextArea = React.createClass({
type: 'TextArea',
render: function () {
var classes = classNames(
'textarea',
this.props.className
);
return (
<FRCTextarea {... this.props} className={classes} />
);
}
});
module.exports = inputHOC(defaultValidationHOC(TextArea));

View file

@ -0,0 +1,25 @@
@import "../../colors";
.textarea {
transition: all 1s ease;
margin: .75rem 0;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-light-gray;
padding: .75rem 1rem;
width: 100%;
min-height: 15rem;
line-height: 1.75em;
color: $type-gray;
font-size: .875rem;
&:focus {
transition: all 1s ease;
outline: none;
border: 1px solid $ui-blue;
}
&.fail {
border: 1px solid $ui-orange;
}
}

View file

@ -0,0 +1,46 @@
var defaults = require('lodash.defaultsdeep');
var libphonenumber = require('google-libphonenumber');
var phoneNumberUtil = libphonenumber.PhoneNumberUtil.getInstance();
var React = require('react');
module.exports = {};
module.exports.validations = {
notEquals: function (values, value, neq) {
return value !== neq;
},
notEqualsField: function (values, value, field) {
return value !== values[field];
},
isPhone: function (values, value) {
if (typeof value === 'undefined') return true;
if (value && value.national_number === '+') return true;
try {
var parsed = phoneNumberUtil.parse(value.national_number, value.country_code.iso2);
} catch (err) {
return false;
}
return phoneNumberUtil.isValidNumber(parsed);
}
};
module.exports.validationHOCFactory = function (defaultValidationErrors) {
return function (Component) {
var ValidatedComponent = React.createClass({
render: function () {
var validationErrors = defaults(
defaultValidationErrors,
this.props.validationErrors
);
return (
<Component {...this.props} validationErrors={validationErrors} />
);
}
});
return ValidatedComponent;
};
};
module.exports.defaultValidationHOC = module.exports.validationHOCFactory({
isDefaultRequiredValue: 'This field is required'
});

View file

@ -1,9 +1,9 @@
var classNames = require('classnames');
var React = require('react');
var Api = require('../../mixins/api.jsx');
var jar = require('../../lib/jar.js');
var languages = require('../../../languages.json');
var Form = require('../forms/form.jsx');
var Select = require('../forms/select.jsx');
/**
@ -11,18 +11,14 @@ var Select = require('../forms/select.jsx');
*/
var LanguageChooser = React.createClass({
type: 'LanguageChooser',
mixins: [
Api
],
getDefaultProps: function () {
return {
languages: languages,
locale: window._locale
};
},
onSetLanguage: function (e) {
e.preventDefault();
jar.set('scratchlanguage', e.target.value);
onSetLanguage: function (name, value) {
jar.set('scratchlanguage', value);
window.location.reload();
},
render: function () {
@ -30,17 +26,17 @@ var LanguageChooser = React.createClass({
'language-chooser',
this.props.className
);
var languageOptions = Object.keys(this.props.languages).map(function (value) {
return {value: value, label: this.props.languages[value]};
}.bind(this));
return (
<div className={classes}>
<Select name="language" defaultValue={this.props.locale} onChange={this.onSetLanguage}>
{Object.keys(this.props.languages).map(function (value) {
return <option value={value} key={value}>
{this.props.languages[value]}
</option>;
}.bind(this))}
</Select>
</div>
<Form className={classes}>
<Select name="language"
options={languageOptions}
defaultValue={this.props.locale}
onChange={this.onSetLanguage}
required />
</Form>
);
}
});

View file

@ -1,9 +1,9 @@
var React = require('react');
var ReactDOM = require('react-dom');
var FormattedMessage = require('react-intl').FormattedMessage;
var log = require('../../lib/log.js');
var Form = require('../forms/form.jsx');
var Input = require('../forms/input.jsx');
var Button = require('../forms/button.jsx');
var Spinner = require('../spinner/spinner.jsx');
@ -21,13 +21,9 @@ var Login = React.createClass({
waiting: false
};
},
handleSubmit: function (event) {
event.preventDefault();
handleSubmit: function (formData) {
this.setState({waiting: true});
this.props.onLogIn({
'username': ReactDOM.findDOMNode(this.refs.username).value,
'password': ReactDOM.findDOMNode(this.refs.password).value
}, function (err) {
this.props.onLogIn(formData, function (err) {
if (err) log.error(err);
this.setState({waiting: false});
}.bind(this));
@ -39,37 +35,29 @@ var Login = React.createClass({
}
return (
<div className="login">
<form onSubmit={this.handleSubmit}>
<Form onSubmit={this.handleSubmit}>
<label htmlFor="username" key="usernameLabel">
<FormattedMessage
id='general.username'
defaultMessage={'Username'} />
<FormattedMessage id='general.username' />
</label>
<Input type="text" ref="username" name="username" maxLength="30" key="usernameInput" />
<Input type="text" ref="username" name="username" maxLength="30" key="usernameInput" required />
<label htmlFor="password" key="passwordLabel">
<FormattedMessage
id='general.password'
defaultMessage={'Password'} />
<FormattedMessage id='general.password' />
</label>
<Input type="password" ref="password" name="password" key="passwordInput" />
<Input type="password" ref="password" name="password" key="passwordInput" required />
{this.state.waiting ? [
<Button className="submit-button white" type="submit" disabled="disabled" key="submitButton">
<Spinner />
</Button>
] : [
<Button className="submit-button white" type="submit" key="submitButton">
<FormattedMessage
id='general.signIn'
defaultMessage={'Sign in'} />
<FormattedMessage id='general.signIn' />
</Button>
]}
<a className="right" href="/accounts/password_reset/" key="passwordResetLink">
<FormattedMessage
id='login.forgotPassword'
defaultMessage={'Forgot Password?'} />
<FormattedMessage id='login.forgotPassword' />
</a>
{error}
</form>
</Form>
</div>
);
}

View file

@ -7,7 +7,7 @@ var injectIntl = ReactIntl.injectIntl;
var sessionActions = require('../../../redux/session.js');
var Api = require('../../../mixins/api.jsx');
var api = require('../../../lib/api');
var Avatar = require('../../avatar/avatar.jsx');
var Button = require('../../forms/button.jsx');
var Dropdown = require('../../dropdown/dropdown.jsx');
@ -24,9 +24,6 @@ Modal.setAppElement(document.getElementById('view'));
var Navigation = React.createClass({
type: 'Navigation',
mixins: [
Api
],
getInitialState: function () {
return {
accountNavOpen: false,
@ -85,7 +82,7 @@ var Navigation = React.createClass({
return '/users/' + this.props.session.session.user.username + '/';
},
getMessageCount: function () {
this.api({
api({
method: 'get',
uri: '/users/' + this.props.session.session.user.username + '/messages/count'
}, function (err, body) {
@ -110,7 +107,7 @@ var Navigation = React.createClass({
handleLogIn: function (formData, callback) {
this.setState({'loginError': null});
formData['useMessages'] = true;
this.api({
api({
method: 'post',
host: '',
uri: '/accounts/login/',
@ -142,7 +139,7 @@ var Navigation = React.createClass({
},
handleLogOut: function (e) {
e.preventDefault();
this.api({
api({
host: '',
method: 'post',
uri: '/accounts/logout/',
@ -220,7 +217,8 @@ var Navigation = React.createClass({
<Input type="text"
aria-label={formatMessage({id: 'general.search'})}
placeholder={formatMessage({id: 'general.search'})}
name="q" />
name="q"
noformsy />
</form>
</li>
{this.props.session.status === sessionActions.Status.FETCHED ? (

View file

@ -101,29 +101,6 @@
section {
padding: 64px 0;
}
.flex-row {
&.uneven {
align-items: flex-start;
.short {
width: $cols3;
}
.long {
width: $cols8;
text-align: left;
}
@media only screen and (max-width: $tablet - 1) {
.short,
.long {
margin: auto;
width: 90%;
}
}
}
}
}
@media only screen and (max-width: $mobile - 1) {

View file

@ -0,0 +1,42 @@
var classNames = require('classnames');
var React = require('react');
module.exports = React.createClass({
displayName: 'Progression',
propTypes: {
step: function (props, propName, componentName) {
var stepValidator = function (props, propName) {
if (props[propName] > -1 && props[propName] < props.children.length) {
return null;
} else {
return new Error('Prop `step` out of range');
}
};
return (
React.PropTypes.number.isRequired(props, propName, componentName) ||
stepValidator(props, propName, componentName)
);
}
},
getDefaultProps: function () {
return {
step: 0
};
},
render: function () {
var childProps = {
activeStep: this.props.step,
totalSteps: React.Children.count(this.props.children)
};
return (
<div {... this.props}
className={classNames('progression', this.props.className)}>
{React.Children.map(this.props.children, function (child, id) {
if (id === this.props.step) {
return React.cloneElement(child, childProps);
}
}, this)}
</div>
);
}
});

View file

@ -0,0 +1,705 @@
var React = require('react');
var api = require('../../lib/api');
var countryData = require('../../lib/country-data');
var intl = require('../../lib/intl.jsx');
var log = require('../../lib/log');
var smartyStreets = require('../../lib/smarty-streets');
var Button = require('../../components/forms/button.jsx');
var Card = require('../../components/card/card.jsx');
var CharCount = require('../../components/forms/charcount.jsx');
var Checkbox = require('../../components/forms/checkbox.jsx');
var CheckboxGroup = require('../../components/forms/checkbox-group.jsx');
var Form = require('../../components/forms/form.jsx');
var Input = require('../../components/forms/input.jsx');
var PhoneInput = require('../../components/forms/phone-input.jsx');
var RadioGroup = require('../../components/forms/radio-group.jsx');
var Select = require('../../components/forms/select.jsx');
var Slide = require('../../components/slide/slide.jsx');
var Spinner = require('../../components/spinner/spinner.jsx');
var StepNavigation = require('../../components/stepnavigation/stepnavigation.jsx');
var TextArea = require('../../components/forms/textarea.jsx');
var Tooltip = require('../../components/tooltip/tooltip.jsx');
var DEFAULT_COUNTRY = 'us';
var NextStepButton = React.createClass({
getDefaultProps: function () {
return {
waiting: false,
text: 'Next Step'
};
},
render: function () {
return (
<Button type="submit" disabled={this.props.waiting} className="card-button">
{this.props.waiting ?
<Spinner /> :
this.props.text
}
</Button>
);
}
});
module.exports = {
UsernameStep: intl.injectIntl(React.createClass({
getDefaultProps: function () {
return {
waiting: false
};
},
getInitialState: function () {
return {
showPassword: false,
waiting: false,
validUsername: ''
};
},
onChangeShowPassword: function (field, value) {
this.setState({showPassword: value});
},
onValidSubmit: function (formData, reset, invalidate) {
this.setState({waiting: true});
api({
host: '',
uri: '/accounts/check_username/' + formData.user.username + '/'
}, function (err, res) {
var formatMessage = this.props.intl.formatMessage;
this.setState({waiting: false});
if (err) return invalidate({all: err});
res = res[0];
switch (res.msg) {
case 'valid username':
this.setState({
validUsername: 'pass'
});
return this.props.onNextStep(formData);
case 'username exists':
return invalidate({
'user.username': formatMessage({id: 'general.validationUsernameExists'})
});
case 'bad username':
return invalidate({
'user.username': formatMessage({id: 'general.validationUsernameVulgar'})
});
case 'invalid username':
default:
return invalidate({
'user.username': formatMessage({id: 'general.validationUsernameInvalid'})
});
}
}.bind(this));
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
return (
<Slide className="username-step">
<h2><intl.FormattedMessage id="teacherRegistration.usernameStepTitle" /></h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.usernameStepDescription" />
</p>
<Card>
<Form onValidSubmit={this.onValidSubmit}>
<Input label={formatMessage({id: 'general.username'})}
className={this.state.validUsername}
type="text"
name="user.username"
validations={{
matchRegexp: /^[\w-]*$/,
minLength: 3,
maxLength: 20
}}
validationErrors={{
matchRegexp: formatMessage({
id: 'teacherRegistration.validationUsernameRegexp'
}),
minLength: formatMessage({
id: 'teacherRegistration.validationUsernameMinLength'
}),
maxLength: formatMessage({
id: 'teacherRegistration.validationUsernameMaxLength'
})
}}
required />
<Input label={formatMessage({id: 'general.password'})}
type={this.state.showPassword ? 'text' : 'password'}
name="user.password"
validations={{
minLength: 6,
notEquals: 'password',
notEqualsField: 'user.username'
}}
validationErrors={{
minLength: formatMessage({
id: 'teacherRegistration.validationPasswordLength'
}),
notEquals: formatMessage({
id: 'teacherRegistration.validationPasswordNotEquals'
}),
notEqualsField: formatMessage({
id: 'teacherRegistration.validationPasswordNotUsername'
})
}}
required />
<Checkbox label={formatMessage({id: 'teacherRegistration.showPassword'})}
value={this.state.showPassword}
onChange={this.onChangeShowPassword}
help={null}
name="showPassword" />
<NextStepButton waiting={this.props.waiting || this.state.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
</Slide>
);
}
})),
DemographicsStep: intl.injectIntl(React.createClass({
getDefaultProps: function () {
return {
defaultCountry: DEFAULT_COUNTRY,
waiting: false
};
},
getInitialState: function () {
return {otherDisabled: true};
},
getMonthOptions: function () {
return [
'January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December'
].map(function (label, id) {
return {
value: id+1,
label: this.props.intl.formatMessage({id: 'general.month' + label})};
}.bind(this));
},
getYearOptions: function () {
return Array.apply(null, Array(100)).map(function (v, id) {
var year = 2016 - id;
return {value: year, label: year};
});
},
onChooseGender: function (name, gender) {
this.setState({otherDisabled: gender !== 'other'});
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
return (
<Slide className="demographics-step">
<h2>
<intl.FormattedMessage id="teacherRegistration.personalStepTitle" />
</h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.personalStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.props.onNextStep}>
<Select label={formatMessage({id: 'general.birthMonth'})}
name="user.birth.month"
options={this.getMonthOptions()}
required />
<Select label={formatMessage({id: 'general.birthYear'})}
name="user.birth.year"
options={this.getYearOptions()} required />
<RadioGroup label={formatMessage({id: 'general.gender'})}
name="user.gender"
onChange={this.onChooseGender}
options={[
{value: 'female', label: formatMessage({id: 'general.female'})},
{value: 'male', label: formatMessage({id: 'general.male'})},
{value: 'other', label: ''}
]}
required />
<div className="gender-input">
<Input name="user.genderOther"
type="text"
disabled={this.state.otherDisabled}
required={!this.state.otherDisabled}
help={null} />
</div>
<Select label={formatMessage({id: 'general.country'})}
name="user.country"
options={countryData.countryOptions}
value={this.props.defaultCountry}
required />
<Checkbox className="demographics-checkbox-is-robot"
label="I'm a robot!"
name="user.isRobot" />
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
</Slide>
);
}
})),
NameStep: intl.injectIntl(React.createClass({
getDefaultProps: function () {
return {
waiting: false
};
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
return (
<Slide className="name-step">
<h2>
<intl.FormattedHTMLMessage id="teacherRegistration.nameStepTitle" />
</h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.nameStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.props.onNextStep}>
<Input label={formatMessage({id: 'teacherRegistration.firstName'})}
type="text"
name="user.name.first"
required />
<Input label={formatMessage({id: 'teacherRegistration.lastName'})}
type="text"
name="user.name.last"
required />
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
</Slide>
);
}
})),
PhoneNumberStep: intl.injectIntl(React.createClass({
getDefaultProps: function () {
return {
defaultCountry: DEFAULT_COUNTRY,
waiting: false
};
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
return (
<Slide className="phone-step">
<h2>
<intl.FormattedMessage id="teacherRegistration.phoneStepTitle" />
</h2>
<p>
<intl.FormattedMessage id="teacherRegistration.phoneStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.props.onNextStep}>
<PhoneInput label={formatMessage({id: 'teacherRegistration.phoneNumber'})}
name="phone"
defaultCountry={this.props.defaultCountry}
required />
<Checkbox label={formatMessage({id: 'teacherRegistration.phoneConsent'})}
name="phoneConsent"
required="isFalse"
validationErrors={{
isFalse: formatMessage({id: 'teacherRegistration.validationPhoneConsent'})
}} />
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
</Slide>
);
}
})),
OrganizationStep: intl.injectIntl(React.createClass({
getInitialState: function () {
return {
otherDisabled: true
};
},
getDefaultProps: function () {
return {
waiting: false
};
},
organizationL10nStems: [
'orgChoiceElementarySchool',
'orgChoiceMiddleSchool',
'orgChoiceHighSchool',
'orgChoiceUniversity',
'orgChoiceAfterschool',
'orgChoiceMuseum',
'orgChoiceLibrary',
'orgChoiceCamp',
'orgChoiceOther'
],
getOrganizationOptions: function () {
return this.organizationL10nStems.map(function (choice, id) {
return {
value: id,
label: this.props.intl.formatMessage({
id: 'teacherRegistration.' + choice
})
};
}.bind(this));
},
onChooseOrganization: function (name, values) {
this.setState({otherDisabled: values.indexOf(this.organizationL10nStems.indexOf('orgChoiceOther')) === -1});
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
return (
<Slide className="organization-step">
<h2>
<intl.FormattedMessage id="teacherRegistration.orgStepTitle" />
</h2>
<p>
<intl.FormattedMessage id="teacherRegistration.orgStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.props.onNextStep}>
<Input label={formatMessage({id: 'teacherRegistration.organization'})}
type="text"
name="organization.name"
required />
<Input label={formatMessage({id: 'teacherRegistration.orgTitle'})}
type="text"
name="organization.title"
required />
<div className="organization-type">
<b><intl.FormattedMessage id="teacherRegistration.orgType" /></b>
<p><intl.FormattedMessage id="teacherRegistration.checkAll" /></p>
<CheckboxGroup name="organization.type"
value={[]}
options={this.getOrganizationOptions()}
onChange={this.onChooseOrganization}
required />
</div>
<div className="other-input">
<Input type="text"
name="organization.other"
disabled={this.state.otherDisabled}
required="isFalse"
placeholder={formatMessage({id: 'general.other'})} />
</div>
<div className="url-input">
<b><intl.FormattedMessage id="general.website" /></b>
<p><intl.FormattedMessage id="teacherRegistration.notRequired" /></p>
<Input type="url"
name="organization.url"
required="isFalse"
placeholder={'http://'} />
</div>
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
</Slide>
);
}
})),
AddressStep: intl.injectIntl(React.createClass({
getDefaultProps: function () {
return {
defaultCountry: DEFAULT_COUNTRY,
waiting: false
};
},
getInitialState: function () {
return {
countryChoice: this.props.defaultCountry,
waiting: false
};
},
onChangeCountry: function (field, choice) {
this.setState({countryChoice: choice});
},
onValidSubmit: function (formData, reset, invalidate) {
if (formData.address.country !== 'us') {
return this.props.onNextStep(formData);
}
this.setState({waiting: true});
var address = {
street: formData.address.line1,
secondary: formData.address.line2 || '',
city: formData.address.city,
state: formData.address.state,
zipcode: formData.address.zip
};
smartyStreets(address, function (err, res) {
this.setState({waiting: false});
if (err) {
// We don't want to prevent registration because
// address validation isn't working. Log it and
// move on.
log.error(err);
return this.props.onNextStep(formData);
}
if (res && res.length > 0) {
return this.props.onNextStep(formData);
} else {
return invalidate({
'all': <FormattedMessage id="teacherRegistration.addressValidationError" />
});
}
}.bind(this));
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
var stateOptions = countryData.subdivisionOptions[this.state.countryChoice];
stateOptions = [{}].concat(stateOptions);
var countryOptions = countryData.countryOptions.concat({
label: formatMessage({id: 'teacherRegistration.selectCountry'}),
disabled: true,
selected: true
}).sort(function (a, b) {
if (a.disabled) return -1;
if (b.disabled) return 1;
if (a.value === this.props.defaultCountry) return -1;
if (b.value === this.props.defaultCountry) return 1;
return 0;
}.bind(this));
return (
<Slide className="address-step">
<h2>
<intl.FormattedMessage id="teacherRegistration.addressStepTitle" />
</h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.addressStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.onValidSubmit}>
<Select label={formatMessage({id: 'general.country'})}
name="address.country"
options={countryOptions}
onChange={this.onChangeCountry}
required />
<Input label={formatMessage({id: 'teacherRegistration.addressLine1'})}
type="text"
name="address.line1"
required />
<Input label={formatMessage({id: 'teacherRegistration.addressLine2'})}
type="text"
name="address.line2"
required="isFalse" />
<Input label={formatMessage({id: 'teacherRegistration.city'})}
type="text"
name="address.city"
required />
{stateOptions.length > 2 ?
<Select label={formatMessage({id: 'teacherRegistration.stateProvince'})}
name="address.state"
options={stateOptions}
required /> :
[]
}
<Input label={formatMessage({id: 'teacherRegistration.zipCode'})}
type="text"
name="address.zip"
required />
<NextStepButton waiting={this.props.waiting || this.state.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
</Slide>
);
}
})),
UseScratchStep: intl.injectIntl(React.createClass({
getDefaultProps: function () {
return {
waiting: false,
maxCharacters: 300
};
},
getInitialState: function () {
return {
characterCount: 0
};
},
handleTyping: function (name, value) {
this.setState({
characterCount: value.length
});
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
var textAreaClass = (this.state.characterCount > this.props.maxCharacters) ? 'fail' : '';
return (
<Slide className="usescratch-step">
<h2>
<intl.FormattedMessage id="teacherRegistration.useScratchStepTitle" />
</h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.useScratchStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.props.onNextStep}>
<TextArea label={formatMessage({id: 'teacherRegistration.howUseScratch'})}
name="useScratch"
className={textAreaClass}
onChange={this.handleTyping}
validations={{
maxLength: this.props.maxCharacters
}}
validationErrors={{
maxLength: formatMessage({
id: 'teacherRegistration.useScratchMaxLength'
})
}}
required />
<CharCount maxCharacters={this.props.maxCharacters}
currentCharacters={this.state.characterCount} />
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
</Slide>
);
}
})),
EmailStep: intl.injectIntl(React.createClass({
getDefaultProps: function () {
return {
waiting: false
};
},
getInitialState: function () {
return {
waiting: false
};
},
onValidSubmit: function (formData, reset, invalidate) {
this.setState({waiting: true});
api({
host: '',
uri: '/accounts/check_email/',
params: {email: formData.user.email}
}, function (err, res) {
this.setState({waiting: false});
if (err) return invalidate({all: err});
res = res[0];
switch (res.msg) {
case 'valid email':
return this.props.onNextStep(formData);
default:
return invalidate({'user.email': res.msg});
}
}.bind(this));
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
return (
<Slide className="email-step">
<h2>
<intl.FormattedMessage id="teacherRegistration.emailStepTitle" />
</h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.emailStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'teacherRegistration.nameStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.onValidSubmit}>
<Input label={formatMessage({id: 'general.emailAddress'})}
type="text"
name="user.email"
validations="isEmail"
validationError={formatMessage({id: 'general.validationEmail'})}
required />
<Input label={formatMessage({id: 'general.confirmEmail'})}
type="text"
name="confirmEmail"
validations="equalsField:user.email"
validationErrors={{
equalsField: formatMessage({id: 'general.validationEmailMatch'})
}}
required />
<NextStepButton waiting={this.props.waiting}
text={<intl.FormattedMessage id="teacherRegistration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
</Slide>
);
}
})),
TeacherApprovalStep: intl.injectIntl(React.createClass({
getDefaultProps: function () {
return {
email: null,
invited: false
};
},
render: function () {
return (
<Slide className="last-step">
<h2>
<intl.FormattedMessage id="registration.lastStepTitle" />
</h2>
<p className="description">
<intl.FormattedMessage id="registration.lastStepDescription" />
</p>
{this.props.confirmed || !this.props.email ?
[]
:
(<Card className="confirm">
<h4><intl.FormattedMessage id="registration.confirmYourEmail" /></h4>
<p>
<intl.FormattedMessage id="registration.confirmYourEmailDescription" /><br />
<strong>{this.props.email}</strong>
</p>
</Card>)
}
{this.props.invited ?
<Card className="wait">
<h4><intl.FormattedMessage id="registration.waitForApproval" /></h4>
<p>
<intl.FormattedMessage id="registration.waitForApprovalDescription" />
</p>
</Card>
:
[]
}
<Card className="resources">
<h4><intl.FormattedMessage id="registration.checkOutResources" /></h4>
<p>
<intl.FormattedHTMLMessage id="registration.checkOutResourcesDescription" />
</p>
</Card>
</Slide>
);
}
})),
RegistrationError: intl.injectIntl(React.createClass({
render: function () {
return (
<Slide className="error-step">
<h2>Something went wrong</h2>
<Card>
<h2>There was an error while processing your registration</h2>
<p>
{this.props.registrationError}
</p>
</Card>
</Slide>
);
}
}))
};

View file

@ -0,0 +1,17 @@
var classNames = require('classnames');
var React = require('react');
require('./slide.scss');
var Slide = React.createClass({
displayName: 'Slide',
render: function () {
return (
<div className={classNames(['slide', this.props.className])}>
{this.props.children}
</div>
);
}
});
module.exports = Slide;

View file

@ -0,0 +1,11 @@
@import "../../frameless";
.slide {
padding: 10px;
}
@media only screen and (max-width: $tablet - 1) {
.slide {
padding: 0;
}
}

View file

@ -2,6 +2,7 @@
.spinner {
position: relative;
margin: 0 auto;
width: 20px;
height: 20px;
@ -17,10 +18,14 @@
animation: circleFadeDelay 1.2s infinite ease-in-out both;
margin: 0 auto;
border-radius: 100%;
background-color: darken($ui-blue, 8%);
background-color: darken($ui-white, 8%);
width: 15%;
height: 15%;
content: "";
.white & {
background-color: darken($ui-blue, 8%);
}
}
}

View file

@ -0,0 +1,28 @@
var classNames = require('classnames');
var React = require('react');
require('./stepnavigation.scss');
var StepNavigation = React.createClass({
type: 'Navigation',
render: function () {
return (
<ul className={classNames('step-navigation', this.props.className)}>
{Array.apply(null, Array(this.props.steps)).map(function (v, step) {
return (
<li key={step}
className={classNames({
active: step < this.props.active,
selected: step === this.props.active
})}
>
<div className="indicator" />
</li>
);
}.bind(this))}
</ul>
);
}
});
module.exports = StepNavigation;

View file

@ -0,0 +1,31 @@
@import "../../colors";
@import "../../frameless";
.step-navigation {
margin: 0;
padding: 0;
list-style: none;
li {
display: inline-block;
border-radius: 50%;
padding: .25rem;
.indicator {
border-radius: .25rem;
background-color: $ui-white;
width: .5rem;
height: .5rem;
}
&.active {
.indicator {
background-color: $ui-aqua;
}
}
&.selected {
border: 1px solid $ui-white;
}
}
}

View file

@ -0,0 +1,33 @@
var classNames = require('classnames');
var React = require('react');
require('./tooltip.scss');
var Tooltip = React.createClass({
type: 'Tooltip',
getDefaultProps: function () {
return {
title: '',
tipContent: ''
};
},
render: function () {
var classes = classNames(
'tooltip',
this.props.className,
{overmax: (this.props.currentCharacters > this.props.maxCharacters)}
);
return (
<span className={classes}>
<span className="tip">
<img src="/svgs/tooltip/info.svg" alt="info icon" />
</span>
<span className="expand">
{this.props.tipContent}
</span>
</span>
);
}
});
module.exports = Tooltip;

View file

@ -0,0 +1,83 @@
@import "../../colors";
@import "../../frameless";
.tooltip {
.tip {
display: inline-flex;
margin: 0 .5rem;
img {
border-radius: 50%;
box-shadow: 0 0 0 .125rem $active-gray;
background-color: $ui-blue;
padding: .25rem;
width: .75rem;
height: .75rem;
}
}
.expand {
$arrow-border-width: 1rem;
position: absolute;
transform: translate(-2.75rem, 2rem);
visibility: hidden;
z-index: 1;
margin-top: $arrow-border-width / 2;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-blue;
padding: 1rem;
width: 13.75rem;
text-align: left;
color: $type-white;
&:before {
display: block;
position: absolute;
top: -$arrow-border-width / 2;
left: $arrow-border-width;
transform: rotate(45deg);
border-top: 1px solid $active-gray;
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-blue;
width: $arrow-border-width;
height: $arrow-border-width;
content: "";
}
}
&:hover {
.expand {
visibility: visible;
}
}
}
@media only screen and (max-width: $desktop - 1) {
.tooltip {
display: block;
.expand {
display: none;
position: relative;
clear: both;
transform: none;
&:before {
display: none;
}
}
&:hover {
.expand {
display: block;
margin: 5px auto;
}
}
}
}

View file

@ -2,20 +2,27 @@
"general.accountSettings": "Account settings",
"general.about": "About",
"general.aboutScratch": "About Scratch",
"general.birthMonth": "Birth Month",
"general.birthYear": "Birth Year",
"general.donate": "Donate",
"general.collaborators": "Collaborators",
"general.community": "Community",
"general.confirmEmail": "Confirm Email",
"general.contactUs": "Contact Us",
"general.copyright": "Scratch is a project of the Lifelong Kindergarten Group at the MIT Media Lab",
"general.country": "Country",
"general.create": "Create",
"general.credits": "Credits",
"general.discuss": "Discuss",
"general.dmca": "DMCA",
"general.emailAddress": "Email Address",
"general.explore": "Explore",
"general.faq": "FAQ",
"general.female": "Female",
"general.forParents": "For Parents",
"general.forEducators": "For Educators",
"general.forDevelopers": "For Developers",
"general.gender": "Gender",
"general.guidelines": "Community Guidelines",
"general.help": "Help",
"general.jobs": "Jobs",
@ -23,11 +30,27 @@
"general.legal": "Legal",
"general.loadMore": "Load More",
"general.learnMore": "Learn More",
"general.male": "Male",
"general.messages": "Messages",
"general.monthJanuary": "January",
"general.monthFebruary": "February",
"general.monthMarch": "March",
"general.monthApril": "April",
"general.monthMay": "May",
"general.monthJune": "June",
"general.monthJuly": "July",
"general.monthAugust": "August",
"general.monthSeptember": "September",
"general.monthOctober": "October",
"general.monthNovember": "November",
"general.monthDecember": "December",
"general.myClass": "My Class",
"general.myClasses": "My Classes",
"general.myStuff": "My Stuff",
"general.notRequired": "Not Required",
"general.other": "Other",
"general.offlineEditor": "Offline Editor",
"general.password": "Password",
"general.press": "Press",
"general.privacyPolicy": "Privacy Policy",
"general.projects": "Projects",
@ -51,7 +74,13 @@
"general.tipsPongGame": "Create a Pong Game",
"general.termsOfUse": "Terms of Use",
"general.username": "Username",
"general.validationEmail": "Please enter a valid email address",
"general.validationEmailMatch": "The emails do not match",
"general.validationUsernameExists": "Sorry, that username already exists",
"general.validationUsernameVulgar": "Hmm, that looks inappropriate",
"general.validationUsernameInvalid": "Invalid username",
"general.viewAll": "View All",
"general.website": "Website",
"general.whatsHappening": "What's Happening?",
"general.wiki": "Scratch Wiki",
@ -74,5 +103,15 @@
"parents.FaqAgeRangeA": "While Scratch is primarily designed for 8 to 16 year olds, it is also used by people of all ages, including younger children with their parents.",
"parents.FaqAgeRangeQ": "What is the age range for Scratch?",
"parents.FaqResourcesQ": "What resources are available for learning Scratch?",
"parents.introDescription": "Scratch is a programming language and an online community where children can program and share interactive media such as stories, games, and animation with people from all over the world. As children create with Scratch, they learn to think creatively, work collaboratively, and reason systematically. Scratch is designed and maintained by the Lifelong Kindergarten group at the MIT Media Lab."
"parents.introDescription": "Scratch is a programming language and an online community where children can program and share interactive media such as stories, games, and animation with people from all over the world. As children create with Scratch, they learn to think creatively, work collaboratively, and reason systematically. Scratch is designed and maintained by the Lifelong Kindergarten group at the MIT Media Lab.",
"registration.lastStepTitle": "Thank you for requesting a Scratch Teacher Account",
"registration.lastStepDescription": "We are currently processing your application. ",
"registration.confirmYourEmail": "Confirm Your Email",
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
"registration.waitForApproval": "Wait for Approval",
"registration.waitForApprovalDescription": "Your information is being reviewed. Please be patient, the approval process can take up to 24 hours. You will receive an email with your login information once your account has been created.",
"registration.checkOutResources": "Get Started with Resources",
"registration.checkOutResourcesDescription": "Explore materials for educators and facilitators written by the Scratch Team, including <a href='/educators#resources'>tips, tutorials, and guides</a>."
}

83
src/lib/api.js Normal file
View file

@ -0,0 +1,83 @@
var defaults = require('lodash.defaults');
var xhr = require('xhr');
var jar = require('./jar');
var log = require('./log');
var urlParams = require('./url-params');
/**
* Helper method that constructs requests to the scratch api.
* Custom arguments:
* - useCsrf [boolean] handles unique csrf token retrieval for POST requests. This prevents
* CSRF forgeries (see: https://www.squarefree.com/securitytips/web-developers.html#CSRF)
*
* It also takes in other arguments specified in the xhr library spec.
*/
module.exports = function (opts, callback) {
defaults(opts, {
host: process.env.API_HOST,
headers: {},
responseType: 'json',
useCsrf: false
});
defaults(opts.headers, {
'X-Requested-With': 'XMLHttpRequest'
});
opts.uri = opts.host + opts.uri;
if (opts.params) {
opts.uri = [opts.uri, urlParams(opts.params)]
.join(opts.uri.indexOf('?') === -1 ? '?' : '&');
}
if (opts.formData) {
opts.body = urlParams(opts.formData);
opts.headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
var apiRequest = function (opts) {
if (opts.host !== '') {
// For IE < 10, we must use XDR for cross-domain requests. XDR does not support
// custom headers.
defaults(opts, {useXDR: true});
delete opts.headers;
if (opts.authentication) {
var authenticationParams = ['x-token=' + opts.authentication];
var parts = opts.uri.split('?');
var qs = (parts[1] || '').split('&').concat(authenticationParams).join('&');
opts.uri = parts[0] + '?' + qs;
}
}
xhr(opts, function (err, res, body) {
if (err) log.error(err);
// Legacy API responses come as lists, and indicate to redirect the client like
// [{success: true, redirect: "/location/to/redirect"}]
try {
if ('redirect' in body[0]) window.location = body[0].redirect;
} catch (err) {
// do nothing
}
callback(err, body, res);
});
}.bind(this);
if (typeof jar.get('scratchlanguage') !== 'undefined') {
opts.headers['Accept-Language'] = jar.get('scratchlanguage') + ', en;q=0.8';
}
if (opts.authentication) {
opts.headers['X-Token'] = opts.authentication;
}
if (opts.useCsrf) {
jar.use('scratchcsrftoken', '/csrf_token/', function (err, csrftoken) {
if (err) return log.error('Error while retrieving CSRF token', err);
opts.headers['X-CSRFToken'] = csrftoken;
apiRequest(opts);
}.bind(this));
} else {
apiRequest(opts);
}
};

22
src/lib/country-data.js Normal file
View file

@ -0,0 +1,22 @@
module.exports = {};
var countries = module.exports.data = require('iso-3166-2').data;
module.exports.countryOptions = Object.keys(countries).map(function (code) {
return {value: code.toLowerCase(), label: countries[code].name};
}).sort(function (a, b) {
return a.label < b.label ? -1 : 1;
});
module.exports.subdivisionOptions =
Object.keys(countries).reduce(function (subByCountry, code) {
subByCountry[code.toLowerCase()] = Object.keys(countries[code].sub).map(function (subCode) {
return {
value: subCode.toLowerCase(),
label: countries[code].sub[subCode].name,
type: countries[code].sub[subCode].type
};
}).sort(function (a, b) {
return a.label < b.label ? -1 : 1;
});
return subByCountry;
}, {});

21
src/lib/smarty-streets.js Normal file
View file

@ -0,0 +1,21 @@
var defaults = require('lodash.defaults');
var api = require('./api');
module.exports = function smartyStreetApi (params, callback) {
defaults(params, {
'auth-id': process.env.SMARTY_STREETS_API_KEY
});
api({
host: 'https://api.smartystreets.com',
uri: '/street-address',
params: params
}, function (err, body, res) {
if (err) return callback(err);
if (res.statusCode !== 200) {
return callback(
'There was an error contacting the address validation server.'
);
}
return callback(err, body);
});
};

22
src/lib/url-params.js Normal file
View file

@ -0,0 +1,22 @@
/* Turn an object into an url param string
* urlParams({a: 1, b: 2, c: 3})
* // a=1&b=2&c=3
*/
module.exports = function urlParams (values) {
return Object
.keys(values)
.map(function (key) {
var value = typeof values[key] === 'undefined' ? '' : values[key];
function encodeKeyValuePair (value) {
return [key, value]
.map(encodeURIComponent)
.join('=');
}
if (Array.isArray(value)) {
return value.map(encodeKeyValuePair).join('&');
} else {
return encodeKeyValuePair(value);
}
})
.join('&');
};

View file

@ -83,7 +83,8 @@ p {
}
}
b {
b,
strong {
font-weight: 500;
}

View file

@ -1,76 +0,0 @@
var defaults = require('lodash.defaults');
var xhr = require('xhr');
var jar = require('../lib/jar.js');
var log = require('../lib/log.js');
/**
* Component mixin that constructs requests to the scratch api.
* Custom arguments:
* - useCsrf [boolean] handles unique csrf token retrieval for POST requests. This prevents
* CSRF forgeries (see: https://www.squarefree.com/securitytips/web-developers.html#CSRF)
*
* It also takes in other arguments specified in the xhr library spec.
*/
var Api = {
api: function (opts, callback) {
defaults(opts, {
host: window.env.API_HOST,
headers: {},
json: {},
useCsrf: false
});
defaults(opts.headers, {
'X-Requested-With': 'XMLHttpRequest'
});
opts.uri = opts.host + opts.uri;
var apiRequest = function (opts) {
if (opts.host !== '') {
// For IE < 10, we must use XDR for cross-domain requests. XDR does not support
// custom headers.
defaults(opts, {useXDR: true});
delete opts.headers;
if (opts.authentication) {
var authenticationParams = ['x-token=' + opts.authentication];
var parts = opts.uri.split('?');
var qs = (parts[1] || '').split('&').concat(authenticationParams).join('&');
opts.uri = parts[0] + '?' + qs;
}
}
xhr(opts, function (err, res, body) {
if (err) log.error(err);
// Legacy API responses come as lists, and indicate to redirect the client like
// [{success: true, redirect: "/location/to/redirect"}]
try {
if ('redirect' in body[0]) window.location = body[0].redirect;
} catch (err) {
// do nothing
}
callback(err, body);
});
}.bind(this);
if (typeof jar.get('scratchlanguage') !== 'undefined') {
opts.headers['Accept-Language'] = jar.get('scratchlanguage') + ', en;q=0.8';
}
if (opts.authentication) {
opts.headers['X-Token'] = opts.authentication;
}
if (opts.useCsrf) {
jar.use('scratchcsrftoken', '/csrf_token/', function (err, csrftoken) {
if (err) return log.error('Error while retrieving CSRF token', err);
opts.json.csrftoken = csrftoken;
opts.headers['X-CSRFToken'] = csrftoken;
apiRequest(opts);
}.bind(this));
} else {
apiRequest(opts);
}
}
};
module.exports = Api;

View file

@ -1,17 +0,0 @@
var jar = require('../lib/jar');
var cookieMixinFactory = function (cookieName, cookieSetter) {
var capitalizedCookieName = cookieName.charAt(0).toUpperCase() + cookieName.slice(1);
var getterName = 'get' + capitalizedCookieName;
var userName = 'use' + capitalizedCookieName;
var mixin = {};
mixin[getterName] = function (callback) {
jar.get(cookieName, callback);
};
mixin[userName] = function (callback) {
jar.use(cookieName, cookieSetter, callback);
};
return mixin;
};
module.exports = cookieMixinFactory;

View file

@ -1,5 +1,5 @@
var keyMirror = require('keymirror');
var api = require('../mixins/api.jsx').api;
var api = require('../lib/api');
var Types = keyMirror({
SET_DETAILS: null,

View file

@ -1,5 +1,5 @@
var keyMirror = require('keymirror');
var api = require('../mixins/api.jsx').api;
var api = require('../lib/api');
var Types = keyMirror({
SET_SCHEDULE: null,

View file

@ -1,7 +1,7 @@
var keyMirror = require('keymirror');
var defaults = require('lodash.defaults');
var api = require('../mixins/api.jsx').api;
var api = require('../lib/api');
var tokenActions = require('./token.js');
var Types = keyMirror({

View file

@ -91,6 +91,19 @@
"view": "jobs/jobs",
"title": "Jobs"
},
{
"name": "teacherregistration",
"pattern": "^/educators/register$",
"view": "teacherregistration/teacherregistration",
"title": "Teacher Registration",
"viewportWidth": "device-width"
},
{
"name": "teacherwaitingroom",
"pattern": "^/educators/waiting",
"view": "teacherwaitingroom/teacherwaitingroom",
"title": "Thank you for requesting a Scratch Teacher Account"
},
{
"name": "wedo2",
"pattern": "^/wedo/?$",

View file

@ -1,7 +1,4 @@
module.exports = {
// Bind environment
api_host: process.env.API_HOST || 'https://api.scratch.mit.edu',
// Search and metadata
title: 'Imagine, Program, Share',
description:

View file

@ -31,13 +31,6 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/lib/normalize.min.css" />
<!-- Environment -->
<script>
window.env = {
API_HOST: "{{&api_host}}"
};
</script>
<!-- Polyfills -->
<script src="/js/polyfill.min.js"></script>

View file

@ -3,7 +3,7 @@ var FormattedMessage = require('react-intl').FormattedMessage;
var React = require('react');
var render = require('../../lib/render.jsx');
var Api = require('../../mixins/api.jsx');
var api = require('../../lib/api');
var Page = require('../../components/page/www/page.jsx');
var Box = require('../../components/box/box.jsx');
@ -16,9 +16,6 @@ require('./explore.scss');
// @todo migrate to React-Router once available
var Explore = injectIntl(React.createClass({
type: 'Explore',
mixins: [
Api
],
getDefaultProps: function () {
var categoryOptions = ['all','animations','art','games','music','stories'];
var typeOptions = ['projects','studios'];
@ -57,7 +54,7 @@ var Explore = injectIntl(React.createClass({
if (this.props.tab != 'all') {
qText = '&q=' + this.props.category;
}
this.api({
api({
uri: '/search/' + this.props.itemType +
'?limit=' + this.props.loadNumber +
'&offset=' + this.state.offset +

View file

@ -3,7 +3,7 @@ var FormattedMessage = require('react-intl').FormattedMessage;
var React = require('react');
var render = require('../../lib/render.jsx');
var Api = require('../../mixins/api.jsx');
var api = require('../../lib/api');
var Page = require('../../components/page/www/page.jsx');
var Box = require('../../components/box/box.jsx');
@ -16,9 +16,6 @@ require('./search.scss');
// @todo migrate to React-Router once available
var Search = injectIntl(React.createClass({
type: 'Search',
mixins: [
Api
],
getDefaultProps: function () {
var query = window.location.search;
var pathname = window.location.pathname.toLowerCase();
@ -60,7 +57,7 @@ var Search = injectIntl(React.createClass({
if (this.props.searchTerm !== '') {
termText = '&q=' + this.props.searchTerm;
}
this.api({
api({
uri: '/search/' + this.props.tab +
'?limit=' + this.props.loadNumber +
'&offset=' + this.state.offset +

View file

@ -2,14 +2,13 @@ var connect = require('react-redux').connect;
var injectIntl = require('react-intl').injectIntl;
var omit = require('lodash.omit');
var React = require('react');
var render = require('../../lib/render.jsx');
var api = require('../../lib/api');
var permissionsActions = require('../../redux/permissions.js');
var render = require('../../lib/render.jsx');
var sessionActions = require('../../redux/session.js');
var shuffle = require('../../lib/shuffle.js').shuffle;
var Api = require('../../mixins/api.jsx');
var Activity = require('../../components/activity/activity.jsx');
var AdminPanel = require('../../components/adminpanel/adminpanel.jsx');
var DropdownBanner = require('../../components/dropdown-banner/banner.jsx');
@ -27,9 +26,6 @@ require('./splash.scss');
var Splash = injectIntl(React.createClass({
type: 'Splash',
mixins: [
Api
],
getInitialState: function () {
return {
projectCount: 14000000, // gets the shared project count
@ -97,42 +93,42 @@ var Splash = injectIntl(React.createClass({
}
},
getActivity: function () {
this.api({
api({
uri: '/proxy/users/' + this.props.session.session.user.username + '/activity?limit=5'
}, function (err, body) {
if (!err) this.setState({activity: body});
}.bind(this));
},
getFeaturedGlobal: function () {
this.api({
api({
uri: '/proxy/featured'
}, function (err, body) {
if (!err) this.setState({featuredGlobal: body});
}.bind(this));
},
getFeaturedCustom: function () {
this.api({
api({
uri: '/proxy/users/' + this.props.session.session.user.id + '/featured'
}, function (err, body) {
if (!err) this.setState({featuredCustom: body});
}.bind(this));
},
getNews: function () {
this.api({
api({
uri: '/news?limit=3'
}, function (err, body) {
if (!err) this.setState({news: body});
}.bind(this));
},
getProjectCount: function () {
this.api({
api({
uri: '/projects/count/all'
}, function (err, body) {
if (!err) this.setState({projectCount: body.count});
}.bind(this));
},
refreshHomepageCache: function () {
this.api({
api({
host: '',
uri: '/scratch_admin/homepage/clear-cache/',
method: 'post',
@ -168,7 +164,7 @@ var Splash = injectIntl(React.createClass({
this.setState({emailConfirmationModalOpen: false});
},
handleDismiss: function (cue) {
this.api({
api({
host: '',
uri: '/site-api/users/set-template-cue/',
method: 'post',

View file

@ -0,0 +1,56 @@
{
"teacherRegistration.usernameStepDescription": "Fill in the following forms to request an account. The approval process may take up to 24 hours.",
"teacherRegistration.usernameStepTitle": "Request a Teacher Account",
"teacherRegistration.validationUsernameRegexp": "Your username may only contain characters and \"-\"",
"teacherRegistration.validationUsernameMinLength": "Usernames must be at least 3 characters",
"teacherRegistration.validationUsernameMaxLength": "Usernames must be at most 20 characters",
"teacherRegistration.validationPasswordLength": "Passwords must be at least six characters",
"teacherRegistration.validationPasswordNotEquals": "Your password may not be \"password\"",
"teacherRegistration.validationPasswordNotUsername": "Your password may not be your username",
"teacherRegistration.showPassword": "Show password",
"teacherRegistration.nextStep": "Next Step",
"teacherRegistration.personalStepTitle": "Personal Information",
"teacherRegistration.personalStepDescription": "Your individual responses will not be displayed publicly, and will be kept confidential and secure",
"teacherRegistration.nameStepTitle": "First &amp; Last Name",
"teacherRegistration.nameStepDescription": "Your name will not be displayed publicly, and will be kept confidential and secure.",
"teacherRegistration.nameStepTooltip": "This information is used for verification and to aggregate usage statistics.",
"teacherRegistration.firstName": "First Name",
"teacherRegistration.lastName": "Last Name",
"teacherRegistration.phoneStepTitle": "Phone Number",
"teacherRegistration.phoneNumber": "Phone Number",
"teacherRegistration.phoneStepDescription": "Your phone number will not be displayed publicly, and will be kept confidential and secure",
"teacherRegistration.phoneConsent": "Yes, the Scratch Team may call me to verify my Teacher Account if needed",
"teacherRegistration.validationPhoneConsent": "You must consent to verification of your Teacher Account",
"teacherRegistration.orgStepTitle": "Organization",
"teacherRegistration.orgStepDescription": "Your information will not be displayed publicly, and will be kept confidential and secure",
"teacherRegistration.organization": "Organization",
"teacherRegistration.orgTitle": "Your Role",
"teacherRegistration.orgType": "Type of Organization",
"teacherRegistration.checkAll": "Check all that apply",
"teacherRegistration.orgChoiceElementarySchool": "Elementary School",
"teacherRegistration.orgChoiceMiddleSchool": "Middle School",
"teacherRegistration.orgChoiceHighSchool": "High School",
"teacherRegistration.orgChoiceUniversity": "College/University",
"teacherRegistration.orgChoiceAfterschool": "Afterscool Program",
"teacherRegistration.orgChoiceMuseum": "Museum",
"teacherRegistration.orgChoiceLibrary": "Library",
"teacherRegistration.orgChoiceCamp": "Camp",
"teacherRegistration.orgChoiceOther": " ",
"teacherRegistration.notRequired": "Not Required",
"teacherRegistration.selectCountry": "select country",
"teacherRegistration.validationAddress": "This doesn't look like a real address",
"teacherRegistration.addressLine1": "Address Line 1",
"teacherRegistration.addressLine2": "Address Line 2 (Optional)",
"teacherRegistration.zipCode": "ZIP",
"teacherRegistration.stateProvince": "State",
"teacherRegistration.city": "City",
"teacherRegistration.addressStepTitle": "Address",
"teacherRegistration.addressStepDescription": "Your information will not be displayed publicly, and will be kept confidential and secure.",
"teacherRegistration.useScratchStepTitle": "How you plan to use Scratch",
"teacherRegistration.useScratchStepDescription": "Tell us a little about how you plan to use Scratch. Why do we ask for this information",
"teacherRegistration.useScratchMaxLength": "Description must be at most 300 characters",
"teacherRegistration.howUseScratch": "How do you plan to use Scratch at your organization?",
"teacherRegistration.emailStepTitle": "Email Address",
"teacherRegistration.emailStepDescription": "We will send you a confirmation email that will allow you to access your Scratch Teacher Account.",
"teacherRegistration.validationEmailMatch": "The emails do not match"
}

View file

@ -0,0 +1,112 @@
var defaults = require('lodash.defaultsdeep');
var React = require('react');
var render = require('../../lib/render.jsx');
var api = require('../../lib/api');
var Deck = require('../../components/deck/deck.jsx');
var Progression = require('../../components/progression/progression.jsx');
var Steps = require('../../components/registration/steps.jsx');
require('./teacherregistration.scss');
var TeacherRegistration = React.createClass({
type: 'TeacherRegistration',
getInitialState: function () {
return {
formData: {},
registrationError: null,
step: 0,
waiting: false
};
},
advanceStep: function (formData) {
formData = formData || {};
this.setState({
step: this.state.step + 1,
formData: defaults({}, formData, this.state.formData)
});
},
register: function (formData) {
this.setState({waiting: true});
api({
host: '',
uri: '/classes/register_educator/',
method: 'post',
useCsrf: true,
formData: {
username: this.state.formData.user.username,
email: formData.user.email,
password: this.state.formData.user.password,
birth_month: this.state.formData.user.birth.month,
birth_year: this.state.formData.user.birth.year,
gender: (
this.state.formData.user.gender === 'other' ?
this.state.formData.user.genderOther :
this.state.formData.user.gender
),
country: this.state.formData.user.country,
is_robot: this.state.formData.user.isRobot,
first_name: this.state.formData.user.name.first,
last_name: this.state.formData.user.name.last,
phone_number: this.state.formData.phone.national_number,
organization_name: this.state.formData.organization.name,
organization_title: this.state.formData.organization.title,
organization_type: this.state.formData.organization.type,
organization_other: this.state.formData.organization.other,
organization_url: this.state.formData.organization.url,
address_country: this.state.formData.address.country,
address_line1: this.state.formData.address.line1,
address_line2: this.state.formData.address.line2,
address_city: this.state.formData.address.city,
address_state: this.state.formData.address.state,
address_zip: this.state.formData.address.zip,
how_use_scratch: this.state.formData.useScratch
}
}, function (err, res) {
this.setState({waiting: false});
if (err) return this.setState({registrationError: err});
if (res[0].success) return this.advanceStep(formData);
this.setState({registrationError: res[0].msg});
}.bind(this));
},
render: function () {
return (
<Deck className="teacher-registration">
{this.state.registrationError ?
<Steps.RegistrationError {... this.state} />
:
<Progression {... this.state}>
<Steps.UsernameStep onNextStep={this.advanceStep}
waiting={this.state.waiting} />
<Steps.DemographicsStep onNextStep={this.advanceStep}
waiting={this.state.waiting} />
<Steps.NameStep onNextStep={this.advanceStep}
waiting={this.state.waiting} />
<Steps.PhoneNumberStep onNextStep={this.advanceStep}
waiting={this.state.waiting}
defaultCountry={
this.state.formData.user && this.state.formData.user.country
} />
<Steps.OrganizationStep onNextStep={this.advanceStep}
waiting={this.state.waiting} />
<Steps.AddressStep onNextStep={this.advanceStep}
waiting={this.state.waiting}
defaultCountry={
this.state.formData.user && this.state.formData.user.country
} />
<Steps.UseScratchStep onNextStep={this.advanceStep}
waiting={this.state.waiting} />
<Steps.EmailStep onNextStep={this.register}
waiting={this.state.waiting} />
<Steps.TeacherApprovalStep email={this.state.formData.user && this.state.formData.user.email} />
</Progression>
}
</Deck>
);
}
});
render(<TeacherRegistration />, document.getElementById('app'));

View file

@ -0,0 +1,202 @@
@import "../../colors";
@import "../../frameless";
@include responsive-layout (".teacher-registration", ".slide");
html,
body {
background-color: darken($ui-purple, 8%);
}
.teacher-registration {
background-color: $ui-purple;
.demographics-checkbox-is-robot {
display: none;
}
.gender-input,
.other-input {
float: right;
width: 90%;
.row {
margin-left: .5rem;
}
}
.username-step,
.name-step,
.address-step,
.email-step {
.help-block {
transform: translate(15.5rem, -4rem);
}
}
.demographics-step {
.gender-input {
margin-top: -5.5rem;
}
.help-block {
transform: translate(14rem, -4rem);
}
.radio {
margin-right: 2.5rem;
line-height: 3rem;
input {
margin-right: 1rem;
}
}
}
.phone-step {
.form-group {
margin-bottom: 2rem;
}
input {
&[type=checkbox] {
margin-bottom: 1.25rem;
}
}
.help-block {
margin-top: .7rem;
}
.checkbox-row {
.help-block {
margin-top: 0;
}
}
}
.organization-step {
.help-block {
transform: translate(16rem, -4rem);
}
.organization-type,
.url-input {
p {
margin: .25rem 0;
text-align: left;
color: $ui-dark-gray;
}
}
input {
&[value="8"] {
margin: 1rem 0;
}
}
.other-input {
margin-top: -5.75rem;
}
}
.usescratch-step {
.form {
.form-group {
margin-bottom: 0;
&.has-error {
.textarea {
border: 1px solid $ui-orange;
}
}
}
}
.help-block {
margin-top: .75rem;
}
p {
&.char-count {
margin-top: 0;
margin-bottom: 1rem;
text-align: right;
}
}
}
.last-step {
&.slide {
max-width: 38.75rem;
}
.card {
margin: 1rem 0;
padding: 1.5rem;
width: initial;
h4,
p {
text-align: left;
}
p {
margin: 0;
}
}
}
}
@media only screen and (max-width: $mobile - 1) {
.teacher-registration {
.demographics-step {
.radio {
width: 100%;
text-align: left;
}
}
.last-step {
.card {
margin: 0 auto;
width: 18.75rem;
}
}
}
}
@media only screen and (max-width: $desktop - 1) {
.teacher-registration {
.form {
text-align: left;
}
.username-step,
.demographics-step,
.name-step {
.help-block {
transform: none;
}
}
.phone-step {
.checkbox,
.help-block {
text-align: left;
}
.checkbox {
margin-bottom: 1rem;
}
}
.organization-step {
.checkbox-group {
text-align: left;
}
}
}
}

View file

@ -0,0 +1,35 @@
var classNames = require('classnames');
var connect = require('react-redux').connect;
var React = require('react');
var render = require('../../lib/render.jsx');
var Deck = require ('../../components/deck/deck.jsx');
var TeacherApprovalStep = require('../../components/registration/steps.jsx').TeacherApprovalStep;
require('./teacherwaitingroom.scss');
var TeacherWaitingRoom = React.createClass({
displayName: 'TeacherWaitingRoom',
render: function () {
var permissions = this.props.session.permissions || {};
var user = this.props.session.user || {};
return (
<Deck className={classNames('teacher-waitingroom', this.props.className)}>
<TeacherApprovalStep confirmed={permissions.social}
invited={permissions.educator_invitee}
educator={permissions.educator}
email={user.email} />
</Deck>
);
}
});
var mapStateToProps = function (state) {
return {
session: state.session.session
};
};
var ConnectedTeacherWaitingRoom = connect(mapStateToProps)(TeacherWaitingRoom);
render(<ConnectedTeacherWaitingRoom />, document.getElementById('app'));

View file

@ -0,0 +1,29 @@
@import "../../colors";
html,
body {
background-color: darken($ui-purple, 8%);
}
.teacher-waitingroom {
background-color: $ui-purple;
.slide {
max-width: 38.75rem;
}
.card {
margin: 1rem 0;
padding: 1.5rem;
width: initial;
h4,
p {
text-align: left;
}
p {
margin: 0;
}
}
}

BIN
static/images/flags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 393 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 398 B

View file

@ -0,0 +1 @@
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" width="8" height="12" viewBox="0 0 8 12"><title>info</title><g id="_Group_" data-name="&lt;Group&gt;"><circle id="_Path_" data-name="&lt;Path&gt;" cx="4.71" cy="2.23" r="1.23" fill="#fff"/><path id="_Path_2" data-name="&lt;Path&gt;" d="M5.88,10.21l-1.63.7A1.14,1.14,0,0,1,2.69,9.65L3.28,6.5A0.43,0.43,0,0,0,2.83,6L2.17,6a0.34,0.34,0,0,1-.36-0.25h0A0.34,0.34,0,0,1,2,5.41l1.63-.86A1.14,1.14,0,0,1,5.26,5.78l-0.6,3a0.43,0.43,0,0,0,.46.51l0.51,0a0.52,0.52,0,0,1,.55.39h0A0.52,0.52,0,0,1,5.88,10.21Z" fill="#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 593 B

View file

@ -79,11 +79,16 @@ module.exports = {
test: /\.scss$/,
loader: 'style!css!postcss-loader!sass'
},
{
test: /\.css$/,
loader: 'style!css!postcss-loader'
},
{
test: /\.(png|jpg|gif|eot|svg|ttf|woff)$/,
loader: 'url-loader'
}
]
],
noParse: /node_modules\/google-libphonenumber\/dist/
},
postcss: function () {
return [autoprefixer({browsers: ['last 3 versions']})];
@ -109,7 +114,9 @@ module.exports = {
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"' + (process.env.NODE_ENV || 'development') + '"',
'process.env.SENTRY_DSN': '"' + (process.env.SENTRY_DSN || '') + '"'
'process.env.SENTRY_DSN': '"' + (process.env.SENTRY_DSN || '') + '"',
'process.env.API_HOST': '"' + (process.env.API_HOST || 'https://api.scratch.mit.edu') + '"',
'process.env.SMARTY_STREETS_API_KEY': '"' + (process.env.SMARTY_STREETS_API_KEY || '') + '"'
}),
new webpack.optimize.CommonsChunkPlugin('common', 'js/common.bundle.js'),
new webpack.optimize.OccurenceOrderPlugin()