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/* static/*
build/* build/*
intl/* intl/*
locales/*
**/*.min.js **/*.min.js

View file

@ -47,6 +47,8 @@ env:
- SENTRY_DSN_VAR=SENTRY_DSN_$TRAVIS_BRANCH - SENTRY_DSN_VAR=SENTRY_DSN_$TRAVIS_BRANCH
- SENTRY_DSN=${!SENTRY_DSN_VAR} - SENTRY_DSN=${!SENTRY_DSN_VAR}
- SENTRY_DSN=${SENTRY_DSN:-$SENTRY_DSN_STAGING} - 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 - SKIP_CLEANUP=true
- NODE_ENV=production - NODE_ENV=production
- WWW_VERSION=${TRAVIS_COMMIT:0:5} - 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 * the express route and a static view file associated with the route
*/ */
var getViewPaths = function (routes) { var getViewPaths = function (routes) {
return routes.map(function (route) { return routes.reduce(function (paths, route) {
return route.pattern; 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>' * key: '<react-intl string id>'
* value: translated version of string * 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 poUiDir = path.resolve(__dirname, '../../node_modules/scratchr2_translations/ui');
var jsFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/djangojs.po'); var jsFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/djangojs.po');
var pyFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/django.po'); var pyFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/django.po');
var translations = {}; var translations = {};
separator = separator || ':';
try { try {
fs.accessSync(jsFile, fs.R_OK); fs.accessSync(jsFile, fs.R_OK);
var jsTranslations = po2icu.poFileToICUSync(lang, jsFile); var jsTranslations = po2icu.poFileToICUSync(lang, jsFile);
@ -104,7 +106,7 @@ Helpers.getTranslationsForLanguage = function (lang, idsWithICU, md5WithIds) {
var translationsByView = {}; var translationsByView = {};
for (var id in translations) { for (var id in translations) {
var ids = id.split('-'); // [viewName, stringId] var ids = id.split(separator); // [viewName, stringId]
var viewName = ids[0]; var viewName = ids[0];
var stringId = ids[1]; 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 // 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' } // Sample structure: { 'general-general.blah': 'blah', 'about-about.blah': 'blahblah' }
Helpers.idToICUMap = function (viewName, ids) { Helpers.idToICUMap = function (viewName, ids, separator) {
var idsToICU = {}; var idsToICU = {};
separator = separator || ':';
for (var id in ids) { for (var id in ids) {
idsToICU[viewName + '-' + id] = ids[id]; idsToICU[viewName + separator + id] = ids[id];
} }
return idsToICU; return idsToICU;
}; };
// Reuturns reverse (i.e. english string with message key as the value) object for searching po files. // 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' } // Sample structure: { 'blah': 'general-general.blah', 'blahblah': 'about-about.blah' }
Helpers.icuToIdMap = function (viewName, ids) { Helpers.icuToIdMap = function (viewName, ids, separator) {
var icuToIds = {}; var icuToIds = {};
separator = separator || ':';
for (var id in ids) { for (var id in ids) {
icuToIds[ids[id]] = viewName + '-' + id; icuToIds[ids[id]] = viewName + separator + id;
} }
return icuToIds; return icuToIds;
}; };

View file

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

View file

@ -1,7 +1,7 @@
/* UI Primary Colors */ /* UI Primary Colors */
$ui-blue: hsla(200, 90, 55, 1); // #25AFF4 $ui-blue: hsla(200, 90, 55, 1); // #25AFF4
$ui-orange: hsla(35, 90, 55, 1); // #F49D25 $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-gray: hsla(0, 0, 95, 1); //#F2F2F2
$ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3 $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; $cols11: (11 * ($column + $gutter) - $gutter) / $em;
$cols12: (12 * ($column + $gutter) - $gutter) / $em; $cols12: (12 * ($column + $gutter) - $gutter) / $em;
$desktop: 942px;
$tablet: 640px;
$mobile: 480px;
// //
// Column-widths in a function, in ems // Column-widths in a function, in ems
// //
@ -42,50 +46,67 @@ $cols12: (12 * ($column + $gutter) - $gutter) / $em;
width: ($cols * ($column + $gutter) - $gutter) / $em; width: ($cols * ($column + $gutter) - $gutter) / $em;
} }
$desktop: 942px;
$tablet: 640px;
$mobile: 480px;
//4 columns //4 columns
@mixin submobile ($parent-selector, $child-selector) {
@media only screen and (max-width: $mobile - 1) { @media only screen and (max-width: $mobile - 1) {
#view { #{$parent-selector} {
text-align: center; text-align: center;
} }
.inner { #{$child-selector} {
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
} }
@content;
}
} }
//6 columns //6 columns
@mixin mobile ($parent-selector, $child-selector) {
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) { @media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
#view { #{$parent-selector} {
text-align: center; text-align: center;
} }
.inner { #{$child-selector} {
margin: 0 auto; margin: 0 auto;
width: $mobile; width: $mobile;
} }
@content;
}
} }
//8 columns //8 columns
@mixin tablet ($parent-selector, $child-selector) {
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) { @media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
#view { #{$parent-selector} {
text-align: center; text-align: center;
} }
.inner { #{$child-selector} {
margin: 0 auto; margin: 0 auto;
width: $tablet; width: $tablet;
} }
} }
}
//12 columns //12 columns
@mixin desktop ($parent-selector, $child-selector) {
@media only screen and (min-width: $desktop) { @media only screen and (min-width: $desktop) {
.inner { #{$child-selector} {
margin: 0 auto; margin: 0 auto;
width: $desktop; 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 () { render: function () {
// make sure user is present before checking if they're an admin. Don't show anything if user not an admin. // make sure user is present before checking if they're an admin. Don't show anything if user not an admin.
var showAdmin = false; var showAdmin = false;
if (this.props.session.user) { if (this.props.session.session.user) {
showAdmin = this.props.session.permissions.admin; showAdmin = this.props.session.session.permissions.admin;
} }
if (!showAdmin) return false; if (!showAdmin) return false;

View file

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

View file

@ -13,7 +13,8 @@ $base-bg: $ui-white;
width: $cols4; width: $cols4;
.box-header { .box-header {
h4 { h4,
h5 {
line-height: .9rem; line-height: .9rem;
font-size: .9rem; font-size: .9rem;
} }
@ -25,7 +26,8 @@ $base-bg: $ui-white;
width: $cols6; width: $cols6;
.box-header { .box-header {
h4 { h4,
h5 {
line-height: 1rem; line-height: 1rem;
font-size: 1rem; font-size: 1rem;
} }
@ -37,7 +39,8 @@ $base-bg: $ui-white;
width: $cols8; width: $cols8;
.box-header { .box-header {
h4 { h4,
h5 {
line-height: 1.1rem; line-height: 1.1rem;
font-size: 1.1rem; font-size: 1.1rem;
} }
@ -49,7 +52,8 @@ $base-bg: $ui-white;
width: $cols12; width: $cols12;
.box-header { .box-header {
h4 { h4,
h5 {
line-height: 1.1rem; line-height: 1.1rem;
font-size: 1.1rem; font-size: 1.1rem;
} }
@ -72,17 +76,25 @@ $base-bg: $ui-white;
height: 20px; height: 20px;
overflow: hidden; overflow: hidden;
h4 { h4,
h5 {
display: inline-block; display: inline-block;
float: left; float: left;
} }
h5 {
margin: 0;
padding-left: 5px;
text-transform: none;
letter-spacing: normal;
font-weight: normal;
}
p { p {
display: inline-block; display: inline-block;
float: right; float: right;
margin: 1px 0 0 0; margin: 1px 0 0 0;
padding: 0; padding: 0;
font-size: .85rem; 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 ( return (
<Thumbnail key={item.id} <Thumbnail key={[this.key, item.id].join('.')}
showLoves={this.props.showLoves} showLoves={this.props.showLoves}
showRemixes={this.props.showRemixes} showRemixes={this.props.showRemixes}
type={item.type} 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-size: .8125rem;
font-weight: normal; font-weight: normal;
&.staging {
background-color: $ui-orange;
}
&.open { &.open {
display: block; display: block;
} }

View file

@ -12,7 +12,28 @@
justify-content: center; 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) { @media only screen and (max-width: $tablet - 1) {
flex-direction: column; flex-direction: column;
&.uneven {
.short,
.long {
margin: auto;
width: 90%;
}
}
} }
} }

View file

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

View file

@ -23,7 +23,19 @@
img { img {
margin: 20px 0; margin: 20px 0;
max-width: 180px; 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' /> <FormattedMessage id='general.forEducators' />
</a> </a>
</dd> </dd>
<dd>
<a href="/developers">
<FormattedMessage id='general.forDevelopers' />
</a>
</dd>
<dd> <dd>
<a href="/info/credits/"> <a href="/info/credits/">
<FormattedMessage id='general.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 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('./input.scss');
require('./row.scss');
var Input = React.createClass({ var Input = React.createClass({
type: 'Input', type: 'Input',
propTypes: { getDefaultProps: function () {
return {};
},
getInitialState: function () {
return {
status: ''
};
},
onValid: function () {
this.setState({
status: 'pass'
});
},
onInvalid: function () {
this.setState({
status: 'fail'
});
}, },
render: function () { render: function () {
var classes = classNames( var classes = classNames(
'input', 'input',
this.state.status,
this.props.className this.props.className
); );
return ( return (this.props.type === 'submit' || this.props.noformsy ?
<input {... this.props} className={classes} /> <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 "../../colors";
@import "../../frameless";
$base-bg: $ui-white; $base-bg: $ui-light-gray;
$focus-bg: lighten($ui-blue, 35%);
$fail-bg: lighten($ui-orange, 35%);
$pass-bg: lighten($ui-aqua, 35%); $pass-bg: lighten($ui-aqua, 35%);
.row {
label {
font-weight: 500;
}
}
.input { .input {
transition: all 1s ease; transition: all .5s ease;
margin: .5em 0; margin: .75rem 0;
border: 1px solid $active-gray; border: 1px solid $active-gray;
border-radius: 5px; border-radius: 5px;
background-color: $base-bg; background-color: $base-bg;
padding: .75em 1em; padding: 0 1rem;
height: 3rem;
color: $type-gray; color: $type-gray;
font-size: .8rem; font-size: .875rem;
&:focus { &:focus {
transition: all 1s ease; transition: all .5s ease;
outline: none; outline: none;
border: 1px solid $active-dark-gray; border: 1px solid $ui-blue;
background-color: $focus-bg;
} }
&.fail { &.fail {
border: 1px solid $active-dark-gray; border: 1px solid $ui-orange;
background-color: $fail-bg;
} }
&.pass { &.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 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'); require('./select.scss');
var Select = React.createClass({ var Select = React.createClass({
@ -13,12 +18,16 @@ var Select = React.createClass({
'select', 'select',
this.props.className 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 ( return (
<select {... this.props} className={classes}> <div className={classes}>
{this.props.children} <FRCSelect {... props} />
</select> </div>
); );
} }
}); });
module.exports = Select; module.exports = inputHOC(defaultValidationHOC(Select));

View file

@ -1,9 +1,42 @@
@import "../../colors"; @import "../../colors";
@import "../../frameless";
.select { .select {
background-color: $ui-white; label {
width: 220px; font-weight: 500;
height: 28px; }
line-height: 28px;
font-size: 1em; 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 classNames = require('classnames');
var React = require('react'); var React = require('react');
var Api = require('../../mixins/api.jsx');
var jar = require('../../lib/jar.js'); var jar = require('../../lib/jar.js');
var languages = require('../../../languages.json'); var languages = require('../../../languages.json');
var Form = require('../forms/form.jsx');
var Select = require('../forms/select.jsx'); var Select = require('../forms/select.jsx');
require('./languagechooser.scss');
/** /**
* Footer dropdown menu that allows one to change their language. * Footer dropdown menu that allows one to change their language.
*/ */
var LanguageChooser = React.createClass({ var LanguageChooser = React.createClass({
type: 'LanguageChooser', type: 'LanguageChooser',
mixins: [
Api
],
getDefaultProps: function () { getDefaultProps: function () {
return { return {
languages: languages, languages: languages,
locale: window._locale locale: window._locale
}; };
}, },
onSetLanguage: function (e) { onSetLanguage: function (name, value) {
e.preventDefault(); jar.set('scratchlanguage', value);
jar.set('scratchlanguage', e.target.value);
window.location.reload(); window.location.reload();
}, },
render: function () { render: function () {
@ -30,17 +28,17 @@ var LanguageChooser = React.createClass({
'language-chooser', 'language-chooser',
this.props.className this.props.className
); );
var languageOptions = Object.keys(this.props.languages).map(function (value) {
return {value: value, label: this.props.languages[value]};
}.bind(this));
return ( return (
<div className={classes}> <Form className={classes}>
<Select name="language" defaultValue={this.props.locale} onChange={this.onSetLanguage}> <Select name="language"
{Object.keys(this.props.languages).map(function (value) { options={languageOptions}
return <option value={value} key={value}> value={this.props.locale}
{this.props.languages[value]} onChange={this.onSetLanguage}
</option>; required />
}.bind(this))} </Form>
</Select>
</div>
); );
} }
}); });

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,18 @@
@import "../../../colors"; @import "../../../colors";
#navigation { #navigation {
&.staging {
.messages {
.message-count {
display: none;
&.show {
background-color: $ui-blue;
}
}
}
}
.logo { .logo {
margin-right: 10px; margin-right: 10px;
@ -39,7 +51,8 @@
margin: 0; margin: 0;
} }
input { input,
button {
display: inline-block; display: inline-block;
margin-top: 5px; margin-top: 5px;
outline: none; outline: none;
@ -47,19 +60,6 @@
background-color: $active-gray; background-color: $active-gray;
height: 14px; 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] { &[type=text] {
transition: .15s ease background-color; transition: .15s ease background-color;
padding: 0; 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 { .login-dropdown {
width: 200px; width: 200px;
.button {
padding: .75em;
}
}
.dropdown {
.row {
margin-bottom: 1.25rem;
input {
margin: 0;
height: 2.25rem;
}
}
} }
.account-nav { .account-nav {
@ -164,6 +197,10 @@
vertical-align: middle; vertical-align: middle;
} }
&.open {
background-color: $active-gray;
}
&:after { &:after {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;
@ -180,9 +217,11 @@
} }
.dropdown { .dropdown {
top: 50px;
padding: 0; padding: 0;
padding-top: 5px; padding-top: 5px;
width: 100%; width: 100%;
box-sizing: border-box;
} }
} }
} }

View file

@ -101,29 +101,6 @@
section { section {
padding: 64px 0; 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) { @media only screen and (max-width: $mobile - 1) {

View file

@ -1,4 +1,5 @@
var React = require('react'); var React = require('react');
var classNames = require('classnames');
var Navigation = require('../../navigation/www/navigation.jsx'); var Navigation = require('../../navigation/www/navigation.jsx');
var Footer = require('../../footer/www/footer.jsx'); var Footer = require('../../footer/www/footer.jsx');
@ -6,9 +7,12 @@ var Footer = require('../../footer/www/footer.jsx');
var Page = React.createClass({ var Page = React.createClass({
type: 'Page', type: 'Page',
render: function () { render: function () {
var classes = classNames({
'staging': process.env.SCRATCH_ENV == 'staging'
});
return ( return (
<div className="page"> <div className="page">
<div id="navigation"> <div id="navigation" className={classes}>
<Navigation /> <Navigation />
</div> </div>
<div id="view"> <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 { .spinner {
position: relative; position: relative;
margin: 0 auto;
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -17,10 +18,14 @@
animation: circleFadeDelay 1.2s infinite ease-in-out both; animation: circleFadeDelay 1.2s infinite ease-in-out both;
margin: 0 auto; margin: 0 auto;
border-radius: 100%; border-radius: 100%;
background-color: darken($ui-blue, 8%); background-color: darken($ui-white, 8%);
width: 15%; width: 15%;
height: 15%; height: 15%;
content: ""; 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: '', src: '',
type: 'project', type: 'project',
showLoves: false, showLoves: false,
showFavorites: false,
showRemixes: false, showRemixes: false,
showViews: false,
linkTitle: true, linkTitle: true,
alt: '' alt: ''
}; };
@ -40,23 +42,40 @@ var Thumbnail = React.createClass({
key="loves" key="loves"
className="thumbnail-loves" className="thumbnail-loves"
title={this.props.loves + ' loves'}> title={this.props.loves + ' loves'}>
{this.props.loves} {this.props.loves}
</div> </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) { if (this.props.remixes && this.props.showRemixes) {
extra.push( extra.push(
<div <div
key="remixes" key="remixes"
className="thumbnail-remixes" className="thumbnail-remixes"
title={this.props.remixes + ' remixes'}> title={this.props.remixes + ' remixes'}>
{this.props.remixes} {this.props.remixes}
</div> </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; var imgElement,titleElement;
if (this.props.linkTitle) { if (this.props.linkTitle) {
imgElement = <a className="thumbnail-image" href={this.props.href}> 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, .thumbnail-title,
#{$extras} { #{$extras} {
@ -41,7 +41,13 @@
} }
.thumbnail-loves, .thumbnail-loves,
.thumbnail-remixes { .thumbnail-favorites,
.thumbnail-remixes,
.thumbnail-views {
display: inline;
margin-right: 10px;
&:before { &:before {
display: inline-block; display: inline-block;
margin-right: .1rem; margin-right: .1rem;
@ -61,12 +67,24 @@
} }
} }
.thumbnail-favorites {
&:before {
background-image: url("/svgs/favorite/favorite_type-gray.svg");
}
}
.thumbnail-remixes { .thumbnail-remixes {
&:before { &:before {
background-image: url("/svgs/remix/remix_type-gray.svg"); background-image: url("/svgs/remix/remix_type-gray.svg");
} }
} }
.thumbnail-views {
&:before {
background-image: url("/svgs/view/view_type-gray.svg");
}
}
&.project { &.project {
$project-width: 144px; $project-width: 144px;
$project-height: 108px; $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.accountSettings": "Account settings",
"general.about": "About", "general.about": "About",
"general.aboutScratch": "About Scratch", "general.aboutScratch": "About Scratch",
"general.birthMonth": "Birth Month",
"general.birthYear": "Birth Year",
"general.donate": "Donate", "general.donate": "Donate",
"general.collaborators": "Collaborators", "general.collaborators": "Collaborators",
"general.community": "Community", "general.community": "Community",
"general.confirmEmail": "Confirm Email",
"general.contactUs": "Contact Us", "general.contactUs": "Contact Us",
"general.copyright": "Scratch is a project of the Lifelong Kindergarten Group at the MIT Media Lab", "general.copyright": "Scratch is a project of the Lifelong Kindergarten Group at the MIT Media Lab",
"general.country": "Country",
"general.create": "Create", "general.create": "Create",
"general.createUsername": "Create a Username",
"general.credits": "Credits", "general.credits": "Credits",
"general.discuss": "Discuss", "general.discuss": "Discuss",
"general.dmca": "DMCA", "general.dmca": "DMCA",
"general.emailAddress": "Email Address",
"general.explore": "Explore", "general.explore": "Explore",
"general.faq": "FAQ", "general.faq": "FAQ",
"general.female": "Female",
"general.forParents": "For Parents", "general.forParents": "For Parents",
"general.forEducators": "For Educators", "general.forEducators": "For Educators",
"general.forDevelopers": "For Developers",
"general.gender": "Gender",
"general.guidelines": "Community Guidelines", "general.guidelines": "Community Guidelines",
"general.help": "Help", "general.help": "Help",
"general.jobs": "Jobs", "general.jobs": "Jobs",
"general.joinScratch": "Join Scratch", "general.joinScratch": "Join Scratch",
"general.legal": "Legal", "general.legal": "Legal",
"general.loadMore": "Load More",
"general.learnMore": "Learn More", "general.learnMore": "Learn More",
"general.male": "Male",
"general.messages": "Messages", "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.myClass": "My Class",
"general.myClasses": "My Classes", "general.myClasses": "My Classes",
"general.myStuff": "My Stuff", "general.myStuff": "My Stuff",
"general.notRequired": "Not Required",
"general.other": "Other",
"general.offlineEditor": "Offline Editor", "general.offlineEditor": "Offline Editor",
"general.password": "Password",
"general.press": "Press", "general.press": "Press",
"general.privacyPolicy": "Privacy Policy", "general.privacyPolicy": "Privacy Policy",
"general.projects": "Projects",
"general.profile": "Profile", "general.profile": "Profile",
"general.scratchConference": "Scratch Conference", "general.scratchConference": "Scratch Conference",
"general.scratchday": "Scratch Day", "general.scratchday": "Scratch Day",
@ -37,6 +64,7 @@
"general.search": "Search", "general.search": "Search",
"general.signIn": "Sign in", "general.signIn": "Sign in",
"general.statistics": "Statistics", "general.statistics": "Statistics",
"general.studios": "Studios",
"general.support": "Support", "general.support": "Support",
"general.tipsWindow": "Tips Window", "general.tipsWindow": "Tips Window",
"general.tipsAnimateYourNameTitle": "Animate Your Name", "general.tipsAnimateYourNameTitle": "Animate Your Name",
@ -47,10 +75,27 @@
"general.tipsPongGame": "Create a Pong Game", "general.tipsPongGame": "Create a Pong Game",
"general.termsOfUse": "Terms of Use", "general.termsOfUse": "Terms of Use",
"general.username": "Username", "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.viewAll": "View All",
"general.website": "Website",
"general.whatsHappening": "What's Happening?", "general.whatsHappening": "What's Happening?",
"general.wiki": "Scratch Wiki", "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.discuss": "Discussion Forums",
"footer.help": "Help Page", "footer.help": "Help Page",
"footer.scratchFamily": "Scratch Family", "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.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.FaqAgeRangeQ": "What is the age range for Scratch?",
"parents.FaqResourcesQ": "What resources are available for learning 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 // Return the usable content portion of a signed, compressed cookie generated by
// Django's signing module // Django's signing module
// https://github.com/django/django/blob/stable/1.8.x/django/core/signing.py // 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 { try {
var b64Data = value.split(':')[0]; var b64Data = value.split(':')[0];
var decompress = false; var decompress = false;
@ -73,6 +74,24 @@ var Jar = {
var expires = '; expires=' + new Date(new Date().setYear(new Date().getFullYear() + 1)).toUTCString(); var expires = '; expires=' + new Date(new Date().setYear(new Date().getFullYear() + 1)).toUTCString();
var path = '; path=/'; var path = '; path=/';
document.cookie = obj + expires + 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 StoreProvider = require('react-redux').Provider;
var IntlProvider = require('./intl.jsx').IntlProvider; var IntlProvider = require('./intl.jsx').IntlProvider;
var permissionsActions = require('../redux/permissions.js');
var sessionActions = require('../redux/session.js'); var sessionActions = require('../redux/session.js');
var reducer = require('../redux/reducer.js'); var reducer = require('../redux/reducer.js');
@ -42,7 +43,8 @@ var render = function (jsx, element) {
element element
); );
// Get initial session // Get initial session & permissions
store.dispatch(permissionsActions.getPermissions());
store.dispatch(sessionActions.refreshSession()); 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; 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 keyMirror = require('keymirror');
var api = require('../mixins/api.jsx').api; var api = require('../lib/api');
var Types = keyMirror({ var Types = keyMirror({
SET_DETAILS: null, SET_DETAILS: null,

View file

@ -1,5 +1,5 @@
var keyMirror = require('keymirror'); var keyMirror = require('keymirror');
var api = require('../mixins/api.jsx').api; var api = require('../lib/api');
var Types = keyMirror({ var Types = keyMirror({
SET_SCHEDULE: null, 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 scheduleReducer = require('./conference-schedule.js').scheduleReducer;
var detailsReducer = require('./conference-details.js').detailsReducer; var detailsReducer = require('./conference-details.js').detailsReducer;
var permissionsReducer = require('./permissions.js').permissionsReducer;
var sessionReducer = require('./session.js').sessionReducer; var sessionReducer = require('./session.js').sessionReducer;
var tokenReducer = require('./token.js').tokenReducer; var tokenReducer = require('./token.js').tokenReducer;
var appReducer = combineReducers({ var appReducer = combineReducers({
session: sessionReducer, session: sessionReducer,
token: tokenReducer, token: tokenReducer,
permissions: permissionsReducer,
conferenceSchedule: scheduleReducer, conferenceSchedule: scheduleReducer,
conferenceDetails: detailsReducer conferenceDetails: detailsReducer
}); });

View file

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

View file

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

View file

@ -11,12 +11,6 @@
"view": "about/about", "view": "about/about",
"title": "About" "title": "About"
}, },
{
"name": "components",
"pattern": "^/components/?$",
"view": "components/components",
"title": "Components"
},
{ {
"name": "developers", "name": "developers",
"pattern": "^/developers/?$", "pattern": "^/developers/?$",
@ -29,12 +23,62 @@
"view": "hoc/hoc", "view": "hoc/hoc",
"title": "Hour of Code" "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", "name": "credits",
"pattern": "^/info/credits/?$", "pattern": "^/info/credits/?$",
"view": "credits/credits", "view": "credits/credits",
"title": "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", "name": "cards",
"pattern": "^/info/cards/?$", "pattern": "^/info/cards/?$",
@ -53,6 +97,19 @@
"view": "jobs/jobs", "view": "jobs/jobs",
"title": "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", "name": "wedo2",
"pattern": "^/wedo/?$", "pattern": "^/wedo/?$",
@ -62,6 +119,7 @@
{ {
"name": "conference-index", "name": "conference-index",
"pattern": "^/conference/?$", "pattern": "^/conference/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/index/index", "view": "conference/index/index",
"title": "Scratch Conference", "title": "Scratch Conference",
"viewportWidth": "device-width" "viewportWidth": "device-width"
@ -69,6 +127,7 @@
{ {
"name": "conference-plan", "name": "conference-plan",
"pattern": "^/conference/plan/?$", "pattern": "^/conference/plan/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/plan/plan", "view": "conference/plan/plan",
"title": "Plan Your Visit", "title": "Plan Your Visit",
"viewportWidth": "device-width" "viewportWidth": "device-width"
@ -76,6 +135,7 @@
{ {
"name": "conference-expectations", "name": "conference-expectations",
"pattern": "^/conference/expect/?$", "pattern": "^/conference/expect/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/expect/expect", "view": "conference/expect/expect",
"title": "What to Expect", "title": "What to Expect",
"viewportWidth": "device-width" "viewportWidth": "device-width"
@ -83,6 +143,7 @@
{ {
"name": "conference-schedule", "name": "conference-schedule",
"pattern": "^/conference/schedule/?$", "pattern": "^/conference/schedule/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/schedule/schedule", "view": "conference/schedule/schedule",
"title": "Conference Schedule", "title": "Conference Schedule",
"viewportWidth": "device-width" "viewportWidth": "device-width"
@ -90,6 +151,7 @@
{ {
"name": "conference-details", "name": "conference-details",
"pattern": "^/conference/:id/details/?$", "pattern": "^/conference/:id/details/?$",
"routeAlias": "^/conference(?!/201[4-5])",
"view": "conference/details/details", "view": "conference/details/details",
"title": "Event Details", "title": "Event Details",
"viewportWidth": "device-width" "viewportWidth": "device-width"
@ -107,7 +169,7 @@
}, },
{ {
"name": "guidelines", "name": "guidelines",
"pattern": "^/community_guidelines$", "pattern": "^/community_guidelines/?$",
"view": "guidelines/guidelines", "view": "guidelines/guidelines",
"title": "Scratch Community Guidelines" "title": "Scratch Community Guidelines"
}, },
@ -116,5 +178,11 @@
"pattern": "^/privacy_policy/?$", "pattern": "^/privacy_policy/?$",
"view": "privacypolicy/privacypolicy", "view": "privacypolicy/privacypolicy",
"title": "Privacy Policy" "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 = { module.exports = {
// Bind environment
api_host: process.env.API_HOST || 'https://api.scratch.mit.edu',
// Search and metadata // Search and metadata
title: 'Imagine, Program, Share', title: 'Imagine, Program, Share',
description: description:

View file

@ -31,25 +31,8 @@
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/lib/normalize.min.css" /> <link rel="stylesheet" href="/css/lib/normalize.min.css" />
<!-- Environment -->
<script>
window.env = {
API_HOST: "{{&api_host}}"
};
</script>
<!-- Polyfills --> <!-- Polyfills -->
<script src="/js/polyfill.min.js"></script> <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) --> <!-- Analytics (GA) -->
<script> <script>
@ -63,8 +46,19 @@
}); });
ga('send', 'pageview'); ga('send', 'pageview');
</script> </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> <script>
var loc = window._locale || 'en'; var loc = window._locale || 'en';
if (typeof window._messages !== 'undefined' && loc !== 'en') { if (typeof window._messages !== 'undefined' && loc !== 'en') {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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