mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 01:25:52 -05:00
Merge pull request #599 from mewtaylor/feature/teacher-registration-flow
[Updates] Teacher registration flow
This commit is contained in:
commit
74bd2b3255
69 changed files with 2520 additions and 262 deletions
|
@ -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}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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");
|
||||
|
|
17
src/components/card/card.jsx
Normal file
17
src/components/card/card.jsx
Normal 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;
|
8
src/components/card/card.scss
Normal file
8
src/components/card/card.scss
Normal 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;
|
||||
}
|
20
src/components/deck/deck.jsx
Normal file
20
src/components/deck/deck.jsx
Normal 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;
|
154
src/components/deck/deck.scss
Normal file
154
src/components/deck/deck.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
28
src/components/forms/charcount.jsx
Normal file
28
src/components/forms/charcount.jsx
Normal 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;
|
13
src/components/forms/charcount.scss
Normal file
13
src/components/forms/charcount.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
25
src/components/forms/checkbox-group.jsx
Normal file
25
src/components/forms/checkbox-group.jsx
Normal 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));
|
9
src/components/forms/checkbox-group.scss
Normal file
9
src/components/forms/checkbox-group.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
.checkbox-group {
|
||||
.col-sm-9 {
|
||||
flex-flow: column wrap;
|
||||
|
||||
.checkbox {
|
||||
margin: .5rem 0;
|
||||
}
|
||||
}
|
||||
}
|
25
src/components/forms/checkbox.jsx
Normal file
25
src/components/forms/checkbox.jsx
Normal 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));
|
43
src/components/forms/checkbox.scss
Normal file
43
src/components/forms/checkbox.scss
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
src/components/forms/form.jsx
Normal file
31
src/components/forms/form.jsx
Normal 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;
|
20
src/components/forms/general-error.jsx
Normal file
20
src/components/forms/general-error.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}));
|
20
src/components/forms/input-hoc.jsx
Normal file
20
src/components/forms/input-hoc.jsx
Normal 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;
|
||||
};
|
|
@ -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));
|
||||
|
|
|
@ -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 {
|
||||
|
|
68
src/components/forms/phone-input.jsx
Normal file
68
src/components/forms/phone-input.jsx
Normal 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)));
|
42
src/components/forms/phone-input.scss
Normal file
42
src/components/forms/phone-input.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
23
src/components/forms/radio-group.jsx
Normal file
23
src/components/forms/radio-group.jsx
Normal 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));
|
46
src/components/forms/radio-group.scss
Normal file
46
src/components/forms/radio-group.scss
Normal 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: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
src/components/forms/row.scss
Normal file
11
src/components/forms/row.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
23
src/components/forms/textarea.jsx
Normal file
23
src/components/forms/textarea.jsx
Normal 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));
|
25
src/components/forms/textarea.scss
Normal file
25
src/components/forms/textarea.scss
Normal 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;
|
||||
}
|
||||
}
|
46
src/components/forms/validations.jsx
Normal file
46
src/components/forms/validations.jsx
Normal 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'
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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) {
|
||||
|
|
42
src/components/progression/progression.jsx
Normal file
42
src/components/progression/progression.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
});
|
705
src/components/registration/steps.jsx
Normal file
705
src/components/registration/steps.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}))
|
||||
};
|
17
src/components/slide/slide.jsx
Normal file
17
src/components/slide/slide.jsx
Normal 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;
|
11
src/components/slide/slide.scss
Normal file
11
src/components/slide/slide.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
@import "../../frameless";
|
||||
|
||||
.slide {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
.slide {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
28
src/components/stepnavigation/stepnavigation.jsx
Normal file
28
src/components/stepnavigation/stepnavigation.jsx
Normal 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;
|
31
src/components/stepnavigation/stepnavigation.scss
Normal file
31
src/components/stepnavigation/stepnavigation.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
33
src/components/tooltip/tooltip.jsx
Normal file
33
src/components/tooltip/tooltip.jsx
Normal 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;
|
83
src/components/tooltip/tooltip.scss
Normal file
83
src/components/tooltip/tooltip.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
83
src/lib/api.js
Normal 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
22
src/lib/country-data.js
Normal 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
21
src/lib/smarty-streets.js
Normal 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
22
src/lib/url-params.js
Normal 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('&');
|
||||
};
|
|
@ -83,7 +83,8 @@ p {
|
|||
}
|
||||
}
|
||||
|
||||
b {
|
||||
b,
|
||||
strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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/?$",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 +
|
||||
|
|
|
@ -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 +
|
||||
|
|
|
@ -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',
|
||||
|
|
56
src/views/teacherregistration/l10n.json
Normal file
56
src/views/teacherregistration/l10n.json
Normal 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 & 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"
|
||||
}
|
112
src/views/teacherregistration/teacherregistration.jsx
Normal file
112
src/views/teacherregistration/teacherregistration.jsx
Normal 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'));
|
202
src/views/teacherregistration/teacherregistration.scss
Normal file
202
src/views/teacherregistration/teacherregistration.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
src/views/teacherwaitingroom/teacherwaitingroom.jsx
Normal file
35
src/views/teacherwaitingroom/teacherwaitingroom.jsx
Normal 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'));
|
29
src/views/teacherwaitingroom/teacherwaitingroom.scss
Normal file
29
src/views/teacherwaitingroom/teacherwaitingroom.scss
Normal 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
BIN
static/images/flags.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
1
static/svgs/forms/carot-hover.svg
Normal file
1
static/svgs/forms/carot-hover.svg
Normal 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 |
1
static/svgs/forms/carot.svg
Normal file
1
static/svgs/forms/carot.svg
Normal 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 |
1
static/svgs/tooltip/info.svg
Normal file
1
static/svgs/tooltip/info.svg
Normal 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="<Group>"><circle id="_Path_" data-name="<Path>" cx="4.71" cy="2.23" r="1.23" fill="#fff"/><path id="_Path_2" data-name="<Path>" 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 |
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue