mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-03-22 19:05:56 -04:00
Merge branch 'feature/teacher-registration-flow' of https://github.com/rschamp/scratch-www into feature/teacher-registration-flow
* 'feature/teacher-registration-flow' of https://github.com/rschamp/scratch-www: (46 commits) Add serverside email validation Clean up Move registration step to main component Submit the rest of the fields when registering This help text is redundant now Fix organization "other" field enablement Get the form creating regular accounts Add method to api for submitting forms Update language chooser for formsy Add missing string Add progress indicators Style pass Remove unused label component Remove unused mixin factory Fixup api refactor Add serverside username validation L10n pass Explain the GeneralError component Add help text when a field is not required L10n WIP ... # Conflicts: # src/l10n.json # src/views/splash/splash.jsx
This commit is contained in:
commit
9d6fb63d18
51 changed files with 1641 additions and 208 deletions
.travis.ymlpackage.json
src
_frameless.scssl10n.json
components
card
deck
forms
checkbox-group.jsxcheckbox.jsxform.jsxgeneral-error.jsxinput-hoc.jsxinput.jsxphone-input.jsxradio-group.jsxrow.scssselect.jsxtextarea.jsxtextarea.scssvalidations.jsx
languagechooser
login
navigation/www
progression
slide
spinner
stepnavigation
lib
mixins
redux
routes.jsontemplate-config.jstemplate.htmlviews
explore
search
splash
teacherregistration
static/images
webpack.config.js
|
@ -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;
|
7
src/components/card/card.scss
Normal file
7
src/components/card/card.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
.card {
|
||||
border-radius: 8px / $em;
|
||||
background-color: $ui-white;
|
||||
}
|
18
src/components/deck/deck.jsx
Normal file
18
src/components/deck/deck.jsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
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])}>
|
||||
<img src="/images/logo_sm.png" />
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Deck;
|
7
src/components/deck/deck.scss
Normal file
7
src/components/deck/deck.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
@import "../../frameless";
|
||||
|
||||
@include responsive-layout (".deck", ".slide");
|
||||
|
||||
.deck {
|
||||
min-height: 100vh;
|
||||
}
|
22
src/components/forms/checkbox-group.jsx
Normal file
22
src/components/forms/checkbox-group.jsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
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');
|
||||
|
||||
var CheckboxGroup = React.createClass({
|
||||
type: 'CheckboxGroup',
|
||||
render: function () {
|
||||
var classes = classNames(
|
||||
'checkbox-group',
|
||||
this.props.className
|
||||
);
|
||||
return (
|
||||
<FRCCheckboxGroup {... this.props} className={classes} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = inputHOC(defaultValidationHOC(CheckboxGroup));
|
22
src/components/forms/checkbox.jsx
Normal file
22
src/components/forms/checkbox.jsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
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');
|
||||
|
||||
var Checkbox = React.createClass({
|
||||
type: 'Checkbox',
|
||||
render: function () {
|
||||
var classes = classNames(
|
||||
'checkbox-row',
|
||||
this.props.className
|
||||
);
|
||||
return (
|
||||
<FRCCheckbox {... this.props} rowClassName={classes} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = inputHOC(defaultValidationHOC(Checkbox));
|
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,27 @@
|
|||
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 {};
|
||||
},
|
||||
render: function () {
|
||||
var classes = classNames(
|
||||
'input',
|
||||
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} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Input;
|
||||
module.exports = inputHOC(defaultValidationHOC(Input));
|
||||
|
|
67
src/components/forms/phone-input.jsx
Normal file
67
src/components/forms/phone-input.jsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
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');
|
||||
|
||||
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)));
|
22
src/components/forms/radio-group.jsx
Normal file
22
src/components/forms/radio-group.jsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
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');
|
||||
|
||||
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));
|
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,14 @@ 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>
|
||||
<FRCSelect {... props} className={classes} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Select;
|
||||
module.exports = inputHOC(defaultValidationHOC(Select));
|
||||
|
|
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));
|
34
src/components/forms/textarea.scss
Normal file
34
src/components/forms/textarea.scss
Normal file
|
@ -0,0 +1,34 @@
|
|||
@import "../../colors";
|
||||
|
||||
$base-bg: $ui-white;
|
||||
$focus-bg: lighten($ui-blue, 35%);
|
||||
$fail-bg: lighten($ui-orange, 35%);
|
||||
$pass-bg: lighten($ui-aqua, 35%);
|
||||
|
||||
.textarea {
|
||||
transition: all 1s ease;
|
||||
margin: .5em 0;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
background-color: $base-bg;
|
||||
padding: .75em 1em;
|
||||
color: $type-gray;
|
||||
font-size: .8rem;
|
||||
|
||||
&:focus {
|
||||
transition: all 1s ease;
|
||||
outline: none;
|
||||
border: 1px solid $active-dark-gray;
|
||||
background-color: $focus-bg;
|
||||
}
|
||||
|
||||
&.fail {
|
||||
border: 1px solid $active-dark-gray;
|
||||
background-color: $fail-bg;
|
||||
}
|
||||
|
||||
&.pass {
|
||||
border: 1px solid $active-dark-gray;
|
||||
background-color: $pass-bg;
|
||||
}
|
||||
}
|
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,19 +35,19 @@ 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'} />
|
||||
</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'} />
|
||||
</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 />
|
||||
|
@ -69,7 +65,7 @@ var Login = React.createClass({
|
|||
defaultMessage={'Forgot Password?'} />
|
||||
</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 ? (
|
||||
|
|
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>
|
||||
);
|
||||
}
|
||||
});
|
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;
|
2
src/components/slide/slide.scss
Normal file
2
src/components/slide/slide.scss
Normal file
|
@ -0,0 +1,2 @@
|
|||
.slide {
|
||||
}
|
|
@ -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: 8px / $em;
|
||||
padding: 4px / $em;
|
||||
|
||||
.indicator {
|
||||
border-radius: 4px / $em;
|
||||
background-color: $ui-white;
|
||||
width: 8px / $em;
|
||||
height: 8px / $em;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.indicator {
|
||||
background-color: $ui-aqua;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border: 1px solid $ui-white;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
||||
|
|
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('&');
|
||||
};
|
|
@ -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,12 @@
|
|||
"view": "jobs/jobs",
|
||||
"title": "Jobs"
|
||||
},
|
||||
{
|
||||
"name": "teacherregistration",
|
||||
"pattern": "^/register-teacher/?$",
|
||||
"view": "teacherregistration/teacherregistration",
|
||||
"title": "Teacher Registration"
|
||||
},
|
||||
{
|
||||
"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>
|
||||
</head>
|
||||
|
|
|
@ -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',
|
||||
|
|
61
src/views/teacherregistration/l10n.json
Normal file
61
src/views/teacherregistration/l10n.json
Normal file
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"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.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": "Other",
|
||||
"teacherRegistration.selectCountry": "select country",
|
||||
"teacherRegistration.validationAddress": "This doesn't look like a real address",
|
||||
"teacherRegistration.addressLine1": "Address Line 1",
|
||||
"teacherRegistration.addressLine2": "Address Line 2",
|
||||
"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.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",
|
||||
"teacherRegistration.lastStepTitle": "Thank you for requesting a Scratch Teacher Account",
|
||||
"teacherRegistration.lastStepDescription": "We are currently processing your application. ",
|
||||
"teacherRegistration.confirmYourEmail": "Confirm Your Email",
|
||||
"teacherRegistration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
|
||||
"teacherRegistration.waitForApproval": "Wait for Approval",
|
||||
"teacherRegistration.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.",
|
||||
"teacherRegistration.checkOutResources": "Get Started with Resources",
|
||||
"teacherRegistration.checkOutResourcesDescription": "Explore materials for educators and facilitators written by the Scratch Team, including <a href='/educators#resources'>tips, tutorials, and guides</a>."
|
||||
}
|
639
src/views/teacherregistration/steps.jsx
Normal file
639
src/views/teacherregistration/steps.jsx
Normal file
|
@ -0,0 +1,639 @@
|
|||
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 urlParams = require('../../lib/url-params');
|
||||
|
||||
var Button = require('../../components/forms/button.jsx');
|
||||
var Card = require('../../components/card/card.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 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
|
||||
};
|
||||
},
|
||||
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':
|
||||
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>
|
||||
<h1><intl.FormattedMessage id="teacherRegistration.usernameStepTitle" /></h1>
|
||||
<p className="description">
|
||||
<intl.FormattedMessage id="teacherRegistration.usernameStepDescription" />
|
||||
</p>
|
||||
<Card>
|
||||
<Form onValidSubmit={this.onValidSubmit}>
|
||||
<Input label={formatMessage({id: 'general.username'})}
|
||||
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>
|
||||
<h1>
|
||||
<intl.FormattedMessage id="teacherRegistration.personalStepTitle" />
|
||||
</h1>
|
||||
<p className="description">
|
||||
<intl.FormattedMessage id="teacherRegistration.personalStepDescription" />
|
||||
</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: formatMessage({id: 'general.other'})}
|
||||
]}
|
||||
required />
|
||||
<Input name="user.genderOther"
|
||||
type="text"
|
||||
disabled={this.state.otherDisabled}
|
||||
required={!this.state.otherDisabled}
|
||||
help={null} />
|
||||
<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>
|
||||
<h1>
|
||||
<intl.FormattedHTMLMessage id="teacherRegistration.nameStepTitle" />
|
||||
</h1>
|
||||
<p className="description">
|
||||
<intl.FormattedMessage id="teacherRegistration.nameStepDescription" />
|
||||
</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>
|
||||
<h1>
|
||||
<intl.FormattedMessage id="teacherRegistration.phoneStepTitle" />
|
||||
</h1>
|
||||
<p>
|
||||
<intl.FormattedMessage id="teacherRegistration.phoneStepDescription" />
|
||||
</p>
|
||||
<Card>
|
||||
<Form onValidSubmit={this.props.onNextStep}>
|
||||
<PhoneInput label={formatMessage({id: 'teacherRegistration.phoneNumber'})}
|
||||
name="phone"
|
||||
defaultCountry={
|
||||
(this.props.formData.user && this.props.formData.user.country) ||
|
||||
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>
|
||||
<h1>
|
||||
<intl.FormattedMessage id="teacherRegistration.orgStepTitle" />
|
||||
</h1>
|
||||
<p>
|
||||
<intl.FormattedMessage id="teacherRegistration.orgStepDescription" />
|
||||
</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 />
|
||||
<CheckboxGroup label={formatMessage({id: 'teacherRegistration.orgType'})}
|
||||
help={formatMessage({id: 'teacherRegistration.checkAll'})}
|
||||
name="organization.type"
|
||||
value={[]}
|
||||
options={this.getOrganizationOptions()}
|
||||
onChange={this.onChooseOrganization}
|
||||
required />
|
||||
<Input type="text"
|
||||
name="organization.other"
|
||||
disabled={this.state.otherDisabled}
|
||||
required={!this.state.otherDisabled} />
|
||||
<Input label={formatMessage({id: 'general.website'})}
|
||||
type="url"
|
||||
name="organization.url" />
|
||||
<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.formData.user && this.props.formData.user.country) ||
|
||||
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>
|
||||
<h1>
|
||||
<intl.FormattedMessage id="teacherRegistration.addressStepTitle" />
|
||||
</h1>
|
||||
<p className="description">
|
||||
<intl.FormattedMessage id="teacherRegistration.addressStepDescription" />
|
||||
</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" />
|
||||
<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
|
||||
};
|
||||
},
|
||||
render: function () {
|
||||
var formatMessage = this.props.intl.formatMessage;
|
||||
return (
|
||||
<Slide>
|
||||
<h1>
|
||||
<intl.FormattedMessage id="teacherRegistration.useScratchStepTitle" />
|
||||
</h1>
|
||||
<p className="description">
|
||||
<intl.FormattedMessage id="teacherRegistration.useScratchStepDescription" />
|
||||
</p>
|
||||
<Card>
|
||||
<Form onValidSubmit={this.props.onNextStep}>
|
||||
<TextArea label={formatMessage({id: 'teacherRegistration.howUseScratch'})}
|
||||
name="useScratch"
|
||||
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>
|
||||
);
|
||||
}
|
||||
})),
|
||||
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>
|
||||
<h1>
|
||||
<intl.FormattedMessage id="teacherRegistration.emailStepTitle" />
|
||||
</h1>
|
||||
<p className="description">
|
||||
<intl.FormattedMessage id="teacherRegistration.emailStepDescription" />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
})),
|
||||
LastStep: intl.injectIntl(React.createClass({
|
||||
render: function () {
|
||||
return (
|
||||
<Slide>
|
||||
<h1>
|
||||
<intl.FormattedMessage id="teacherRegistration.lastStepTitle" />
|
||||
</h1>
|
||||
<p className="description">
|
||||
<intl.FormattedMessage id="teacherRegistration.lastStepDescription" />
|
||||
</p>
|
||||
<Card className="confirm">
|
||||
<h2><intl.FormattedMessage id="teacherRegistration.confirmYourEmail" /></h2>
|
||||
<p>
|
||||
<intl.FormattedMessage id="teacherRegistration.confirmYourEmailDescription" /><br />
|
||||
<strong>{this.props.formData.user && this.props.formData.user.email}</strong>
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="wait">
|
||||
<h2><intl.FormattedMessage id="teacherRegistration.waitForApproval" /></h2>
|
||||
<p>
|
||||
<intl.FormattedMessage id="teacherRegistration.waitForApprovalDescription" />
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="resources">
|
||||
<h2><intl.FormattedMessage id="teacherRegistration.checkOutResources" /></h2>
|
||||
<p>
|
||||
<intl.FormattedHTMLMessage id="teacherRegistration.checkOutResourcesDescription" />
|
||||
</p>
|
||||
</Card>
|
||||
</Slide>
|
||||
);
|
||||
}
|
||||
})),
|
||||
RegistrationError: intl.injectIntl(React.createClass({
|
||||
render: function () {
|
||||
return (
|
||||
<Slide>
|
||||
<h1>Something went wrong</h1>
|
||||
<Card>
|
||||
<h2>There was an error while processing your registration</h2>
|
||||
<p>
|
||||
{this.props.registrationError}
|
||||
</p>
|
||||
</Card>
|
||||
</Slide>
|
||||
);
|
||||
}
|
||||
}))
|
||||
};
|
98
src/views/teacherregistration/teacherregistration.jsx
Normal file
98
src/views/teacherregistration/teacherregistration.jsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
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('./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 {... this.state} onNextStep={this.advanceStep} />
|
||||
<Steps.DemographicsStep {... this.state} onNextStep={this.advanceStep} />
|
||||
<Steps.NameStep {... this.state} onNextStep={this.advanceStep} />
|
||||
<Steps.PhoneNumberStep {... this.state} onNextStep={this.advanceStep} />
|
||||
<Steps.OrganizationStep {... this.state} onNextStep={this.advanceStep} />
|
||||
<Steps.AddressStep {... this.state} onNextStep={this.advanceStep} />
|
||||
<Steps.UseScratchStep {... this.state} onNextStep={this.advanceStep} />
|
||||
<Steps.EmailStep {... this.state} onNextStep={this.register} />
|
||||
<Steps.LastStep {... this.state} />
|
||||
</Progression>
|
||||
}
|
||||
</Deck>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
render(<TeacherRegistration />, document.getElementById('app'));
|
36
src/views/teacherregistration/teacherregistration.scss
Normal file
36
src/views/teacherregistration/teacherregistration.scss
Normal file
|
@ -0,0 +1,36 @@
|
|||
@import "../../colors";
|
||||
|
||||
.teacher-registration {
|
||||
background-color: $ui-purple;
|
||||
|
||||
.slide {
|
||||
h1,
|
||||
p {
|
||||
color: $type-white;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
&,
|
||||
h2,
|
||||
p {
|
||||
color: $type-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.card-button {
|
||||
display: block;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
background-color: $ui-aqua;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.step-navigation {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demographics-checkbox-is-robot {
|
||||
display: none;
|
||||
}
|
||||
}
|
BIN
static/images/flags.png
Normal file
BIN
static/images/flags.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 69 KiB |
|
@ -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