Merge pull request #704 from LLK/release/2.2.10

[Master] Release 2.2.10
This commit is contained in:
Matthew Taylor 2016-07-08 08:42:05 -04:00 committed by GitHub
commit b5cca385a7
142 changed files with 6321 additions and 753 deletions

View file

@ -2,4 +2,5 @@ node_modules/*
static/*
build/*
intl/*
locales/*
**/*.min.js

View file

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

View file

@ -44,9 +44,13 @@ var getStaticPaths = function (pathToStatic) {
* the express route and a static view file associated with the route
*/
var getViewPaths = function (routes) {
return routes.map(function (route) {
return route.pattern;
});
return routes.reduce(function (paths, route) {
var path = route.routeAlias || route.pattern;
if (paths.indexOf(path) === -1) {
paths.push(path);
}
return paths;
}, []);
};
/*

View file

@ -76,12 +76,14 @@ Helpers.getMD5Map = function (ICUIdMap) {
* key: '<react-intl string id>'
* value: translated version of string
*/
Helpers.getTranslationsForLanguage = function (lang, idsWithICU, md5WithIds) {
Helpers.getTranslationsForLanguage = function (lang, idsWithICU, md5WithIds, separator) {
var poUiDir = path.resolve(__dirname, '../../node_modules/scratchr2_translations/ui');
var jsFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/djangojs.po');
var pyFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/django.po');
var translations = {};
separator = separator || ':';
try {
fs.accessSync(jsFile, fs.R_OK);
var jsTranslations = po2icu.poFileToICUSync(lang, jsFile);
@ -104,7 +106,7 @@ Helpers.getTranslationsForLanguage = function (lang, idsWithICU, md5WithIds) {
var translationsByView = {};
for (var id in translations) {
var ids = id.split('-'); // [viewName, stringId]
var ids = id.split(separator); // [viewName, stringId]
var viewName = ids[0];
var stringId = ids[1];
@ -127,20 +129,24 @@ Helpers.writeTranslationsToJS = function (outputDir, viewName, translationObject
// Returns a FormattedMessage id with english string as value. Use for default values in translations
// Sample structure: { 'general-general.blah': 'blah', 'about-about.blah': 'blahblah' }
Helpers.idToICUMap = function (viewName, ids) {
Helpers.idToICUMap = function (viewName, ids, separator) {
var idsToICU = {};
separator = separator || ':';
for (var id in ids) {
idsToICU[viewName + '-' + id] = ids[id];
idsToICU[viewName + separator + id] = ids[id];
}
return idsToICU;
};
// Reuturns reverse (i.e. english string with message key as the value) object for searching po files.
// Sample structure: { 'blah': 'general-general.blah', 'blahblah': 'about-about.blah' }
Helpers.icuToIdMap = function (viewName, ids) {
Helpers.icuToIdMap = function (viewName, ids, separator) {
var icuToIds = {};
separator = separator || ':';
for (var id in ids) {
icuToIds[ids[id]] = viewName + '-' + id;
icuToIds[ids[id]] = viewName + separator + id;
}
return icuToIds;
};

View file

@ -43,8 +43,12 @@
"exenv": "1.2.0",
"fastly": "1.2.1",
"file-loader": "0.8.4",
"formsy-react": "0.18.0",
"formsy-react-components": "0.7.1",
"git-bundle-sha": "0.0.2",
"glob": "5.0.15",
"google-libphonenumber": "1.0.21",
"iso-3166-2": "0.4.0",
"json-loader": "0.5.2",
"json2po-stream": "1.0.3",
"jsx-loader": "0.13.2",
@ -60,13 +64,14 @@
"po2icu": "0.0.2",
"postcss-loader": "0.8.2",
"raven-js": "3.0.4",
"react": "0.14.0",
"react-dom": "0.14.0",
"react": "15.1.0",
"react-dom": "15.0.1",
"react-intl": "2.1.2",
"react-modal": "0.6.1",
"react-modal": "1.3.0",
"react-onclickoutside": "4.1.1",
"react-redux": "4.4.0",
"react-slick": "0.9.2",
"react-redux": "4.4.5",
"react-slick": "0.12.2",
"react-telephone-input": "3.4.5",
"redux": "3.5.2",
"redux-thunk": "2.0.1",
"sass-lint": "1.5.1",

View file

@ -1,7 +1,7 @@
/* UI Primary Colors */
$ui-blue: hsla(200, 90, 55, 1); // #25AFF4
$ui-orange: hsla(35, 90, 55, 1); // #F49D25
$ui-light-gray: hsla(0, 0, 98, 1);
$ui-light-gray: hsla(0, 0, 98, 1); //#FAFAFA
$ui-gray: hsla(0, 0, 95, 1); //#F2F2F2
$ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3

View file

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

View file

@ -19,8 +19,8 @@ var AdminPanel = React.createClass({
render: function () {
// make sure user is present before checking if they're an admin. Don't show anything if user not an admin.
var showAdmin = false;
if (this.props.session.user) {
showAdmin = this.props.session.permissions.admin;
if (this.props.session.session.user) {
showAdmin = this.props.session.session.permissions.admin;
}
if (!showAdmin) return false;

View file

@ -7,6 +7,7 @@ var Box = React.createClass({
type: 'Box',
propTypes: {
title: React.PropTypes.string.isRequired,
subtitle: React.PropTypes.string,
moreTitle: React.PropTypes.string,
moreHref: React.PropTypes.string,
moreProps: React.PropTypes.object
@ -20,6 +21,7 @@ var Box = React.createClass({
<div className={classes}>
<div className="box-header">
<h4>{this.props.title}</h4>
<h5>{this.props.subtitle}</h5>
<p>
<a href={this.props.moreHref} {...this.props.moreProps}>
{this.props.moreTitle}

View file

@ -13,7 +13,8 @@ $base-bg: $ui-white;
width: $cols4;
.box-header {
h4 {
h4,
h5 {
line-height: .9rem;
font-size: .9rem;
}
@ -25,7 +26,8 @@ $base-bg: $ui-white;
width: $cols6;
.box-header {
h4 {
h4,
h5 {
line-height: 1rem;
font-size: 1rem;
}
@ -37,7 +39,8 @@ $base-bg: $ui-white;
width: $cols8;
.box-header {
h4 {
h4,
h5 {
line-height: 1.1rem;
font-size: 1.1rem;
}
@ -49,7 +52,8 @@ $base-bg: $ui-white;
width: $cols12;
.box-header {
h4 {
h4,
h5 {
line-height: 1.1rem;
font-size: 1.1rem;
}
@ -72,17 +76,25 @@ $base-bg: $ui-white;
height: 20px;
overflow: hidden;
h4 {
h4,
h5 {
display: inline-block;
float: left;
}
h5 {
margin: 0;
padding-left: 5px;
text-transform: none;
letter-spacing: normal;
font-weight: normal;
}
p {
display: inline-block;
float: right;
margin: 1px 0 0 0;
padding: 0;
font-size: .85rem;
}
}

View file

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

View file

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

View file

@ -55,7 +55,7 @@ var Carousel = React.createClass({
}
return (
<Thumbnail key={item.id}
<Thumbnail key={[this.key, item.id].join('.')}
showLoves={this.props.showLoves}
showRemixes={this.props.showRemixes}
type={item.type}

View file

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

View file

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

View file

@ -15,6 +15,10 @@
font-size: .8125rem;
font-weight: normal;
&.staging {
background-color: $ui-orange;
}
&.open {
display: block;
}

View file

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

View file

@ -13,36 +13,48 @@ var ConferenceFooter = React.createClass({
<div className="collaborators">
<h4>Sponsors</h4>
<FlexRow as="ul">
<li>
<li className="odl">
<a href="https://odl.mit.edu/">
<img src="/images/conference/footer/mit-odl.png"
alt="MIT Office of Digital Learning" />
</a>
</li>
<li>
<a href="http://www.scratchfoundation.org/">
<img src="/images/conference/footer/scratch-foundation.png"
alt="Scratch Foundation" />
<li className="intel">
<a href="http://www.intel.com/content/www/us/en/homepage.html">
<img src="/images/conference/footer/intel.png"
alt="Intel" />
</a>
</li>
<li>
<li className="lego">
<a href="http://www.legofoundation.com/">
<img src="/images/conference/footer/lego-foundation.png"
alt="LEGO Foundation" />
</a>
</li>
<li>
<li className="google">
<a href="http://www.google.com/">
<img src="/images/conference/footer/google.png"
alt="Google" />
</a>
</li>
<li>
<li className="siegel">
<a href="http://www.siegelendowment.org/">
<img src="/images/conference/footer/siegel-endowment.png"
alt="Siegel Family Endowment" />
</a>
</li>
<li className="nostarch">
<a href="https://www.nostarch.com/">
<img src="/images/conference/footer/no-starch.png"
alt="No Starch Press" />
</a>
</li>
<li className="scratchfoundation">
<a href="http://www.scratchfoundation.org/">
<img src="/images/conference/footer/scratch-foundation.png"
alt="Scratch Foundation" />
</a>
</li>
</FlexRow>
</div>
<FlexRow className="scratch-links">

View file

@ -23,7 +23,19 @@
img {
margin: 20px 0;
max-width: 180px;
max-height: 30px;
max-height: 25px;
}
}
.nostarch {
img {
max-height: 40px;
}
}
.intel {
img {
max-height: 50px
}
}
}

View file

@ -31,6 +31,11 @@ var Footer = React.createClass({
<FormattedMessage id='general.forEducators' />
</a>
</dd>
<dd>
<a href="/developers">
<FormattedMessage id='general.forDevelopers' />
</a>
</dd>
<dd>
<a href="/info/credits/">
<FormattedMessage id='general.credits' />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,47 @@
var classNames = require('classnames');
var Formsy = require('formsy-react');
var omit = require('lodash.omit');
var React = require('react');
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,
onChange: function () {}
};
},
getInitialState: function () {
return {
allValues: {}
};
},
onChange: function (currentValues, isChanged) {
this.setState({allValues: omit(currentValues, 'all')});
this.props.onChange(currentValues, isChanged);
},
render: function () {
var classes = classNames(
'form',
this.props.className
);
return (
<Formsy.Form {... this.props} className={classes} ref="formsy" onChange={this.onChange}>
{React.Children.map(this.props.children, function (child) {
if (!child) return child;
if (child.props.name === 'all') {
return React.cloneElement(child, {value: this.state.allValues});
} else {
return child;
}
}.bind(this))}
</Formsy.Form>
);
}
});
module.exports = Form;

View file

@ -0,0 +1,22 @@
var Formsy = require('formsy-react');
var React = require('react');
require('./general-error.scss');
/*
* 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="general-error">
{this.props.getErrorMessage()}
</p>
);
}
}));

View file

@ -0,0 +1,9 @@
@import "../../colors";
.general-error {
border: 1px solid $active-gray;
border-radius: 4px;
background-color: $ui-orange;
padding: 1rem;
color: $type-white;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,130 @@
[
{
"id": 1,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 2,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 3,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 4,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 5,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 6,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 7,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 8,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 9,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 10,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 11,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 12,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 13,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 14,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 15,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 16,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
}
]

View file

@ -0,0 +1,67 @@
var classNames = require('classnames');
var React = require('react');
var Thumbnail = require('../thumbnail/thumbnail.jsx');
var FlexRow = require('../flex-row/flex-row.jsx');
require('./grid.scss');
var Grid = React.createClass({
type: 'Grid',
getDefaultProps: function () {
return {
items: require('./grid.json'),
itemType: 'projects',
showLoves: false,
showFavorites: false,
showRemixes: false,
showViews: false
};
},
render: function () {
var classes = classNames(
'grid',
this.props.className
);
return (
<div className={classes}>
<FlexRow>
{this.props.items.map(function (item) {
var href = '/' + this.props.itemType + '/' + item.id + '/';
if (this.props.itemType == 'projects') {
return (
<Thumbnail key={item.id}
showLoves={this.props.showLoves}
showFavorites={this.props.showFavorites}
showRemixes={this.props.showRemixes}
showViews={this.props.showViews}
type={'project'}
href={href}
title={item.title}
src={item.image}
creator={item.creator}
loves={item.stats.loves}
favorites={item.stats.favorites}
remixes={item.stats.remixes}
views={item.stats.views} />
);
}
else {
return (
<Thumbnail key={item.id}
type={'gallery'}
href={href}
title={item.title}
src={item.image}
owner={item.owner} />
);
}
}.bind(this))}
</FlexRow>
</div>
);
}
});
module.exports = Grid;

View file

@ -0,0 +1,58 @@
@import "../../frameless";
.grid {
display: inline-block;
width: 100%;
$project-width: 200px;
$project-height: 150px;
$gallery-width: 200px;
$gallery-height: 118px;
.flex-row {
margin: 0 auto;
padding: 12px;
width: (96px + (4 * $project-width)) / $em;
justify-content: flex-start;
}
.thumbnail {
padding: 12px;
&.project {
width: $project-width;
img {
width: $project-width;
height: $project-height;
}
}
&.gallery {
width: $gallery-width;
img {
width: $gallery-width;
height: $gallery-height;
}
}
}
&.column {
flex-direction: column;
justify-content: center;
}
@media only screen and (max-width: $tablet - 1) {
flex-direction: column;
}
@media only screen and (max-width: $desktop - 1) {
.flex-row {
padding: 12px 0;
width: 100%;
justify-content: space-around;
}
}
}

View file

@ -0,0 +1,37 @@
var classNames = require('classnames');
var React = require('react');
var TitleBanner = require('../../components/title-banner/title-banner.jsx');
require('./informationpage.scss');
/**
* Container for a table of contents
* alongside a long body of text
*/
var InformationPage = React.createClass({
type: 'InformationPage',
propTypes: {
title: React.PropTypes.string.isRequired
},
render: function () {
var classes = classNames(
'info-outer',
'inner',
this.props.className
);
return (
<div className="information-page">
<TitleBanner className="masthead">
<div className="inner">
<h1>{this.props.title}</h1>
</div>
</TitleBanner>
<div className={classes}>
{this.props.children}
</div>
</div>
);
}
});
module.exports = InformationPage;

View file

@ -0,0 +1,100 @@
#view {
padding: 0;
}
.information-page {
@import "../../colors";
@import "../../frameless";
$ui-secondary: darken($ui-blue, 10%);
.title-banner {
&.masthead {
background-color: $ui-secondary;
padding-bottom: .5rem;
h1 {
margin: 0 0 1rem 0;
text-align: left;
color: $ui-white;
}
p {
margin: 0;
width: $cols6;
text-align: left;
color: $ui-white;
a {
border-bottom: 1px solid $ui-white;
color: $ui-white;
}
}
}
&.faq-banner {
margin-bottom: 0;
background-color: $ui-gray;
}
}
.info-outer {
margin: 0 auto;
nav {
float: right;
border-left: 1px solid $ui-border;
width: 30%;
ol {
list-style: none;
}
}
.info-inner {
float: left;
width: 60%;
p {
margin-bottom: 1.25rem;
margin-left: 0;
max-width: $cols8;
text-align: left;
}
dl {
dt {
margin-bottom: .25rem;
font-size: 1.1rem;
}
dd {
margin-bottom: 1.25rem;
margin-left: 0;
max-width: $cols8;
text-align: left;
}
}
ul {
max-width: $cols8;
}
section {
margin-bottom: 3rem;
ul {
max-width: $cols8;
}
.nav-spacer {
display: block;
visibility: hidden;
margin-top: -50px; // height of nav bar
height: 50px;
}
}
}
}
}

View file

@ -1,28 +1,26 @@
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');
require('./languagechooser.scss');
/**
* Footer dropdown menu that allows one to change their language.
*/
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 +28,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}
value={this.props.locale}
onChange={this.onSetLanguage}
required />
</Form>
);
}
});

View file

@ -0,0 +1,10 @@
@import "../../frameless";
.language-chooser {
.select {
select {
width: 13.75rem;
/* 3 columns */
}
}
}

View file

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

View file

@ -11,6 +11,7 @@ var defaultStyle = {
backgroundColor: 'rgba(0, 0, 0, .75)'
},
content: {
position: 'absolute',
overflow: 'visible',
borderRadius: '6px',
width: 500,

View file

@ -1,6 +1,10 @@
@import "../../colors";
.ReactModal__Content {
&:focus {
outline: none;
}
iframe {
border: 0;
}

View file

@ -11,6 +11,10 @@
box-shadow: 0 0 3px $box-shadow-gray;
background-color: $ui-blue;
&.staging {
background-color: $ui-orange;
}
width: 100%;
/* NOTE: Height should match offset settings in main.scss file */
@ -73,7 +77,7 @@
.link {
> a {
display: block;
padding: 13px 15px 3px 15px;
padding: 13px 15px 4px 15px;
height: 33px;
text-decoration: none;

View file

@ -7,8 +7,9 @@ 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');
var Input = require('../../forms/input.jsx');
var log = require('../../../lib/log.js');
@ -23,9 +24,6 @@ Modal.setAppElement(document.getElementById('view'));
var Navigation = React.createClass({
type: 'Navigation',
mixins: [
Api
],
getInitialState: function () {
return {
accountNavOpen: false,
@ -43,19 +41,19 @@ var Navigation = React.createClass({
};
},
componentDidMount: function () {
if (this.props.session.user) {
if (this.props.session.session.user) {
this.getMessageCount();
var intervalId = setInterval(this.getMessageCount, 120000); // check for new messages every 2 mins.
this.setState({'messageCountIntervalId': intervalId});
}
},
componentDidUpdate: function (prevProps) {
if (prevProps.session.user != this.props.session.user) {
if (prevProps.session.session.user != this.props.session.session.user) {
this.setState({
'loginOpen': false,
'accountNavOpen': false
});
if (this.props.session.user) {
if (this.props.session.session.user) {
this.getMessageCount();
var intervalId = setInterval(this.getMessageCount, 120000);
this.setState({'messageCountIntervalId': intervalId});
@ -80,13 +78,13 @@ var Navigation = React.createClass({
}
},
getProfileUrl: function () {
if (!this.props.session.user) return;
return '/users/' + this.props.session.user.username + '/';
if (!this.props.session.session.user) return;
return '/users/' + this.props.session.session.user.username + '/';
},
getMessageCount: function () {
this.api({
api({
method: 'get',
uri: '/users/' + this.props.session.user.username + '/messages/count'
uri: '/users/' + this.props.session.session.user.username + '/messages/count'
}, function (err, body) {
if (err) return this.setState({'unreadMessageCount': 0});
if (body) {
@ -109,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/',
@ -141,7 +139,7 @@ var Navigation = React.createClass({
},
handleLogOut: function (e) {
e.preventDefault();
this.api({
api({
host: '',
method: 'post',
uri: '/accounts/logout/',
@ -174,14 +172,18 @@ var Navigation = React.createClass({
},
render: function () {
var classes = classNames({
'logged-in': this.props.session.user
'logged-in': this.props.session.session.user
});
var messageClasses = classNames({
'message-count': true,
'show': this.state.unreadMessageCount > 0
});
var dropdownClasses = classNames({
'user-info': true,
'open': this.state.accountNavOpen
});
var formatMessage = this.props.intl.formatMessage;
var createLink = this.props.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
var createLink = this.props.session.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
return (
<NavigationBox className={classes}>
<ul>
@ -193,7 +195,7 @@ var Navigation = React.createClass({
</a>
</li>
<li className="link explore">
<a href="/explore?date=this_month">
<a href="/explore/projects/all">
<FormattedMessage id="general.explore" />
</a>
</li>
@ -214,17 +216,17 @@ var Navigation = React.createClass({
</li>
<li className="search">
<form action="/search/google_results" method="get">
<Input type="submit" value="" />
<form action="/search/projects" method="get">
<Button type="submit" className="btn-search" />
<Input type="text"
aria-label={formatMessage({id: 'general.search'})}
placeholder={formatMessage({id: 'general.search'})}
name="q" />
<Input type="hidden" name="date" value="anytime" />
<Input type="hidden" name="sort_by" value="datetime_shared" />
name="q"
noformsy />
</form>
</li>
{this.props.session.user ? [
{this.props.session.status === sessionActions.Status.FETCHED ? (
this.props.session.session.user ? [
<li className="link right messages" key="messages">
<a
href="/messages/"
@ -243,14 +245,16 @@ var Navigation = React.createClass({
</a>
</li>,
<li className="link right account-nav" key="account-nav">
<a className="user-info" href="#" onClick={this.handleAccountNavClick}>
<Avatar src={this.props.session.user.thumbnailUrl} alt="" />
{this.props.session.user.username}
<a className={dropdownClasses}
href="#" onClick={this.handleAccountNavClick}>
<Avatar src={this.props.session.session.user.thumbnailUrl} alt="" />
{this.props.session.session.user.username}
</a>
<Dropdown
as="ul"
isOpen={this.state.accountNavOpen}
onRequestClose={this.closeAccountNav}>
onRequestClose={this.closeAccountNav}
className={process.env.SCRATCH_ENV}>
<li>
<a href={this.getProfileUrl()}>
<FormattedMessage id="general.profile" />
@ -261,16 +265,16 @@ var Navigation = React.createClass({
<FormattedMessage id="general.myStuff" />
</a>
</li>
{this.props.session.permissions.educator ? [
<li>
{this.props.permissions.educator ? [
<li key="my-classes-li">
<a href="/educators/classes/">
<FormattedMessage id="general.myClasses" />
</a>
</li>
] : []}
{this.props.session.permissions.student ? [
{this.props.permissions.student ? [
<li>
<a href={'/classes/' + this.props.session.user.classroomId + '/'}>
<a href={'/classes/' + this.props.session.session.user.classroomId + '/'}>
<FormattedMessage id="general.myClass" />
</a>
</li>
@ -316,6 +320,7 @@ var Navigation = React.createClass({
error={this.state.loginError} />
</Dropdown>
</li>
]) : [
]}
</ul>
<Modal isOpen={this.state.canceledDeletionOpen}
@ -336,7 +341,8 @@ var Navigation = React.createClass({
var mapStateToProps = function (state) {
return {
session: state.session
session: state.session,
permissions: state.permissions
};
};

View file

@ -1,6 +1,18 @@
@import "../../../colors";
#navigation {
&.staging {
.messages {
.message-count {
display: none;
&.show {
background-color: $ui-blue;
}
}
}
}
.logo {
margin-right: 10px;
@ -39,7 +51,8 @@
margin: 0;
}
input {
input,
button {
display: inline-block;
margin-top: 5px;
outline: none;
@ -47,19 +60,6 @@
background-color: $active-gray;
height: 14px;
&[type=submit] {
position: absolute;
background-color: transparent;
background-image: url("/images/nav-search-glass.png");
background-repeat: no-repeat;
background-position: center center;
background-size: 14px 14px;
width: 40px;
height: 40px;
}
&[type=text] {
transition: .15s ease background-color;
padding: 0;
@ -85,6 +85,24 @@
}
}
}
.btn-search {
position: absolute;
box-shadow: none;
background-color: transparent;
background-image: url("/images/nav-search-glass.png");
background-repeat: no-repeat;
background-position: center center;
background-size: 14px 14px;
width: 40px;
height: 40px;
&:hover {
box-shadow: none;
}
}
}
}
@ -141,6 +159,21 @@
.login-dropdown {
width: 200px;
.button {
padding: .75em;
}
}
.dropdown {
.row {
margin-bottom: 1.25rem;
input {
margin: 0;
height: 2.25rem;
}
}
}
.account-nav {
@ -164,6 +197,10 @@
vertical-align: middle;
}
&.open {
background-color: $active-gray;
}
&:after {
display: inline-block;
margin-left: 8px;
@ -180,9 +217,11 @@
}
.dropdown {
top: 50px;
padding: 0;
padding-top: 5px;
width: 100%;
box-sizing: border-box;
}
}
}

View file

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

View file

@ -1,4 +1,5 @@
var React = require('react');
var classNames = require('classnames');
var Navigation = require('../../navigation/www/navigation.jsx');
var Footer = require('../../footer/www/footer.jsx');
@ -6,9 +7,12 @@ var Footer = require('../../footer/www/footer.jsx');
var Page = React.createClass({
type: 'Page',
render: function () {
var classes = classNames({
'staging': process.env.SCRATCH_ENV == 'staging'
});
return (
<div className="page">
<div id="navigation">
<div id="navigation" className={classes}>
<Navigation />
</div>
<div id="view">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,26 @@
var classNames = require('classnames');
var SubNavigation = require('../../components/subnavigation/subnavigation.jsx');
var React = require('react');
require('./tabs.scss');
/**
* Container for a custom, horizontal list of navigation elements
* that can be displayed within a view or component.
*/
var Tabs = React.createClass({
type: 'Tabs',
render: function () {
var classes = classNames(
'tabs',
this.props.className
);
return (
<SubNavigation className={classes}>
{this.props.children}
</SubNavigation>
);
}
});
module.exports = Tabs;

View file

@ -0,0 +1,39 @@
@import "../../colors";
.tabs {
background-color: $ui-gray;
padding: 0 0 0 20px;
width: calc(100% - 20px);
justify-content: flex-start;
}
.tabs li {
opacity: .75;
margin-bottom: -4px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-color: $ui-white;
color: $header-gray;
&:hover {
opacity: 1;
border-color: $active-gray;
background-color: $ui-white;
color: $header-gray;
}
}
.tabs li.active {
opacity: 1;
border-bottom: 4px solid $ui-white;
&:hover {
opacity: 1;
border-bottom: 4px solid $ui-white;
background-color: $ui-white;
color: $header-gray;
}
}

View file

@ -0,0 +1,83 @@
var classNames = require('classnames');
var connect = require('react-redux').connect;
var React = require('react');
var sessionActions = require('../../redux/session.js');
var TitleBanner = require('../title-banner/title-banner.jsx');
var Button = require('../forms/button.jsx');
var FlexRow = require('../flex-row/flex-row.jsx');
require('./teacher-banner.scss');
var TeacherBanner = React.createClass({
type: 'TeacherBanner',
getDefaultProps: function () {
return {
messages: {
'teacherbanner.greeting': 'Hi',
'teacherbanner.subgreeting': 'Teacher Account',
'teacherbanner.classesButton': 'My Classes',
'teacherbanner.resourcesButton': 'Educator Resources',
'teacherbanner.faqButton': 'Teacher Account FAQ'
},
session: {}
};
},
render: function () {
var classes = classNames(
'teacher-banner',
this.props.className
);
return (
<TitleBanner className={classes}>
<FlexRow className="inner">
<div className="welcome">
{this.props.session.status === sessionActions.Status.FETCHED ? (
this.props.session.session.user ? [
<h3 key="greeting">
{this.props.messages['teacherbanner.greeting']},{' '}
{this.props.session.session.user.username}
</h3>,
<p key="subgreeting">
{this.props.messages['teacherbanner.subgreeting']}
</p>
] : []
): []}
</div>
<FlexRow className="quick-links">
{this.props.session.status === sessionActions.Status.FETCHED ? (
this.props.session.session.user ? [
<a href="/educators/classes" key="classes-button">
<Button>
{this.props.messages['teacherbanner.classesButton']}
</Button>
</a>,
<a href="/info/educators" key="resources-button">
<Button>
{this.props.messages['teacherbanner.resourcesButton']}
</Button>
</a>,
<a href="/educators/faq" key="faq-button">
<Button>
{this.props.messages['teacherbanner.faqButton']}
</Button>
</a>
] : []
): []}
</FlexRow>
</FlexRow>
</TitleBanner>
);
}
});
var mapStateToProps = function (state) {
return {
session: state.session
};
};
var ConnectedTeacherBanner = connect(mapStateToProps)(TeacherBanner);
module.exports = ConnectedTeacherBanner;

View file

@ -0,0 +1,38 @@
@import "../../colors";
@import "../../frameless";
.teacher-banner {
background-color: $ui-purple;
min-height: 65px;
&.title-banner {
transition: none;
margin-bottom: 20px;
text-align: left;
h3,
p {
margin: 0;
padding: 0;
width: 100%;
text-align: left;
}
}
h3 {
color: $ui-white;
}
.flex-row {
&.inner {
justify-content: space-between;
}
}
.button {
margin-left: 10px;
background-color: $ui-white;
padding: 13px 20px;
color: $ui-blue;
}
}

View file

@ -15,7 +15,9 @@ var Thumbnail = React.createClass({
src: '',
type: 'project',
showLoves: false,
showFavorites: false,
showRemixes: false,
showViews: false,
linkTitle: true,
alt: ''
};
@ -40,23 +42,40 @@ var Thumbnail = React.createClass({
key="loves"
className="thumbnail-loves"
title={this.props.loves + ' loves'}>
{this.props.loves}
</div>
);
}
if (this.props.favorites && this.props.showFavorites) {
extra.push(
<div
key="favorites"
className="thumbnail-favorites"
title={this.favorites + ' favorites'}>
{this.props.favorites}
</div>
);
}
if (this.props.remixes && this.props.showRemixes) {
extra.push(
<div
key="remixes"
className="thumbnail-remixes"
title={this.props.remixes + ' remixes'}>
{this.props.remixes}
</div>
);
}
if (this.props.views && this.props.showViews) {
extra.push(
<div
key="views"
className="thumbnail-views"
title={this.props.views + ' views'}>
{this.props.views}
</div>
);
}
var imgElement,titleElement;
if (this.props.linkTitle) {
imgElement = <a className="thumbnail-image" href={this.props.href}>

View file

@ -10,7 +10,7 @@
}
}
$extras: ".thumbnail-creator, .thumbnail-loves, .thumbnail-remixes";
$extras: ".thumbnail-creator, .thumbnail-loves, .thumbnail-favorites,.thumbnail-remixes,.thumbnail-views";
.thumbnail-title,
#{$extras} {
@ -41,7 +41,13 @@
}
.thumbnail-loves,
.thumbnail-remixes {
.thumbnail-favorites,
.thumbnail-remixes,
.thumbnail-views {
display: inline;
margin-right: 10px;
&:before {
display: inline-block;
margin-right: .1rem;
@ -61,12 +67,24 @@
}
}
.thumbnail-favorites {
&:before {
background-image: url("/svgs/favorite/favorite_type-gray.svg");
}
}
.thumbnail-remixes {
&:before {
background-image: url("/svgs/remix/remix_type-gray.svg");
}
}
.thumbnail-views {
&:before {
background-image: url("/svgs/view/view_type-gray.svg");
}
}
&.project {
$project-width: 144px;
$project-height: 108px;

View file

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

View file

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

View file

@ -2,32 +2,59 @@
"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.createUsername": "Create a Username",
"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",
"general.joinScratch": "Join Scratch",
"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",
"general.profile": "Profile",
"general.scratchConference": "Scratch Conference",
"general.scratchday": "Scratch Day",
@ -37,6 +64,7 @@
"general.search": "Search",
"general.signIn": "Sign in",
"general.statistics": "Statistics",
"general.studios": "Studios",
"general.support": "Support",
"general.tipsWindow": "Tips Window",
"general.tipsAnimateYourNameTitle": "Animate Your Name",
@ -47,10 +75,27 @@
"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",
"general.all": "All",
"general.animations": "Animations",
"general.art": "Art",
"general.games": "Games",
"general.music": "Music",
"general.stories": "Stories",
"general.results": "Results",
"general.teacherAccounts": "Teacher Accounts",
"footer.discuss": "Discussion Forums",
"footer.help": "Help Page",
"footer.scratchFamily": "Scratch Family",
@ -62,5 +107,15 @@
"parents.FaqAgeRangeA": "While Scratch is primarily designed for 8 to 16 year olds, it is also used by people of all ages, including younger children with their parents.",
"parents.FaqAgeRangeQ": "What is the age range for Scratch?",
"parents.FaqResourcesQ": "What resources are available for learning Scratch?",
"parents.introDescription": "Scratch is a programming language and an online community where children can program and share interactive media such as stories, games, and animation with people from all over the world. As children create with Scratch, they learn to think creatively, work collaboratively, and reason systematically. Scratch is designed and maintained by the Lifelong Kindergarten group at the MIT Media Lab."
"parents.introDescription": "Scratch is a programming language and an online community where children can program and share interactive media such as stories, games, and animation with people from all over the world. As children create with Scratch, they learn to think creatively, work collaboratively, and reason systematically. Scratch is designed and maintained by the Lifelong Kindergarten group at the MIT Media Lab.",
"registration.lastStepTitle": "Thank you for requesting a Scratch Teacher Account",
"registration.lastStepDescription": "We are currently processing your application. ",
"registration.confirmYourEmail": "Confirm Your Email",
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
"registration.waitForApproval": "Wait for Approval",
"registration.waitForApprovalDescription": "Your information is being reviewed. Please be patient, the approval process can take up to 24 hours. You will receive an email with your login information once your account has been created.",
"registration.checkOutResources": "Get Started with Resources",
"registration.checkOutResourcesDescription": "Explore materials for educators and facilitators written by the Scratch Team, including <a href='/educators#resources'>tips, tutorials, and guides</a>."
}

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

@ -0,0 +1,93 @@
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);
if (opts.responseType === 'json' && typeof body === 'string') {
// IE doesn't parse responses as JSON without the json attribute,
// even with responseType: 'json'.
// See https://github.com/Raynos/xhr/issues/123
try {
body = JSON.parse(body);
} catch (e) {
// Not parseable anyway, don't worry about it
}
}
// Legacy API responses come as lists, and indicate to redirect the client like
// [{success: true, redirect: "/location/to/redirect"}]
try {
if ('redirect' in body[0]) window.location = body[0].redirect;
} catch (err) {
// do nothing
}
callback(err, body, res);
});
}.bind(this);
if (typeof jar.get('scratchlanguage') !== 'undefined') {
opts.headers['Accept-Language'] = jar.get('scratchlanguage') + ', en;q=0.8';
}
if (opts.authentication) {
opts.headers['X-Token'] = opts.authentication;
}
if (opts.useCsrf) {
jar.use('scratchcsrftoken', '/csrf_token/', function (err, csrftoken) {
if (err) return log.error('Error while retrieving CSRF token', err);
opts.headers['X-CSRFToken'] = csrftoken;
apiRequest(opts);
}.bind(this));
} else {
apiRequest(opts);
}
};

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

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

View file

@ -15,7 +15,8 @@ var Jar = {
// Return the usable content portion of a signed, compressed cookie generated by
// Django's signing module
// https://github.com/django/django/blob/stable/1.8.x/django/core/signing.py
if (!value) return callback('No value to unsign');
if (typeof value === 'undefined') return callback(null, value);
try {
var b64Data = value.split(':')[0];
var decompress = false;
@ -73,6 +74,24 @@ var Jar = {
var expires = '; expires=' + new Date(new Date().setYear(new Date().getFullYear() + 1)).toUTCString();
var path = '; path=/';
document.cookie = obj + expires + path;
},
getUnsignedValue: function (cookieName, signedValue, callback) {
// Get a value from a signed object
Jar.get(cookieName, function (err, value) {
if (err) return callback(err);
if (typeof value === 'undefined') return callback(null, value);
Jar.unsign(value, function (err, contents) {
if (err) return callback(err);
try {
var data = JSON.parse(contents);
} catch (err) {
return callback(err);
}
return callback(null, data[signedValue]);
});
});
}
};

View file

@ -6,6 +6,7 @@ var ReactDOM = require('react-dom');
var StoreProvider = require('react-redux').Provider;
var IntlProvider = require('./intl.jsx').IntlProvider;
var permissionsActions = require('../redux/permissions.js');
var sessionActions = require('../redux/session.js');
var reducer = require('../redux/reducer.js');
@ -42,7 +43,8 @@ var render = function (jsx, element) {
element
);
// Get initial session
// Get initial session & permissions
store.dispatch(permissionsActions.getPermissions());
store.dispatch(sessionActions.refreshSession());
};

21
src/lib/shuffle.js Normal file
View file

@ -0,0 +1,21 @@
/*
* Function that shuffles an array using a Fisher-Yates shuffle.
*/
module.exports.shuffle = function (arr) {
var i, j = 0;
var temp = null;
if (arr) {
var tempArray = arr.slice(0);
} else {
return arr;
}
for (i = arr.length - 1; i > 0; i -= 1) {
j = Math.floor(Math.random() * (i + 1));
temp = tempArray[i];
tempArray[i] = tempArray[j];
tempArray[j] = temp;
}
return tempArray;
};

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

46
src/redux/permissions.js Normal file
View file

@ -0,0 +1,46 @@
var keyMirror = require('keymirror');
var jar = require('../lib/jar.js');
var Types = keyMirror({
SET_PERMISSIONS: null,
SET_PERMISSIONS_ERROR: null
});
module.exports.permissionsReducer = function (state, action) {
if (typeof state === 'undefined') {
state = '';
}
switch (action.type) {
case Types.SET_PERMISSIONS:
return action.permissions;
case Types.SET_PERMISSIONS_ERROR:
return state;
default:
return state;
}
};
module.exports.getPermissions = function () {
return function (dispatch) {
jar.getUnsignedValue('scratchsessionsid', 'permissions', function (err, value) {
if (err) return dispatch(module.exports.setPermissionsError(err));
value = value || {};
return dispatch(module.exports.setPermissions(value));
});
};
};
module.exports.setPermissions = function (permissions) {
return {
type: Types.SET_PERMISSIONS,
permissions: permissions
};
};
module.exports.setPermissionsError = function (error) {
return {
type: Types.SET_PERMISSIONS_ERROR,
error: error
};
};

View file

@ -2,12 +2,14 @@ var combineReducers = require('redux').combineReducers;
var scheduleReducer = require('./conference-schedule.js').scheduleReducer;
var detailsReducer = require('./conference-details.js').detailsReducer;
var permissionsReducer = require('./permissions.js').permissionsReducer;
var sessionReducer = require('./session.js').sessionReducer;
var tokenReducer = require('./token.js').tokenReducer;
var appReducer = combineReducers({
session: sessionReducer,
token: tokenReducer,
permissions: permissionsReducer,
conferenceSchedule: scheduleReducer,
conferenceDetails: detailsReducer
});

View file

@ -1,21 +1,36 @@
var keyMirror = require('keymirror');
var defaults = require('lodash.defaults');
var api = require('../mixins/api.jsx').api;
var api = require('../lib/api');
var permissionsActions = require('./permissions.js');
var tokenActions = require('./token.js');
var Types = keyMirror({
SET_SESSION: null,
SET_SESSION_ERROR: null
SET_SESSION_ERROR: null,
SET_STATUS: null
});
module.exports.Status = keyMirror({
FETCHED: null,
NOT_FETCHED: null,
FETCHING: null
});
module.exports.getInitialState = function (){
return {status: module.exports.Status.NOT_FETCHED, session:{}};
};
module.exports.sessionReducer = function (state, action) {
// Reducer for handling changes to session state
if (typeof state === 'undefined') {
state = {};
state = module.exports.getInitialState();
}
switch (action.type) {
case Types.SET_SESSION:
return action.session;
return defaults({session: action.session}, state);
case Types.SET_STATUS:
return defaults({status: action.status}, state);
case Types.SET_SESSION_ERROR:
// TODO: do something with action.error
return state;
@ -38,8 +53,16 @@ module.exports.setSession = function (session) {
};
};
module.exports.setStatus = function (status){
return {
type: Types.SET_STATUS,
status: status
};
};
module.exports.refreshSession = function () {
return function (dispatch) {
dispatch(module.exports.setStatus(module.exports.Status.FETCHING));
api({
host: '',
uri: '/session/'
@ -52,6 +75,10 @@ module.exports.refreshSession = function () {
} else {
dispatch(tokenActions.getToken());
dispatch(module.exports.setSession(body));
dispatch(module.exports.setStatus(module.exports.Status.FETCHED));
// get the permissions from the updated session
dispatch(permissionsActions.getPermissions());
return;
}
}

View file

@ -25,17 +25,11 @@ module.exports.tokenReducer = function (state, action) {
module.exports.getToken = function () {
return function (dispatch) {
jar.get('scratchsessionsid', function (err, value) {
jar.getUnsignedValue('scratchsessionsid', 'token', function (err, value) {
if (err) return dispatch(module.exports.setTokenError(err));
jar.unsign(value, function (err, contents) {
if (err) return dispatch(module.exports.setTokenError(err));
try {
var sessionData = JSON.parse(contents);
} catch (err) {
return dispatch(module.exports.setTokenError(err));
}
return dispatch(module.exports.setToken(sessionData.token));
});
value = value || '';
return dispatch(module.exports.setToken(value));
});
};
};

View file

@ -11,12 +11,6 @@
"view": "about/about",
"title": "About"
},
{
"name": "components",
"pattern": "^/components/?$",
"view": "components/components",
"title": "Components"
},
{
"name": "developers",
"pattern": "^/developers/?$",
@ -29,12 +23,62 @@
"view": "hoc/hoc",
"title": "Hour of Code"
},
{
"name": "explore",
"pattern": "^/explore/:projects/:all/?$",
"routeAlias": "^/explore",
"view": "explore/explore",
"title": "Explore"
},
{
"name": "explore-redirect",
"pattern": "^/explore/?$",
"routeAlias": "^/explore",
"redirect": "/explore/projects/all"
},
{
"name": "explore-projects-redirect",
"pattern": "^/explore/projects/?$",
"routeAlias": "^/explore",
"redirect": "/explore/projects/all"
},
{
"name": "explore-studios-redirect",
"pattern": "^/explore/studios/?$",
"routeAlias": "^/explore",
"redirect": "/explore/studios/all"
},
{
"name": "search",
"pattern": "^/search/:projects?$/?$",
"routeAlias": "^/search",
"view": "search/search",
"title": "Search"
},
{
"name": "credits",
"pattern": "^/info/credits/?$",
"view": "credits/credits",
"title": "Credits"
},
{
"name": "faq",
"pattern": "^/info/faq/?$",
"view": "faq/faq",
"title": "FAQ"
},
{
"name": "educator-landing",
"pattern": "^/educators/?$",
"view": "teachers/landing/landing",
"title": "Educators"
},
{
"name": "teacher-faq",
"pattern": "^/educators/faq/?$",
"view": "teachers/faq/faq",
"title": "Teacher Accounts FAQ"
},
{
"name": "cards",
"pattern": "^/info/cards/?$",
@ -53,6 +97,19 @@
"view": "jobs/jobs",
"title": "Jobs"
},
{
"name": "teacherregistration",
"pattern": "^/educators/register$",
"view": "teacherregistration/teacherregistration",
"title": "Teacher Registration",
"viewportWidth": "device-width"
},
{
"name": "teacherwaitingroom",
"pattern": "^/educators/waiting",
"view": "teacherwaitingroom/teacherwaitingroom",
"title": "Thank you for requesting a Scratch Teacher Account"
},
{
"name": "wedo2",
"pattern": "^/wedo/?$",
@ -62,6 +119,7 @@
{
"name": "conference-index",
"pattern": "^/conference/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/index/index",
"title": "Scratch Conference",
"viewportWidth": "device-width"
@ -69,6 +127,7 @@
{
"name": "conference-plan",
"pattern": "^/conference/plan/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/plan/plan",
"title": "Plan Your Visit",
"viewportWidth": "device-width"
@ -76,6 +135,7 @@
{
"name": "conference-expectations",
"pattern": "^/conference/expect/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/expect/expect",
"title": "What to Expect",
"viewportWidth": "device-width"
@ -83,6 +143,7 @@
{
"name": "conference-schedule",
"pattern": "^/conference/schedule/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/schedule/schedule",
"title": "Conference Schedule",
"viewportWidth": "device-width"
@ -90,6 +151,7 @@
{
"name": "conference-details",
"pattern": "^/conference/:id/details/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/details/details",
"title": "Event Details",
"viewportWidth": "device-width"
@ -107,7 +169,7 @@
},
{
"name": "guidelines",
"pattern": "^/community_guidelines$",
"pattern": "^/community_guidelines/?$",
"view": "guidelines/guidelines",
"title": "Scratch Community Guidelines"
},
@ -116,5 +178,11 @@
"pattern": "^/privacy_policy/?$",
"view": "privacypolicy/privacypolicy",
"title": "Privacy Policy"
},
{
"name": "terms",
"pattern": "^/terms_of_use/?$",
"view": "terms/terms",
"title": "Scratch Terms of Use"
}
]

View file

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

View file

@ -31,25 +31,8 @@
<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>
<body>
<div id="app"></div>
<!-- Vendor & Initialize (Session & Localization)-->
<script src="/js/common.bundle.js"></script>
<!-- Scripts -->
<script src="/js/{{name}}.intl.js"></script>
<script src="/js/{{name}}.bundle.js"></script>
<!-- Analytics (GA) -->
<script>
@ -63,8 +46,19 @@
});
ga('send', 'pageview');
</script>
</head>
<!-- translate title element -->
<body>
<div id="app"></div>
<!-- Vendor & Initialize (Session & Localization)-->
<script src="/js/common.bundle.js"></script>
<!-- Scripts -->
<script src="/js/{{name}}.intl.js"></script>
<script src="/js/{{name}}.bundle.js"></script>
<!-- Translate title element -->
<script>
var loc = window._locale || 'en';
if (typeof window._messages !== 'undefined' && loc !== 'en') {

View file

@ -11,8 +11,14 @@ require('./cards.scss');
var Cards = injectIntl(React.createClass({
type: 'Cards',
pdfLocaleMismatch: function (locale, pdf, englishPdf) {
if (pdf === englishPdf && locale.indexOf('en') !== 0) {
return true;
}
return false;
},
render: function () {
var locale = window._locale || 'en';
var locale = this.props.intl.locale || 'en';
var formatMessage = this.props.intl.formatMessage;
var englishLinks = {
'cards.starterLink': '//scratch.mit.edu/scratchr2/static/pdfs/help/Scratch2Cards.pdf',
@ -51,8 +57,11 @@ var Cards = injectIntl(React.createClass({
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
<FormattedMessage id='cards.viewCard' />
{(
formattedLinks['cards.starterLink'] === englishLinks['cards.starterLink'] &&
locale !== 'en'
this.pdfLocaleMismatch(
locale,
formattedLinks['cards.starterLink'],
englishLinks['cards.starterLink']
)
) ? [
<span> <FormattedMessage id='cards.english' /></span>
] : []}
@ -67,8 +76,11 @@ var Cards = injectIntl(React.createClass({
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
<FormattedMessage id='cards.viewCard' />
{(
formattedLinks['cards.nameLink'] === englishLinks['cards.nameLink'] &&
locale !== 'en'
this.pdfLocaleMismatch(
locale,
formattedLinks['cards.nameLink'],
englishLinks['cards.nameLink']
)
) ? [
<span> (<FormattedMessage id='cards.english' />)</span>
] : []}
@ -83,8 +95,11 @@ var Cards = injectIntl(React.createClass({
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
<FormattedMessage id='cards.viewCard' />
{(
formattedLinks['cards.pongLink'] === englishLinks['cards.pongLink'] &&
locale !== 'en'
this.pdfLocaleMismatch(
locale,
formattedLinks['cards.pongLink'],
englishLinks['cards.pongLink']
)
) ? [
<span> (<FormattedMessage id='cards.english' />)</span>
] : []}
@ -101,8 +116,11 @@ var Cards = injectIntl(React.createClass({
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
<FormattedMessage id='cards.viewCard' />
{(
formattedLinks['cards.storyLink'] === englishLinks['cards.storyLink'] &&
locale !== 'en'
this.pdfLocaleMismatch(
locale,
formattedLinks['cards.storyLink'],
englishLinks['cards.storyLink']
)
) ? [
<span> (<FormattedMessage id='cards.english' />)</span>
] : []}
@ -117,8 +135,11 @@ var Cards = injectIntl(React.createClass({
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
<FormattedMessage id='cards.viewCard' />
{(
formattedLinks['cards.danceLink'] === englishLinks['cards.danceLink'] &&
locale !== 'en'
this.pdfLocaleMismatch(
locale,
formattedLinks['cards.danceLink'],
englishLinks['cards.danceLink']
)
) ? [
<span> (<FormattedMessage id='cards.english' />)</span>
] : []}
@ -133,8 +154,11 @@ var Cards = injectIntl(React.createClass({
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
<FormattedMessage id='cards.viewCard' />
{(
formattedLinks['cards.hideLink'] === englishLinks['cards.hideLink'] &&
locale !== 'en'
this.pdfLocaleMismatch(
locale,
formattedLinks['cards.hideLink'],
englishLinks['cards.hideLink']
)
) ? [
<span> (<FormattedMessage id='cards.english' />)</span>
] : []}

View file

@ -33,7 +33,7 @@ var ConferenceExpectations = React.createClass({
<p className="intro">
The Scratch community keeps growing and growing.{' '}
Young people around the world have shared more than{' '}
14 million projects in the Scratch online community{' '}
15 million projects in the Scratch online community{' '}
with 20,000 new projects every day.
</p>
<p className="intro">
@ -58,7 +58,7 @@ var ConferenceExpectations = React.createClass({
<p className="intro">
We are planning a very participatory conference, with lots of{' '}
hands-on workshops and opportunities for collaboration and sharing.{' '}
We hope youll join us. Lets learn together!
Lets learn together!
</p>
</div>
</FlexRow>
@ -167,7 +167,7 @@ var ConferenceExpectations = React.createClass({
<tr>
<td>
<b>2:00p</b>
<p>Afternoon workshops</p>
<p>Afternoon Workshops</p>
</td>
</tr>
<tr>
@ -271,7 +271,7 @@ var ConferenceExpectations = React.createClass({
<tr>
<td>
<b>12:00p</b>
<p>Lunch (provided)</p>
<p>Lunch (provided) and Wrap-up Session</p>
</td>
</tr>
</tbody>

View file

@ -30,7 +30,7 @@ var ConferenceSplash = React.createClass({
</p>
<p className="sub-button">
<b>
We're sold out! <a href="//scratchconference2016.eventbrite.com">Join the Waitlist</a>
Scratch@MIT is sold out!
</b>
</p>
</TitleBanner>

View file

@ -31,11 +31,6 @@
b {
margin-top: 2rem;
a {
text-decoration: underline;
color: $type-white;
}
}
a {

View file

@ -266,11 +266,11 @@ var ConferencePlan = React.createClass({
The conference is sold out. What can I do?
</dt>
<dd>
Scratch@MIT is sold out and at capacity. However, you can{' '}
<a href="//scratchconference2016.eventbrite.com">
join the waitlist
</a>. People on the waitlist will be notified of openings on{' '}
a first come, first served basis.
Scratch@MIT is sold out and at capacity. Regrettably, we are{' '}
unable to add any additional guests. Please keep in mind that{' '}
you must have registered on Eventbrite to attend Scratch@MIT;{' '}
people who are not registered / do not have a ticket will not be{' '}
able to attend the conference.
</dd>
<dt>

View file

@ -228,11 +228,10 @@ var Developers = React.createClass({
<div className="faq column">
<h4>Are there rules to using this code in my application?</h4>
<p>
You may use this code in accordance with the{' '}
<a href="http://www.apache.org/licenses/LICENSE-2.0">Apache 2.0</a> license
which governs this project. We also strongly encourage you to consider{' '}
the learning and design principles (above, on this page) when building{' '}
creative learning experiences for kids of all ages.
You may use this code in accordance with the license which governs{' '}
each project. We also strongly encourage you to consider the learning{' '}
and design principles (above, on this page) when building creative{' '}
learning experiences for kids of all ages.
</p>
</div>
<div className="faq column">
@ -243,7 +242,7 @@ var Developers = React.createClass({
<p>
If you wish, you can publicly state that your application is powered by{' '}
Scratch Blocks. If you do so, we would also encourage you to link back to{' '}
code repository.
the code repository.
</p>
</div>
<div className="faq column">

View file

@ -1,15 +1,16 @@
var React = require('react');
var FormattedMessage = require('react-intl').FormattedMessage;
var render = require('../../lib/render.jsx');
var InformationPage = require('../../components/informationpage/informationpage.jsx');
var Page = require('../../components/page/www/page.jsx');
var Box = require('../../components/box/box.jsx');
var Dmca = React.createClass({
type: 'Dmca',
render: function () {
return (
<div className="inner dmca">
<Box title={'DMCA'}>
<InformationPage title={'DMCA'}>
<div className="inner info-inner">
<p><FormattedMessage id='dmca.intro' /></p>
<p>
Copyright Agent / Mitchel Resnick<br/>
@ -23,8 +24,8 @@ var Dmca = React.createClass({
<p><FormattedMessage id='dmca.assessment' /></p>
<p><FormattedMessage id='dmca.eyetoeye' /></p>
<p><FormattedMessage id='dmca.afterfiling' /></p>
</Box>
</div>
</InformationPage>
);
}
});

View file

@ -0,0 +1,152 @@
var classNames = require('classnames');
var injectIntl = require('react-intl').injectIntl;
var FormattedMessage = require('react-intl').FormattedMessage;
var React = require('react');
var render = require('../../lib/render.jsx');
var api = require('../../lib/api');
var Page = require('../../components/page/www/page.jsx');
var Box = require('../../components/box/box.jsx');
var Tabs = require('../../components/tabs/tabs.jsx');
var SubNavigation = require('../../components/subnavigation/subnavigation.jsx');
var Grid = require('../../components/grid/grid.jsx');
require('./explore.scss');
// @todo migrate to React-Router once available
var Explore = injectIntl(React.createClass({
type: 'Explore',
getDefaultProps: function () {
var categoryOptions = {
all: '*',
animations: 'animations',
art: 'art',
games: 'games',
music: 'music',
stories: 'stories'
};
var typeOptions = ['projects','studios'];
var pathname = window.location.pathname.toLowerCase();
if (pathname[pathname.length - 1] === '/') {
pathname = pathname.substring(0, pathname.length - 1);
}
var slash = pathname.lastIndexOf('/');
var currentCategory = pathname.substring(slash + 1,pathname.length);
var typeStart = pathname.indexOf('explore/');
var type = pathname.substring(typeStart + 8,slash);
if (Object.keys(categoryOptions).indexOf(currentCategory) === -1 || typeOptions.indexOf(type) === -1) {
window.location = window.location.origin + '/explore/projects/all/';
}
return {
category: currentCategory,
acceptableTabs: categoryOptions,
acceptableTypes: typeOptions,
itemType: type,
loadNumber: 16
};
},
getInitialState: function () {
return {
loaded: [],
offset: 0
};
},
componentDidMount: function () {
this.getExploreMore();
},
getExploreMore: function () {
var qText = '&q=' + this.props.acceptableTabs[this.props.category] || '*';
api({
uri: '/search/' + this.props.itemType +
'?limit=' + this.props.loadNumber +
'&offset=' + this.state.offset +
'&language=' + this.props.intl.locale +
qText
}, function (err, body) {
if (!err) {
var loadedSoFar = this.state.loaded;
Array.prototype.push.apply(loadedSoFar,body);
this.setState({loaded: loadedSoFar});
var currentOffset = this.state.offset + this.props.loadNumber;
this.setState({offset: currentOffset});
}
}.bind(this));
},
changeItemType: function () {
var newType;
for (var t in this.props.acceptableTypes) {
if (this.props.itemType !== t) {
newType = t;
break;
}
}
window.location = window.location.origin + '/explore/' + newType + '/' + this.props.tab;
},
getBubble: function (type) {
var classes = classNames({
active: (this.props.category === type)
});
return (
<a href={'/explore/' + this.props.itemType + '/' + type + '/'}>
<li className={classes}>
<FormattedMessage id={'general.' + type} />
</li>
</a>
);
},
getTab: function (type) {
var classes = classNames({
active: (this.props.itemType === type)
});
return (
<a href={'/explore/' + type + '/' + this.props.category + '/'}>
<li className={classes}>
<FormattedMessage id={'general.' + type} />
</li>
</a>
);
},
render: function () {
return (
<div>
<div className='outer'>
<Box title={'Explore'}>
<SubNavigation className='categories'>
{this.getBubble('all')}
{this.getBubble('animations')}
{this.getBubble('art')}
{this.getBubble('games')}
{this.getBubble('music')}
{this.getBubble('stories')}
</SubNavigation>
<Tabs>
{this.getTab('projects')}
{this.getTab('studios')}
</Tabs>
<div id='projectBox' key='projectBox'>
<Grid items={this.state.loaded}
itemType={this.props.itemType}
showLoves={true}
showFavorites={true}
showViews={true} />
<SubNavigation className='load'>
<button onClick={this.getExploreMore}>
<li>
<FormattedMessage id='general.loadMore' />
</li>
</button>
</SubNavigation>
</div>
</Box>
</div>
</div>
);
}
}));
render(<Page><Explore /></Page>, document.getElementById('app'));

Some files were not shown because too many files have changed in this diff Show more