mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-23 07:38:07 -05:00
Merge pull request #704 from LLK/release/2.2.10
[Master] Release 2.2.10
This commit is contained in:
commit
b5cca385a7
142 changed files with 6321 additions and 753 deletions
|
@ -2,4 +2,5 @@ node_modules/*
|
|||
static/*
|
||||
build/*
|
||||
intl/*
|
||||
locales/*
|
||||
**/*.min.js
|
||||
|
|
|
@ -47,6 +47,8 @@ env:
|
|||
- SENTRY_DSN_VAR=SENTRY_DSN_$TRAVIS_BRANCH
|
||||
- SENTRY_DSN=${!SENTRY_DSN_VAR}
|
||||
- SENTRY_DSN=${SENTRY_DSN:-$SENTRY_DSN_STAGING}
|
||||
# SMARTY_STREETS_API_KEY
|
||||
- secure: "uQKNgJaJEju8ErGUxIbLE0Y6ee4j6OFFbBpqyuKrNMk6apvvvXLp3lTdGZJq6j/ZwQeQ384m5bbfmhFwr7piPFj7W/zBXVKcifbF6ShfP7skMl834Kkefs3uEWU0VZw3nURgzNInSOPqqGLsISFywpwBXUWKfL0Q87KHNU0/I0EkwvImm3SAlNpR38m3yKcMF3h0zK8Fh2mO7iyHEIhtssdWabaRjf3t6Mr5vikACeXYJg+k4oEQZtsnSNnlLYWumdEDsxwonMozGKUBqlXwhHCdYNOJ1DUGuntbXOnylLt1/LA9I9B4hWQOrRDwqjyIOI+2dpADoCN040+Zr1VSrJhk7Wb7ogeaQLzZ4W/3dX54rbsnFHa+MuKqOsAxQ0Tjfk5xWq/pbLRsAyW6Pl7Q1v4yWOQ2COnM/tfJ6UaH9bxppOyKsX8n33rFjlvZU6CtY1GGa7fpB2zOKI5B5OovLjHeokIe/Tx+4coEDZqt44qkTGWr/eWDxrvkQqpQ29F9My3wBgB3gdou+3lWExS0a9M2wwp4EIduXEKNZXLGDuVefH5f3eFy09wH+nhctmMF8uhMbPefFubEi7fqXTkxntmDTy+/pD2A2w1jJhBwLhwlik335k+Wrbl3dclt7cjJ6fRVX9b+AuBCbGr633vM4xbk90whwXizSECIt5InGSw="
|
||||
- SKIP_CLEANUP=true
|
||||
- NODE_ENV=production
|
||||
- WWW_VERSION=${TRAVIS_COMMIT:0:5}
|
||||
|
|
|
@ -44,9 +44,13 @@ var getStaticPaths = function (pathToStatic) {
|
|||
* the express route and a static view file associated with the route
|
||||
*/
|
||||
var getViewPaths = function (routes) {
|
||||
return routes.map(function (route) {
|
||||
return route.pattern;
|
||||
});
|
||||
return routes.reduce(function (paths, route) {
|
||||
var path = route.routeAlias || route.pattern;
|
||||
if (paths.indexOf(path) === -1) {
|
||||
paths.push(path);
|
||||
}
|
||||
return paths;
|
||||
}, []);
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
|
@ -76,12 +76,14 @@ Helpers.getMD5Map = function (ICUIdMap) {
|
|||
* key: '<react-intl string id>'
|
||||
* value: translated version of string
|
||||
*/
|
||||
Helpers.getTranslationsForLanguage = function (lang, idsWithICU, md5WithIds) {
|
||||
Helpers.getTranslationsForLanguage = function (lang, idsWithICU, md5WithIds, separator) {
|
||||
var poUiDir = path.resolve(__dirname, '../../node_modules/scratchr2_translations/ui');
|
||||
var jsFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/djangojs.po');
|
||||
var pyFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/django.po');
|
||||
|
||||
var translations = {};
|
||||
separator = separator || ':';
|
||||
|
||||
try {
|
||||
fs.accessSync(jsFile, fs.R_OK);
|
||||
var jsTranslations = po2icu.poFileToICUSync(lang, jsFile);
|
||||
|
@ -104,7 +106,7 @@ Helpers.getTranslationsForLanguage = function (lang, idsWithICU, md5WithIds) {
|
|||
|
||||
var translationsByView = {};
|
||||
for (var id in translations) {
|
||||
var ids = id.split('-'); // [viewName, stringId]
|
||||
var ids = id.split(separator); // [viewName, stringId]
|
||||
var viewName = ids[0];
|
||||
var stringId = ids[1];
|
||||
|
||||
|
@ -127,20 +129,24 @@ Helpers.writeTranslationsToJS = function (outputDir, viewName, translationObject
|
|||
|
||||
// Returns a FormattedMessage id with english string as value. Use for default values in translations
|
||||
// Sample structure: { 'general-general.blah': 'blah', 'about-about.blah': 'blahblah' }
|
||||
Helpers.idToICUMap = function (viewName, ids) {
|
||||
Helpers.idToICUMap = function (viewName, ids, separator) {
|
||||
var idsToICU = {};
|
||||
separator = separator || ':';
|
||||
|
||||
for (var id in ids) {
|
||||
idsToICU[viewName + '-' + id] = ids[id];
|
||||
idsToICU[viewName + separator + id] = ids[id];
|
||||
}
|
||||
return idsToICU;
|
||||
};
|
||||
|
||||
// Reuturns reverse (i.e. english string with message key as the value) object for searching po files.
|
||||
// Sample structure: { 'blah': 'general-general.blah', 'blahblah': 'about-about.blah' }
|
||||
Helpers.icuToIdMap = function (viewName, ids) {
|
||||
Helpers.icuToIdMap = function (viewName, ids, separator) {
|
||||
var icuToIds = {};
|
||||
separator = separator || ':';
|
||||
|
||||
for (var id in ids) {
|
||||
icuToIds[ids[id]] = viewName + '-' + id;
|
||||
icuToIds[ids[id]] = viewName + separator + id;
|
||||
}
|
||||
return icuToIds;
|
||||
};
|
||||
|
|
15
package.json
15
package.json
|
@ -43,8 +43,12 @@
|
|||
"exenv": "1.2.0",
|
||||
"fastly": "1.2.1",
|
||||
"file-loader": "0.8.4",
|
||||
"formsy-react": "0.18.0",
|
||||
"formsy-react-components": "0.7.1",
|
||||
"git-bundle-sha": "0.0.2",
|
||||
"glob": "5.0.15",
|
||||
"google-libphonenumber": "1.0.21",
|
||||
"iso-3166-2": "0.4.0",
|
||||
"json-loader": "0.5.2",
|
||||
"json2po-stream": "1.0.3",
|
||||
"jsx-loader": "0.13.2",
|
||||
|
@ -60,13 +64,14 @@
|
|||
"po2icu": "0.0.2",
|
||||
"postcss-loader": "0.8.2",
|
||||
"raven-js": "3.0.4",
|
||||
"react": "0.14.0",
|
||||
"react-dom": "0.14.0",
|
||||
"react": "15.1.0",
|
||||
"react-dom": "15.0.1",
|
||||
"react-intl": "2.1.2",
|
||||
"react-modal": "0.6.1",
|
||||
"react-modal": "1.3.0",
|
||||
"react-onclickoutside": "4.1.1",
|
||||
"react-redux": "4.4.0",
|
||||
"react-slick": "0.9.2",
|
||||
"react-redux": "4.4.5",
|
||||
"react-slick": "0.12.2",
|
||||
"react-telephone-input": "3.4.5",
|
||||
"redux": "3.5.2",
|
||||
"redux-thunk": "2.0.1",
|
||||
"sass-lint": "1.5.1",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* UI Primary Colors */
|
||||
$ui-blue: hsla(200, 90, 55, 1); // #25AFF4
|
||||
$ui-orange: hsla(35, 90, 55, 1); // #F49D25
|
||||
$ui-light-gray: hsla(0, 0, 98, 1);
|
||||
$ui-light-gray: hsla(0, 0, 98, 1); //#FAFAFA
|
||||
$ui-gray: hsla(0, 0, 95, 1); //#F2F2F2
|
||||
$ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ $cols10: (10 * ($column + $gutter) - $gutter) / $em;
|
|||
$cols11: (11 * ($column + $gutter) - $gutter) / $em;
|
||||
$cols12: (12 * ($column + $gutter) - $gutter) / $em;
|
||||
|
||||
$desktop: 942px;
|
||||
$tablet: 640px;
|
||||
$mobile: 480px;
|
||||
|
||||
//
|
||||
// Column-widths in a function, in ems
|
||||
//
|
||||
|
@ -42,50 +46,67 @@ $cols12: (12 * ($column + $gutter) - $gutter) / $em;
|
|||
width: ($cols * ($column + $gutter) - $gutter) / $em;
|
||||
}
|
||||
|
||||
$desktop: 942px;
|
||||
$tablet: 640px;
|
||||
$mobile: 480px;
|
||||
|
||||
//4 columns
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
#view {
|
||||
text-align: center;
|
||||
}
|
||||
@mixin submobile ($parent-selector, $child-selector) {
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
#{$parent-selector} {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inner {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
#{$child-selector} {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
//6 columns
|
||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
||||
#view {
|
||||
text-align: center;
|
||||
}
|
||||
@mixin mobile ($parent-selector, $child-selector) {
|
||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
||||
#{$parent-selector} {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inner {
|
||||
margin: 0 auto;
|
||||
width: $mobile;
|
||||
#{$child-selector} {
|
||||
margin: 0 auto;
|
||||
width: $mobile;
|
||||
}
|
||||
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
//8 columns
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
#view {
|
||||
text-align: center;
|
||||
}
|
||||
@mixin tablet ($parent-selector, $child-selector) {
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
#{$parent-selector} {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inner {
|
||||
margin: 0 auto;
|
||||
width: $tablet;
|
||||
#{$child-selector} {
|
||||
margin: 0 auto;
|
||||
width: $tablet;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//12 columns
|
||||
@media only screen and (min-width: $desktop) {
|
||||
.inner {
|
||||
margin: 0 auto;
|
||||
width: $desktop;
|
||||
@mixin desktop ($parent-selector, $child-selector) {
|
||||
@media only screen and (min-width: $desktop) {
|
||||
#{$child-selector} {
|
||||
margin: 0 auto;
|
||||
width: $desktop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin responsive-layout ($parent-selector, $child-selector) {
|
||||
@include submobile($parent-selector, $child-selector);
|
||||
@include mobile($parent-selector, $child-selector);
|
||||
@include tablet($parent-selector, $child-selector);
|
||||
@include desktop($parent-selector, $child-selector);
|
||||
}
|
||||
|
||||
@include responsive-layout("#view", ".inner");
|
||||
|
|
|
@ -19,8 +19,8 @@ var AdminPanel = React.createClass({
|
|||
render: function () {
|
||||
// make sure user is present before checking if they're an admin. Don't show anything if user not an admin.
|
||||
var showAdmin = false;
|
||||
if (this.props.session.user) {
|
||||
showAdmin = this.props.session.permissions.admin;
|
||||
if (this.props.session.session.user) {
|
||||
showAdmin = this.props.session.session.permissions.admin;
|
||||
}
|
||||
|
||||
if (!showAdmin) return false;
|
||||
|
|
|
@ -7,6 +7,7 @@ var Box = React.createClass({
|
|||
type: 'Box',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string.isRequired,
|
||||
subtitle: React.PropTypes.string,
|
||||
moreTitle: React.PropTypes.string,
|
||||
moreHref: React.PropTypes.string,
|
||||
moreProps: React.PropTypes.object
|
||||
|
@ -20,6 +21,7 @@ var Box = React.createClass({
|
|||
<div className={classes}>
|
||||
<div className="box-header">
|
||||
<h4>{this.props.title}</h4>
|
||||
<h5>{this.props.subtitle}</h5>
|
||||
<p>
|
||||
<a href={this.props.moreHref} {...this.props.moreProps}>
|
||||
{this.props.moreTitle}
|
||||
|
|
|
@ -13,7 +13,8 @@ $base-bg: $ui-white;
|
|||
width: $cols4;
|
||||
|
||||
.box-header {
|
||||
h4 {
|
||||
h4,
|
||||
h5 {
|
||||
line-height: .9rem;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
@ -25,7 +26,8 @@ $base-bg: $ui-white;
|
|||
width: $cols6;
|
||||
|
||||
.box-header {
|
||||
h4 {
|
||||
h4,
|
||||
h5 {
|
||||
line-height: 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
@ -37,7 +39,8 @@ $base-bg: $ui-white;
|
|||
width: $cols8;
|
||||
|
||||
.box-header {
|
||||
h4 {
|
||||
h4,
|
||||
h5 {
|
||||
line-height: 1.1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
@ -49,7 +52,8 @@ $base-bg: $ui-white;
|
|||
width: $cols12;
|
||||
|
||||
.box-header {
|
||||
h4 {
|
||||
h4,
|
||||
h5 {
|
||||
line-height: 1.1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
@ -72,17 +76,25 @@ $base-bg: $ui-white;
|
|||
height: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
h4 {
|
||||
h4,
|
||||
h5 {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
padding-left: 5px;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
p {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin: 1px 0 0 0;
|
||||
padding: 0;
|
||||
|
||||
font-size: .85rem;
|
||||
}
|
||||
}
|
||||
|
|
17
src/components/card/card.jsx
Normal file
17
src/components/card/card.jsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
var classNames = require('classnames');
|
||||
var React = require('react');
|
||||
|
||||
require('./card.scss');
|
||||
|
||||
var Card = React.createClass({
|
||||
displayName: 'Card',
|
||||
render: function () {
|
||||
return (
|
||||
<div className={classNames(['card', this.props.className])}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Card;
|
8
src/components/card/card.scss
Normal file
8
src/components/card/card.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
.card {
|
||||
border-radius: 8px / $em;
|
||||
box-shadow: 0 0 0 .125rem $active-gray;
|
||||
background-color: $ui-white;
|
||||
}
|
|
@ -55,7 +55,7 @@ var Carousel = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<Thumbnail key={item.id}
|
||||
<Thumbnail key={[this.key, item.id].join('.')}
|
||||
showLoves={this.props.showLoves}
|
||||
showRemixes={this.props.showRemixes}
|
||||
type={item.type}
|
||||
|
|
22
src/components/deck/deck.jsx
Normal file
22
src/components/deck/deck.jsx
Normal 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;
|
151
src/components/deck/deck.scss
Normal file
151
src/components/deck/deck.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,10 @@
|
|||
font-size: .8125rem;
|
||||
font-weight: normal;
|
||||
|
||||
&.staging {
|
||||
background-color: $ui-orange;
|
||||
}
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,28 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
&.uneven {
|
||||
align-items: flex-start;
|
||||
|
||||
.short {
|
||||
width: $cols3;
|
||||
}
|
||||
|
||||
.long {
|
||||
width: $cols8;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
flex-direction: column;
|
||||
|
||||
&.uneven {
|
||||
.short,
|
||||
.long {
|
||||
margin: auto;
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,36 +13,48 @@ var ConferenceFooter = React.createClass({
|
|||
<div className="collaborators">
|
||||
<h4>Sponsors</h4>
|
||||
<FlexRow as="ul">
|
||||
<li>
|
||||
<li className="odl">
|
||||
<a href="https://odl.mit.edu/">
|
||||
<img src="/images/conference/footer/mit-odl.png"
|
||||
alt="MIT Office of Digital Learning" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://www.scratchfoundation.org/">
|
||||
<img src="/images/conference/footer/scratch-foundation.png"
|
||||
alt="Scratch Foundation" />
|
||||
<li className="intel">
|
||||
<a href="http://www.intel.com/content/www/us/en/homepage.html">
|
||||
<img src="/images/conference/footer/intel.png"
|
||||
alt="Intel" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li className="lego">
|
||||
<a href="http://www.legofoundation.com/">
|
||||
<img src="/images/conference/footer/lego-foundation.png"
|
||||
alt="LEGO Foundation" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li className="google">
|
||||
<a href="http://www.google.com/">
|
||||
<img src="/images/conference/footer/google.png"
|
||||
alt="Google" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li className="siegel">
|
||||
<a href="http://www.siegelendowment.org/">
|
||||
<img src="/images/conference/footer/siegel-endowment.png"
|
||||
alt="Siegel Family Endowment" />
|
||||
</a>
|
||||
</li>
|
||||
<li className="nostarch">
|
||||
<a href="https://www.nostarch.com/">
|
||||
<img src="/images/conference/footer/no-starch.png"
|
||||
alt="No Starch Press" />
|
||||
</a>
|
||||
</li>
|
||||
<li className="scratchfoundation">
|
||||
<a href="http://www.scratchfoundation.org/">
|
||||
<img src="/images/conference/footer/scratch-foundation.png"
|
||||
alt="Scratch Foundation" />
|
||||
</a>
|
||||
</li>
|
||||
</FlexRow>
|
||||
</div>
|
||||
<FlexRow className="scratch-links">
|
||||
|
|
|
@ -23,7 +23,19 @@
|
|||
img {
|
||||
margin: 20px 0;
|
||||
max-width: 180px;
|
||||
max-height: 30px;
|
||||
max-height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.nostarch {
|
||||
img {
|
||||
max-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.intel {
|
||||
img {
|
||||
max-height: 50px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,11 @@ var Footer = React.createClass({
|
|||
<FormattedMessage id='general.forEducators' />
|
||||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a href="/developers">
|
||||
<FormattedMessage id='general.forDevelopers' />
|
||||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a href="/info/credits/">
|
||||
<FormattedMessage id='general.credits' />
|
||||
|
|
28
src/components/forms/charcount.jsx
Normal file
28
src/components/forms/charcount.jsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
var classNames = require('classnames');
|
||||
var React = require('react');
|
||||
|
||||
require('./charcount.scss');
|
||||
|
||||
var CharCount = React.createClass({
|
||||
type: 'CharCount',
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
maxCharacters: 0,
|
||||
currentCharacters: 0
|
||||
};
|
||||
},
|
||||
render: function () {
|
||||
var classes = classNames(
|
||||
'char-count',
|
||||
this.props.className,
|
||||
{overmax: (this.props.currentCharacters > this.props.maxCharacters)}
|
||||
);
|
||||
return (
|
||||
<p className={classes}>
|
||||
{this.props.currentCharacters}/{this.props.maxCharacters}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = CharCount;
|
11
src/components/forms/charcount.scss
Normal file
11
src/components/forms/charcount.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
@import "../../colors";
|
||||
|
||||
.char-count {
|
||||
letter-spacing: 1px;
|
||||
color: lighten($type-gray, 30%);
|
||||
font-weight: 500;
|
||||
|
||||
&.overmax {
|
||||
color: $ui-orange;
|
||||
}
|
||||
}
|
25
src/components/forms/checkbox-group.jsx
Normal file
25
src/components/forms/checkbox-group.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
var classNames = require('classnames');
|
||||
var FRCCheckboxGroup = require('formsy-react-components').CheckboxGroup;
|
||||
var React = require('react');
|
||||
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
|
||||
var inputHOC = require('./input-hoc.jsx');
|
||||
|
||||
require('./row.scss');
|
||||
require('./checkbox-group.scss');
|
||||
|
||||
var CheckboxGroup = React.createClass({
|
||||
type: 'CheckboxGroup',
|
||||
render: function () {
|
||||
var classes = classNames(
|
||||
'checkbox-group',
|
||||
this.props.className
|
||||
);
|
||||
return (
|
||||
<div className={classes}>
|
||||
<FRCCheckboxGroup {... this.props} className={classes} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = inputHOC(defaultValidationHOC(CheckboxGroup));
|
11
src/components/forms/checkbox-group.scss
Normal file
11
src/components/forms/checkbox-group.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
.checkbox-group {
|
||||
.row {
|
||||
.col-sm-9 {
|
||||
flex-flow: column wrap;
|
||||
|
||||
.checkbox {
|
||||
margin: .5rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
src/components/forms/checkbox.jsx
Normal file
25
src/components/forms/checkbox.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
var classNames = require('classnames');
|
||||
var FRCCheckbox = require('formsy-react-components').Checkbox;
|
||||
var React = require('react');
|
||||
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
|
||||
var inputHOC = require('./input-hoc.jsx');
|
||||
|
||||
require('./row.scss');
|
||||
require('./checkbox.scss');
|
||||
|
||||
var Checkbox = React.createClass({
|
||||
type: 'Checkbox',
|
||||
render: function () {
|
||||
var classes = classNames(
|
||||
'checkbox-row',
|
||||
this.props.className
|
||||
);
|
||||
return (
|
||||
<div className={classes}>
|
||||
<FRCCheckbox {... this.props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = inputHOC(defaultValidationHOC(Checkbox));
|
43
src/components/forms/checkbox.scss
Normal file
43
src/components/forms/checkbox.scss
Normal file
|
@ -0,0 +1,43 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
.row {
|
||||
.checkbox {
|
||||
label {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
input {
|
||||
&[type=checkbox] {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 1rem;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 3px;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
appearance: none;
|
||||
|
||||
&:checked,
|
||||
&:focus {
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
47
src/components/forms/form.jsx
Normal file
47
src/components/forms/form.jsx
Normal 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;
|
22
src/components/forms/general-error.jsx
Normal file
22
src/components/forms/general-error.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}));
|
9
src/components/forms/general-error.scss
Normal file
9
src/components/forms/general-error.scss
Normal 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;
|
||||
}
|
20
src/components/forms/input-hoc.jsx
Normal file
20
src/components/forms/input-hoc.jsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
var React = require('react');
|
||||
|
||||
module.exports = function InputComponentMixin (Component) {
|
||||
var InputComponent = React.createClass({
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
messages: {
|
||||
'general.notRequired': 'Not Required'
|
||||
}
|
||||
};
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<Component help={this.props.required ? null : this.props.messages['general.notRequired']}
|
||||
{...this.props} />
|
||||
);
|
||||
}
|
||||
});
|
||||
return InputComponent;
|
||||
};
|
|
@ -1,22 +1,46 @@
|
|||
var React = require('react');
|
||||
var classNames = require('classnames');
|
||||
var FRCInput = require('formsy-react-components').Input;
|
||||
var React = require('react');
|
||||
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
|
||||
var inputHOC = require('./input-hoc.jsx');
|
||||
|
||||
require('./input.scss');
|
||||
require('./row.scss');
|
||||
|
||||
var Input = React.createClass({
|
||||
type: 'Input',
|
||||
propTypes: {
|
||||
|
||||
getDefaultProps: function () {
|
||||
return {};
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
status: ''
|
||||
};
|
||||
},
|
||||
onValid: function () {
|
||||
this.setState({
|
||||
status: 'pass'
|
||||
});
|
||||
},
|
||||
onInvalid: function () {
|
||||
this.setState({
|
||||
status: 'fail'
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
var classes = classNames(
|
||||
'input',
|
||||
this.state.status,
|
||||
this.props.className
|
||||
);
|
||||
return (
|
||||
<input {... this.props} className={classes} />
|
||||
return (this.props.type === 'submit' || this.props.noformsy ?
|
||||
<input {... this.props} className={classes} /> :
|
||||
<FRCInput {... this.props}
|
||||
className={classes}
|
||||
onValid={this.onValid}
|
||||
onInvalid={this.onInvalid} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Input;
|
||||
module.exports = inputHOC(defaultValidationHOC(Input));
|
||||
|
|
|
@ -1,30 +1,34 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
$base-bg: $ui-white;
|
||||
$focus-bg: lighten($ui-blue, 35%);
|
||||
$fail-bg: lighten($ui-orange, 35%);
|
||||
$base-bg: $ui-light-gray;
|
||||
$pass-bg: lighten($ui-aqua, 35%);
|
||||
|
||||
.row {
|
||||
label {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
transition: all 1s ease;
|
||||
margin: .5em 0;
|
||||
transition: all .5s ease;
|
||||
margin: .75rem 0;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
background-color: $base-bg;
|
||||
padding: .75em 1em;
|
||||
padding: 0 1rem;
|
||||
height: 3rem;
|
||||
color: $type-gray;
|
||||
font-size: .8rem;
|
||||
font-size: .875rem;
|
||||
|
||||
&:focus {
|
||||
transition: all 1s ease;
|
||||
transition: all .5s ease;
|
||||
outline: none;
|
||||
border: 1px solid $active-dark-gray;
|
||||
background-color: $focus-bg;
|
||||
border: 1px solid $ui-blue;
|
||||
}
|
||||
|
||||
&.fail {
|
||||
border: 1px solid $active-dark-gray;
|
||||
background-color: $fail-bg;
|
||||
border: 1px solid $ui-orange;
|
||||
}
|
||||
|
||||
&.pass {
|
||||
|
|
68
src/components/forms/phone-input.jsx
Normal file
68
src/components/forms/phone-input.jsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
var classNames = require('classnames');
|
||||
var React = require('react');
|
||||
var FormsyMixin = require('formsy-react').Mixin;
|
||||
var ReactPhoneInput = require('react-telephone-input/lib/withStyles');
|
||||
var allCountries = require('react-telephone-input/lib/country_data').allCountries;
|
||||
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
|
||||
var validationHOCFactory = require('./validations.jsx').validationHOCFactory;
|
||||
var Row = require('formsy-react-components').Row;
|
||||
var ComponentMixin = require('formsy-react-components').ComponentMixin;
|
||||
var inputHOC = require('./input-hoc.jsx');
|
||||
|
||||
var allIso2 = allCountries.map(function (country) {return country.iso2;});
|
||||
|
||||
require('./row.scss');
|
||||
require('./phone-input.scss');
|
||||
|
||||
var PhoneInput = React.createClass({
|
||||
displayName: 'PhoneInput',
|
||||
mixins: [
|
||||
FormsyMixin,
|
||||
ComponentMixin
|
||||
],
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
validations: {
|
||||
isPhone: true
|
||||
},
|
||||
flagsImagePath: '/images/flags.png',
|
||||
defaultCountry: 'us'
|
||||
};
|
||||
},
|
||||
onChangeInput: function (number, country) {
|
||||
var value = {national_number: number, country_code: country};
|
||||
this.setValue(value);
|
||||
this.props.onChange(this.props.name, value);
|
||||
},
|
||||
render: function () {
|
||||
var defaultCountry = PhoneInput.getDefaultProps().defaultCountry;
|
||||
if (allIso2.indexOf(this.props.defaultCountry.toLowerCase()) !== -1) {
|
||||
defaultCountry = this.props.defaultCountry.toLowerCase();
|
||||
}
|
||||
return (
|
||||
<Row {... this.getRowProperties()}
|
||||
htmlFor={this.getId()}
|
||||
className={classNames('phone-input', this.props.className)}
|
||||
>
|
||||
<div className="input-group">
|
||||
<ReactPhoneInput className="form-control"
|
||||
{... this.props}
|
||||
defaultCountry={defaultCountry}
|
||||
onChange={this.onChangeInput}
|
||||
id={this.getId()}
|
||||
label={null}
|
||||
disabled={this.isFormDisabled() || this.props.disabled}
|
||||
/>
|
||||
</div>
|
||||
{this.renderHelp()}
|
||||
{this.renderErrorMessage()}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var phoneValidationHOC = validationHOCFactory({
|
||||
isPhone: 'Please enter a valid phone number'
|
||||
});
|
||||
|
||||
module.exports = inputHOC(defaultValidationHOC(phoneValidationHOC(PhoneInput)));
|
42
src/components/forms/phone-input.scss
Normal file
42
src/components/forms/phone-input.scss
Normal file
|
@ -0,0 +1,42 @@
|
|||
@import "../../colors";
|
||||
|
||||
.input-group {
|
||||
margin: .75rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-tel-input {
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
&[type=tel] {
|
||||
background-color: $ui-light-gray;
|
||||
height: 3rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flag-dropdown {
|
||||
background-color: $ui-light-gray;
|
||||
height: 3rem;
|
||||
|
||||
.selected-flag {
|
||||
background-color: $ui-light-gray;
|
||||
height: 100%;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background-color: $active-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.country-list {
|
||||
top: 3rem;
|
||||
background-color: $ui-light-gray;
|
||||
}
|
||||
}
|
||||
}
|
23
src/components/forms/radio-group.jsx
Normal file
23
src/components/forms/radio-group.jsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
var classNames = require('classnames');
|
||||
var FRCRadioGroup = require('formsy-react-components').RadioGroup;
|
||||
var React = require('react');
|
||||
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
|
||||
var inputHOC = require('./input-hoc.jsx');
|
||||
|
||||
require('./row.scss');
|
||||
require('./radio-group.scss');
|
||||
|
||||
var RadioGroup = React.createClass({
|
||||
type: 'RadioGroup',
|
||||
render: function () {
|
||||
var classes = classNames(
|
||||
'radio-group',
|
||||
this.props.className
|
||||
);
|
||||
return (
|
||||
<FRCRadioGroup {... this.props} className={classes} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = inputHOC(defaultValidationHOC(RadioGroup));
|
46
src/components/forms/radio-group.scss
Normal file
46
src/components/forms/radio-group.scss
Normal file
|
@ -0,0 +1,46 @@
|
|||
@import "../../colors";
|
||||
|
||||
.row {
|
||||
.radio {
|
||||
label {
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.col-sm-9 {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
input {
|
||||
&[type="radio"] {
|
||||
margin-top: 1px;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 50%;
|
||||
width: .875rem;
|
||||
height: .875rem;
|
||||
appearance: none;
|
||||
|
||||
&:checked,
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:checked {
|
||||
transition: all .25s ease;
|
||||
box-shadow: 0 0 0 .25rem $active-gray;
|
||||
background-color: $ui-blue;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
transform: translate(.25rem, .25rem);
|
||||
border-radius: 50%;
|
||||
background-color: $ui-white;
|
||||
width: .25rem;
|
||||
height: .25rem;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
src/components/forms/row.scss
Normal file
11
src/components/forms/row.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Styles for the Row component used by formsy-react-components
|
||||
* Should be imported for each component that extends one of
|
||||
* the formsy-react-components
|
||||
*/
|
||||
|
||||
.form-group {
|
||||
.required-symbol {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,11 @@
|
|||
var React = require('react');
|
||||
var classNames = require('classnames');
|
||||
var defaults = require('lodash.defaultsdeep');
|
||||
var FRCSelect = require('formsy-react-components').Select;
|
||||
var React = require('react');
|
||||
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
|
||||
var inputHOC = require('./input-hoc.jsx');
|
||||
|
||||
require('./row.scss');
|
||||
require('./select.scss');
|
||||
|
||||
var Select = React.createClass({
|
||||
|
@ -13,12 +18,16 @@ var Select = React.createClass({
|
|||
'select',
|
||||
this.props.className
|
||||
);
|
||||
var props = this.props;
|
||||
if (this.props.required && !this.props.value) {
|
||||
props = defaults({}, this.props, {value: this.props.options[0].value});
|
||||
}
|
||||
return (
|
||||
<select {... this.props} className={classes}>
|
||||
{this.props.children}
|
||||
</select>
|
||||
<div className={classes}>
|
||||
<FRCSelect {... props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Select;
|
||||
module.exports = inputHOC(defaultValidationHOC(Select));
|
||||
|
|
|
@ -1,9 +1,42 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
.select {
|
||||
background-color: $ui-white;
|
||||
width: 220px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
font-size: 1em;
|
||||
label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
select {
|
||||
transition: all .5s ease;
|
||||
margin: .75rem 0;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
background: $ui-light-gray url("../../../static/svgs/forms/carot.svg") no-repeat right center;
|
||||
padding-right: 4rem;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
text-indent: 1rem;
|
||||
font-size: .875rem;
|
||||
appearance: none;
|
||||
|
||||
&::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
transition: all .5s ease;
|
||||
outline: none;
|
||||
border: 1px solid $ui-blue;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
background: $ui-light-gray url("../../../static/svgs/forms/carot-hover.svg") no-repeat right center;
|
||||
}
|
||||
|
||||
> option {
|
||||
background-color: $ui-white;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
23
src/components/forms/textarea.jsx
Normal file
23
src/components/forms/textarea.jsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
var classNames = require('classnames');
|
||||
var FRCTextarea = require('formsy-react-components').Textarea;
|
||||
var React = require('react');
|
||||
var defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
|
||||
var inputHOC = require('./input-hoc.jsx');
|
||||
|
||||
require('./row.scss');
|
||||
require('./textarea.scss');
|
||||
|
||||
var TextArea = React.createClass({
|
||||
type: 'TextArea',
|
||||
render: function () {
|
||||
var classes = classNames(
|
||||
'textarea',
|
||||
this.props.className
|
||||
);
|
||||
return (
|
||||
<FRCTextarea {... this.props} className={classes} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = inputHOC(defaultValidationHOC(TextArea));
|
25
src/components/forms/textarea.scss
Normal file
25
src/components/forms/textarea.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
@import "../../colors";
|
||||
|
||||
.textarea {
|
||||
transition: all 1s ease;
|
||||
margin: .75rem 0;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
background-color: $ui-light-gray;
|
||||
padding: .75rem 1rem;
|
||||
width: 100%;
|
||||
min-height: 15rem;
|
||||
line-height: 1.75em;
|
||||
color: $type-gray;
|
||||
font-size: .875rem;
|
||||
|
||||
&:focus {
|
||||
transition: all 1s ease;
|
||||
outline: none;
|
||||
border: 1px solid $ui-blue;
|
||||
}
|
||||
|
||||
&.fail {
|
||||
border: 1px solid $ui-orange;
|
||||
}
|
||||
}
|
47
src/components/forms/validations.jsx
Normal file
47
src/components/forms/validations.jsx
Normal 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'
|
||||
});
|
130
src/components/grid/grid.json
Normal file
130
src/components/grid/grid.json
Normal 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": "#"
|
||||
}
|
||||
]
|
67
src/components/grid/grid.jsx
Normal file
67
src/components/grid/grid.jsx
Normal 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;
|
58
src/components/grid/grid.scss
Normal file
58
src/components/grid/grid.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
37
src/components/informationpage/informationpage.jsx
Normal file
37
src/components/informationpage/informationpage.jsx
Normal 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;
|
100
src/components/informationpage/informationpage.scss
Normal file
100
src/components/informationpage/informationpage.scss
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +1,26 @@
|
|||
var classNames = require('classnames');
|
||||
var React = require('react');
|
||||
|
||||
var Api = require('../../mixins/api.jsx');
|
||||
var jar = require('../../lib/jar.js');
|
||||
var languages = require('../../../languages.json');
|
||||
var Form = require('../forms/form.jsx');
|
||||
var Select = require('../forms/select.jsx');
|
||||
|
||||
require('./languagechooser.scss');
|
||||
|
||||
/**
|
||||
* Footer dropdown menu that allows one to change their language.
|
||||
*/
|
||||
var LanguageChooser = React.createClass({
|
||||
type: 'LanguageChooser',
|
||||
mixins: [
|
||||
Api
|
||||
],
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
languages: languages,
|
||||
locale: window._locale
|
||||
};
|
||||
},
|
||||
onSetLanguage: function (e) {
|
||||
e.preventDefault();
|
||||
jar.set('scratchlanguage', e.target.value);
|
||||
onSetLanguage: function (name, value) {
|
||||
jar.set('scratchlanguage', value);
|
||||
window.location.reload();
|
||||
},
|
||||
render: function () {
|
||||
|
@ -30,17 +28,17 @@ var LanguageChooser = React.createClass({
|
|||
'language-chooser',
|
||||
this.props.className
|
||||
);
|
||||
|
||||
var languageOptions = Object.keys(this.props.languages).map(function (value) {
|
||||
return {value: value, label: this.props.languages[value]};
|
||||
}.bind(this));
|
||||
return (
|
||||
<div className={classes}>
|
||||
<Select name="language" defaultValue={this.props.locale} onChange={this.onSetLanguage}>
|
||||
{Object.keys(this.props.languages).map(function (value) {
|
||||
return <option value={value} key={value}>
|
||||
{this.props.languages[value]}
|
||||
</option>;
|
||||
}.bind(this))}
|
||||
</Select>
|
||||
</div>
|
||||
<Form className={classes}>
|
||||
<Select name="language"
|
||||
options={languageOptions}
|
||||
value={this.props.locale}
|
||||
onChange={this.onSetLanguage}
|
||||
required />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
10
src/components/languagechooser/languagechooser.scss
Normal file
10
src/components/languagechooser/languagechooser.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
@import "../../frameless";
|
||||
|
||||
.language-chooser {
|
||||
.select {
|
||||
select {
|
||||
width: 13.75rem;
|
||||
/* 3 columns */
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var FormattedMessage = require('react-intl').FormattedMessage;
|
||||
|
||||
var log = require('../../lib/log.js');
|
||||
|
||||
var Form = require('../forms/form.jsx');
|
||||
var Input = require('../forms/input.jsx');
|
||||
var Button = require('../forms/button.jsx');
|
||||
var Spinner = require('../spinner/spinner.jsx');
|
||||
|
@ -21,13 +21,9 @@ var Login = React.createClass({
|
|||
waiting: false
|
||||
};
|
||||
},
|
||||
handleSubmit: function (event) {
|
||||
event.preventDefault();
|
||||
handleSubmit: function (formData) {
|
||||
this.setState({waiting: true});
|
||||
this.props.onLogIn({
|
||||
'username': ReactDOM.findDOMNode(this.refs.username).value,
|
||||
'password': ReactDOM.findDOMNode(this.refs.password).value
|
||||
}, function (err) {
|
||||
this.props.onLogIn(formData, function (err) {
|
||||
if (err) log.error(err);
|
||||
this.setState({waiting: false});
|
||||
}.bind(this));
|
||||
|
@ -39,37 +35,29 @@ var Login = React.createClass({
|
|||
}
|
||||
return (
|
||||
<div className="login">
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<label htmlFor="username" key="usernameLabel">
|
||||
<FormattedMessage
|
||||
id='general.username'
|
||||
defaultMessage={'Username'} />
|
||||
<FormattedMessage id='general.username' />
|
||||
</label>
|
||||
<Input type="text" ref="username" name="username" maxLength="30" key="usernameInput" />
|
||||
<Input type="text" ref="username" name="username" maxLength="30" key="usernameInput" required />
|
||||
<label htmlFor="password" key="passwordLabel">
|
||||
<FormattedMessage
|
||||
id='general.password'
|
||||
defaultMessage={'Password'} />
|
||||
<FormattedMessage id='general.password' />
|
||||
</label>
|
||||
<Input type="password" ref="password" name="password" key="passwordInput" />
|
||||
<Input type="password" ref="password" name="password" key="passwordInput" required />
|
||||
{this.state.waiting ? [
|
||||
<Button className="submit-button white" type="submit" disabled="disabled" key="submitButton">
|
||||
<Spinner />
|
||||
</Button>
|
||||
] : [
|
||||
<Button className="submit-button white" type="submit" key="submitButton">
|
||||
<FormattedMessage
|
||||
id='general.signIn'
|
||||
defaultMessage={'Sign in'} />
|
||||
<FormattedMessage id='general.signIn' />
|
||||
</Button>
|
||||
]}
|
||||
<a className="right" href="/accounts/password_reset/" key="passwordResetLink">
|
||||
<FormattedMessage
|
||||
id='login.forgotPassword'
|
||||
defaultMessage={'Forgot Password?'} />
|
||||
<FormattedMessage id='login.forgotPassword' />
|
||||
</a>
|
||||
{error}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ var defaultStyle = {
|
|||
backgroundColor: 'rgba(0, 0, 0, .75)'
|
||||
},
|
||||
content: {
|
||||
position: 'absolute',
|
||||
overflow: 'visible',
|
||||
borderRadius: '6px',
|
||||
width: 500,
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
@import "../../colors";
|
||||
|
||||
.ReactModal__Content {
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
box-shadow: 0 0 3px $box-shadow-gray;
|
||||
background-color: $ui-blue;
|
||||
|
||||
&.staging {
|
||||
background-color: $ui-orange;
|
||||
}
|
||||
|
||||
|
||||
width: 100%;
|
||||
/* NOTE: Height should match offset settings in main.scss file */
|
||||
|
@ -73,7 +77,7 @@
|
|||
.link {
|
||||
> a {
|
||||
display: block;
|
||||
padding: 13px 15px 3px 15px;
|
||||
padding: 13px 15px 4px 15px;
|
||||
height: 33px;
|
||||
|
||||
text-decoration: none;
|
||||
|
|
|
@ -7,8 +7,9 @@ var injectIntl = ReactIntl.injectIntl;
|
|||
|
||||
var sessionActions = require('../../../redux/session.js');
|
||||
|
||||
var Api = require('../../../mixins/api.jsx');
|
||||
var api = require('../../../lib/api');
|
||||
var Avatar = require('../../avatar/avatar.jsx');
|
||||
var Button = require('../../forms/button.jsx');
|
||||
var Dropdown = require('../../dropdown/dropdown.jsx');
|
||||
var Input = require('../../forms/input.jsx');
|
||||
var log = require('../../../lib/log.js');
|
||||
|
@ -23,9 +24,6 @@ Modal.setAppElement(document.getElementById('view'));
|
|||
|
||||
var Navigation = React.createClass({
|
||||
type: 'Navigation',
|
||||
mixins: [
|
||||
Api
|
||||
],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
accountNavOpen: false,
|
||||
|
@ -43,19 +41,19 @@ var Navigation = React.createClass({
|
|||
};
|
||||
},
|
||||
componentDidMount: function () {
|
||||
if (this.props.session.user) {
|
||||
if (this.props.session.session.user) {
|
||||
this.getMessageCount();
|
||||
var intervalId = setInterval(this.getMessageCount, 120000); // check for new messages every 2 mins.
|
||||
this.setState({'messageCountIntervalId': intervalId});
|
||||
}
|
||||
},
|
||||
componentDidUpdate: function (prevProps) {
|
||||
if (prevProps.session.user != this.props.session.user) {
|
||||
if (prevProps.session.session.user != this.props.session.session.user) {
|
||||
this.setState({
|
||||
'loginOpen': false,
|
||||
'accountNavOpen': false
|
||||
});
|
||||
if (this.props.session.user) {
|
||||
if (this.props.session.session.user) {
|
||||
this.getMessageCount();
|
||||
var intervalId = setInterval(this.getMessageCount, 120000);
|
||||
this.setState({'messageCountIntervalId': intervalId});
|
||||
|
@ -80,13 +78,13 @@ var Navigation = React.createClass({
|
|||
}
|
||||
},
|
||||
getProfileUrl: function () {
|
||||
if (!this.props.session.user) return;
|
||||
return '/users/' + this.props.session.user.username + '/';
|
||||
if (!this.props.session.session.user) return;
|
||||
return '/users/' + this.props.session.session.user.username + '/';
|
||||
},
|
||||
getMessageCount: function () {
|
||||
this.api({
|
||||
api({
|
||||
method: 'get',
|
||||
uri: '/users/' + this.props.session.user.username + '/messages/count'
|
||||
uri: '/users/' + this.props.session.session.user.username + '/messages/count'
|
||||
}, function (err, body) {
|
||||
if (err) return this.setState({'unreadMessageCount': 0});
|
||||
if (body) {
|
||||
|
@ -109,7 +107,7 @@ var Navigation = React.createClass({
|
|||
handleLogIn: function (formData, callback) {
|
||||
this.setState({'loginError': null});
|
||||
formData['useMessages'] = true;
|
||||
this.api({
|
||||
api({
|
||||
method: 'post',
|
||||
host: '',
|
||||
uri: '/accounts/login/',
|
||||
|
@ -141,7 +139,7 @@ var Navigation = React.createClass({
|
|||
},
|
||||
handleLogOut: function (e) {
|
||||
e.preventDefault();
|
||||
this.api({
|
||||
api({
|
||||
host: '',
|
||||
method: 'post',
|
||||
uri: '/accounts/logout/',
|
||||
|
@ -174,14 +172,18 @@ var Navigation = React.createClass({
|
|||
},
|
||||
render: function () {
|
||||
var classes = classNames({
|
||||
'logged-in': this.props.session.user
|
||||
'logged-in': this.props.session.session.user
|
||||
});
|
||||
var messageClasses = classNames({
|
||||
'message-count': true,
|
||||
'show': this.state.unreadMessageCount > 0
|
||||
});
|
||||
var dropdownClasses = classNames({
|
||||
'user-info': true,
|
||||
'open': this.state.accountNavOpen
|
||||
});
|
||||
var formatMessage = this.props.intl.formatMessage;
|
||||
var createLink = this.props.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
|
||||
var createLink = this.props.session.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
|
||||
return (
|
||||
<NavigationBox className={classes}>
|
||||
<ul>
|
||||
|
@ -193,7 +195,7 @@ var Navigation = React.createClass({
|
|||
</a>
|
||||
</li>
|
||||
<li className="link explore">
|
||||
<a href="/explore?date=this_month">
|
||||
<a href="/explore/projects/all">
|
||||
<FormattedMessage id="general.explore" />
|
||||
</a>
|
||||
</li>
|
||||
|
@ -214,109 +216,112 @@ var Navigation = React.createClass({
|
|||
</li>
|
||||
|
||||
<li className="search">
|
||||
<form action="/search/google_results" method="get">
|
||||
<Input type="submit" value="" />
|
||||
<form action="/search/projects" method="get">
|
||||
<Button type="submit" className="btn-search" />
|
||||
<Input type="text"
|
||||
aria-label={formatMessage({id: 'general.search'})}
|
||||
placeholder={formatMessage({id: 'general.search'})}
|
||||
name="q" />
|
||||
<Input type="hidden" name="date" value="anytime" />
|
||||
<Input type="hidden" name="sort_by" value="datetime_shared" />
|
||||
name="q"
|
||||
noformsy />
|
||||
</form>
|
||||
</li>
|
||||
{this.props.session.user ? [
|
||||
<li className="link right messages" key="messages">
|
||||
<a
|
||||
href="/messages/"
|
||||
title={formatMessage({id: 'general.messages'})}>
|
||||
{this.props.session.status === sessionActions.Status.FETCHED ? (
|
||||
this.props.session.session.user ? [
|
||||
<li className="link right messages" key="messages">
|
||||
<a
|
||||
href="/messages/"
|
||||
title={formatMessage({id: 'general.messages'})}>
|
||||
|
||||
<span className={messageClasses}>{this.state.unreadMessageCount}</span>
|
||||
<FormattedMessage id="general.messages" />
|
||||
</a>
|
||||
</li>,
|
||||
<li className="link right mystuff" key="mystuff">
|
||||
<a
|
||||
href="/mystuff/"
|
||||
title={formatMessage({id: 'general.myStuff'})}>
|
||||
<span className={messageClasses}>{this.state.unreadMessageCount}</span>
|
||||
<FormattedMessage id="general.messages" />
|
||||
</a>
|
||||
</li>,
|
||||
<li className="link right mystuff" key="mystuff">
|
||||
<a
|
||||
href="/mystuff/"
|
||||
title={formatMessage({id: 'general.myStuff'})}>
|
||||
|
||||
<FormattedMessage id="general.myStuff" />
|
||||
</a>
|
||||
</li>,
|
||||
<li className="link right account-nav" key="account-nav">
|
||||
<a className="user-info" href="#" onClick={this.handleAccountNavClick}>
|
||||
<Avatar src={this.props.session.user.thumbnailUrl} alt="" />
|
||||
{this.props.session.user.username}
|
||||
</a>
|
||||
<Dropdown
|
||||
as="ul"
|
||||
isOpen={this.state.accountNavOpen}
|
||||
onRequestClose={this.closeAccountNav}>
|
||||
<li>
|
||||
<a href={this.getProfileUrl()}>
|
||||
<FormattedMessage id="general.profile" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/mystuff/">
|
||||
<FormattedMessage id="general.myStuff" />
|
||||
</a>
|
||||
</li>
|
||||
{this.props.session.permissions.educator ? [
|
||||
<FormattedMessage id="general.myStuff" />
|
||||
</a>
|
||||
</li>,
|
||||
<li className="link right account-nav" key="account-nav">
|
||||
<a className={dropdownClasses}
|
||||
href="#" onClick={this.handleAccountNavClick}>
|
||||
<Avatar src={this.props.session.session.user.thumbnailUrl} alt="" />
|
||||
{this.props.session.session.user.username}
|
||||
</a>
|
||||
<Dropdown
|
||||
as="ul"
|
||||
isOpen={this.state.accountNavOpen}
|
||||
onRequestClose={this.closeAccountNav}
|
||||
className={process.env.SCRATCH_ENV}>
|
||||
<li>
|
||||
<a href="/educators/classes/">
|
||||
<FormattedMessage id="general.myClasses" />
|
||||
<a href={this.getProfileUrl()}>
|
||||
<FormattedMessage id="general.profile" />
|
||||
</a>
|
||||
</li>
|
||||
] : []}
|
||||
{this.props.session.permissions.student ? [
|
||||
<li>
|
||||
<a href={'/classes/' + this.props.session.user.classroomId + '/'}>
|
||||
<FormattedMessage id="general.myClass" />
|
||||
<a href="/mystuff/">
|
||||
<FormattedMessage id="general.myStuff" />
|
||||
</a>
|
||||
</li>
|
||||
] : []}
|
||||
<li>
|
||||
<a href="/accounts/settings/">
|
||||
<FormattedMessage id="general.accountSettings" />
|
||||
</a>
|
||||
</li>
|
||||
<li className="divider">
|
||||
<a href="#" onClick={this.handleLogOut}>
|
||||
<FormattedMessage id="navigation.signOut" />
|
||||
</a>
|
||||
</li>
|
||||
</Dropdown>
|
||||
</li>
|
||||
] : [
|
||||
<li className="link right join" key="join">
|
||||
<a href="#" onClick={this.handleJoinClick}>
|
||||
<FormattedMessage id="general.joinScratch" />
|
||||
</a>
|
||||
</li>,
|
||||
<Registration
|
||||
key="registration"
|
||||
isOpen={this.state.registrationOpen}
|
||||
onRequestClose={this.closeRegistration}
|
||||
onRegistrationDone={this.completeRegistration} />,
|
||||
<li className="link right login-item" key="login">
|
||||
<a
|
||||
href="#"
|
||||
onClick={this.handleLoginClick}
|
||||
className="ignore-react-onclickoutside"
|
||||
key="login-link">
|
||||
<FormattedMessage id="general.signIn" />
|
||||
</a>
|
||||
<Dropdown
|
||||
className="login-dropdown with-arrow"
|
||||
isOpen={this.state.loginOpen}
|
||||
onRequestClose={this.closeLogin}
|
||||
key="login-dropdown">
|
||||
<Login
|
||||
onLogIn={this.handleLogIn}
|
||||
error={this.state.loginError} />
|
||||
</Dropdown>
|
||||
</li>
|
||||
]}
|
||||
{this.props.permissions.educator ? [
|
||||
<li key="my-classes-li">
|
||||
<a href="/educators/classes/">
|
||||
<FormattedMessage id="general.myClasses" />
|
||||
</a>
|
||||
</li>
|
||||
] : []}
|
||||
{this.props.permissions.student ? [
|
||||
<li>
|
||||
<a href={'/classes/' + this.props.session.session.user.classroomId + '/'}>
|
||||
<FormattedMessage id="general.myClass" />
|
||||
</a>
|
||||
</li>
|
||||
] : []}
|
||||
<li>
|
||||
<a href="/accounts/settings/">
|
||||
<FormattedMessage id="general.accountSettings" />
|
||||
</a>
|
||||
</li>
|
||||
<li className="divider">
|
||||
<a href="#" onClick={this.handleLogOut}>
|
||||
<FormattedMessage id="navigation.signOut" />
|
||||
</a>
|
||||
</li>
|
||||
</Dropdown>
|
||||
</li>
|
||||
] : [
|
||||
<li className="link right join" key="join">
|
||||
<a href="#" onClick={this.handleJoinClick}>
|
||||
<FormattedMessage id="general.joinScratch" />
|
||||
</a>
|
||||
</li>,
|
||||
<Registration
|
||||
key="registration"
|
||||
isOpen={this.state.registrationOpen}
|
||||
onRequestClose={this.closeRegistration}
|
||||
onRegistrationDone={this.completeRegistration} />,
|
||||
<li className="link right login-item" key="login">
|
||||
<a
|
||||
href="#"
|
||||
onClick={this.handleLoginClick}
|
||||
className="ignore-react-onclickoutside"
|
||||
key="login-link">
|
||||
<FormattedMessage id="general.signIn" />
|
||||
</a>
|
||||
<Dropdown
|
||||
className="login-dropdown with-arrow"
|
||||
isOpen={this.state.loginOpen}
|
||||
onRequestClose={this.closeLogin}
|
||||
key="login-dropdown">
|
||||
<Login
|
||||
onLogIn={this.handleLogIn}
|
||||
error={this.state.loginError} />
|
||||
</Dropdown>
|
||||
</li>
|
||||
]) : [
|
||||
]}
|
||||
</ul>
|
||||
<Modal isOpen={this.state.canceledDeletionOpen}
|
||||
onRequestClose={this.closeCanceledDeletion}
|
||||
|
@ -336,7 +341,8 @@ var Navigation = React.createClass({
|
|||
|
||||
var mapStateToProps = function (state) {
|
||||
return {
|
||||
session: state.session
|
||||
session: state.session,
|
||||
permissions: state.permissions
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
@import "../../../colors";
|
||||
|
||||
#navigation {
|
||||
&.staging {
|
||||
.messages {
|
||||
.message-count {
|
||||
display: none;
|
||||
|
||||
&.show {
|
||||
background-color: $ui-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-right: 10px;
|
||||
|
||||
|
@ -39,7 +51,8 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
input,
|
||||
button {
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
outline: none;
|
||||
|
@ -47,19 +60,6 @@
|
|||
background-color: $active-gray;
|
||||
height: 14px;
|
||||
|
||||
&[type=submit] {
|
||||
position: absolute;
|
||||
|
||||
background-color: transparent;
|
||||
background-image: url("/images/nav-search-glass.png");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 14px 14px;
|
||||
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
&[type=text] {
|
||||
transition: .15s ease background-color;
|
||||
padding: 0;
|
||||
|
@ -85,6 +85,24 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
position: absolute;
|
||||
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
background-image: url("/images/nav-search-glass.png");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 14px 14px;
|
||||
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,6 +159,21 @@
|
|||
|
||||
.login-dropdown {
|
||||
width: 200px;
|
||||
|
||||
.button {
|
||||
padding: .75em;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.row {
|
||||
margin-bottom: 1.25rem;
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
height: 2.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-nav {
|
||||
|
@ -164,6 +197,10 @@
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&.open {
|
||||
background-color: $active-gray;
|
||||
}
|
||||
|
||||
&:after {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
@ -180,9 +217,11 @@
|
|||
}
|
||||
|
||||
.dropdown {
|
||||
top: 50px;
|
||||
padding: 0;
|
||||
padding-top: 5px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,29 +101,6 @@
|
|||
section {
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
&.uneven {
|
||||
align-items: flex-start;
|
||||
|
||||
.short {
|
||||
width: $cols3;
|
||||
}
|
||||
|
||||
.long {
|
||||
width: $cols8;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
.short,
|
||||
.long {
|
||||
margin: auto;
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
var React = require('react');
|
||||
var classNames = require('classnames');
|
||||
|
||||
var Navigation = require('../../navigation/www/navigation.jsx');
|
||||
var Footer = require('../../footer/www/footer.jsx');
|
||||
|
@ -6,9 +7,12 @@ var Footer = require('../../footer/www/footer.jsx');
|
|||
var Page = React.createClass({
|
||||
type: 'Page',
|
||||
render: function () {
|
||||
var classes = classNames({
|
||||
'staging': process.env.SCRATCH_ENV == 'staging'
|
||||
});
|
||||
return (
|
||||
<div className="page">
|
||||
<div id="navigation">
|
||||
<div id="navigation" className={classes}>
|
||||
<Navigation />
|
||||
</div>
|
||||
<div id="view">
|
||||
|
|
42
src/components/progression/progression.jsx
Normal file
42
src/components/progression/progression.jsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
var classNames = require('classnames');
|
||||
var React = require('react');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'Progression',
|
||||
propTypes: {
|
||||
step: function (props, propName, componentName) {
|
||||
var stepValidator = function (props, propName) {
|
||||
if (props[propName] > -1 && props[propName] < props.children.length) {
|
||||
return null;
|
||||
} else {
|
||||
return new Error('Prop `step` out of range');
|
||||
}
|
||||
};
|
||||
return (
|
||||
React.PropTypes.number.isRequired(props, propName, componentName) ||
|
||||
stepValidator(props, propName, componentName)
|
||||
);
|
||||
}
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
step: 0
|
||||
};
|
||||
},
|
||||
render: function () {
|
||||
var childProps = {
|
||||
activeStep: this.props.step,
|
||||
totalSteps: React.Children.count(this.props.children)
|
||||
};
|
||||
return (
|
||||
<div {... this.props}
|
||||
className={classNames('progression', this.props.className)}>
|
||||
{React.Children.map(this.props.children, function (child, id) {
|
||||
if (id === this.props.step) {
|
||||
return React.cloneElement(child, childProps);
|
||||
}
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
725
src/components/registration/steps.jsx
Normal file
725
src/components/registration/steps.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}))
|
||||
};
|
17
src/components/slide/slide.jsx
Normal file
17
src/components/slide/slide.jsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
var classNames = require('classnames');
|
||||
var React = require('react');
|
||||
|
||||
require('./slide.scss');
|
||||
|
||||
var Slide = React.createClass({
|
||||
displayName: 'Slide',
|
||||
render: function () {
|
||||
return (
|
||||
<div className={classNames(['slide', this.props.className])}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Slide;
|
11
src/components/slide/slide.scss
Normal file
11
src/components/slide/slide.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
@import "../../frameless";
|
||||
|
||||
.slide {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
.slide {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
.spinner {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
|
@ -17,10 +18,14 @@
|
|||
animation: circleFadeDelay 1.2s infinite ease-in-out both;
|
||||
margin: 0 auto;
|
||||
border-radius: 100%;
|
||||
background-color: darken($ui-blue, 8%);
|
||||
background-color: darken($ui-white, 8%);
|
||||
width: 15%;
|
||||
height: 15%;
|
||||
content: "";
|
||||
|
||||
.white & {
|
||||
background-color: darken($ui-blue, 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
28
src/components/stepnavigation/stepnavigation.jsx
Normal file
28
src/components/stepnavigation/stepnavigation.jsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
var classNames = require('classnames');
|
||||
var React = require('react');
|
||||
|
||||
require('./stepnavigation.scss');
|
||||
|
||||
var StepNavigation = React.createClass({
|
||||
type: 'Navigation',
|
||||
render: function () {
|
||||
return (
|
||||
<ul className={classNames('step-navigation', this.props.className)}>
|
||||
{Array.apply(null, Array(this.props.steps)).map(function (v, step) {
|
||||
return (
|
||||
<li key={step}
|
||||
className={classNames({
|
||||
active: step < this.props.active,
|
||||
selected: step === this.props.active
|
||||
})}
|
||||
>
|
||||
<div className="indicator" />
|
||||
</li>
|
||||
);
|
||||
}.bind(this))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = StepNavigation;
|
31
src/components/stepnavigation/stepnavigation.scss
Normal file
31
src/components/stepnavigation/stepnavigation.scss
Normal file
|
@ -0,0 +1,31 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
.step-navigation {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
padding: .25rem;
|
||||
|
||||
.indicator {
|
||||
border-radius: .25rem;
|
||||
background-color: $ui-white;
|
||||
width: .5rem;
|
||||
height: .5rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.indicator {
|
||||
background-color: $ui-aqua;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border: 1px solid $ui-white;
|
||||
}
|
||||
}
|
||||
}
|
26
src/components/tabs/tabs.jsx
Normal file
26
src/components/tabs/tabs.jsx
Normal 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;
|
39
src/components/tabs/tabs.scss
Normal file
39
src/components/tabs/tabs.scss
Normal 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;
|
||||
}
|
||||
}
|
83
src/components/teacher-banner/teacher-banner.jsx
Normal file
83
src/components/teacher-banner/teacher-banner.jsx
Normal 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;
|
38
src/components/teacher-banner/teacher-banner.scss
Normal file
38
src/components/teacher-banner/teacher-banner.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -15,7 +15,9 @@ var Thumbnail = React.createClass({
|
|||
src: '',
|
||||
type: 'project',
|
||||
showLoves: false,
|
||||
showFavorites: false,
|
||||
showRemixes: false,
|
||||
showViews: false,
|
||||
linkTitle: true,
|
||||
alt: ''
|
||||
};
|
||||
|
@ -40,23 +42,40 @@ var Thumbnail = React.createClass({
|
|||
key="loves"
|
||||
className="thumbnail-loves"
|
||||
title={this.props.loves + ' loves'}>
|
||||
|
||||
{this.props.loves}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (this.props.favorites && this.props.showFavorites) {
|
||||
extra.push(
|
||||
<div
|
||||
key="favorites"
|
||||
className="thumbnail-favorites"
|
||||
title={this.favorites + ' favorites'}>
|
||||
{this.props.favorites}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (this.props.remixes && this.props.showRemixes) {
|
||||
extra.push(
|
||||
<div
|
||||
key="remixes"
|
||||
className="thumbnail-remixes"
|
||||
title={this.props.remixes + ' remixes'}>
|
||||
|
||||
{this.props.remixes}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.views && this.props.showViews) {
|
||||
extra.push(
|
||||
<div
|
||||
key="views"
|
||||
className="thumbnail-views"
|
||||
title={this.props.views + ' views'}>
|
||||
{this.props.views}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
var imgElement,titleElement;
|
||||
if (this.props.linkTitle) {
|
||||
imgElement = <a className="thumbnail-image" href={this.props.href}>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
$extras: ".thumbnail-creator, .thumbnail-loves, .thumbnail-remixes";
|
||||
$extras: ".thumbnail-creator, .thumbnail-loves, .thumbnail-favorites,.thumbnail-remixes,.thumbnail-views";
|
||||
|
||||
.thumbnail-title,
|
||||
#{$extras} {
|
||||
|
@ -41,7 +41,13 @@
|
|||
}
|
||||
|
||||
.thumbnail-loves,
|
||||
.thumbnail-remixes {
|
||||
.thumbnail-favorites,
|
||||
.thumbnail-remixes,
|
||||
.thumbnail-views {
|
||||
|
||||
display: inline;
|
||||
margin-right: 10px;
|
||||
|
||||
&:before {
|
||||
display: inline-block;
|
||||
margin-right: .1rem;
|
||||
|
@ -61,12 +67,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
.thumbnail-favorites {
|
||||
&:before {
|
||||
background-image: url("/svgs/favorite/favorite_type-gray.svg");
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail-remixes {
|
||||
&:before {
|
||||
background-image: url("/svgs/remix/remix_type-gray.svg");
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail-views {
|
||||
&:before {
|
||||
background-image: url("/svgs/view/view_type-gray.svg");
|
||||
}
|
||||
}
|
||||
|
||||
&.project {
|
||||
$project-width: 144px;
|
||||
$project-height: 108px;
|
||||
|
|
33
src/components/tooltip/tooltip.jsx
Normal file
33
src/components/tooltip/tooltip.jsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
var classNames = require('classnames');
|
||||
var React = require('react');
|
||||
|
||||
require('./tooltip.scss');
|
||||
|
||||
var Tooltip = React.createClass({
|
||||
type: 'Tooltip',
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
title: '',
|
||||
tipContent: ''
|
||||
};
|
||||
},
|
||||
render: function () {
|
||||
var classes = classNames(
|
||||
'tooltip',
|
||||
this.props.className,
|
||||
{overmax: (this.props.currentCharacters > this.props.maxCharacters)}
|
||||
);
|
||||
return (
|
||||
<span className={classes}>
|
||||
<span className="tip">
|
||||
<img src="/svgs/tooltip/info.svg" alt="info icon" />
|
||||
</span>
|
||||
<span className="expand">
|
||||
{this.props.tipContent}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Tooltip;
|
83
src/components/tooltip/tooltip.scss
Normal file
83
src/components/tooltip/tooltip.scss
Normal file
|
@ -0,0 +1,83 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
.tooltip {
|
||||
.tip {
|
||||
display: inline-flex;
|
||||
margin: 0 .5rem;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 .125rem $active-gray;
|
||||
background-color: $ui-blue;
|
||||
padding: .25rem;
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.expand {
|
||||
$arrow-border-width: 1rem;
|
||||
position: absolute;
|
||||
transform: translate(-2.75rem, 2rem);
|
||||
visibility: hidden;
|
||||
z-index: 1;
|
||||
margin-top: $arrow-border-width / 2;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
background-color: $ui-blue;
|
||||
padding: 1rem;
|
||||
width: 13.75rem;
|
||||
text-align: left;
|
||||
color: $type-white;
|
||||
|
||||
&:before {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -$arrow-border-width / 2;
|
||||
left: $arrow-border-width;
|
||||
|
||||
transform: rotate(45deg);
|
||||
|
||||
border-top: 1px solid $active-gray;
|
||||
border-left: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
|
||||
background-color: $ui-blue;
|
||||
width: $arrow-border-width;
|
||||
height: $arrow-border-width;
|
||||
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.expand {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
.tooltip {
|
||||
display: block;
|
||||
|
||||
.expand {
|
||||
display: none;
|
||||
position: relative;
|
||||
clear: both;
|
||||
transform: none;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.expand {
|
||||
display: block;
|
||||
margin: 5px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,32 +2,59 @@
|
|||
"general.accountSettings": "Account settings",
|
||||
"general.about": "About",
|
||||
"general.aboutScratch": "About Scratch",
|
||||
"general.birthMonth": "Birth Month",
|
||||
"general.birthYear": "Birth Year",
|
||||
"general.donate": "Donate",
|
||||
"general.collaborators": "Collaborators",
|
||||
"general.community": "Community",
|
||||
"general.confirmEmail": "Confirm Email",
|
||||
"general.contactUs": "Contact Us",
|
||||
"general.copyright": "Scratch is a project of the Lifelong Kindergarten Group at the MIT Media Lab",
|
||||
"general.country": "Country",
|
||||
"general.create": "Create",
|
||||
"general.createUsername": "Create a Username",
|
||||
"general.credits": "Credits",
|
||||
"general.discuss": "Discuss",
|
||||
"general.dmca": "DMCA",
|
||||
"general.emailAddress": "Email Address",
|
||||
"general.explore": "Explore",
|
||||
"general.faq": "FAQ",
|
||||
"general.female": "Female",
|
||||
"general.forParents": "For Parents",
|
||||
"general.forEducators": "For Educators",
|
||||
"general.forDevelopers": "For Developers",
|
||||
"general.gender": "Gender",
|
||||
"general.guidelines": "Community Guidelines",
|
||||
"general.help": "Help",
|
||||
"general.jobs": "Jobs",
|
||||
"general.joinScratch": "Join Scratch",
|
||||
"general.legal": "Legal",
|
||||
"general.loadMore": "Load More",
|
||||
"general.learnMore": "Learn More",
|
||||
"general.male": "Male",
|
||||
"general.messages": "Messages",
|
||||
"general.monthJanuary": "January",
|
||||
"general.monthFebruary": "February",
|
||||
"general.monthMarch": "March",
|
||||
"general.monthApril": "April",
|
||||
"general.monthMay": "May",
|
||||
"general.monthJune": "June",
|
||||
"general.monthJuly": "July",
|
||||
"general.monthAugust": "August",
|
||||
"general.monthSeptember": "September",
|
||||
"general.monthOctober": "October",
|
||||
"general.monthNovember": "November",
|
||||
"general.monthDecember": "December",
|
||||
"general.myClass": "My Class",
|
||||
"general.myClasses": "My Classes",
|
||||
"general.myStuff": "My Stuff",
|
||||
"general.notRequired": "Not Required",
|
||||
"general.other": "Other",
|
||||
"general.offlineEditor": "Offline Editor",
|
||||
"general.password": "Password",
|
||||
"general.press": "Press",
|
||||
"general.privacyPolicy": "Privacy Policy",
|
||||
"general.projects": "Projects",
|
||||
"general.profile": "Profile",
|
||||
"general.scratchConference": "Scratch Conference",
|
||||
"general.scratchday": "Scratch Day",
|
||||
|
@ -37,6 +64,7 @@
|
|||
"general.search": "Search",
|
||||
"general.signIn": "Sign in",
|
||||
"general.statistics": "Statistics",
|
||||
"general.studios": "Studios",
|
||||
"general.support": "Support",
|
||||
"general.tipsWindow": "Tips Window",
|
||||
"general.tipsAnimateYourNameTitle": "Animate Your Name",
|
||||
|
@ -47,10 +75,27 @@
|
|||
"general.tipsPongGame": "Create a Pong Game",
|
||||
"general.termsOfUse": "Terms of Use",
|
||||
"general.username": "Username",
|
||||
"general.validationEmail": "Please enter a valid email address",
|
||||
"general.validationEmailMatch": "The emails do not match",
|
||||
"general.validationUsernameExists": "Sorry, that username already exists",
|
||||
"general.validationUsernameVulgar": "Hmm, that looks inappropriate",
|
||||
"general.validationUsernameInvalid": "Invalid username",
|
||||
"general.viewAll": "View All",
|
||||
"general.website": "Website",
|
||||
"general.whatsHappening": "What's Happening?",
|
||||
"general.wiki": "Scratch Wiki",
|
||||
|
||||
"general.all": "All",
|
||||
"general.animations": "Animations",
|
||||
"general.art": "Art",
|
||||
"general.games": "Games",
|
||||
"general.music": "Music",
|
||||
"general.stories": "Stories",
|
||||
"general.results": "Results",
|
||||
|
||||
"general.teacherAccounts": "Teacher Accounts",
|
||||
|
||||
|
||||
"footer.discuss": "Discussion Forums",
|
||||
"footer.help": "Help Page",
|
||||
"footer.scratchFamily": "Scratch Family",
|
||||
|
@ -62,5 +107,15 @@
|
|||
"parents.FaqAgeRangeA": "While Scratch is primarily designed for 8 to 16 year olds, it is also used by people of all ages, including younger children with their parents.",
|
||||
"parents.FaqAgeRangeQ": "What is the age range for Scratch?",
|
||||
"parents.FaqResourcesQ": "What resources are available for learning Scratch?",
|
||||
"parents.introDescription": "Scratch is a programming language and an online community where children can program and share interactive media such as stories, games, and animation with people from all over the world. As children create with Scratch, they learn to think creatively, work collaboratively, and reason systematically. Scratch is designed and maintained by the Lifelong Kindergarten group at the MIT Media Lab."
|
||||
"parents.introDescription": "Scratch is a programming language and an online community where children can program and share interactive media such as stories, games, and animation with people from all over the world. As children create with Scratch, they learn to think creatively, work collaboratively, and reason systematically. Scratch is designed and maintained by the Lifelong Kindergarten group at the MIT Media Lab.",
|
||||
|
||||
"registration.lastStepTitle": "Thank you for requesting a Scratch Teacher Account",
|
||||
"registration.lastStepDescription": "We are currently processing your application. ",
|
||||
"registration.confirmYourEmail": "Confirm Your Email",
|
||||
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
|
||||
"registration.waitForApproval": "Wait for Approval",
|
||||
"registration.waitForApprovalDescription": "Your information is being reviewed. Please be patient, the approval process can take up to 24 hours. You will receive an email with your login information once your account has been created.",
|
||||
"registration.checkOutResources": "Get Started with Resources",
|
||||
"registration.checkOutResourcesDescription": "Explore materials for educators and facilitators written by the Scratch Team, including <a href='/educators#resources'>tips, tutorials, and guides</a>."
|
||||
|
||||
}
|
||||
|
|
93
src/lib/api.js
Normal file
93
src/lib/api.js
Normal 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
22
src/lib/country-data.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
module.exports = {};
|
||||
var countries = module.exports.data = require('iso-3166-2').data;
|
||||
|
||||
module.exports.countryOptions = Object.keys(countries).map(function (code) {
|
||||
return {value: code.toLowerCase(), label: countries[code].name};
|
||||
}).sort(function (a, b) {
|
||||
return a.label < b.label ? -1 : 1;
|
||||
});
|
||||
|
||||
module.exports.subdivisionOptions =
|
||||
Object.keys(countries).reduce(function (subByCountry, code) {
|
||||
subByCountry[code.toLowerCase()] = Object.keys(countries[code].sub).map(function (subCode) {
|
||||
return {
|
||||
value: subCode.toLowerCase(),
|
||||
label: countries[code].sub[subCode].name,
|
||||
type: countries[code].sub[subCode].type
|
||||
};
|
||||
}).sort(function (a, b) {
|
||||
return a.label < b.label ? -1 : 1;
|
||||
});
|
||||
return subByCountry;
|
||||
}, {});
|
|
@ -15,7 +15,8 @@ var Jar = {
|
|||
// Return the usable content portion of a signed, compressed cookie generated by
|
||||
// Django's signing module
|
||||
// https://github.com/django/django/blob/stable/1.8.x/django/core/signing.py
|
||||
if (!value) return callback('No value to unsign');
|
||||
if (typeof value === 'undefined') return callback(null, value);
|
||||
|
||||
try {
|
||||
var b64Data = value.split(':')[0];
|
||||
var decompress = false;
|
||||
|
@ -73,6 +74,24 @@ var Jar = {
|
|||
var expires = '; expires=' + new Date(new Date().setYear(new Date().getFullYear() + 1)).toUTCString();
|
||||
var path = '; path=/';
|
||||
document.cookie = obj + expires + path;
|
||||
},
|
||||
getUnsignedValue: function (cookieName, signedValue, callback) {
|
||||
// Get a value from a signed object
|
||||
Jar.get(cookieName, function (err, value) {
|
||||
if (err) return callback(err);
|
||||
if (typeof value === 'undefined') return callback(null, value);
|
||||
|
||||
Jar.unsign(value, function (err, contents) {
|
||||
if (err) return callback(err);
|
||||
|
||||
try {
|
||||
var data = JSON.parse(contents);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, data[signedValue]);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ var ReactDOM = require('react-dom');
|
|||
var StoreProvider = require('react-redux').Provider;
|
||||
|
||||
var IntlProvider = require('./intl.jsx').IntlProvider;
|
||||
var permissionsActions = require('../redux/permissions.js');
|
||||
var sessionActions = require('../redux/session.js');
|
||||
var reducer = require('../redux/reducer.js');
|
||||
|
||||
|
@ -42,7 +43,8 @@ var render = function (jsx, element) {
|
|||
element
|
||||
);
|
||||
|
||||
// Get initial session
|
||||
// Get initial session & permissions
|
||||
store.dispatch(permissionsActions.getPermissions());
|
||||
store.dispatch(sessionActions.refreshSession());
|
||||
};
|
||||
|
||||
|
|
21
src/lib/shuffle.js
Normal file
21
src/lib/shuffle.js
Normal 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
21
src/lib/smarty-streets.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
var defaults = require('lodash.defaults');
|
||||
var api = require('./api');
|
||||
|
||||
module.exports = function smartyStreetApi (params, callback) {
|
||||
defaults(params, {
|
||||
'auth-id': process.env.SMARTY_STREETS_API_KEY
|
||||
});
|
||||
api({
|
||||
host: 'https://api.smartystreets.com',
|
||||
uri: '/street-address',
|
||||
params: params
|
||||
}, function (err, body, res) {
|
||||
if (err) return callback(err);
|
||||
if (res.statusCode !== 200) {
|
||||
return callback(
|
||||
'There was an error contacting the address validation server.'
|
||||
);
|
||||
}
|
||||
return callback(err, body);
|
||||
});
|
||||
};
|
22
src/lib/url-params.js
Normal file
22
src/lib/url-params.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/* Turn an object into an url param string
|
||||
* urlParams({a: 1, b: 2, c: 3})
|
||||
* // a=1&b=2&c=3
|
||||
*/
|
||||
module.exports = function urlParams (values) {
|
||||
return Object
|
||||
.keys(values)
|
||||
.map(function (key) {
|
||||
var value = typeof values[key] === 'undefined' ? '' : values[key];
|
||||
function encodeKeyValuePair (value) {
|
||||
return [key, value]
|
||||
.map(encodeURIComponent)
|
||||
.join('=');
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(encodeKeyValuePair).join('&');
|
||||
} else {
|
||||
return encodeKeyValuePair(value);
|
||||
}
|
||||
})
|
||||
.join('&');
|
||||
};
|
|
@ -83,7 +83,8 @@ p {
|
|||
}
|
||||
}
|
||||
|
||||
b {
|
||||
b,
|
||||
strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
var defaults = require('lodash.defaults');
|
||||
var xhr = require('xhr');
|
||||
|
||||
var jar = require('../lib/jar.js');
|
||||
var log = require('../lib/log.js');
|
||||
|
||||
/**
|
||||
* Component mixin that constructs requests to the scratch api.
|
||||
* Custom arguments:
|
||||
* - useCsrf [boolean] – handles unique csrf token retrieval for POST requests. This prevents
|
||||
* CSRF forgeries (see: https://www.squarefree.com/securitytips/web-developers.html#CSRF)
|
||||
*
|
||||
* It also takes in other arguments specified in the xhr library spec.
|
||||
*/
|
||||
var Api = {
|
||||
api: function (opts, callback) {
|
||||
defaults(opts, {
|
||||
host: window.env.API_HOST,
|
||||
headers: {},
|
||||
json: {},
|
||||
useCsrf: false
|
||||
});
|
||||
|
||||
defaults(opts.headers, {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
});
|
||||
|
||||
opts.uri = opts.host + opts.uri;
|
||||
|
||||
var apiRequest = function (opts) {
|
||||
if (opts.host !== '') {
|
||||
// For IE < 10, we must use XDR for cross-domain requests. XDR does not support
|
||||
// custom headers.
|
||||
defaults(opts, {useXDR: true});
|
||||
delete opts.headers;
|
||||
if (opts.authentication) {
|
||||
var authenticationParams = ['x-token=' + opts.authentication];
|
||||
var parts = opts.uri.split('?');
|
||||
var qs = (parts[1] || '').split('&').concat(authenticationParams).join('&');
|
||||
opts.uri = parts[0] + '?' + qs;
|
||||
|
||||
}
|
||||
}
|
||||
xhr(opts, function (err, res, body) {
|
||||
if (err) log.error(err);
|
||||
// Legacy API responses come as lists, and indicate to redirect the client like
|
||||
// [{success: true, redirect: "/location/to/redirect"}]
|
||||
try {
|
||||
if ('redirect' in body[0]) window.location = body[0].redirect;
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
callback(err, body);
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
if (typeof jar.get('scratchlanguage') !== 'undefined') {
|
||||
opts.headers['Accept-Language'] = jar.get('scratchlanguage') + ', en;q=0.8';
|
||||
}
|
||||
if (opts.authentication) {
|
||||
opts.headers['X-Token'] = opts.authentication;
|
||||
}
|
||||
if (opts.useCsrf) {
|
||||
jar.use('scratchcsrftoken', '/csrf_token/', function (err, csrftoken) {
|
||||
if (err) return log.error('Error while retrieving CSRF token', err);
|
||||
opts.json.csrftoken = csrftoken;
|
||||
opts.headers['X-CSRFToken'] = csrftoken;
|
||||
apiRequest(opts);
|
||||
}.bind(this));
|
||||
} else {
|
||||
apiRequest(opts);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Api;
|
|
@ -1,17 +0,0 @@
|
|||
var jar = require('../lib/jar');
|
||||
|
||||
var cookieMixinFactory = function (cookieName, cookieSetter) {
|
||||
var capitalizedCookieName = cookieName.charAt(0).toUpperCase() + cookieName.slice(1);
|
||||
var getterName = 'get' + capitalizedCookieName;
|
||||
var userName = 'use' + capitalizedCookieName;
|
||||
var mixin = {};
|
||||
mixin[getterName] = function (callback) {
|
||||
jar.get(cookieName, callback);
|
||||
};
|
||||
mixin[userName] = function (callback) {
|
||||
jar.use(cookieName, cookieSetter, callback);
|
||||
};
|
||||
return mixin;
|
||||
};
|
||||
|
||||
module.exports = cookieMixinFactory;
|
|
@ -1,5 +1,5 @@
|
|||
var keyMirror = require('keymirror');
|
||||
var api = require('../mixins/api.jsx').api;
|
||||
var api = require('../lib/api');
|
||||
|
||||
var Types = keyMirror({
|
||||
SET_DETAILS: null,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var keyMirror = require('keymirror');
|
||||
var api = require('../mixins/api.jsx').api;
|
||||
var api = require('../lib/api');
|
||||
|
||||
var Types = keyMirror({
|
||||
SET_SCHEDULE: null,
|
||||
|
|
46
src/redux/permissions.js
Normal file
46
src/redux/permissions.js
Normal 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
|
||||
};
|
||||
};
|
|
@ -2,12 +2,14 @@ var combineReducers = require('redux').combineReducers;
|
|||
|
||||
var scheduleReducer = require('./conference-schedule.js').scheduleReducer;
|
||||
var detailsReducer = require('./conference-details.js').detailsReducer;
|
||||
var permissionsReducer = require('./permissions.js').permissionsReducer;
|
||||
var sessionReducer = require('./session.js').sessionReducer;
|
||||
var tokenReducer = require('./token.js').tokenReducer;
|
||||
|
||||
var appReducer = combineReducers({
|
||||
session: sessionReducer,
|
||||
token: tokenReducer,
|
||||
permissions: permissionsReducer,
|
||||
conferenceSchedule: scheduleReducer,
|
||||
conferenceDetails: detailsReducer
|
||||
});
|
||||
|
|
|
@ -1,21 +1,36 @@
|
|||
var keyMirror = require('keymirror');
|
||||
var defaults = require('lodash.defaults');
|
||||
|
||||
var api = require('../mixins/api.jsx').api;
|
||||
var api = require('../lib/api');
|
||||
var permissionsActions = require('./permissions.js');
|
||||
var tokenActions = require('./token.js');
|
||||
|
||||
var Types = keyMirror({
|
||||
SET_SESSION: null,
|
||||
SET_SESSION_ERROR: null
|
||||
SET_SESSION_ERROR: null,
|
||||
SET_STATUS: null
|
||||
});
|
||||
|
||||
module.exports.Status = keyMirror({
|
||||
FETCHED: null,
|
||||
NOT_FETCHED: null,
|
||||
FETCHING: null
|
||||
});
|
||||
|
||||
module.exports.getInitialState = function (){
|
||||
return {status: module.exports.Status.NOT_FETCHED, session:{}};
|
||||
};
|
||||
|
||||
module.exports.sessionReducer = function (state, action) {
|
||||
// Reducer for handling changes to session state
|
||||
if (typeof state === 'undefined') {
|
||||
state = {};
|
||||
state = module.exports.getInitialState();
|
||||
}
|
||||
switch (action.type) {
|
||||
case Types.SET_SESSION:
|
||||
return action.session;
|
||||
return defaults({session: action.session}, state);
|
||||
case Types.SET_STATUS:
|
||||
return defaults({status: action.status}, state);
|
||||
case Types.SET_SESSION_ERROR:
|
||||
// TODO: do something with action.error
|
||||
return state;
|
||||
|
@ -38,8 +53,16 @@ module.exports.setSession = function (session) {
|
|||
};
|
||||
};
|
||||
|
||||
module.exports.setStatus = function (status){
|
||||
return {
|
||||
type: Types.SET_STATUS,
|
||||
status: status
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.refreshSession = function () {
|
||||
return function (dispatch) {
|
||||
dispatch(module.exports.setStatus(module.exports.Status.FETCHING));
|
||||
api({
|
||||
host: '',
|
||||
uri: '/session/'
|
||||
|
@ -52,6 +75,10 @@ module.exports.refreshSession = function () {
|
|||
} else {
|
||||
dispatch(tokenActions.getToken());
|
||||
dispatch(module.exports.setSession(body));
|
||||
dispatch(module.exports.setStatus(module.exports.Status.FETCHED));
|
||||
|
||||
// get the permissions from the updated session
|
||||
dispatch(permissionsActions.getPermissions());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,17 +25,11 @@ module.exports.tokenReducer = function (state, action) {
|
|||
|
||||
module.exports.getToken = function () {
|
||||
return function (dispatch) {
|
||||
jar.get('scratchsessionsid', function (err, value) {
|
||||
jar.getUnsignedValue('scratchsessionsid', 'token', function (err, value) {
|
||||
if (err) return dispatch(module.exports.setTokenError(err));
|
||||
jar.unsign(value, function (err, contents) {
|
||||
if (err) return dispatch(module.exports.setTokenError(err));
|
||||
try {
|
||||
var sessionData = JSON.parse(contents);
|
||||
} catch (err) {
|
||||
return dispatch(module.exports.setTokenError(err));
|
||||
}
|
||||
return dispatch(module.exports.setToken(sessionData.token));
|
||||
});
|
||||
|
||||
value = value || '';
|
||||
return dispatch(module.exports.setToken(value));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -11,12 +11,6 @@
|
|||
"view": "about/about",
|
||||
"title": "About"
|
||||
},
|
||||
{
|
||||
"name": "components",
|
||||
"pattern": "^/components/?$",
|
||||
"view": "components/components",
|
||||
"title": "Components"
|
||||
},
|
||||
{
|
||||
"name": "developers",
|
||||
"pattern": "^/developers/?$",
|
||||
|
@ -29,12 +23,62 @@
|
|||
"view": "hoc/hoc",
|
||||
"title": "Hour of Code"
|
||||
},
|
||||
{
|
||||
"name": "explore",
|
||||
"pattern": "^/explore/:projects/:all/?$",
|
||||
"routeAlias": "^/explore",
|
||||
"view": "explore/explore",
|
||||
"title": "Explore"
|
||||
},
|
||||
{
|
||||
"name": "explore-redirect",
|
||||
"pattern": "^/explore/?$",
|
||||
"routeAlias": "^/explore",
|
||||
"redirect": "/explore/projects/all"
|
||||
},
|
||||
{
|
||||
"name": "explore-projects-redirect",
|
||||
"pattern": "^/explore/projects/?$",
|
||||
"routeAlias": "^/explore",
|
||||
"redirect": "/explore/projects/all"
|
||||
},
|
||||
{
|
||||
"name": "explore-studios-redirect",
|
||||
"pattern": "^/explore/studios/?$",
|
||||
"routeAlias": "^/explore",
|
||||
"redirect": "/explore/studios/all"
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"pattern": "^/search/:projects?$/?$",
|
||||
"routeAlias": "^/search",
|
||||
"view": "search/search",
|
||||
"title": "Search"
|
||||
},
|
||||
{
|
||||
"name": "credits",
|
||||
"pattern": "^/info/credits/?$",
|
||||
"view": "credits/credits",
|
||||
"title": "Credits"
|
||||
},
|
||||
{
|
||||
"name": "faq",
|
||||
"pattern": "^/info/faq/?$",
|
||||
"view": "faq/faq",
|
||||
"title": "FAQ"
|
||||
},
|
||||
{
|
||||
"name": "educator-landing",
|
||||
"pattern": "^/educators/?$",
|
||||
"view": "teachers/landing/landing",
|
||||
"title": "Educators"
|
||||
},
|
||||
{
|
||||
"name": "teacher-faq",
|
||||
"pattern": "^/educators/faq/?$",
|
||||
"view": "teachers/faq/faq",
|
||||
"title": "Teacher Accounts FAQ"
|
||||
},
|
||||
{
|
||||
"name": "cards",
|
||||
"pattern": "^/info/cards/?$",
|
||||
|
@ -53,6 +97,19 @@
|
|||
"view": "jobs/jobs",
|
||||
"title": "Jobs"
|
||||
},
|
||||
{
|
||||
"name": "teacherregistration",
|
||||
"pattern": "^/educators/register$",
|
||||
"view": "teacherregistration/teacherregistration",
|
||||
"title": "Teacher Registration",
|
||||
"viewportWidth": "device-width"
|
||||
},
|
||||
{
|
||||
"name": "teacherwaitingroom",
|
||||
"pattern": "^/educators/waiting",
|
||||
"view": "teacherwaitingroom/teacherwaitingroom",
|
||||
"title": "Thank you for requesting a Scratch Teacher Account"
|
||||
},
|
||||
{
|
||||
"name": "wedo2",
|
||||
"pattern": "^/wedo/?$",
|
||||
|
@ -62,6 +119,7 @@
|
|||
{
|
||||
"name": "conference-index",
|
||||
"pattern": "^/conference/?$",
|
||||
"routeAlias": "^/conference(?!/201[4-5])",
|
||||
"view": "conference/index/index",
|
||||
"title": "Scratch Conference",
|
||||
"viewportWidth": "device-width"
|
||||
|
@ -69,6 +127,7 @@
|
|||
{
|
||||
"name": "conference-plan",
|
||||
"pattern": "^/conference/plan/?$",
|
||||
"routeAlias": "^/conference(?!/201[4-5])",
|
||||
"view": "conference/plan/plan",
|
||||
"title": "Plan Your Visit",
|
||||
"viewportWidth": "device-width"
|
||||
|
@ -76,6 +135,7 @@
|
|||
{
|
||||
"name": "conference-expectations",
|
||||
"pattern": "^/conference/expect/?$",
|
||||
"routeAlias": "^/conference(?!/201[4-5])",
|
||||
"view": "conference/expect/expect",
|
||||
"title": "What to Expect",
|
||||
"viewportWidth": "device-width"
|
||||
|
@ -83,6 +143,7 @@
|
|||
{
|
||||
"name": "conference-schedule",
|
||||
"pattern": "^/conference/schedule/?$",
|
||||
"routeAlias": "^/conference(?!/201[4-5])",
|
||||
"view": "conference/schedule/schedule",
|
||||
"title": "Conference Schedule",
|
||||
"viewportWidth": "device-width"
|
||||
|
@ -90,6 +151,7 @@
|
|||
{
|
||||
"name": "conference-details",
|
||||
"pattern": "^/conference/:id/details/?$",
|
||||
"routeAlias": "^/conference(?!/201[4-5])",
|
||||
"view": "conference/details/details",
|
||||
"title": "Event Details",
|
||||
"viewportWidth": "device-width"
|
||||
|
@ -107,7 +169,7 @@
|
|||
},
|
||||
{
|
||||
"name": "guidelines",
|
||||
"pattern": "^/community_guidelines$",
|
||||
"pattern": "^/community_guidelines/?$",
|
||||
"view": "guidelines/guidelines",
|
||||
"title": "Scratch Community Guidelines"
|
||||
},
|
||||
|
@ -116,5 +178,11 @@
|
|||
"pattern": "^/privacy_policy/?$",
|
||||
"view": "privacypolicy/privacypolicy",
|
||||
"title": "Privacy Policy"
|
||||
},
|
||||
{
|
||||
"name": "terms",
|
||||
"pattern": "^/terms_of_use/?$",
|
||||
"view": "terms/terms",
|
||||
"title": "Scratch Terms of Use"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
module.exports = {
|
||||
// Bind environment
|
||||
api_host: process.env.API_HOST || 'https://api.scratch.mit.edu',
|
||||
|
||||
// Search and metadata
|
||||
title: 'Imagine, Program, Share',
|
||||
description:
|
||||
|
|
|
@ -31,25 +31,8 @@
|
|||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/css/lib/normalize.min.css" />
|
||||
|
||||
<!-- Environment -->
|
||||
<script>
|
||||
window.env = {
|
||||
API_HOST: "{{&api_host}}"
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Polyfills -->
|
||||
<script src="/js/polyfill.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- Vendor & Initialize (Session & Localization)-->
|
||||
<script src="/js/common.bundle.js"></script>
|
||||
<!-- Scripts -->
|
||||
<script src="/js/{{name}}.intl.js"></script>
|
||||
<script src="/js/{{name}}.bundle.js"></script>
|
||||
|
||||
<!-- Analytics (GA) -->
|
||||
<script>
|
||||
|
@ -63,8 +46,19 @@
|
|||
});
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<!-- translate title element -->
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- Vendor & Initialize (Session & Localization)-->
|
||||
<script src="/js/common.bundle.js"></script>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/js/{{name}}.intl.js"></script>
|
||||
<script src="/js/{{name}}.bundle.js"></script>
|
||||
|
||||
<!-- Translate title element -->
|
||||
<script>
|
||||
var loc = window._locale || 'en';
|
||||
if (typeof window._messages !== 'undefined' && loc !== 'en') {
|
||||
|
|
|
@ -11,8 +11,14 @@ require('./cards.scss');
|
|||
|
||||
var Cards = injectIntl(React.createClass({
|
||||
type: 'Cards',
|
||||
pdfLocaleMismatch: function (locale, pdf, englishPdf) {
|
||||
if (pdf === englishPdf && locale.indexOf('en') !== 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
render: function () {
|
||||
var locale = window._locale || 'en';
|
||||
var locale = this.props.intl.locale || 'en';
|
||||
var formatMessage = this.props.intl.formatMessage;
|
||||
var englishLinks = {
|
||||
'cards.starterLink': '//scratch.mit.edu/scratchr2/static/pdfs/help/Scratch2Cards.pdf',
|
||||
|
@ -51,8 +57,11 @@ var Cards = injectIntl(React.createClass({
|
|||
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
|
||||
<FormattedMessage id='cards.viewCard' />
|
||||
{(
|
||||
formattedLinks['cards.starterLink'] === englishLinks['cards.starterLink'] &&
|
||||
locale !== 'en'
|
||||
this.pdfLocaleMismatch(
|
||||
locale,
|
||||
formattedLinks['cards.starterLink'],
|
||||
englishLinks['cards.starterLink']
|
||||
)
|
||||
) ? [
|
||||
<span> <FormattedMessage id='cards.english' /></span>
|
||||
] : []}
|
||||
|
@ -67,8 +76,11 @@ var Cards = injectIntl(React.createClass({
|
|||
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
|
||||
<FormattedMessage id='cards.viewCard' />
|
||||
{(
|
||||
formattedLinks['cards.nameLink'] === englishLinks['cards.nameLink'] &&
|
||||
locale !== 'en'
|
||||
this.pdfLocaleMismatch(
|
||||
locale,
|
||||
formattedLinks['cards.nameLink'],
|
||||
englishLinks['cards.nameLink']
|
||||
)
|
||||
) ? [
|
||||
<span> (<FormattedMessage id='cards.english' />)</span>
|
||||
] : []}
|
||||
|
@ -83,8 +95,11 @@ var Cards = injectIntl(React.createClass({
|
|||
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
|
||||
<FormattedMessage id='cards.viewCard' />
|
||||
{(
|
||||
formattedLinks['cards.pongLink'] === englishLinks['cards.pongLink'] &&
|
||||
locale !== 'en'
|
||||
this.pdfLocaleMismatch(
|
||||
locale,
|
||||
formattedLinks['cards.pongLink'],
|
||||
englishLinks['cards.pongLink']
|
||||
)
|
||||
) ? [
|
||||
<span> (<FormattedMessage id='cards.english' />)</span>
|
||||
] : []}
|
||||
|
@ -101,8 +116,11 @@ var Cards = injectIntl(React.createClass({
|
|||
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
|
||||
<FormattedMessage id='cards.viewCard' />
|
||||
{(
|
||||
formattedLinks['cards.storyLink'] === englishLinks['cards.storyLink'] &&
|
||||
locale !== 'en'
|
||||
this.pdfLocaleMismatch(
|
||||
locale,
|
||||
formattedLinks['cards.storyLink'],
|
||||
englishLinks['cards.storyLink']
|
||||
)
|
||||
) ? [
|
||||
<span> (<FormattedMessage id='cards.english' />)</span>
|
||||
] : []}
|
||||
|
@ -117,8 +135,11 @@ var Cards = injectIntl(React.createClass({
|
|||
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
|
||||
<FormattedMessage id='cards.viewCard' />
|
||||
{(
|
||||
formattedLinks['cards.danceLink'] === englishLinks['cards.danceLink'] &&
|
||||
locale !== 'en'
|
||||
this.pdfLocaleMismatch(
|
||||
locale,
|
||||
formattedLinks['cards.danceLink'],
|
||||
englishLinks['cards.danceLink']
|
||||
)
|
||||
) ? [
|
||||
<span> (<FormattedMessage id='cards.english' />)</span>
|
||||
] : []}
|
||||
|
@ -133,8 +154,11 @@ var Cards = injectIntl(React.createClass({
|
|||
<img src="/svgs/pdf-icon-ui-blue.svg" alt="" className='pdf-icon' />
|
||||
<FormattedMessage id='cards.viewCard' />
|
||||
{(
|
||||
formattedLinks['cards.hideLink'] === englishLinks['cards.hideLink'] &&
|
||||
locale !== 'en'
|
||||
this.pdfLocaleMismatch(
|
||||
locale,
|
||||
formattedLinks['cards.hideLink'],
|
||||
englishLinks['cards.hideLink']
|
||||
)
|
||||
) ? [
|
||||
<span> (<FormattedMessage id='cards.english' />)</span>
|
||||
] : []}
|
||||
|
|
|
@ -33,7 +33,7 @@ var ConferenceExpectations = React.createClass({
|
|||
<p className="intro">
|
||||
The Scratch community keeps growing and growing.{' '}
|
||||
Young people around the world have shared more than{' '}
|
||||
14 million projects in the Scratch online community{' '}
|
||||
15 million projects in the Scratch online community{' '}
|
||||
– with 20,000 new projects every day.
|
||||
</p>
|
||||
<p className="intro">
|
||||
|
@ -58,7 +58,7 @@ var ConferenceExpectations = React.createClass({
|
|||
<p className="intro">
|
||||
We are planning a very participatory conference, with lots of{' '}
|
||||
hands-on workshops and opportunities for collaboration and sharing.{' '}
|
||||
We hope you’ll join us. Let’s learn together!
|
||||
Let’s learn together!
|
||||
</p>
|
||||
</div>
|
||||
</FlexRow>
|
||||
|
@ -167,7 +167,7 @@ var ConferenceExpectations = React.createClass({
|
|||
<tr>
|
||||
<td>
|
||||
<b>2:00p</b>
|
||||
<p>Afternoon workshops</p>
|
||||
<p>Afternoon Workshops</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -271,7 +271,7 @@ var ConferenceExpectations = React.createClass({
|
|||
<tr>
|
||||
<td>
|
||||
<b>12:00p</b>
|
||||
<p>Lunch (provided)</p>
|
||||
<p>Lunch (provided) and Wrap-up Session</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -30,7 +30,7 @@ var ConferenceSplash = React.createClass({
|
|||
</p>
|
||||
<p className="sub-button">
|
||||
<b>
|
||||
We're sold out! <a href="//scratchconference2016.eventbrite.com">Join the Waitlist</a>
|
||||
Scratch@MIT is sold out!
|
||||
</b>
|
||||
</p>
|
||||
</TitleBanner>
|
||||
|
|
|
@ -31,11 +31,6 @@
|
|||
|
||||
b {
|
||||
margin-top: 2rem;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: $type-white;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
@ -266,11 +266,11 @@ var ConferencePlan = React.createClass({
|
|||
The conference is sold out. What can I do?
|
||||
</dt>
|
||||
<dd>
|
||||
Scratch@MIT is sold out and at capacity. However, you can{' '}
|
||||
<a href="//scratchconference2016.eventbrite.com">
|
||||
join the waitlist
|
||||
</a>. People on the waitlist will be notified of openings on{' '}
|
||||
a first come, first served basis.
|
||||
Scratch@MIT is sold out and at capacity. Regrettably, we are{' '}
|
||||
unable to add any additional guests. Please keep in mind that{' '}
|
||||
you must have registered on Eventbrite to attend Scratch@MIT;{' '}
|
||||
people who are not registered / do not have a ticket will not be{' '}
|
||||
able to attend the conference.
|
||||
</dd>
|
||||
|
||||
<dt>
|
||||
|
|
|
@ -228,11 +228,10 @@ var Developers = React.createClass({
|
|||
<div className="faq column">
|
||||
<h4>Are there rules to using this code in my application?</h4>
|
||||
<p>
|
||||
You may use this code in accordance with the{' '}
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0">Apache 2.0</a> license
|
||||
which governs this project. We also strongly encourage you to consider{' '}
|
||||
the learning and design principles (above, on this page) when building{' '}
|
||||
creative learning experiences for kids of all ages.
|
||||
You may use this code in accordance with the license which governs{' '}
|
||||
each project. We also strongly encourage you to consider the learning{' '}
|
||||
and design principles (above, on this page) when building creative{' '}
|
||||
learning experiences for kids of all ages.
|
||||
</p>
|
||||
</div>
|
||||
<div className="faq column">
|
||||
|
@ -243,7 +242,7 @@ var Developers = React.createClass({
|
|||
<p>
|
||||
If you wish, you can publicly state that your application is powered by{' '}
|
||||
Scratch Blocks. If you do so, we would also encourage you to link back to{' '}
|
||||
code repository.
|
||||
the code repository.
|
||||
</p>
|
||||
</div>
|
||||
<div className="faq column">
|
||||
|
|
|
@ -1,30 +1,31 @@
|
|||
var React = require('react');
|
||||
var FormattedMessage = require('react-intl').FormattedMessage;
|
||||
var render = require('../../lib/render.jsx');
|
||||
|
||||
var InformationPage = require('../../components/informationpage/informationpage.jsx');
|
||||
var Page = require('../../components/page/www/page.jsx');
|
||||
var Box = require('../../components/box/box.jsx');
|
||||
|
||||
var Dmca = React.createClass({
|
||||
type: 'Dmca',
|
||||
render: function () {
|
||||
return (
|
||||
<div className="inner dmca">
|
||||
<Box title={'DMCA'}>
|
||||
<p><FormattedMessage id='dmca.intro' /></p>
|
||||
<p>
|
||||
Copyright Agent / Mitchel Resnick<br/>
|
||||
MIT Media Laboratory<br/>
|
||||
77 Massachusetts Ave<br/>
|
||||
Room E14-445A<br/>
|
||||
Cambridge, MA 02139<br/>
|
||||
Tel: (617) 253-9783
|
||||
</p>
|
||||
<p><FormattedMessage id='dmca.llkresponse' /></p>
|
||||
<p><FormattedMessage id='dmca.assessment' /></p>
|
||||
<p><FormattedMessage id='dmca.eyetoeye' /></p>
|
||||
<p><FormattedMessage id='dmca.afterfiling' /></p>
|
||||
</Box>
|
||||
</div>
|
||||
<InformationPage title={'DMCA'}>
|
||||
<div className="inner info-inner">
|
||||
<p><FormattedMessage id='dmca.intro' /></p>
|
||||
<p>
|
||||
Copyright Agent / Mitchel Resnick<br/>
|
||||
MIT Media Laboratory<br/>
|
||||
77 Massachusetts Ave<br/>
|
||||
Room E14-445A<br/>
|
||||
Cambridge, MA 02139<br/>
|
||||
Tel: (617) 253-9783
|
||||
</p>
|
||||
<p><FormattedMessage id='dmca.llkresponse' /></p>
|
||||
<p><FormattedMessage id='dmca.assessment' /></p>
|
||||
<p><FormattedMessage id='dmca.eyetoeye' /></p>
|
||||
<p><FormattedMessage id='dmca.afterfiling' /></p>
|
||||
</div>
|
||||
</InformationPage>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
152
src/views/explore/explore.jsx
Normal file
152
src/views/explore/explore.jsx
Normal 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
Loading…
Reference in a new issue