diff --git a/.travis.yml b/.travis.yml index f0faf2485..99aaa1b5b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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} diff --git a/package.json b/package.json index 6b39e6348..fa3b9c2d9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/_frameless.scss b/src/_frameless.scss index c8e2b9457..854e109c1 100644 --- a/src/_frameless.scss +++ b/src/_frameless.scss @@ -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"); diff --git a/src/components/card/card.jsx b/src/components/card/card.jsx new file mode 100644 index 000000000..5ac22fae1 --- /dev/null +++ b/src/components/card/card.jsx @@ -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; diff --git a/src/components/card/card.scss b/src/components/card/card.scss new file mode 100644 index 000000000..06f02770d --- /dev/null +++ b/src/components/card/card.scss @@ -0,0 +1,7 @@ +@import "../../colors"; +@import "../../frameless"; + +.card { + border-radius: 8px / $em; + background-color: $ui-white; +} diff --git a/src/components/deck/deck.jsx b/src/components/deck/deck.jsx new file mode 100644 index 000000000..dc1c85375 --- /dev/null +++ b/src/components/deck/deck.jsx @@ -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; diff --git a/src/components/deck/deck.scss b/src/components/deck/deck.scss new file mode 100644 index 000000000..4451f8b90 --- /dev/null +++ b/src/components/deck/deck.scss @@ -0,0 +1,7 @@ +@import "../../frameless"; + +@include responsive-layout (".deck", ".slide"); + +.deck { + min-height: 100vh; +} diff --git a/src/components/forms/checkbox-group.jsx b/src/components/forms/checkbox-group.jsx new file mode 100644 index 000000000..0a96857d9 --- /dev/null +++ b/src/components/forms/checkbox-group.jsx @@ -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)); diff --git a/src/components/forms/checkbox.jsx b/src/components/forms/checkbox.jsx new file mode 100644 index 000000000..16aca05ee --- /dev/null +++ b/src/components/forms/checkbox.jsx @@ -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)); diff --git a/src/components/forms/form.jsx b/src/components/forms/form.jsx new file mode 100644 index 000000000..3b8d0f5f3 --- /dev/null +++ b/src/components/forms/form.jsx @@ -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; diff --git a/src/components/forms/general-error.jsx b/src/components/forms/general-error.jsx new file mode 100644 index 000000000..bca50e20d --- /dev/null +++ b/src/components/forms/general-error.jsx @@ -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> + ); + } +})); diff --git a/src/components/forms/input-hoc.jsx b/src/components/forms/input-hoc.jsx new file mode 100644 index 000000000..1532eaefd --- /dev/null +++ b/src/components/forms/input-hoc.jsx @@ -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; +}; diff --git a/src/components/forms/input.jsx b/src/components/forms/input.jsx index e35a09e2e..d3427321e 100644 --- a/src/components/forms/input.jsx +++ b/src/components/forms/input.jsx @@ -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)); diff --git a/src/components/forms/phone-input.jsx b/src/components/forms/phone-input.jsx new file mode 100644 index 000000000..c16de6b5c --- /dev/null +++ b/src/components/forms/phone-input.jsx @@ -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))); diff --git a/src/components/forms/radio-group.jsx b/src/components/forms/radio-group.jsx new file mode 100644 index 000000000..cefed8d18 --- /dev/null +++ b/src/components/forms/radio-group.jsx @@ -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)); diff --git a/src/components/forms/row.scss b/src/components/forms/row.scss new file mode 100644 index 000000000..10a8e12e8 --- /dev/null +++ b/src/components/forms/row.scss @@ -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; + } +} diff --git a/src/components/forms/select.jsx b/src/components/forms/select.jsx index 62b6d9aa8..b2bca4c8f 100644 --- a/src/components/forms/select.jsx +++ b/src/components/forms/select.jsx @@ -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)); diff --git a/src/components/forms/textarea.jsx b/src/components/forms/textarea.jsx new file mode 100644 index 000000000..9ac9e01fb --- /dev/null +++ b/src/components/forms/textarea.jsx @@ -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)); diff --git a/src/components/forms/textarea.scss b/src/components/forms/textarea.scss new file mode 100644 index 000000000..24d0fc3f6 --- /dev/null +++ b/src/components/forms/textarea.scss @@ -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; + } +} diff --git a/src/components/forms/validations.jsx b/src/components/forms/validations.jsx new file mode 100644 index 000000000..db0b71632 --- /dev/null +++ b/src/components/forms/validations.jsx @@ -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' +}); diff --git a/src/components/languagechooser/languagechooser.jsx b/src/components/languagechooser/languagechooser.jsx index 765f883ba..c9e0af69d 100644 --- a/src/components/languagechooser/languagechooser.jsx +++ b/src/components/languagechooser/languagechooser.jsx @@ -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> ); } }); diff --git a/src/components/login/login.jsx b/src/components/login/login.jsx index d12ce2361..d49419f4c 100644 --- a/src/components/login/login.jsx +++ b/src/components/login/login.jsx @@ -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> ); } diff --git a/src/components/navigation/www/navigation.jsx b/src/components/navigation/www/navigation.jsx index 19073ca74..c786fad79 100644 --- a/src/components/navigation/www/navigation.jsx +++ b/src/components/navigation/www/navigation.jsx @@ -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 ? ( diff --git a/src/components/progression/progression.jsx b/src/components/progression/progression.jsx new file mode 100644 index 000000000..b4a7a4b56 --- /dev/null +++ b/src/components/progression/progression.jsx @@ -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> + ); + } +}); diff --git a/src/components/slide/slide.jsx b/src/components/slide/slide.jsx new file mode 100644 index 000000000..cc297e33a --- /dev/null +++ b/src/components/slide/slide.jsx @@ -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; diff --git a/src/components/slide/slide.scss b/src/components/slide/slide.scss new file mode 100644 index 000000000..d96ab04ab --- /dev/null +++ b/src/components/slide/slide.scss @@ -0,0 +1,2 @@ +.slide { +} diff --git a/src/components/spinner/spinner.scss b/src/components/spinner/spinner.scss index e4fd45305..4310723cb 100644 --- a/src/components/spinner/spinner.scss +++ b/src/components/spinner/spinner.scss @@ -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%); + } } } diff --git a/src/components/stepnavigation/stepnavigation.jsx b/src/components/stepnavigation/stepnavigation.jsx new file mode 100644 index 000000000..1f049e3ab --- /dev/null +++ b/src/components/stepnavigation/stepnavigation.jsx @@ -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; diff --git a/src/components/stepnavigation/stepnavigation.scss b/src/components/stepnavigation/stepnavigation.scss new file mode 100644 index 000000000..6ab68e1b1 --- /dev/null +++ b/src/components/stepnavigation/stepnavigation.scss @@ -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; + } + } +} diff --git a/src/l10n.json b/src/l10n.json index e453c4f6d..cabffe592 100644 --- a/src/l10n.json +++ b/src/l10n.json @@ -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", diff --git a/src/lib/api.js b/src/lib/api.js new file mode 100644 index 000000000..05cc2bf1d --- /dev/null +++ b/src/lib/api.js @@ -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); + } +}; diff --git a/src/lib/country-data.js b/src/lib/country-data.js new file mode 100644 index 000000000..46e8ebbe5 --- /dev/null +++ b/src/lib/country-data.js @@ -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; +}, {}); diff --git a/src/lib/smarty-streets.js b/src/lib/smarty-streets.js new file mode 100644 index 000000000..42400626a --- /dev/null +++ b/src/lib/smarty-streets.js @@ -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); + }); +}; diff --git a/src/lib/url-params.js b/src/lib/url-params.js new file mode 100644 index 000000000..f9b283608 --- /dev/null +++ b/src/lib/url-params.js @@ -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('&'); +}; diff --git a/src/mixins/api.jsx b/src/mixins/api.jsx deleted file mode 100644 index 47ee8faff..000000000 --- a/src/mixins/api.jsx +++ /dev/null @@ -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; diff --git a/src/mixins/cookieMixinFactory.jsx b/src/mixins/cookieMixinFactory.jsx deleted file mode 100644 index 621e536f4..000000000 --- a/src/mixins/cookieMixinFactory.jsx +++ /dev/null @@ -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; diff --git a/src/redux/conference-details.js b/src/redux/conference-details.js index 0d46b1282..0e3cf4b25 100644 --- a/src/redux/conference-details.js +++ b/src/redux/conference-details.js @@ -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, diff --git a/src/redux/conference-schedule.js b/src/redux/conference-schedule.js index 874226d29..82f5166ee 100644 --- a/src/redux/conference-schedule.js +++ b/src/redux/conference-schedule.js @@ -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, diff --git a/src/redux/session.js b/src/redux/session.js index 9b53435a8..16be21028 100644 --- a/src/redux/session.js +++ b/src/redux/session.js @@ -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({ diff --git a/src/routes.json b/src/routes.json index 8a6b5e3cf..11b127925 100644 --- a/src/routes.json +++ b/src/routes.json @@ -91,6 +91,12 @@ "view": "jobs/jobs", "title": "Jobs" }, + { + "name": "teacherregistration", + "pattern": "^/register-teacher/?$", + "view": "teacherregistration/teacherregistration", + "title": "Teacher Registration" + }, { "name": "wedo2", "pattern": "^/wedo/?$", diff --git a/src/template-config.js b/src/template-config.js index af9ee0050..297328d74 100644 --- a/src/template-config.js +++ b/src/template-config.js @@ -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: diff --git a/src/template.html b/src/template.html index d389b2d47..de9f93349 100644 --- a/src/template.html +++ b/src/template.html @@ -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> diff --git a/src/views/explore/explore.jsx b/src/views/explore/explore.jsx index 507f61759..0b1cd7d69 100644 --- a/src/views/explore/explore.jsx +++ b/src/views/explore/explore.jsx @@ -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 + diff --git a/src/views/search/search.jsx b/src/views/search/search.jsx index 17a0983ac..ecd6cff29 100644 --- a/src/views/search/search.jsx +++ b/src/views/search/search.jsx @@ -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 + diff --git a/src/views/splash/splash.jsx b/src/views/splash/splash.jsx index 33dec0dfd..c4372f7e1 100644 --- a/src/views/splash/splash.jsx +++ b/src/views/splash/splash.jsx @@ -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', diff --git a/src/views/teacherregistration/l10n.json b/src/views/teacherregistration/l10n.json new file mode 100644 index 000000000..1212b8499 --- /dev/null +++ b/src/views/teacherregistration/l10n.json @@ -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>." +} diff --git a/src/views/teacherregistration/steps.jsx b/src/views/teacherregistration/steps.jsx new file mode 100644 index 000000000..341d6a749 --- /dev/null +++ b/src/views/teacherregistration/steps.jsx @@ -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> + ); + } + })) +}; diff --git a/src/views/teacherregistration/teacherregistration.jsx b/src/views/teacherregistration/teacherregistration.jsx new file mode 100644 index 000000000..8ea60998e --- /dev/null +++ b/src/views/teacherregistration/teacherregistration.jsx @@ -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')); diff --git a/src/views/teacherregistration/teacherregistration.scss b/src/views/teacherregistration/teacherregistration.scss new file mode 100644 index 000000000..fcf134883 --- /dev/null +++ b/src/views/teacherregistration/teacherregistration.scss @@ -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; + } +} diff --git a/static/images/flags.png b/static/images/flags.png new file mode 100644 index 000000000..7cd9161a7 Binary files /dev/null and b/static/images/flags.png differ diff --git a/webpack.config.js b/webpack.config.js index 91ad38cb8..aa1ab7b6f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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()