Merge pull request #190 from LLK/develop

Release 2.2.2
This commit is contained in:
Ray Schamp 2015-10-29 13:29:46 -04:00
commit 365b934eec
112 changed files with 12598 additions and 9262 deletions

View file

@ -16,6 +16,9 @@
"browser": true,
"node": true
},
"globals": {
"formatMessage": true
},
"ecmaFeatures": {
"jsx": true
},

3
.gitignore vendored
View file

@ -8,6 +8,9 @@ npm-*
# Build
/build
# Locales
/locales
# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml

24
.sass-lint.yml Normal file
View file

@ -0,0 +1,24 @@
rules:
color-literals: 2
final-newline: 2
hex-notation: 2
indentation:
- 2
-
size: 4
leading-zero: 2
nesting-depth:
- 1
-
max-depth: 4
no-css-comments: 0
no-ids: 0
property-sort-order:
- 2
-
order: concentric
quotes:
- 2
-
style: double
zero-unit: 2

View file

@ -7,3 +7,41 @@ cache:
notifications:
slack:
secure: ezESiG7JnuSLZc2/PPhOvWUv5BHBCr+g86MsuLLw+S+zz3DUfzWHMQ1g5tUvkeSDTPmfEIX41EnPkaoWtsD3OGO0PGXgseAfA8+6Z4N1rICNZZrhXZB2s6UdwRK1e+0Jol4W3kHmt96BHyN2scLNgJYeWMgSJllVsuPhMTlKBZIXI9u540NH8Nxjl3f2WvoIg64Q1mZvMxkpPbw4xssx6U4HSFE8kTTE6+EFsSxzombFX0cLGjPiJ9QZgGVUk4UkIjyiFLQQDfQlLllCaUpqJ9+qbuCNoMSKA2yty/qyZ8Y+r4OlMberjmBzR9GRLLyXWWcaAfMIgwlRhjtLYIDAUSsGM1iwUWCgyB9maG2IiXuYLSueuMx8DcDwbpUepoDgnqBYnM2AJmT8gcsxqlKYzJpYpHDgZgBlLZQgMXqjrVJHs/Tf9XVcLS6HAn1Ww0OOT01jThfy4gClpAuqLayYexsXOoL+RaFg25E2NzuTtaFWgRfWZgcAeqYNDiUzwun2D4vZ5I+NtdRP0gzpbG2fxhFz05vAqyf1Kp6ZYb17Li3A38dIm6Lsvv3qawAIAgNaZpIZX3f89+uq6jHU8kJy1Iv823JK2Xac3vEz3SHUKJnuXFF0LO07om9AcNEXhP/JrJ617S8nfvDtZRJODMFhz8qQwie+65Ql1I871goBpVs=
env:
global:
- secure: kXRyOECCfmTmIyibSKyHFz9cC5YGDsLIZJyiSpepvjRvuuJErxpD3yokp++JCJXdj/CRfJKazeMPkgek78zGiI/xnHR2aVxuQraU5ELIVNBCaFDtM4lKxtTVvEAtErwhWrH9zMiJkgXGF/MwID0QgZVlD/hpKI3MoS8sS1dmvDlqlregTvUZWBnlqMnrQuOXFNLPT+/QPgO6myd+nJn+XogSw8HceUo76cOADBphLtxFvE+R3FEbkHOwgJzUR3p8FstNXjmXZocSAYlGEgf1QIAN7M+3fH3wBHUBL2XELlr3w6eFr0qPCT5GCIxc4DNYsNt1360nmhSUqcm+k30HcbGmM5oWiRTmo2NrNpKhCUyF9wKHKmS4JYqGBEBjLxkTZe/zCv18gNVy0s9x/IXP3qP9SoRnlNEt9H6MjaxBc3lWD47UmcDJoLrs7OUdM4HDxgmPJyTzJsg059GEWgHuEMGIGGCYBGdpNlu4ZH6yEgsji73+kAkYbnVzhz4QtfhGNgQv3kEhTmDHW5muca5EMuSyOLW5v4ffpLJgirJQi9lvjZ/pZ+XJU0DSfIHdViqop6hRrsPxo2ewle3RcZrlQuw7lJJp9IoDT5Ku2PU1m8+705CR8S96DrMP8UtbC1Plcv91MMGmwgPwYAQwEcTnj7Fsq9QKReus+CTUXYqaMQM=
- secure: D7TVtzhDPvSsipXB9jiokA00rUAENWjK5Lrv+JOgdNe35D9j3tSSgT+iKj2Et5LcSeKXpvC2gMDXakHMflo2tT8uYPx7NI6J9XZMro2VP+ebTHlG57Fuoves9XxwCvHDFk2yW/K7uma8a92rs4PNydJRB6SPm3pWjL29Ih2n9ZFy37ZHCdL26R61EJ9SZh5siOVuXhqB36mu0Z9ANjXeXcLrKzpRf8mmORsK9NT/0A9kg4a0Q9ZKiHhUp3Wh3VKfDlDvqYszdofBNSpUGSyj/J4IlpYld8q+o+husxr3yLbV+FR1xdJ8NS04iXEmd1yOhJKy7ienpNQJ7NLwSOgDQ+Y8VZJUf0ZvSX/acHqNFQC86tW15KTAEVfnY8Js7mqmZrsWoY6+jzC8RyaQoZiD1HQfJLHkG+uqrfPYhWy1BNz+4QtBwnnQO+E/B2CM+fGAmjoJ+UjquWQo+sHWwoatNrG85JumA3GsA1FSlkzEVy3AAcST/CFZ1IyGCDVTar++2VwYCH691DuJy1gyeqSukSbRQIhGTSktArv0FjIiVsoMTCB/Ntg8HcfL6ADTfsijZVL9v5hN2VUXg3BjuF4TEBsrN78WMNI+U3g1+W1UW+036eP09Z7QDxIvLoQdIaQncGBny2KnR2j/Gmgz9eG0eg4dlV+2W+9DqE4y+tmU4Jw=
- EB_REGION=us-east-1
- EB_APP=scratch-www
- EB_AWS_BUCKET_NAME=elasticbeanstalk-us-east-1-307680192167
- SKIP_CLEANUP=true
- BUILD_ARCHIVE=$TRAVIS_BUILD_ID.zip
before_deploy:
- zip -r $BUILD_ARCHIVE .
deploy:
- provider: elasticbeanstalk
access_key_id: $EB_AWS_ACCESS_KEY_ID
secret_access_key: $EB_AWS_SECRET_ACCESS_KEY
bucket_name: $EB_AWS_BUCKET_NAME
bucket_path: $EB_APP
zip_file: $BUILD_ARCHIVE
skip_cleanup: $SKIP_CLEANUP
region: $EB_REGION
app: $EB_APP
env: scratch-www-staging
on:
repo: LLK/scratch-www
branch: develop
- provider: elasticbeanstalk
access_key_id: $EB_AWS_ACCESS_KEY_ID
secret_access_key: $EB_AWS_SECRET_ACCESS_KEY
bucket_name: $EB_AWS_BUCKET_NAME
bucket_path: $EB_APP
zip_file: $BUILD_ARCHIVE
skip_cleanup: $SKIP_CLEANUP
region: $EB_REGION
app: $EB_APP
env: scratch-www-production
on:
repo: LLK/scratch-www
branch: master

View file

@ -1,5 +1,6 @@
ESLINT=./node_modules/.bin/eslint
NODE=node
SASSLINT=./node_modules/.bin/sass-lint -v
WATCH=./node_modules/.bin/watch
WEBPACK=./node_modules/.bin/webpack
@ -8,17 +9,22 @@ WEBPACK=./node_modules/.bin/webpack
build:
@make clean
@make static
@make translations
@make webpack
clean:
rm -rf ./build
mkdir -p build
mkdir -p locales
static:
cp -a ./static/. ./build/
translations:
./src/scripts/build-locales locales/translations.json
webpack:
$(WEBPACK)
$(WEBPACK) --bail
# ------------------------------------
@ -28,30 +34,31 @@ watch:
wait
stop:
pkill -f "node $(WEBPACK) -d --watch"
pkill -f "node $(WATCH) make clean && make static ./static"
-pkill -f "$(WEBPACK) -d --watch"
-pkill -f "$(WATCH) make clean && make static ./static"
-pkill -f "$(NODE) ./server/index.js"
start:
$(NODE) ./server/index.js
# ------------------------------------
nginx_conf:
node server/nginx.js
# ------------------------------------
test:
@make lint
@make build
lint:
$(ESLINT) ./*.js
$(ESLINT) ./server/*.js
$(ESLINT) ./src/*.js
$(ESLINT) ./src/*.jsx
$(ESLINT) ./src/mixins/*.jsx
$(ESLINT) ./src/views/**/*.jsx
$(ESLINT) ./src/components/**/*.jsx
$(SASSLINT) ./src/*.scss
$(SASSLINT) ./src/views/**/*.scss
$(SASSLINT) ./src/components/**/*.scss
# ------------------------------------
.PHONY: build clean static webpack watch stop start nginx_conf test lint
.PHONY: build clean static translations webpack watch stop start test lint

View file

@ -16,7 +16,29 @@ During development, you can use `npm run watch` to cause any update you make to
npm start
```
Once running, open `http://localhost:8888` in your browser. If you wish to have the server reload automatically, you can install either [nodemon](https://github.com/remy/nodemon) or [forever](https://github.com/foreverjs/forever).
or to start and watch at once
```bash
npm run dev
```
Once running, open `http://localhost:8333` in your browser. If you wish to have the server reload automatically, you can install either [nodemon](https://github.com/remy/nodemon) or [forever](https://github.com/foreverjs/forever).
### To stop
```bash
# Stops all `start` and `watch` processes
npm stop
```
#### Configuration
`npm start` and `npm run watch` can be configured with the following environment variables
| Variable | Default | Description |
| ------------- | ------------------------------------- | ---------------------------------------------- |
| `API_HOST` | `https://api-staging.scratch.mit.edu` | Hostname for API requests |
| `NODE_ENV` | `null` | If not `production`, app acts like development |
| `PORT` | `8333` | Port for devserver (http://localhost:XXXX) |
| `PROXY_HOST` | `https://staging.scratch.mit.edu` | Pass-through location for scratchr2 |
### To Test
```bash

69
en.json Normal file
View file

@ -0,0 +1,69 @@
{
"general.accountSettings": "Account settings",
"general.about": "About",
"general.donate": "Donate",
"general.community": "Community",
"general.contactUs": "Contact Us",
"general.copyright": "Scratch is a project of the Lifelong Kindergarten Group at the MIT Media Lab",
"general.create": "Create",
"general.credits": "Credits",
"general.discuss": "Discuss",
"general.dmca": "DMCA",
"general.explore": "Explore",
"general.faq": "FAQ",
"general.forParents": "For Parents",
"general.forEducators": "For Educators",
"general.guidelines": "Community Guidelines",
"general.jobs": "Jobs",
"general.joinScratch": "Join Scratch",
"general.legal": "Legal",
"general.learnMore": "Learn More",
"general.messages": "Messages",
"general.myStuff": "My Stuff",
"general.offlineEditor": "Offline Editor",
"general.profile": "Profile",
"general.scratchConference": "Scratch Conference",
"general.scratchday": "Scratch Day",
"general.scratchEd": "ScratchEd",
"general.scratchFoundation": "Scratch Foundation",
"general.scratchJr": "ScratchJr",
"general.signIn": "Sign in",
"general.statistics": "Statistics",
"general.support": "Support",
"general.termsOfUse": "Terms of Use",
"general.username": "Username",
"general.whatsHappening": "What's Happening?",
"general.wiki": "Scratch Wiki",
"footer.about": "About Scratch",
"footer.discuss": "Discussion Forums",
"footer.help": "Help Page",
"footer.scratchFamily": "Scratch Family",
"intro.aboutScratch": "ABOUT SCRATCH",
"intro.forEducators": "FOR EDUCATORS",
"infro.forParents": "FOR PARENTS",
"intro.joinScratch": "JOIN SCRATCH",
"intro.seeExamples": "SEE EXAMPLES",
"intro.tagLine": "Create stories, games, and animations<br /> Share with others around the world",
"intro.tryItOut": "TRY IT OUT",
"login.forgotPassword": "Forgot Password?",
"navigation.signOut": "Sign out",
"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.",
"splash.featuredProjects": "Featured Projects",
"splash.featuredStudios": "Featured Studios",
"splash.projectsCuratedBy": "Projects Curated by",
"splash.scratchDesignStudioTitle": "Scratch Design Studio",
"splash.visitTheStudio": "Visit the studio",
"splash.recentlySharedProjects": "Recently Shared Projects",
"splash.projectsByScratchersFollowing": "Projects by Scratchers I'm Following",
"splash.projectsLovedByScratchersFollowing": "Projects Loved by Scratchers I'm Following",
"splash.projectsInStudiosFollowing": "Projects in Studios I'm Following",
"splash.communityRemixing": "What the Community is Remixing",
"splash.communityLoving": "What the Community is Loving",
"welcome.welcomeToScratch": "Welcome to Scratch!",
"welcome.learn": "Learn how to make a project in Scratch",
"welcome.tryOut": "Try out starter projects",
"welcome.connect": "Connect with other Scratchers"
}

72
languages.json Normal file
View file

@ -0,0 +1,72 @@
{
"en": "English",
"an": "Aragonés",
"ast": "Asturianu",
"id": "Bahasa Indonesia",
"ms": "Bahasa Melayu",
"ca": "Català",
"cs": "Česky",
"cy": "Cymraeg",
"da": "Dansk",
"fa-af": "Dari",
"de": "Deutsch",
"yum": "Edible Scratch",
"et": "Eesti",
"eo": "Esperanto",
"es": "Español",
"eu": "Euskara",
"fr": "Français",
"fr-ca": "Français (Canada)",
"ga": "Gaeilge",
"gd": "Gàidhlig",
"gl": "Galego",
"hr": "Hrvatski",
"is": "Íslenska",
"it": "Italiano",
"rw": "Kinyarwanda",
"ku": "Kurdî",
"la": "Latina",
"lv": "Latviešu",
"lt": "Lietuvių",
"hu": "Magyar",
"mt": "Malti",
"cat": "Meow",
"nl": "Nederlands",
"nb": "Norsk Bokmål",
"pl": "Polski",
"pt": "Português",
"pt-br": "Português Brasileiro",
"ro": "Română",
"sc": "Sardu",
"sk": "Slovenčina",
"sl": "Slovenščina",
"fi": "suomi",
"sv": "Svenska",
"nai": "Tepehuan",
"vi": "Tiếng Việt",
"tr": "Türkçe",
"ab": "Аҧсшәа",
"ar": "العربية",
"bg": "Български",
"el": "Ελληνικά",
"fa": "فارسی",
"he": "עִבְרִית",
"hi": "हिन्दी",
"hy": "Հայերեն",
"ja": "日本語",
"ja-hr": "にほんご",
"km": "សំលៀកបំពាក",
"kn": "ಭಾಷೆ-ಹೆಸರು",
"ko": "한국어",
"mk": "Македонски",
"ml": "മലയാളം",
"mn": "Монгол хэл",
"mr": "मराठी",
"my": "မြန်မာဘာသာ",
"ru": "Русский",
"sr": "Српски",
"th": "ไทย",
"uk": "Українська",
"zh-cn": "简体中文",
"zh-tw": "正體中文"
}

View file

@ -4,11 +4,11 @@
"description": "Standalone WWW client for Scratch",
"scripts": {
"start": "make start",
"stop": "make stop",
"test": "make test",
"watch": "make watch",
"stop-watch": "make stop-watch",
"build": "make build",
"prestart": "make build"
"dev": "make watch && make start &"
},
"repository": {
"type": "git",
@ -21,34 +21,53 @@
},
"homepage": "https://github.com/llk/scratch-www#readme",
"dependencies": {
"bunyan": "1.4.0",
"bunyan": "1.5.1",
"compression": "1.5.2",
"express": "4.13.3",
"express-http-proxy": "0.6.0",
"lodash.defaults": "3.1.2",
"mustache": "2.1.3"
"mustache": "2.1.3",
"newrelic": "1.22.1",
"raven": "0.8.1"
},
"devDependencies": {
"autoprefixer-loader": "2.1.0",
"classnames": "2.1.3",
"cookie": "0.2.2",
"css-loader": "0.17.0",
"eslint": "1.3.1",
"eslint-plugin-react": "3.3.1",
"exenv": "1.2.0",
"file-loader": "0.8.4",
"glob": "5.0.15",
"json-loader": "0.5.2",
"json2po-stream": "1.0.3",
"jsx-loader": "0.13.2",
"node-sass": "3.3.2",
"react": "0.13.3",
"react-modal": "0.3.0",
"lodash.clone": "3.0.3",
"lodash.defaultsdeep": "3.10.0",
"lodash.omit": "3.1.0",
"minilog": "2.0.8",
"node-sass": "3.3.3",
"po2icu": "git://github.com/LLK/po2icu.git#develop",
"react": "0.14.0",
"react-addons-test-utils": "0.14.0",
"react-dom": "0.14.0",
"react-intl": "2.0.0-pr-3",
"react-modal": "git://github.com/mewtaylor/react-modal.git#react-14",
"react-onclickoutside": "0.3.1",
"react-slick": "0.7.0",
"react-slick": "git://github.com/mewtaylor/react-slick.git#remove-deprecation-warnings",
"routes-to-nginx-conf": "0.0.4",
"sass-lint": "1.2.0",
"sass-loader": "2.0.1",
"scratchr2_translations": "git://github.com/mewtaylor/scratchr2_translations.git#feature/packageify",
"slick-carousel": "1.5.8",
"source-map-support": "0.3.2",
"style-loader": "0.12.3",
"tap": "2.0.0",
"tape": "4.2.0",
"url-loader": "0.5.6",
"watch": "0.16.0",
"webpack": "1.12.0",
"webpack": "1.12.2",
"xhr": "2.0.4"
}
}

17
server/config.js Normal file
View file

@ -0,0 +1,17 @@
module.exports = {
// Search and metadata
title: 'Imagine, Program, Share',
description:
'Scratch is a free programming language and online community ' +
'where you can create your own interactive stories, games, ' +
'and animations.',
// Open graph
og_image: 'https://scratch.mit.edu/images/og_image.jpg',
og_image_type: 'image/jpeg',
og_image_width: 986,
og_image_height: 860,
// Analytics & Monitoring
ga_tracker: process.env.GA_TRACKER || ''
};

View file

@ -1,4 +0,0 @@
{
"title": "Scratch - Imagine, Program, Share",
"description": "Scratch is a free programming language and online community where you can create your own interactive stories, games, and animations."
}

View file

@ -4,12 +4,14 @@ var fs = require('fs');
var mustache = require('mustache');
var path = require('path');
var config = require('./config');
/**
* Constructor
*/
function Handler (route) {
// Route definition
defaults(route, require('./defaults.json'));
defaults(route, config);
// Render template
var location = path.resolve(__dirname, './template.html');

View file

@ -1,9 +1,16 @@
if (typeof process.env.NEW_RELIC_LICENSE_KEY === 'string') {
require('newrelic');
}
var compression = require('compression');
var express = require('express');
var _path = require('path');
var path = require('path');
var proxy = require('express-http-proxy');
var url = require('url');
var handler = require('./handler');
var log = require('./log');
var proxies = require('./proxies.json');
var routes = require('./routes.json');
// Server setup
@ -11,19 +18,61 @@ var app = express();
app.disable('x-powered-by');
app.use(log());
app.use(compression());
app.use(express.static(path.resolve(__dirname, '../build'), {
lastModified: true,
maxAge: '1y'
}));
app.use(function (req, res, next) {
req._path = url.parse(req.url).path;
next();
});
// Bind routes
for (var item in routes) {
var route = routes[item];
if ( route.static ) {
app.use( express.static( eval( route.resolve ), route.attributes ) );
} else {
app.get(route.pattern, handler(route));
}
for (var routeId in routes) {
var route = routes[routeId];
app.get(route.pattern, handler(route));
}
if (typeof process.env.SENTRY_DSN === 'string') {
var raven = require('raven');
app.get('/sentrythrow', function mainHandler () { throw new Error('Sentry Test'); });
// These handlers must be applied _AFTER_ other routes have been applied
app.use(raven.middleware.express.requestHandler(process.env.SENTRY_DSN));
app.use(raven.middleware.express.errorHandler(process.env.SENTRY_DSN));
app.use(function errorHandler (err, req, res, next) {
res.append('X-Sentry-ID:' + res.sentry);
res.status(500);
next(err);
});
raven.patchGlobal(process.env.SENTRY_DSN, function () {
process.exit(-1);
});
}
// Bind proxies in development
if (process.env.NODE_ENV !== 'production') {
var proxyHost = process.env.PROXY_HOST || 'https://staging.scratch.mit.edu';
app.use('/', proxy(proxyHost, {
filter: function (req) {
for (var i in proxies) {
if (req._path.indexOf(proxies[i]) === 0) return true;
}
return false;
},
forwardPath: function (req) {
return req._path;
}
}));
}
// Start listening
var port = process.env.PORT || 8888;
var port = process.env.PORT || 8333;
app.listen(port, function () {
process.stdout.write('Server listening on port ' + port + '\n');
if (proxyHost) {
process.stdout.write('Proxy host: ' + proxyHost + '\n');
}
});

View file

@ -1,5 +0,0 @@
var routes = require('./routes.json');
var nginx_conf = require('routes-to-nginx-conf');
nginx_conf.generate_nginx_conf( routes, function ( v ) { process.stdout.write(v); } );

12
server/proxies.json Normal file
View file

@ -0,0 +1,12 @@
[
"/accounts/",
"/csrf_token/",
"/fragment/",
"/get_image/",
"/i18n/setlang/",
"/login_retry/",
"/media/",
"/session/",
"/site-api",
"/static/"
]

View file

@ -2,23 +2,21 @@
{
"pattern": "/",
"view": "splash",
"static": false
"title": "Imagine, Program, Share"
},
{
"pattern": "/about",
"view": "about",
"title": "About",
"static": false
"title": "About"
},
{
"pattern": "/components",
"view": "components",
"static": false
"title": "Components"
},
{
"static": true,
"resolve": "_path.resolve(__dirname, '../build')",
"attributes": { "lastModified": true, "maxAge": "1y" },
"_todo": " TODO: Define a specification for how each entry is used/expected to look like, given the nginx conf generator's needs and stand-alone run-time needs. An outline of this so far: static requires resolve/attributes but could use pattern too. ..."
"pattern": "/hoc",
"view": "hoc",
"title": "Hour of Code"
}
]

View file

@ -6,23 +6,33 @@
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta http-equiv="x-frame-options" content="deny">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scratch - {{title}}</title>
<meta name="description" content="{{description}}" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<!-- Search & Open Graph-->
<meta name="description" content="{{description}}" />
<meta name="google-site-verification" content="m_3TAXDreGTFyoYnEmU9mcKB4Xtw5mw6yRkuJtXRKxM" />
<meta property="og:url" content="https://scratch.mit.edu/" />
<meta property="og:title" content="Scratch - Imagine, Program, Share" />
<meta property="og:description" content="Make games, stories and interactive art with Scratch. (scratch.mit.edu)" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Scratch - {{title}}" />
<meta property="og:description" content="{{description}}" />
<meta property="og:image" content="{{&og_image}}" />
<meta property="og:image:type" content="{{&og_image_type}}" />
<meta property="og:image:width" content="{{&og_image_width}}" />
<meta property="og:image:height" content="{{&og_image_height}}" />
<!-- Favicon & CSS normalize -->
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/lib/normalize.min.css" />
<!-- Shim/Sham ES5 polyfill for older browsers -->
<!-- Polyfill -->
<script src="/js/lib/polyfill.min.js"></script>
<!-- Initialize (Session & Localization) -->
<script src="/js/init.bundle.js"></script>
</head>
<body>
@ -32,11 +42,24 @@
<!-- Scripts -->
<script src="/js/lib/react.js"></script>
<script src="/js/lib/react-dom.js"></script>
<script src="/js/main.bundle.js"></script>
<script src="/js/{{view}}.bundle.js"></script>
<!-- @todo Analytics (GA) -->
<!-- @todo Monitoring (Raven) -->
<!-- Analytics (GA) -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{&ga_tracker}}', {
'sampleRate': 10
});
ga('send', 'pageview');
</script>
<!-- @todo New Relic -->
</body>
</html>

33
src/_colors.scss Normal file
View file

@ -0,0 +1,33 @@
/* UI Primary Colors */
$ui-blue: hsla(200, 90, 55, 1); // #25AFF4
$ui-orange: hsla(35, 90, 55, 1); // #F49D25
$ui-gray: hsla(0, 0, 95, 1); //#F2F2F2
$ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3
$background-color: hsla(0, 0, 99, 1); //#FDFDFD
/* UI Secondary Colors */
$ui-aqua: hsla(170, 70, 50, 1); //#26D9BB
$ui-white: #fff;
$ui-border: hsla(0, 0, 85, 1); //#D9D9D9
/* Overlay UI Gray Colors */
$active-gray: hsla(0, 0, 0, .1);
$active-dark-gray: hsla(0, 0, 0, .2);
$box-shadow-gray: hsla(0, 0, 0, .25);
$overlay-gray: hsla(0, 0, 0, .75);
/* Typography Colors */
$header-gray: hsla(0, 0, 42, 1); //#6B6B6B
$type-gray: hsla(0, 0, 42, 1); //#6B6B6B
$type-white: #fff;
$link-blue: $ui-blue;
/* Component colors */
$splash-green: #9c0;
$splash-pink: #c2479d;
$splash-blue: #199ed7;

View file

@ -1,57 +1,66 @@
[
{
"id": 5,
"author": {
"id": "lynxkitten101",
"username": "lynxkitten101",
"avatar": "https://cdn2.scratch.mit.edu/get_image/user/10122882_32x32.png?v=1438781835.82"
"obj_id": 82475328,
"datetime_created": "2015-10-20T15:13:36",
"actor": {
"username": "ceebee",
"pk": 2755634,
"thumbnail_url": "//cdn.scratch.mit.edu/static/site/users/avatars/275/5634.png",
"admin": true
},
"stamp": "2015-09-08T19:40:00Z",
"message": "became a curator of A bit of everything",
"url": "/studios/1511220/"
"pk": 186757838,
"message": "\nfavorited\n <a href=\"/projects/82475328/\">miner man</a>",
"extra_data": {
"project_title": "miner man"
},
"type": 3
},
{
"id": 4,
"author": {
"id": "sheep_tester",
"username": "Sheep_tester",
"avatar": "https://cdn2.scratch.mit.edu/get_image/user/3290075_32x32.png?v=1439575340.77"
"obj_id": 82475328,
"datetime_created": "2015-10-20T15:13:36",
"actor": {
"username": "ceebee",
"pk": 2755634,
"thumbnail_url": "//cdn.scratch.mit.edu/static/site/users/avatars/275/5634.png",
"admin": true
},
"stamp": "2015-09-08T03:20:00Z",
"message": "became a curator of To All My Great Followers & People",
"url": "/studios/1511360/"
"pk": 186757836,
"message": "\nloved\n <a href=\"/projects/82475328/\">miner man</a>",
"extra_data": {
"project_title": "miner man"
},
"type": 2
},
{
"id": 3,
"author": {
"id": "-vexvorlo-",
"username": "-VexVorlo-",
"avatar": "https://cdn2.scratch.mit.edu/get_image/user/10930092_32x32.png?v=1439735067.99"
"obj_id": 82475328,
"datetime_created": "2015-10-20T15:12:39",
"actor": {
"username": "speakvisually",
"pk": 3484484,
"thumbnail_url": "//cdn.scratch.mit.edu/static/site/users/avatars/348/4484.png",
"admin": true
},
"stamp": "2015-09-01T03:20:00Z",
"message": "loved Virtual Chicken Coop",
"url": "/projects/73311484/"
"pk": 186757510,
"message": "\nfavorited\n <a href=\"/projects/82475328/\">miner man</a>",
"extra_data": {
"project_title": "miner man"
},
"type": 3
},
{
"id": 2,
"author": {
"id": "-vexvorlo-",
"username": "-VexVorlo-",
"avatar": "https://cdn2.scratch.mit.edu/get_image/user/10930092_32x32.png?v=1439735067.99"
"obj_id": 82475328,
"datetime_created": "2015-10-20T15:12:37",
"actor": {
"username": "speakvisually",
"pk": 3484484,
"thumbnail_url": "//cdn.scratch.mit.edu/static/site/users/avatars/348/4484.png",
"admin": true
},
"stamp": "2015-08-22T02:00:00Z",
"message": "loved Bread bread bread..",
"url": "/projects/75677832/"
},
{
"id": 1,
"author": {
"id": "getbent",
"username": "getbent",
"avatar": "https://cdn2.scratch.mit.edu/get_image/user/676422_32x32.png?v=1436728562.25"
"pk": 186757500,
"message": "\nloved\n <a href=\"/projects/82475328/\">miner man</a>",
"extra_data": {
"project_title": "miner man"
},
"stamp": "2014-09-08T00:00:00Z",
"message": "loved Is ______ An Instrurment?",
"url": "/projects/75347968/"
"type": 2
}
]

View file

@ -1,11 +1,23 @@
var React = require('react');
var ReactIntl = require('react-intl');
var defineMessages = ReactIntl.defineMessages;
var FormattedMessage = ReactIntl.FormattedMessage;
var FormattedRelative = ReactIntl.FormattedRelative;
var injectIntl = ReactIntl.injectIntl;
var Box = require('../box/box.jsx');
var Format = require('../../lib/format.js');
require('./activity.scss');
module.exports = React.createClass({
var defaultMessages = defineMessages({
whatsHappening: {
id: 'general.whatsHappening',
defaultMessage: 'What\'s Happening?'
}
});
var Activity = React.createClass({
type: 'Activity',
propTypes: {
items: React.PropTypes.array
},
@ -15,25 +27,55 @@ module.exports = React.createClass({
};
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
return (
<Box
className="activity"
title="What's Happening?">
title={formatMessage(defaultMessages.whatsHappening)}>
<ul>
{this.props.items.map(function (item) {
return (
<li key={item.id}>
<a href={item.url}>
<img src={item.author.avatar} width="34" height="34" />
<p>{item.author.username} {item.message}</p>
<p><span className="stamp">{Format.date(item.stamp)}</span></p>
</a>
</li>
);
})}
</ul>
{this.props.items && this.props.items.length > 0 ? [
<ul>
{this.props.items.map(function (item) {
if (item.message.replace(/\s/g, '')) {
var actorProfileUrl = '/users/' + item.actor.username + '/';
var actionDate = new Date(item.datetime_created + 'Z');
var activityMessageHTML = (
'<a href=' + actorProfileUrl + '>' + item.actor.username + '</a>' +
item.message
);
return (
<li key={item.pk}>
<a href={actorProfileUrl}>
<img src={item.actor.thumbnail_url} width="34" height="34" />
<p dangerouslySetInnerHTML={{__html: activityMessageHTML}}></p>
<p>
<span className="stamp">
<FormattedRelative value={actionDate} />
</span>
</p>
</a>
</li>
);
}
})}
</ul>
] : [
<div className="empty">
<h4>
<FormattedMessage
id="activity.seeUpdates"
defaultMessage="This is where you will see updates from Scratchers you follow" />
</h4>
<a href="/studios/146521/">
<FormattedMessage
id="activity.checkOutScratchers"
defaultMessage="Check out some Scratchers you might like to follow" />
</a>
</div>
]}
</Box>
);
}
});
module.exports = injectIntl(Activity);

View file

@ -1,3 +1,5 @@
@import "../../colors";
.activity {
ul {
display: block;
@ -29,14 +31,14 @@
margin: 0;
padding: 0;
font-size: 0.9rem;
white-space: nowrap;
font-size: .9rem;
overflow-x: hidden;
}
.stamp {
color: #999;
font-size: 0.65rem;
color: $ui-dark-gray;
font-size: .65rem;
}
}
}

View file

@ -0,0 +1,81 @@
var React = require('react');
var Button = require('../../components/forms/button.jsx');
var Session = require('../../mixins/session.jsx');
require('./adminpanel.scss');
var AdminPanel = React.createClass({
type: 'AdminPanel',
mixins: [
Session
],
getInitialState: function () {
return {
showPanel: false
};
},
handleToggleVisibility: function (e) {
e.preventDefault();
this.setState({showPanel: !this.state.showPanel});
},
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.state.session.user) {
showAdmin = this.state.session.permissions.admin;
}
if (!showAdmin) return false;
if (this.state.showPanel) {
return (
<div id="admin-panel" className="visible">
<span
className="toggle"
onClick={this.handleToggleVisibility}>
x
</span>
<div className="admin-header">
<h3>Admin Panel</h3>
</div>
<div className="admin-content">
<dl>
{this.props.children}
<dt>Page Cache</dt>
<dd>
<ul className="cache-list">
<li>
<form method="post" action="/scratch_admin/page/clear-anon-cache/">
<input type="hidden" name="path" value="/" />
<div className="button-row">
<span>For anonymous users:</span>
<Button type="submit">
<span>Clear</span>
</Button>
</div>
</form>
</li>
</ul>
</dd>
</dl>
</div>
</div>
);
} else {
return (
<div id="admin-panel" className="hidden">
<span
className="toggle"
onClick={this.handleToggleVisibility}>
&gt;
</span>
</div>
);
}
}
});
module.exports = AdminPanel;

View file

@ -0,0 +1,68 @@
@import "../../colors";
#admin-panel {
position: fixed;
top: 0;
left: 0;
z-index: 99;
border: 1px solid $ui-gray;
box-shadow: 0 2px 5px $box-shadow-gray;
background-color: $ui-gray;
padding: 1rem;
height: 100%;
overflow: scroll;
text-shadow: none;
&.visible {
width: 20%;
min-width: 180px;
max-width: 230px;
}
&.hidden {
width: 10px;
}
.toggle {
float: right;
cursor: pointer;
}
.admin-content {
dl {
list-style: none;
dt {
margin: 2rem 0 1rem 0;
border-bottom: 1px solid $ui-dark-gray;
font-size: large;
font-weight: 700;
}
dd {
margin-left: 0;
}
}
ul {
padding: 0;
li {
margin: 0;
list-style: none;
.button-row {
display: flex;
font-size: small;
align-items: center;
justify-content: space-between;
.button {
background-color: $ui-dark-gray;
padding: .5rem 1rem;
}
}
}
}
}
}

View file

@ -0,0 +1,23 @@
var React = require('react');
var classNames = require('classnames');
var Avatar = React.createClass({
type: 'Avatar',
propTypes: {
src: React.PropTypes.string
},
getDefaultProps: function () {
return {
src: '//cdn2.scratch.mit.edu/get_image/user/2584924_24x24.png?v=1438702210.96'
};
},
render: function () {
var classes = classNames(
'avatar',
this.props.className
);
return <img {... this.props} className={classes} />;
}
});
module.exports = Avatar;

View file

@ -0,0 +1,3 @@
.avatar {
border: 1px solid $ui-border;
}

View file

@ -0,0 +1,29 @@
var classNames = require('classnames');
var React = require('react');
require('./banner.scss');
var Banner = React.createClass({
type: 'Banner',
propTypes: {
onRequestDismiss: React.PropTypes.func
},
render: function () {
var classes = classNames(
'banner',
this.props.className
);
return (
<div className={classes}>
<div className="inner">
{this.props.children}
{this.props.onRequestDismiss ? [
<a className="close" key="close" href="#" onClick={this.props.onRequestDismiss}>x</a>
] : []}
</div>
</div>
);
}
});
module.exports = Banner;

View file

@ -0,0 +1,41 @@
@import "../../colors";
$navigation-height: 50px;
.banner {
position: fixed;
top: $navigation-height;
z-index: 99;
box-shadow: 0 1px 1px $ui-dark-gray;
background-color: $ui-orange;
width: 100%;
overflow: hidden;
text-align: center;
line-height: $navigation-height;
&, a {
color: $ui-white;
}
a {
text-decoration: underline;
}
.close {
float: right;
margin-top: $navigation-height/4;
border-radius: $navigation-height/4;
background-color: $box-shadow-gray;
width: $navigation-height/2;
height: $navigation-height/2;
text-decoration: none;
text-shadow: none;
line-height: $navigation-height/2;
color: $ui-white;
font-weight: normal;
}
&.warning {
background-color: $ui-orange;
}
}

View file

@ -1,20 +1,27 @@
var classNames = require('classnames');
var React = require('react');
require('./box.scss');
module.exports = React.createClass({
var Box = React.createClass({
type: 'Box',
propTypes: {
title: React.PropTypes.string.isRequired,
moreTitle: React.PropTypes.string,
moreHref: React.PropTypes.string
moreHref: React.PropTypes.string,
moreProps: React.PropTypes.object
},
render: function () {
var classes = classNames(
'box',
this.props.className
);
return (
<div className={'box ' + this.props.className}>
<div className={classes}>
<div className="box-header">
<h4>{this.props.title}</h4>
<p>
<a href={this.props.moreHref}>
<a href={this.props.moreHref} {...this.props.moreProps}>
{this.props.moreTitle}
</a>
</p>
@ -27,3 +34,5 @@ module.exports = React.createClass({
);
}
});
module.exports = Box;

View file

@ -1,23 +1,26 @@
@import "../../colors";
$base-bg: $ui-white;
.box {
display: inline-block;
width: 100%;
border: 1px solid #e0e0e0;
border: 1px solid $ui-border;
border-radius: 10px 10px 0 0;
box-shadow: 0 2px 3px rgba(34, 25, 25, 0.3);
background-color: $ui-white;
width: 100%;
.box-header {
display: block;
margin: 0;
height: 20px;
padding: 8px 20px;
clear: both;
overflow: hidden;
background-color: #efefef;
margin: 0;
border-top: 1px solid $ui-white;
border-bottom: 1px solid $ui-border;
border-radius: 10px 10px 0 0;
border-top: 1px solid white;
border-bottom: 1px solid #ccc;
background-color: $ui-gray;
padding: 8px 20px;
height: 20px;
overflow: hidden;
h4 {
display: inline-block;
@ -32,15 +35,18 @@
margin: 1px 0 0 0;
padding: 0;
font-size: 0.85rem;
font-size: .85rem;
}
}
.box-content {
display: block;
padding: 8px 20px;
clear: both;
background-color: $base-bg;
padding: 8px 20px;
}
background-color: white;
.empty {
margin-top: 20px;
}
}

View file

@ -2,48 +2,49 @@
{
"id": 1,
"type": "project",
"title": "Example Project",
"thumbnailUrl": "http://www.lorempixel.com/144/108/",
"creator": "raimondious",
"href": "/projects/1000/"
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 2,
"type": "project",
"title": "Example Project",
"thumbnailUrl": "http://www.lorempixel.com/144/108/",
"href": "/projects/1000/"
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 3,
"type": "project",
"title": "Example Project",
"thumbnailUrl": "http://www.lorempixel.com/144/108/",
"creator": "raimondious",
"href": "/projects/1000/"
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 4,
"type": "project",
"title": "Example Project",
"thumbnailUrl": "http://www.lorempixel.com/144/108/",
"creator": "raimondious",
"href": "/projects/1000/"
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 5,
"type": "project",
"title": "Example Project",
"thumbnailUrl": "http://www.lorempixel.com/144/108/",
"creator": "raimondious",
"href": "/projects/1000/"
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
},
{
"id": 6,
"type": "project",
"title": "Example Project",
"thumbnailUrl": "http://www.lorempixel.com/144/108/",
"creator": "raimondious",
"href": "/projects/1000/"
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
}
]

View file

@ -1,42 +1,72 @@
var classNames = require('classnames');
var defaults = require('lodash.defaults');
var React = require('react');
var Slider = require('react-slick');
var Thumbnail = require('../thumbnail/thumbnail.jsx');
require('slick-carousel/slick/slick.scss');
require('slick-carousel/slick/slick-theme.scss');
require('./carousel.scss');
module.exports = React.createClass({
var Carousel = React.createClass({
type: 'Carousel',
propTypes: {
items: React.PropTypes.array
},
getDefaultProps: function () {
return {
items: require('./carousel.json'),
settings: {
arrows: true,
dots: false,
infinite: false,
lazyLoad: true,
slidesToShow: 5,
slidesToScroll: 5,
variableWidth: true
}
showRemixes: false,
showLoves: false
};
},
render: function () {
var settings = this.props.settings || {};
defaults(settings, {
dots: false,
infinite: false,
lazyLoad: true,
slidesToShow: 5,
slidesToScroll: 5,
variableWidth: true
});
var arrows = this.props.items.length > settings.slidesToShow;
var classes = classNames(
'carousel',
this.props.className
);
return (
<Slider className={'carousel ' + this.props.className} {... this.props.settings}>
<Slider className={classes} arrows={arrows} {... settings}>
{this.props.items.map(function (item) {
var href = '';
switch (item.type) {
case 'gallery':
href = '/studios/' + item.id + '/';
break;
case 'project':
href = '/projects/' + item.id + '/';
break;
default:
href = '/' + item.type + '/' + item.id + '/';
}
return (
<Thumbnail key={item.id}
href={item.href}
showLoves={this.props.showLoves}
showRemixes={this.props.showRemixes}
type={item.type}
href={href}
title={item.title}
src={item.thumbnailUrl}
extra={item.creator ? 'by ' + item.creator:null} />
src={item.thumbnail_url}
creator={item.creator}
remixes={item.remixers_count}
loves={item.love_count} />
);
})}
}.bind(this))}
</Slider>
);
}
});
module.exports = Carousel;

View file

@ -1,25 +1,34 @@
@import "../../colors";
.carousel {
$icon-size: 40px;
$button-offset: $icon-size + 5px;
$box-content-offset: 20px;
padding: 0 $button-offset;
padding: 12px $button-offset;
.box-content & {
padding: 0 $button-offset - 20px;
padding: 12px $button-offset - 20px;
}
.slick-next, .slick-prev {
.slick-next,
.slick-prev {
margin-top: -$icon-size/2;
width: $icon-size;
height: $icon-size;
margin-top: -$icon-size/2;
&:before {
display: block;
background-repeat: no-repeat;
background-position: center center;
background-size: 70%;
width: $icon-size;
height: $icon-size;
font-size: $icon-size;
color: #ddd;
content: "";
}
&.slick-disabled:before{
&.slick-disabled:before {
opacity: 1;
}
}
@ -27,6 +36,16 @@
.slick-prev {
left: 0;
&:before {
background-image: url("/svgs/carousel/prev_ui-dark-gray.svg");
}
&:hover:before {
background-image: url("/svgs/carousel/prev_ui-blue.svg");
background-size: 90%;
}
.box-content & {
left: -$box-content-offset;
}
@ -35,6 +54,15 @@
.slick-next {
right: 0;
&:before {
background-image: url("/svgs/carousel/next_ui-dark-gray.svg");
}
&:hover:before {
background-image: url("/svgs/carousel/next_ui-blue.svg");
background-size: 90%;
}
.box-content & {
right: -$box-content-offset;
}

View file

@ -1,60 +1,232 @@
var React = require('react');
var FormattedMessage = require('react-intl').FormattedMessage;
var LanguageChooser = require('../languagechooser/languagechooser.jsx');
require('./footer.scss');
module.exports = React.createClass({
var Footer = React.createClass({
type: 'Footer',
render: function () {
return (
<div className="inner">
<div className="lists">
<dl>
<dt>About</dt>
<dd><a href="/about/">About Scratch</a></dd>
<dd><a href="/parents/">For Parents</a></dd>
<dd><a href="/educators/">For Educators</a></dd>
<dd><a href="/info/credits/">Credits</a></dd>
<dd><a href="/jobs/">Jobs</a></dd>
<dd><a href="http://wiki.scratch.mit.edu/wiki/Scratch_Press">Press</a></dd>
<dt>
<FormattedMessage
id='general.about'
defaultMessage={'About'} />
</dt>
<dd>
<a href="/about/">
<FormattedMessage
id='footer.about'
defaultMessage={'About Scratch'} />
</a>
</dd>
<dd>
<a href="/parents/">
<FormattedMessage
id='general.forParents'
defaultMessage={'For Parents'} />
</a>
</dd>
<dd>
<a href="/educators/">
<FormattedMessage
id='general.forEducators'
defaultMessage={'For Educators'} />
</a>
</dd>
<dd>
<a href="/info/credits/">
<FormattedMessage
id='general.credits'
defaultMessage={'Credits'} />
</a>
</dd>
<dd>
<a href="/jobs/">
<FormattedMessage
id='general.jobs'
defaultMessage={'Jobs'} />
</a>
</dd>
<dd>
<a href="http://wiki.scratch.mit.edu/wiki/Scratch_Press">
<FormattedMessage
id='general.press'
defaultMessage={'Press'} />
</a>
</dd>
</dl>
<dl>
<dt>Community</dt>
<dd><a href="/community_guidelines/">Community Guidelines</a></dd>
<dd><a href="/discuss/">Discussion Forums</a></dd>
<dd><a href="https://wiki.scratch.mit.edu/">Scratch Wiki</a></dd>
<dd><a href="/statistics/">Statistics</a></dd>
<dt>
<FormattedMessage
id='general.communityHeader'
defaultMessage={'Community'} />
</dt>
<dd>
<a href="/community_guidelines/">
<FormattedMessage
id='general.guidelines'
defaultMessage={'Community Guidelines'} />
</a>
</dd>
<dd>
<a href="/discuss/">
<FormattedMessage
id='footer.discuss'
defaultMessage={'Discussion Forums'} />
</a>
</dd>
<dd>
<a href="https://wiki.scratch.mit.edu/">
<FormattedMessage
id='general.wiki'
defaultMessage={'Scratch Wiki'} />
</a>
</dd>
<dd>
<a href="/statistics/">
<FormattedMessage
id='general.statistics'
defaultMessage={'Statistics'} />
</a>
</dd>
</dl>
<dl>
<dt>Support</dt>
<dd><a href="/help/">Help Page</a></dd>
<dd><a href="/info/faq/">FAQ</a></dd>
<dd><a href="/scratch2download/">Offline Editor</a></dd>
<dd><a href="/contact-us/">Contact Us</a></dd>
<dd><a href="https://secure.donationpay.org/codetolearn/">Donate</a></dd>
<dt>
<FormattedMessage
id='general.support'
defaultMessage={'Support'} />
</dt>
<dd>
<a href="/help/">
<FormattedMessage
id='footer.help'
defaultMessage={'Help Page'} />
</a>
</dd>
<dd>
<a href="/info/faq/">
<FormattedMessage
id='general.faq'
defaultMessage={'FAQ'} />
</a>
</dd>
<dd>
<a href="/scratch2download/">
<FormattedMessage
id='general.offlineEditor'
defaultMessage={'Offline Editor'} />
</a>
</dd>
<dd>
<a href="/contact-us/">
<FormattedMessage
id='general.contactUs'
defaultMessage={'Contact Us'} />
</a>
</dd>
<dd>
<a href="https://secure.donationpay.org/codetolearn/">
<FormattedMessage
id='general.donate'
defaultMessage={'Donate'} />
</a>
</dd>
</dl>
<dl>
<dt>Legal</dt>
<dd><a href="/terms_of_use/">Terms of Use</a></dd>
<dd><a href="/privacy_policy/">Privacy Policy</a></dd>
<dd><a href="/DMCA/">DMCA</a></dd>
<dt>
<FormattedMessage
id='general.legal'
defaultMessage={'Legal'} />
</dt>
<dd>
<a href="/terms_of_use/">
<FormattedMessage
id='general.termsOfUse'
defaultMessage={'Terms of Use'} />
</a>
</dd>
<dd>
<a href="/privacy_policy/">
<FormattedMessage
id='privacyPolicy'
defaultMessage={'Privacy Policy'} />
</a>
</dd>
<dd>
<a href="/DMCA/">
<FormattedMessage
id='general.dmca'
defaultMessage={'DMCA'} />
</a>
</dd>
</dl>
<dl>
<dt>Scratch Family</dt>
<dd><a href="http://scratched.gse.harvard.edu/">ScratchEd</a></dd>
<dd><a href="http://www.scratchjr.org/">ScratchJr</a></dd>
<dd><a href="http://day.scratch.mit.edu/">Scratch Day</a></dd>
<dd><a href="/conference/">Scratch Conference</a></dd>
<dd><a href="http://www.scratchfoundation.org/">Scratch Foundation</a></dd>
<dt>
<FormattedMessage
id='footer.scratchFamily'
defaultMessage={'Scratch Family'} />
</dt>
<dd>
<a href="http://scratched.gse.harvard.edu/">
<FormattedMessage
id='general.scratchEd'
defaultMessage={'ScratchEd'} />
</a>
</dd>
<dd>
<a href="http://www.scratchjr.org/">
<FormattedMessage
id='general.scratchJr'
defaultMessage={'ScratchJr'} />
</a>
</dd>
<dd>
<a href="http://day.scratch.mit.edu/">
<FormattedMessage
id='general.scratchday'
defaultMessage={'Scratch Day'} />
</a>
</dd>
<dd>
<a href="/conference/">
<FormattedMessage
id='general.scratchConference'
defaultMessage={'Scratch Conference'} />
</a>
</dd>
<dd>
<a href="http://www.scratchfoundation.org/">
<FormattedMessage
id='general.scratchFoundation'
defaultMessage={'Scratch Foundation'} />
</a>
</dd>
</dl>
</div>
<LanguageChooser />
<div className="copyright">
<p>Scratch is a project of the Lifelong Kindergarten Group at the MIT Media Lab</p>
<p>
<FormattedMessage
id='general.copyright'
defaultMessage={
'Scratch is a project of the Lifelong Kindergarten Group at the MIT Media Lab'
} />
</p>
</div>
</div>
);
}
});
module.exports = Footer;

View file

@ -1,22 +1,24 @@
@import "../../colors";
#footer {
display: block;
background-color: $ui-gray;
padding: 10px 0;
color: #666;
background-color: #ececec;
color: $type-gray;
font-size: .85rem;
.lists {
display: flex;
text-align: center;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
text-align: center;
dl {
display: inline-block;
width: 130pt;
font-size: 0.85rem;
text-align: left;
vertical-align: top;
text-align: left;
}
dt {
@ -37,7 +39,11 @@
text-align: center;
p {
font-size: 0.7rem;
font-size: .7rem;
}
}
.language-chooser {
text-align: center;
}
}

View file

@ -3,7 +3,8 @@ var classNames = require('classnames');
require('./button.scss');
module.exports = React.createClass({
var Button = React.createClass({
type: 'Button',
propTypes: {
},
@ -17,3 +18,5 @@ module.exports = React.createClass({
);
}
});
module.exports = Button;

View file

@ -1,27 +1,35 @@
@import "../../colors";
$base-bg: $ui-white;
.button {
display: inline-block;
font-size: .8rem;
padding: .75em 1em;
margin: .5em 0;
background-color: #24a3ec;
color: white;
font-weight: bold;
border: 0;
border-radius: 5px;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25);
box-shadow: 0 1px 1px $box-shadow-gray;
background-color: $ui-blue;
cursor: pointer;
border: none;
padding: .75em 1em;
color: $type-white;
font-size: .8rem;
font-weight: bold;
&.white {
background-color: white;
border-top: 1px inset rgba(0, 0, 0, 0.1);
color: #24a3ec;
border-top: 1px inset $active-gray;
background-color: $base-bg;
color: $ui-blue;
}
&:hover {
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.25);
box-shadow: 0 2px 2px $box-shadow-gray;
}
&:active {
box-shadow: inset 0px 1px 2px rgba(0, 0, 0, 0.25);
box-shadow: inset 0 1px 2px $box-shadow-gray;
}
&:focus {
outline: none;
}
}

View file

@ -3,7 +3,8 @@ var classNames = require('classnames');
require('./input.scss');
module.exports = React.createClass({
var Input = React.createClass({
type: 'Input',
propTypes: {
},
@ -17,3 +18,5 @@ module.exports = React.createClass({
);
}
});
module.exports = Input;

View file

@ -1,28 +1,34 @@
@import "../../colors";
$base-bg: $ui-white;
$focus-bg: lighten($ui-blue, 35%);
$fail-bg: lighten($ui-orange, 35%);
$pass-bg: lighten($ui-aqua, 35%);
.input {
color:black;
border-radius: 5px;
border: 1px solid rgba(0, 0, 0, 0.1);
font-size: .8rem;
padding: .75em 1em;
transition: all 1s ease;
margin: .5em 0;
background-color: #f7f7f7;
transition:all 1s ease;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $base-bg;
padding: .75em 1em;
color: $type-gray;
font-size: .8rem;
&:focus {
background-color: #d3eaf8;
outline:none;
border: 1px solid rgba(0, 0, 0, 0.1);
transition:all 1s ease;
transition: all 1s ease;
outline: none;
border: 1px solid $active-dark-gray;
background-color: $focus-bg;
}
&.fail {
border: 1px solid #eab012;
background-color: #fff7df;
border: 1px solid $active-dark-gray;
background-color: $fail-bg;
}
&.pass {
border: 1px solid #55db58;
background-color: #eafdea;
border: 1px solid $active-dark-gray;
background-color: $pass-bg;
}
}

View file

@ -0,0 +1,24 @@
var React = require('react');
var classNames = require('classnames');
require('./select.scss');
var Select = React.createClass({
type: 'Select',
propTypes: {
},
render: function () {
var classes = classNames(
'select',
this.props.className
);
return (
<select {... this.props} className={classes}>
{this.props.children}
</select>
);
}
});
module.exports = Select;

View file

@ -0,0 +1,9 @@
@import "../../colors";
.select {
background-color: $ui-white;
width: 220px;
height: 28px;
line-height: 28px;
font-size: 1em;
}

View file

@ -1,11 +1,18 @@
var omit = require('lodash.omit');
var React = require('react');
var ReactIntl = require('react-intl');
var FormattedMessage = ReactIntl.FormattedMessage;
var FormattedHTMLMessage = ReactIntl.FormattedHTMLMessage;
var Modal = require('../modal/modal.jsx');
var Registration = require('../registration/registration.jsx');
require('./intro.scss');
Modal.setAppElement(document.getElementById('view'));
module.exports = React.createClass({
var Intro = React.createClass({
type: 'Intro',
propTypes: {
projectCount: React.PropTypes.number
},
@ -25,13 +32,33 @@ module.exports = React.createClass({
closeVideo: function () {
this.setState({videoOpen: false});
},
handleJoinClick: function (e) {
e.preventDefault();
this.setState({'registrationOpen': true});
},
closeRegistration: function () {
this.setState({'registrationOpen': false});
},
completeRegistration: function () {
window.refreshSession();
this.closeRegistration();
},
render: function () {
var frameProps = {
width: 570,
height: 357,
padding: 15
};
return (
<div className="intro">
<div className="content">
<h1>
Create stories, games, and animations<br />
Share with others around the world
<FormattedHTMLMessage
id='intro.tagLine'
defaultMessage={
'Create stories, games, and animations<br /> ' +
'Share with others around the world'
} />
</h1>
<div className="sprites">
<a className="sprite sprite-1" href="/projects/editor/?tip_bar=getStarted">
@ -42,7 +69,11 @@ module.exports = React.createClass({
className="costume costume-2"
src="//cdn.scratch.mit.edu/scratchr2/static/images/cat-b.png" />
<div className="circle"></div>
<div className="text">TRY IT OUT</div>
<div className="text">
<FormattedMessage
id='intro.tryItOut'
defaultMessage='TRY IT OUT' />
</div>
</a>
<a className="sprite sprite-2" href="/starter_projects/">
<img
@ -52,9 +83,13 @@ module.exports = React.createClass({
className="costume costume-2"
src="//cdn.scratch.mit.edu/scratchr2/static/images/tera-b.png" />
<div className="circle"></div>
<div className="text">SEE EXAMPLES</div>
<div className="text">
<FormattedMessage
id='intro.seeExamples'
defaultMessage='SEE EXAMPLES' />
</div>
</a>
<a className="sprite sprite-3" href="#">
<a className="sprite sprite-3" href="#" onClick={this.handleJoinClick}>
<img
className="costume costume-1"
src="//cdn.scratch.mit.edu/scratchr2/static/images/gobo-a.png" />
@ -62,9 +97,17 @@ module.exports = React.createClass({
className="costume costume-2"
src="//cdn.scratch.mit.edu/scratchr2/static/images/gobo-b.png" />
<div className="circle"></div>
<div className="text">JOIN SCRATCH</div>
<div className="text">
<FormattedMessage
id='intro.joinScratch'
defaultMessage='JOIN SCRATCH' />
</div>
<div className="text subtext">( it&rsquo;s free )</div>
</a>
<Registration key="registration"
isOpen={this.state.registrationOpen}
onRequestClose={this.closeRegistration}
onRegistrationDone={this.completeRegistration} />
</div>
<div className="description">
A creative learning community with
@ -72,9 +115,21 @@ module.exports = React.createClass({
projects shared
</div>
<div className="links">
<a href="/about/">ABOUT SCRATCH</a>
<a href="/educators/">FOR EDUCATORS</a>
<a className="last" href="/parents/">FOR PARENTS</a>
<a href="/about/">
<FormattedMessage
id='intro.aboutScratch'
defaultMessage='ABOUT SCRATCH' />
</a>
<a href="/educators/">
<FormattedMessage
id='intro.forEducators'
defaultMessage='FOR EDUCATORS' />
</a>
<a className="last" href="/parents/">
<FormattedMessage
id='intro.forParents'
defaultMessage='FOR PARENTS' />
</a>
</div>
</div>
<div className="video">
@ -82,12 +137,17 @@ module.exports = React.createClass({
<img src="//cdn.scratch.mit.edu/scratchr2/static/images/hp-video-screenshot.png" />
</div>
<Modal
className="video-modal"
isOpen={this.state.videoOpen}
onRequestClose={this.closeVideo}>
<iframe src="//player.vimeo.com/video/65583694?title=0&amp;byline=0&amp;portrait=0" />
className="video-modal"
isOpen={this.state.videoOpen}
onRequestClose={this.closeVideo}
style={{content:frameProps}}>
<iframe
src="//player.vimeo.com/video/65583694?title=0&amp;byline=0&amp;portrait=0"
{...omit(frameProps, 'padding')} />
</Modal>
</div>
);
}
});
module.exports = Intro;

View file

@ -1,18 +1,20 @@
@import "../../colors";
.intro {
display: flex;
margin-top: 20px;
margin-bottom: 30px;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-content: flex-start;
margin-top: 20px;
margin-bottom: 20px;
.content {
display: inline-block;
width: calc(66% - 20px);
h1 {
color: #F9A739;
color: $ui-orange;
font-weight: 400;
}
}
@ -20,13 +22,15 @@
.sprites {
position: relative;
clear: both;
margin: 20px 0;
overflow: hidden;
&:after {
display: block;
visibility: hidden;
content: " ";
clear: both;
visibility: hidden;
height: 0;
content: " ";
}
}
@ -37,13 +41,15 @@
height: 136px;
overflow: hidden;
.costume, .circle, .text {
.costume,
.circle,
.text {
position: absolute;
}
.costume {
z-index: 2;
left: 0;
z-index: 2;
}
.costume-2 {
@ -59,144 +65,162 @@
}
.circle {
z-index: 0;
border-radius: 50%;
display: block;
width: 112px;
height: 112px;
box-shadow: 0px 0px 5px #fff;
top: 15px;
left: 43px;
z-index: 0;
border-radius: 50%;
box-shadow: 0 0 5px $ui-white;
width: 112px;
height: 112px;
}
$text-bg-color: #F1F3F4;
.text {
$text-bg-color: $background-color;
left: 35px;
z-index: 1;
border: 2px solid $text-bg-color;
background-color: $text-bg-color;
padding-right: 10px;
padding-left: 40px;
white-space: nowrap;
font-size: 12px;
font-weight: 700;
left: 35px;
padding-left: 40px;
padding-right: 10px;
white-space: nowrap;
background-color: $text-bg-color;
border: 2px solid $text-bg-color;
}
.subtext {
border: 0;
background-color: transparent;
font-weight: 400;
text-shadow: 0;
font-size: 12px;
text-shadow: none;
border: none;
font-weight: 400;
}
&.sprite-1 .circle {
background-color: $splash-green;
}
&.sprite-2 .circle {
background-color: $splash-pink;
}
&.sprite-3 .circle {
background-color: $splash-blue;
}
&:hover.sprite-1 .circle {
box-shadow: 0 0 10px 2px $splash-green;
}
&:hover.sprite-2 .circle {
box-shadow: 0 0 10px 2px $splash-pink;
}
&:hover.sprite-3 .circle {
box-shadow: 0 0 10px 2px $splash-blue;
}
&.sprite-1 .text {
top: 60px;
left: 50px;
color: $splash-green;
}
&.sprite-2 .text {
top: 77px;
left: 50px;
color: $splash-pink;
}
&.sprite-3 .text {
top: 37px;
left: 45px;
color: $splash-blue;
}
$sprite-1-bgcolor: #9C0;
$sprite-2-bgcolor: #C2479D;
$sprite-3-bgcolor: #199ED7;
&.sprite-1 .circle { background-color: $sprite-1-bgcolor; }
&.sprite-2 .circle { background-color: $sprite-2-bgcolor; }
&.sprite-3 .circle { background-color: $sprite-3-bgcolor; }
&:hover.sprite-1 .circle { box-shadow: 0 0 10px 2px $sprite-1-bgcolor; }
&:hover.sprite-2 .circle { box-shadow: 0 0 10px 2px $sprite-2-bgcolor; }
&:hover.sprite-3 .circle { box-shadow: 0 0 10px 2px $sprite-3-bgcolor; }
&.sprite-1 .text { color: $sprite-1-bgcolor; top: 60px; left: 50px; }
&.sprite-2 .text { color: $sprite-2-bgcolor; top: 77px; left: 50px; }
&.sprite-3 .text { color: $sprite-3-bgcolor; top: 37px; left: 45px; }
&.sprite-3 .subtext {
top: 63px;
left: 60px;
color: #fff;
color: $ui-white;
}
}
.description {
font-size: 17px;
margin-top: 10px;
font-size: 17px;
}
.project-count {
color: hsl(318, 50%, 52%);
font-weight: 700;
$project-count-color: hsl(318, 50%, 52%);
color: $project-count-color;
font-size: 18px;
font-weight: 700;
}
.links {
font-size: 12px;
margin-top: 20px;
letter-spacing: .5px;
font-size: 12px;
a {
border-right: 1px solid #000;
border-right: 1px solid $type-gray;
padding: 0 5px;
&:last-child { border-right: 0; }
&:first-child { padding-left: 0; }
}
}
.video {
display: inline-block;
height: 208px;
width: 34%;
position: relative;
padding: 10px;
border: 1px solid #eee;
border-radius: 5px;
background-color: #f7f7f7;
border: 1px solid $ui-border;
border-radius: 10px;
background-color: $ui-white;
padding: 14px 10px;
width: 34%;
height: 208px;
text-align: center;
box-shadow: 0 2px 3px;
img {
border-radius: 5px;
}
}
.play-button {
border-radius: 20px;
display: block;
top: calc(50% - 25px);
left: calc(50% - 35px);
opacity: .8;
border: 5px solid $ui-border;
border-radius: 20px;
background-color: $type-gray;
width: 70px;
height: 50px;
left: calc(50% - 35px);
top: calc(50% - 25px);
background-color: #666;
border: 5px solid #ccc;
opacity: 0.8;
&, &:after {
&,
&:after {
position: absolute;
cursor: pointer;
margin: 0;
cursor: pointer;
padding: 0;
}
&:after {
left: 28px;
border: solid transparent;
content: " ";
height: 0;
width: 0;
pointer-events: none;
border-color: rgba(255, 255, 255, 0);
border-left-color: #fff;
border-width: 18px;
$play-arrow: rgba(255, 255, 255, 0);
top: 37px;
left: 28px;
margin-top: -30px;
border: solid transparent;
border-width: 18px;
border-color: $play-arrow;
border-left-color: $ui-white;
width: 0;
height: 0;
content: " ";
pointer-events: none;
}
}
}
.video-modal {
$video-width: 570px;
$video-height: 357px;
$padding: 15px;
width: $video-width;
height: $video-height;
padding: $padding;
top: 50%;
bottom: auto;
left: 50%;
right: auto;
margin-left: -($video-width + $padding * 2)/2;
margin-top: -($video-height + $padding * 2)/2;
iframe {
width: $video-width;
height: $video-height;
border: 0;
}
}

View file

@ -0,0 +1,51 @@
var classNames = require('classnames');
var React = require('react');
var ReactDOM = require('react-dom');
var Api = require('../../mixins/api.jsx');
var languages = require('../../../languages.json');
var Select = require('../forms/select.jsx');
require('./languagechooser.scss');
var LanguageChooser = React.createClass({
type: 'LanguageChooser',
mixins: [
Api
],
getInitialState: function () {
return {
choice: window._locale
};
},
getDefaultProps: function () {
return {
languages: languages
};
},
onSetLanguage: function (e) {
e.preventDefault();
this.setState({'choice': e.target.value});
ReactDOM.findDOMNode(this.refs.languageForm).submit();
},
render: function () {
var classes = classNames(
'language-chooser',
this.props.className
);
return (
<form ref="languageForm" className={classes} action="/i18n/setlang/" method="POST">
<Select name="language" defaultValue={this.state.choice} 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>
</form>
);
}
});
module.exports = LanguageChooser;

View file

@ -0,0 +1,3 @@
.language-chooser {
}

View file

@ -1,29 +1,60 @@
var React = require('react');
var ReactDOM = require('react-dom');
var FormattedMessage = require('react-intl').FormattedMessage;
var Input = require('../forms/input.jsx');
var Button = require('../forms/button.jsx');
require('./login.scss');
module.exports = React.createClass({
var Login = React.createClass({
type: 'Login',
propTypes: {
onLogIn: React.PropTypes.func
onLogIn: React.PropTypes.func,
error: React.PropTypes.string
},
handleSubmit: function (event) {
event.preventDefault();
this.props.onLogIn();
this.props.onLogIn({
'username': ReactDOM.findDOMNode(this.refs.username).value,
'password': ReactDOM.findDOMNode(this.refs.password).value
});
},
render: function () {
var error;
if (this.props.error) {
error = <div className="error">{this.props.error}</div>;
}
return (
<div className="login">
<form onSubmit={this.handleSubmit}>
<label htmlFor="username">Username</label>
<Input type="text" name="username" maxLength="30" />
<label htmlFor="password">Password</label>
<Input type="password" name="password" />
<Button className="submit-button white" type="submit">Sign in</Button>
<a className="right" href="/accounts/password_reset/">Forgot password?</a>
<label htmlFor="username">
<FormattedMessage
id='general.username'
defaultMessage={'Username'} />
</label>
<Input type="text" ref="username" name="username" maxLength="30" />
<label htmlFor="password">
<FormattedMessage
id='general.password'
defaultMessage={'Password'} />
</label>
<Input type="password" ref="password" name="password" />
<Button className="submit-button white" type="submit">
<FormattedMessage
id='general.signIn'
defaultMessage={'Sign in'} />
</Button>
<a className="right" href="/accounts/password_reset/">
<FormattedMessage
id='login.forgotPassword'
defaultMessage={'Forgot Password?'} />
</a>
{error}
</form>
</div>
);
}
});
module.exports = Login;

View file

@ -1,3 +1,5 @@
@import "../../colors";
.login {
padding: 10px;
@ -15,6 +17,13 @@
}
a:hover {
background-color: transparent !important;
background-color: transparent;
}
.error {
border: 1px solid $active-dark-gray;
border-radius: 5px;
background-color: $ui-orange;
padding: .75em 1em;
}
}

View file

@ -1,22 +1,60 @@
var clone = require('lodash.clone');
var defaultsDeep = require('lodash.defaultsdeep');
var React = require('react');
var Modal = require('react-modal');
var ReactModal = require('react-modal');
require('./modal.scss');
var defaultStyle = {
overlay: {
zIndex: 100,
backgroundColor: 'rgba(0, 0, 0, .75)'
},
content: {
overflow: 'visible',
borderRadius: '6px',
width: 500,
height: 250,
padding: 0,
top: '50%',
right: 'auto',
bottom: 'auto',
left: '50%',
marginTop: -125,
marginLeft: -250
}
};
module.exports = React.createClass({
var Modal = React.createClass({
type: 'Modal',
statics: {
setAppElement: Modal.setAppElement
setAppElement: ReactModal.setAppElement
},
getDefaultProps: function () {
return {
style: defaultStyle
};
},
calculateStyle: function () {
var style = clone(this.props.style, true);
defaultsDeep(style, defaultStyle);
style.content.marginTop = (style.content.height + style.content.padding*2) / -2;
style.content.marginLeft = (style.content.width + style.content.padding*2) / -2;
return style;
},
requestClose: function () {
return this.refs.modal.portal.requestClose();
},
render: function () {
return (
<Modal ref="modal" {... this.props}>
<ReactModal ref="modal"
{...this.props}
style={this.calculateStyle()}>
<div className="modal-close" onClick={this.requestClose}></div>
{this.props.children}
</Modal>
</ReactModal>
);
}
});
module.exports = Modal;

View file

@ -1,49 +1,29 @@
/* Copied from the un-styleable react-modal */
@import "../../colors";
.ReactModal__Overlay {
background-color: rgba(0, 0, 0, 0.75);
z-index: 100;
}
.ReactModal__Content {
position: absolute;
top: 40px;
left: 40px;
right: 40px;
bottom: 40px;
background: #fff;
overflow: visible;
-webkit-overflow-scrolling: touch;
border-radius: 6px;
outline: none;
padding: 20px;
}
@media (max-width: 768px) {
.ReactModal__Content {
top: 10px;
left: 10px;
right: 10px;
bottom: 10px;
padding: 10px;
}
&.ReactModal__Content {
iframe {
border: 0;
}
}
.modal-close {
$modal-close-size: 20px;
position: absolute;
right: 0;
top: 0;
border-radius: $modal-close-size/2;
border: 2px solid #ddd;
background-color: #666;
color: #fff;
width: $modal-close-size;
height: $modal-close-size;
right: 0;
margin-top: -$modal-close-size/2;
margin-right: -$modal-close-size/2;
border: 2px solid $ui-border;
border-radius: $modal-close-size/2;
background-color: $active-dark-gray;
cursor: pointer;
width: $modal-close-size;
height: $modal-close-size;
text-align: center;
line-height: $modal-close-size;
color: $type-white;
font-size: $modal-close-size;
cursor: pointer;
&:before {
content: "x";
}

View file

@ -1,3 +0,0 @@
$base-background-color: #2aa3ef;
$active-background-color: rgba(0, 0, 0, 0.1);
$border-color: rgb(20, 154, 203);

View file

@ -3,7 +3,8 @@ var classNames = require('classnames');
require('./dropdown.scss');
module.exports = React.createClass({
var Dropdown = React.createClass({
type: 'Dropdown',
mixins: [
require('react-onclickoutside')
],
@ -35,3 +36,5 @@ module.exports = React.createClass({
);
}
});
module.exports = Dropdown;

View file

@ -1,33 +1,33 @@
@import 'colors';
@import "../../colors";
.dropdown {
display: none;
position: absolute;
right: 0;
min-width: 240px;
max-width: 260px;
background-color: $base-background-color;
overflow: visible;
border: 1px solid $active-gray;
border-radius: 0 0 5px 5px;
background-color: $ui-blue;
padding: 10px;
color: white;
min-width: 160px;
max-width: 260px;
overflow: visible;
color: $type-white;
font-size: .8125rem;
font-weight: normal;
font-size: 0.8125rem;
border: 1px solid $active-background-color;
display: none;
&.open {
display: block;
}
a {
color: white;
background-color: transparent;
color: $type-white;
}
input {
// 100% minus border and padding
width: calc(100% - 30px);
margin-bottom: 12px;
width: calc(100% - 30px);
}
label {
@ -40,8 +40,8 @@
line-height: 30px;
&.divider {
border-top: 1px solid #149acb;
margin-top: 10px;
border-top: 1px solid $active-gray;
}
a {
@ -49,8 +49,8 @@
padding: 0 10px;
&:hover {
background-color: $active-background-color;
text-decoration: none;
background-color: $active-gray;
text-decoration: none;
}
}
}
@ -60,22 +60,24 @@
margin-top: $arrow-border-width;
border-radius: 5px;
overflow: visible;
&:before {
position: absolute;
display: block;
position: absolute;
top: -$arrow-border-width/2;
right: 10%;
height: $arrow-border-width;
width: $arrow-border-width;
content: '';
transform: rotate(45deg);
background-color: $base-background-color;
border-top: 1px solid $active-background-color;
border-left: 1px solid $active-background-color;
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: "";
}
}
}

View file

@ -1,58 +1,223 @@
var React = require('react');
var classNames = require('classnames');
var React = require('react');
var ReactIntl = require('react-intl');
var defineMessages = ReactIntl.defineMessages;
var FormattedMessage = ReactIntl.FormattedMessage;
var injectIntl = ReactIntl.injectIntl;
var xhr = require('xhr');
var Api = require('../../mixins/api.jsx');
var Avatar = require('../avatar/avatar.jsx');
var Dropdown = require('./dropdown.jsx');
var Input = require('../forms/input.jsx');
var log = require('../../lib/log.js');
var Login = require('../login/login.jsx');
var Modal = require('../modal/modal.jsx');
var Registration = require('../registration/registration.jsx');
var Session = require('../../mixins/session.jsx');
require('./navigation.scss');
module.exports = React.createClass({
Modal.setAppElement(document.getElementById('view'));
var defaultMessages = defineMessages({
messages: {
id: 'general.messages',
defaultMessage: 'Messages'
},
myStuff: {
id: 'general.myStuff',
defaultMessage: 'My Stuff'
}
});
var Navigation = React.createClass({
type: 'Navigation',
mixins: [
Api,
Session
],
getInitialState: function () {
return {
'loginOpen': false,
'loggedIn': false,
'loggedInUser': {
'username': 'raimondious',
'thumbnail': '//cdn2.scratch.mit.edu/get_image/user/2584924_32x32.png'
},
'accountNavOpen': false
accountNavOpen: false,
canceledDeletionOpen: false,
loginOpen: false,
loginError: null,
registrationOpen: false,
unreadMessageCount: 0,
messageCountIntervalId: -1
};
},
componentDidMount: function () {
if (this.state.session.user) {
this.getMessageCount();
var intervalId = setInterval(this.getMessageCount, 120000);
this.setState({'messageCountIntervalId': intervalId});
}
},
componentDidUpdate: function (prevProps, prevState) {
if (prevState.session.user != this.state.session.user) {
this.setState({
'loginOpen': false,
'accountNavOpen': false
});
if (this.state.session.user) {
this.getMessageCount();
var intervalId = setInterval(this.getMessageCount, 120000);
this.setState({'messageCountIntervalId': intervalId});
} else {
// clear message count check, and set to default id.
clearInterval(this.state.messageCountIntervalId);
this.setState({'messageCountIntervalId': -1});
}
}
},
componentWillUnmount: function () {
// clear message interval if it exists
if (this.state.messageCountIntervalId != -1) {
clearInterval(this.state.messageCountIntervalId);
this.setState({'messageCountIntervalId': -1});
}
},
getProfileUrl: function () {
if (!this.state.session.user) return;
return '/users/' + this.state.session.user.username + '/';
},
getMessageCount: function () {
this.api({
method: 'get',
uri: '/proxy/users/' + this.state.session.user.username + '/activity/count'
}, function (err, body) {
if (body) {
var count = parseInt(body.msg_count, this.state.unreadMessageCount);
this.setState({'unreadMessageCount': count});
}
}.bind(this));
},
handleJoinClick: function (e) {
e.preventDefault();
this.setState({'registrationOpen': true});
},
handleLoginClick: function (e) {
e.preventDefault();
this.setState({'loginOpen': true});
this.setState({'loginOpen': !this.state.loginOpen});
},
closeLogin: function () {
this.setState({'loginOpen': false});
},
handleLogIn: function () {
this.setState({'loggedIn': true});
handleLogIn: function (formData) {
this.setState({'loginError': null});
formData['useMessages'] = true;
this.api({
method: 'post',
host: '',
uri: '/accounts/login/',
json: formData,
useCsrf: true
}, function (err, body) {
if (body) {
body = body[0];
if (!body.success) {
if (body.redirect) {
window.location = body.redirect;
}
this.setState({'loginError': body.msg});
} else {
this.closeLogin();
body.messages.map(function (message) {
if (message.message == 'canceled-deletion') {
this.showCanceledDeletion();
}
}.bind(this));
window.refreshSession();
}
}
}.bind(this));
},
handleLogOut: function () {
this.setState({'loggedIn': false});
handleLogOut: function (e) {
e.preventDefault();
xhr({
host: '',
uri: '/accounts/logout/'
}, function (err) {
if (err) {
log.error(err);
} else {
this.closeLogin();
window.refreshSession();
}
}.bind(this));
},
handleClickAccountNav: function () {
handleAccountNavClick: function (e) {
e.preventDefault();
this.setState({'accountNavOpen': true});
},
closeAccountNav: function () {
this.setState({'accountNavOpen': false});
},
showCanceledDeletion: function () {
this.setState({'canceledDeletionOpen': true});
},
closeCanceledDeletion: function () {
this.setState({'canceledDeletionOpen': false});
},
closeRegistration: function () {
this.setState({'registrationOpen': false});
},
completeRegistration: function () {
window.refreshSession();
this.closeRegistration();
},
render: function () {
var classes = classNames({
'inner': true,
'logged-in': this.state.loggedIn
'logged-in': this.state.session.user
});
var messageClasses = classNames({
'messageCount': true,
'show': this.state.unreadMessageCount > 0
});
var formatMessage = this.props.intl.formatMessage;
return (
<div className={classes}>
<ul>
<li className="logo"><a href="/"></a></li>
<li className="link"><a href="/projects/editor">Create</a></li>
<li className="link"><a href="/explore">Explore</a></li>
<li className="link"><a href="/discuss">Discuss</a></li>
<li className="link"><a href="/about">About</a></li>
<li className="link"><a href="/help">Help</a></li>
<li className="link create">
<a href="/projects/editor">
<FormattedMessage
id="general.create"
defaultMessage={'Create'} />
</a>
</li>
<li className="link explore">
<a href="/explore?date=this_month">
<FormattedMessage
id="general.explore"
defaultMessage={'Explore'} />
</a>
</li>
<li className="link discuss">
<a href="/discuss">
<FormattedMessage
id="general.discuss"
defaultMessage={'Discuss'} />
</a>
</li>
<li className="link about">
<a href="/about">
<FormattedMessage
id="general.about"
defaultMessage={'About'} />
</a>
</li>
<li className="link help">
<a href="/help">
<FormattedMessage
id="general.help"
defaultMessage={'Help'} />
</a>
</li>
<li className="search">
<form action="/search/google_results" method="get">
@ -62,40 +227,108 @@ module.exports = React.createClass({
<Input type="hidden" name="sort_by" value="datetime_shared" />
</form>
</li>
{this.state.loggedIn ? [
<li className="link right messages"><a href="/messages/" title="Messages">Messages</a></li>,
<li className="link right mystuff"><a href="/mystuff/" title="My Stuff">My Stuff</a></li>,
<li className="link right account-nav">
<a className="userInfo" href="#" onClick={this.handleClickAccountNav}>
<img src={this.state.loggedInUser.thumbnail} />
{this.state.loggedInUser.username}
{this.state.session.user ? [
<li className="link right messages" key="messages">
<a
href="/messages/"
title={formatMessage(defaultMessages.messages)}>
<span className={messageClasses}>{this.state.unreadMessageCount}</span>
<FormattedMessage {...defaultMessages.messages} />
</a>
</li>,
<li className="link right mystuff" key="mystuff">
<a
href="/mystuff/"
title={formatMessage(defaultMessages.myStuff)}>
<FormattedMessage {...defaultMessages.myStuff} />
</a>
</li>,
<li className="link right account-nav" key="account-nav">
<a className="userInfo" href="#" onClick={this.handleAccountNavClick}>
<Avatar src={this.state.session.user.thumbnailUrl} />
{this.state.session.user.username}
</a>
<Dropdown
as="ul"
isOpen={this.state.accountNavOpen}
onRequestClose={this.closeAccountNav}>
<li><a href="/users/raimondious/">Profile</a></li>
<li><a href="/mystuff/">My Stuff</a></li>
<li><a href="/accounts/settings/">Account settings</a></li>
<li>
<a href={this.getProfileUrl()}>
<FormattedMessage
id='general.profile'
defaultMessage={'Profile'} />
</a>
</li>
<li>
<a href="/mystuff/">
<FormattedMessage {...defaultMessages.myStuff} />
</a>
</li>
<li>
<a href="/accounts/settings/">
<FormattedMessage
id='general.accountSettings'
defaultMessage={'Account settings'} />
</a>
</li>
<li className="divider">
<a href="#" onClick={this.handleLogOut}>Sign out</a>
<a href="#" onClick={this.handleLogOut}>
<FormattedMessage
id='navigation.signOut'
defaultMessage={'Sign out'} />
</a>
</li>
</Dropdown>
</li>
] : [
<li className="link right join"><a href="/join">Join Scratch</a></li>,
<li className="link right">
<a href="#" onClick={this.handleLoginClick}>Sign In</a>
<li className="link right join" key="join">
<a href="#" onClick={this.handleJoinClick}>
<FormattedMessage
id='general.joinScratch'
defaultMessage={'Join Scratch'} />
</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">
<FormattedMessage
id='general.signIn'
defaultMessage={'Sign In'} />
</a>
<Dropdown
className="login-dropdown with-arrow"
isOpen={this.state.loginOpen}
onRequestClose={this.closeLogin}>
<Login onLogIn={this.handleLogIn} />
<Login
onLogIn={this.handleLogIn}
error={this.state.loginError} />
</Dropdown>
</li>
]}
</ul>
<Modal isOpen={this.state.canceledDeletionOpen}
onRequestClose={this.closeCanceledDeletion}
style={{content:{padding: 15}}}>
<h4>Your Account Will Not Be Deleted</h4>
<p>
Your account was scheduled for deletion but you logged in. Your account has been reactivated.
If you didnt request for your account to be deleted, you should
{' '}<a href="/accounts/password_reset/">change your password</a>{' '}
to make sure your account is secure.
</p>
</Modal>
</div>
);
}
});
module.exports = injectIntl(Navigation);

View file

@ -1,54 +1,60 @@
@import 'colors';
@import "../../colors";
#navigation {
position: fixed;
z-index: 10;
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: $base-background-color;
z-index: 10;
border-bottom: 1px solid $active-gray;
box-shadow: 0 0 3px $box-shadow-gray;
background-color: $ui-blue;
width: 100%;
/* NOTE: Height should match offset settings in main.scss file */
height: 50px;
.inner > ul {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
height: 50px;
margin: 0;
padding: 0;
height: 50px;
list-style: none;
flex-wrap: nowrap;
flex-direction: row;
justify-content: flex-start;
> li {
display: inline-block;
align-self: flex-start;
position: relative;
float: left;
height: 100%;
position: relative;
align-self: flex-start;
}
.logo {
margin-right: 10px;
a {
display: block;
width: 81px;
height: 50px;
margin: 0px 6px 0 0;
transition: .15s ease all;
margin: 0 6px 0 0;
border: 0;
border: none;
background-image: url('/images/logo_sm.png');
background-image: url("/images/logo_sm.png");
background-repeat: no-repeat;
background-position: center center;
background-size: 95%;
transition: .15s ease all;
width: 81px;
height: 50px;
&:hover {
background-size: 100%;
transition: .15s ease all;
transition: .15s ease all;
background-size: 100%;
}
}
}
@ -56,26 +62,26 @@
.link {
> a {
display: block;
padding: 17px 15px 0 15px;
height: 33px;
padding: 17px 15px 0px 15px;
color: white;
text-decoration: none;
font-size: 0.85rem;
white-space: nowrap;
color: $type-white;
font-size: .85rem;
font-weight: bold;
}
> a:hover {
background-color: $active-background-color;
background-color: $active-gray;
}
}
.search {
margin: 0 20px;
border-right: 0;
color: $type-white;
flex-grow: 3;
border-right: none;
color: white;
margin:0px 20px;
form {
margin: 0;
@ -83,44 +89,44 @@
input {
display: inline-block;
height: 14px;
margin-top: 5px;
outline: none;
border: none;
background-color: $active-background-color;
margin-top:5px;
border: 0;
background-color: $active-gray;
height: 14px;
}
input[type=submit] {
position: absolute;
width: 40px;
height: 40px;
background-color: transparent;
background-image: url('/images/nav-search-glass.png');
background-size: 14px 14px;
background-image: url("/images/nav-search-glass.png");
background-repeat: no-repeat;
background-position: center center;
background-size: 14px 14px;
width: 40px;
height: 40px;
}
input[type=text] {
transition: .15s ease background-color;
padding: 0;
padding-right: 10px;
padding-left: 40px;
width: calc(100% - 50px);
height: 40px;
padding: 0;
color: white;
padding-left: 40px;
padding-right:10px;
font-size: 0.85em;
transition: .15s ease background-color;
color: $type-white;
font-size: .85em;
&::placeholder {
color:rgba(255, 255, 255, 0.75);
$placeholder-transparent: rgba(255, 255, 255, .75);
color: $placeholder-transparent;
}
&:focus {
background-color: rgba(0, 0, 0, 0.2);
transition: .15s ease background-color;
background-color: $active-dark-gray;
}
}
@ -130,38 +136,62 @@
}
.right {
align-self: flex-end;
float: right;
margin-left: auto;
align-self: flex-end;
a:hover {
background-color: $active-background-color;
background-color: $active-gray;
}
}
.messages, .mystuff {
.messages,
.mystuff {
> a {
background-repeat: no-repeat;
background-position: center center;
padding-left: 10px;
background-size: 45%;
padding-right: 10px;
text-indent: 100%;
white-space: nowrap;
padding-left: 10px;
width: 30px;
overflow: hidden;
text-indent: 50px;
white-space: nowrap;
}
> a:hover {
background-size: 50%;
}
}
.messages {
> a {
background-image: url('/images/nav-notifications.png');
width: 22px;
background-image: url("/images/nav-notifications.png");
}
.messageCount {
display: none;
&.show {
display: block;
position: absolute;
top: .5rem;
right: .25rem;
border-radius: 1rem;
background-color: $ui-orange;
padding: 0 .25rem;
text-indent: 0;
line-height: 1rem;
color: $type-white;
font-size: .7rem;
font-weight: bold;
}
}
}
.mystuff {
> a {
background-image: url('/images/mystuff.png');
width: 25px;
background-image: url("/images/mystuff.png");
}
}
@ -171,41 +201,41 @@
.account-nav {
.userInfo {
padding-top: 11px;
padding-bottom: 6px;
}
padding-top: 14px;
padding-bottom: 3px;
}
> a {
font-size: .8125rem;
font-weight: normal;
font-size: 0.8125rem;
img {
width: 30px;
height: 30px;
.avatar {
margin-right: 10px;
vertical-align: middle;
border-radius: 3px;
width: 24px;
height: 24px;
vertical-align: middle;
}
&:after {
$caret-border-width: 4px;
margin-left: $caret-border-width;
border: $caret-border-width solid transparent;
border-bottom-width: 0;
border-top-color: white;
content: " ";
opacity: 0.5;
vertical-align: middle;
width: 0;
height: 0;
display: inline-block;
margin-left: 8px;
background-image: url("/images/dropdown.png");
background-repeat: no-repeat;
background-position: center center;
background-size: 50%;
width: 20px;
height: 20px;
vertical-align: middle;
content: " ";
}
}
.dropdown {
width: 100%;
padding: 0;
padding-top: 5px;
width: 100%;
}
}
}

View file

@ -1,10 +1,25 @@
var React = require('react');
var ReactIntl = require('react-intl');
var defineMessages = ReactIntl.defineMessages;
var injectIntl = ReactIntl.injectIntl;
var Box = require('../box/box.jsx');
require('./news.scss');
module.exports = React.createClass({
var defaultMessages = defineMessages({
scratchNews: {
id: 'news.scratchNews',
defaultMessage: 'Scratch News'
},
viewAll: {
id: 'general.viewAll',
defaultMessage: 'View All'
}
});
var News = React.createClass({
type: 'News',
propTypes: {
items: React.PropTypes.array
},
@ -14,11 +29,12 @@ module.exports = React.createClass({
};
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
return (
<Box
className="news"
title="Scratch News"
moreTitle="View All"
title={formatMessage(defaultMessages.scratchNews)}
moreTitle={formatMessage(defaultMessages.viewAll)}
moreHref="/news">
<ul>
@ -38,3 +54,5 @@ module.exports = React.createClass({
);
}
});
module.exports = injectIntl(News);

View file

@ -1,3 +1,5 @@
@import "../../colors";
.news {
ul {
display: block;
@ -9,10 +11,10 @@
li {
display: block;
min-height: 53px;
clear: both;
margin: 0;
padding: 12px 0;
clear: both;
min-height: 53px;
a {
display: block;
@ -32,8 +34,8 @@
h4 {
display: block;
color: #1aa0d8;
font-size: 0.85rem;
color: $link-blue;
font-size: .85rem;
}
p {
@ -41,13 +43,13 @@
margin: 0;
padding: 0;
color: #322f31;
font-size: 0.85rem;
color: $type-gray;
font-size: .85rem;
}
}
li:nth-child(even) {
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
border-top: 1px solid $ui-border;
border-bottom: 1px solid $ui-border;
}
}

View file

@ -0,0 +1,55 @@
var React = require('react');
var Modal = require('../modal/modal.jsx');
require('./registration.scss');
Modal.setAppElement(document.getElementById('view'));
var Registration = React.createClass({
propTypes: {
isOpen: React.PropTypes.bool,
onRegistrationDone: React.PropTypes.func,
onRequestClose: React.PropTypes.func
},
onMessage: function (e) {
if (e.origin != window.location.origin) return;
if (e.source != this.refs.registrationIframe.contentWindow) return;
if (e.data == 'registration-done') this.props.onRegistrationDone();
if (e.data == 'registration-relaunch') {
this.refs.registrationIframe.contentWindow.location.reload();
}
},
toggleMessageListener: function (present) {
if (present) {
window.addEventListener('message', this.onMessage);
} else {
window.removeEventListener('message', this.onMessage);
}
},
componentDidMount: function () {
if (this.props.isOpen) this.toggleMessageListener(true);
},
componentDidUpdate: function (prevProps) {
this.toggleMessageListener(this.props.isOpen && !prevProps.isOpen);
},
componentWillUnmount: function () {
this.toggleMessageListener(false);
},
render: function () {
var frameProps = {
width: 610,
height: 438
};
return (
<Modal
isOpen={this.props.isOpen}
onRequestClose={this.props.onRequestClose}
className="registration"
style={{content:frameProps}}>
<iframe ref="registrationIframe" src="/accounts/standalone-registration/" {...frameProps} />
</Modal>
);
}
});
module.exports = Registration;

View file

@ -0,0 +1,3 @@
.registration {
overflow: hidden;
}

View file

@ -1,28 +1,71 @@
var React = require('react');
var classNames = require('classnames');
require('./thumbnail.scss');
module.exports = React.createClass({
var Thumbnail = React.createClass({
type: 'Thumbnail',
propTypes: {
src: React.PropTypes.string
},
getDefaultProps: function () {
return {
href: '/projects/1000/',
title: 'Example Project',
src: 'http://www.lorempixel.com/144/108/',
extra: 'by raimondious'
href: '#',
title: 'Project',
src: '',
type: 'project',
showLoves: false,
showRemixes: false
};
},
render: function () {
var classes = classNames(
'thumbnail',
this.props.type,
this.props.className
);
var extra = [];
if (this.props.creator) {
extra.push(
<div key="creator" className="thumbnail-creator">
by <a href={'/users/' + this.props.creator + '/'}>{this.props.creator}</a>
</div>
);
}
if (this.props.loves && this.props.showLoves) {
extra.push(
<div
key="loves"
className="thumbnail-loves"
title={this.props.loves + ' loves'}>
{this.props.loves}
</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>
);
}
return (
<div className={'thumbnail ' + this.props.className}>
<div className={classes} >
<a className="thumbnail-image" href={this.props.href}>
<img src={this.props.src} />
</a>
<span className="thumbnail-title"><a href={this.props.href}>{this.props.title}</a></span>
<span className="thumbnail-extra">{this.props.extra}</span>
<div className="thumbnail-title">
<a href={this.props.href}>{this.props.title}</a>
</div>
{extra}
</div>
);
}
});
module.exports = Thumbnail;

View file

@ -1,33 +1,90 @@
@import "../../colors";
.thumbnail {
.thumbnail-image,
.thumbnail-title,
.thumbnail-extra {
.thumbnail-image {
display: block;
}
.thumbnail-image img {
margin-bottom: 2px;
border: 1px solid #ddd;
border: 1px solid $ui-border;
}
$extras: ".thumbnail-creator, .thumbnail-loves, .thumbnail-remixes";
.thumbnail-title,
.thumbnail-extra {
#{$extras} {
line-height: normal;
word-wrap: break-word;
a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.thumbnail-title {
margin-bottom: 1px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
font-weight: 800;
font-size: .9230em;
font-weight: 800;
a {
display: block;
}
}
.thumbnail-extra {
#{$extras} {
color: $type-gray;
font-size: .8462em;
color: #666;
a {
display: inline;
}
}
.thumbnail-loves,
.thumbnail-remixes {
&:before {
display: inline-block;
margin-right: .1rem;
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
width: .93rem;
height: .93rem;
vertical-align: text-top;
content: "";
}
}
.thumbnail-loves:before {
background-image: url("/svgs/love/love_type-gray.svg");
}
.thumbnail-remixes:before {
background-image: url("/svgs/remix/remix_type-gray.svg");
}
&.project {
$project-width: 144px;
$project-height: 108px;
width: $project-width;
img {
width: $project-width;
height: $project-height;
}
}
&.gallery {
$gallery-width: 170px;
$gallery-height: 100px;
width: $gallery-width;
img {
width: $gallery-width;
height: $gallery-height;
}
}
}

View file

@ -0,0 +1,70 @@
var React = require('react');
var ReactIntl = require('react-intl');
var injectIntl = ReactIntl.injectIntl;
var FormattedMessage = ReactIntl.FormattedMessage;
var Box = require('../box/box.jsx');
require('./welcome.scss');
var Welcome = React.createClass({
type: 'Welcome',
propTypes: {
onDismiss: React.PropTypes.func
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
return (
<Box title={formatMessage({id: 'welcome.welcomeToScratch', defaultMessage: 'Welcome to Scratch!'})}
className="welcome"
moreTitle="x"
moreHref="#"
moreProps={{
className: 'close',
title: 'Dismiss',
onClick: this.props.onDismiss
}}>
<div className="welcome-col blue">
<h4>
<a href="/projects/editor/?tip_bar=getStarted">
<FormattedMessage
id="welcome.learn"
defaultMessage="Learn how to make a project in Scratch" />
</a>
</h4>
<a href="/projects/editor/?tip_bar=getStarted">
<img src="/images/welcome-learn.png" />
</a>
</div>
<div className="welcome-col green">
<h4>
<a href="/starter_projects/">
<FormattedMessage
id="welcome.tryOut"
defaultMessage="Try out starter projects" />
</a>
</h4>
<a href="/starter_projects/">
<img src="/images/welcome-try.png" />
</a>
</div>
<div className="welcome-col pink">
<h4>
<a href="/studios/146521/">
<FormattedMessage
id="welcome.connect"
defaultMessage="Connect with other Scratchers" />
</a>
</h4>
<a href="/studios/146521/">
<img src="/images/welcome-connect.png" />
</a>
</div>
</Box>
);
}
});
module.exports = injectIntl(Welcome);

View file

@ -0,0 +1,61 @@
@import "../../colors";
.welcome {
.box-content {
padding: 0;
}
.welcome-col {
display: inline-block;
margin: 10px 15px;
width: 150px;
height: 253px;
h4 {
margin-top: 12px;
padding: 0;
font-weight: 200;
}
> a {
display: block;
margin-top: 20px;
margin-bottom: 35px;
height: 100px;
}
$color-bars: "h4:before, > a:after";
#{$color-bars} {
display: block;
margin: 10px 0;
border-radius: 5px;
width: 100%;
height: 10px;
content: "";
}
&.blue {
#{$color-bars} {
background-color: $splash-blue;
}
a {
color: $splash-blue;
}
}
&.green {
#{$color-bars} {
background-color: $splash-green;
}
a {
color: $splash-green;
}
}
&.pink {
#{$color-bars} {
background-color: $splash-pink;
}
a {
color: $splash-pink;
}
}
}
}

6
src/environment.js Normal file
View file

@ -0,0 +1,6 @@
var Environment = {
NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
API_HOST: JSON.stringify(process.env.API_HOST || 'https://api-staging.scratch.mit.edu')
};
module.exports = Environment;

81
src/init.js Normal file
View file

@ -0,0 +1,81 @@
var api = require('./mixins/api.jsx').api;
var jar = require('./lib/jar');
var translations = require('../locales/translations.json');
/**
* -----------------------------------------------------------------------------
* Session
* -----------------------------------------------------------------------------
*/
(function () {
window._session = {};
/**
* Binds the object to private session variable and dispatches a global
* "session" event.
*
* @param {object} Session object
*
* @return {void}
*/
window.updateSession = function (session) {
window._session = session;
var sessionEvent = new CustomEvent('session', session);
window.dispatchEvent(sessionEvent);
};
/**
* Gets a session object from the local proxy method. Calls "updateSession"
* once session has been returned from the proxy.
*
* @return {void}
*/
window.refreshSession = function () {
api({
host: '',
uri: '/session/'
}, function (err, body) {
if (body.banned) {
return window.location = body.redirectUrl;
} else {
window.updateSession(body);
}
});
};
// Fetch session
window.refreshSession();
})();
/**
* -----------------------------------------------------------------------------
* L10N
* -----------------------------------------------------------------------------
*/
(function () {
/**
* Bind locale code from cookie if available. Uses navigator language API as a fallback.
*
* @return {string}
*/
function updateLocale () {
var obj = jar.get('scratchlanguage');
if (typeof obj === 'undefined') {
obj = window.navigator.userLanguage || window.navigator.language;
}
if (typeof translations[obj] === 'undefined') {
// Fall back on the split
obj = obj.split('-')[0];
}
if (typeof translations[obj] === 'undefined') {
// Language appears to not be supported return `null`
obj = null;
}
return obj;
}
window._locale = updateLocale() || 'en';
window._translations = translations;
})();

View file

@ -7,7 +7,7 @@
* Licensed under the MIT and GPL licenses.
*/
module.exports = {
var Format = {
date: function (stamp) {
stamp = (stamp || '').replace(/-/g,'/').replace(/[TZ]/g,' ');
@ -30,3 +30,5 @@ module.exports = {
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago';
}
};
module.exports = Format;

41
src/lib/jar.js Normal file
View file

@ -0,0 +1,41 @@
var cookie = require('cookie');
var xhr = require('xhr');
var Jar = {};
Jar.get = function (name, callback) {
// Get cookie by name
var obj = cookie.parse(document.cookie) || {};
// Handle optional callback
if (typeof callback === 'function') {
if (typeof obj === 'undefined') return callback('Cookie not found.');
return callback(null, obj[name]);
}
return obj[name];
};
Jar.use = function (name, uri, callback) {
// Attempt to get cookie
Jar.get(name, function (err, obj) {
if (typeof obj !== 'undefined') return callback(null, obj);
// Make XHR request to cookie setter uri
xhr({
uri: uri
}, function (err) {
if (err) return callback(err);
Jar.get(name, callback);
});
});
};
Jar.set = function (name, value) {
var obj = cookie.serialize(name, value);
var expires = '; expires=' + new Date(new Date().setYear(new Date().getFullYear() + 1)).toUTCString();
var path = '; path=/';
document.cookie = obj + expires + path;
};
module.exports = Jar;

4
src/lib/log.js Normal file
View file

@ -0,0 +1,4 @@
var minilog = require('minilog');
minilog.enable();
module.exports = minilog('www');

25
src/lib/render.jsx Normal file
View file

@ -0,0 +1,25 @@
var ReactDOM = require('react-dom');
var ReactIntl = require('react-intl');
var IntlProvider = ReactIntl.IntlProvider;
var render = function (jsx, element) {
// Get locale and messages from global namespace (see "init.js")
var locale = window._locale;
var messages = window._translations[locale];
// Render component
var component = ReactDOM.render(
<IntlProvider locale={locale} messages={messages}>
{jsx}
</IntlProvider>,
element
);
// If in production, provide list of rendered components
if (process.env.NODE_ENV != 'production') {
window._renderedComponents = window._renderedComponents || [];
window._renderedComponents.push(component);
}
};
module.exports = render;

View file

@ -1,9 +1,9 @@
var React = require('react');
var render = require('./lib/render.jsx');
require('./main.scss');
var Navigation = require('./components/navigation/navigation.jsx');
var Footer = require('./components/footer/footer.jsx');
React.render(<Navigation />, document.getElementById('navigation'));
React.render(<Footer />, document.getElementById('footer'));
render(<Navigation />, document.getElementById('navigation'));
render(<Footer />, document.getElementById('footer'));

View file

@ -1,39 +1,45 @@
@import "colors";
/* Tags */
html, body {
html,
body {
display: block;
margin: 0;
background-color: darken($ui-blue, 8%);
padding: 0;
color: #322f31;
background-color: #fdfdfd;
color: $type-gray;
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
}
/* Typography */
h1, h2, h3, h4 {
h1,
h2,
h3,
h4 {
margin: 0;
padding: 0;
border: 0;
padding: 0;
color: #554747;
color: $header-gray;
font-weight: 700;
}
h1 {
font-size: 1.625rem;
line-height: 2.125rem;
font-size: 1.625rem;
}
h4 {
font-size: 1.0rem;
line-height: 1.1rem;
font-size: 1.0rem;
}
/* Links */
a:link, a:visited, a:active {
color: #1aa0d8;
a:link,
a:visited,
a:active {
text-decoration: none;
color: $link-blue;
}
a:hover {
@ -42,14 +48,30 @@ a:hover {
/* Classes */
.inner {
width: 942px;
margin: 0 auto;
width: 942px;
}
.empty {
$bg-blue: #d9edf7;
$bg-blue-accent: #bce8f1;
border: 1px solid $bg-blue-accent;
border-radius: 5px;
background-color: $bg-blue;
padding: 10px;
text-align: center;
line-height: 2rem;
color: $type-gray;
h4 {
color: $type-gray;
}
}
#view {
min-height: 768px;
padding: 20px 0;
/* NOTE: Margin should match height in navigation.scss */
margin-top: 50px;
background-color: $background-color;
padding: 20px 0;
min-height: 768px;
}

View file

@ -1,14 +1,51 @@
var defaults = require('lodash.defaults');
var xhr = require('xhr');
module.exports = {
var jar = require('../lib/jar.js');
var log = require('../lib/log.js');
var CookieMixinFactory = require('./cookieMixinFactory.jsx');
var Api = {
mixins: [
// Provides useScratchcsrftoken
CookieMixinFactory('scratchcsrftoken', '/csrf_token/')
],
api: function (opts, callback) {
xhr(opts, function (err, res, body) {
if (err) {
// emit global "error" event
return callback(err);
}
// @todo Global error handler
callback(err, body);
defaults(opts, {
host: process.env.API_HOST,
headers: {},
json: {},
useCsrf: false
});
defaults(opts.headers, {
'X-Requested-With': 'XMLHttpRequest'
});
opts.uri = opts.host + opts.uri;
var apiRequest = function (opts) {
xhr(opts, function (err, res, body) {
if (err) log.error(err);
callback(err, body);
});
}.bind(this);
if (typeof jar.get('scratchlanguage') !== 'undefined') {
opts.headers['Accept-Language'] = jar.get('scratchlanguage') + ', en;q=0.8';
}
if (opts.useCsrf) {
this.useScratchcsrftoken(function (err, csrftoken) {
if (err) return log.error('Error while retrieving CSRF token', err);
opts.json.csrftoken = csrftoken;
opts.headers['X-CSRFToken'] = csrftoken;
apiRequest(opts);
}.bind(this));
} else {
apiRequest(opts);
}
}
};
module.exports = Api;

View file

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

View file

@ -1,10 +1,18 @@
module.exports = {
var Session = {
getInitialState: function () {
return {
session: {}
session: window._session
};
},
updateSession: function () {
this.setState({'session': window._session});
},
componentWillMount: function () {
// @todo Fetch session from API
window.addEventListener('session', this.updateSession);
},
componentWillUnmount: function () {
window.removeEventListener('session', this.updateSession);
}
};
module.exports = Session;

87
src/scripts/build-locales Executable file
View file

@ -0,0 +1,87 @@
#!/usr/bin/env node
/*
Converts the existing .po translation files in the module to JSON files.
Requires po2json in order to work. Takes as input a directory
in which to store the resulting json translation files.
*/
var fs = require('fs');
var glob = require('glob');
var path = require('path');
var po2icu = require('po2icu');
/*
Existing translations should be in the key value format specified by react-intl (i.e.
formatted message id, with icu string as the value). New Translations should be in the
format returned by po2icu (i.e. a source language icu string for key, and a localized
language icu string for value).
ICU Map is an object in the reverse react-intl formatting (icu string as key), which will
help determine if the translation belongs in www currently.
*/
var mergeNewTranslations = function (existingTranslations, newTranslations, icuMap) {
for (var id in newTranslations) {
if (icuMap.hasOwnProperty(id) && newTranslations[id].length > 0) {
existingTranslations[icuMap[id]] = newTranslations[id];
}
}
return existingTranslations;
};
var args = process.argv.slice(2);
if (!args.length) {
process.stdout.write('A destination directory must be specified.');
process.exit(1);
}
var poUiDir = path.resolve(__dirname, '../../node_modules/scratchr2_translations/ui');
var outputFile = path.resolve(__dirname, '../../', args[0]);
// Create the directory if it doesn't exist.
var fileInfo = path.parse(outputFile);
try {
fs.accessSync(fileInfo.dir, fs.F_OK);
} catch (err) {
// Doesn't exist create it.
fs.mkdirSync(fileInfo.dir);
}
var icuTemplateFile = path.resolve(__dirname, '../../en.json');
var idsWithICU = JSON.parse(fs.readFileSync(icuTemplateFile, 'utf8'));
var icuWithIds = {};
for (var id in idsWithICU) {
icuWithIds[idsWithICU[id]] = id;
}
var locales = {
en: idsWithICU
};
// Get ui localization strings first
glob(poUiDir + '/*', function (err, files) {
if (err) throw new Error(err);
files.forEach(function (file) {
var lang = file.split('/').pop();
var jsFile = path.resolve(file, 'LC_MESSAGES/djangojs.po');
var pyFile = path.resolve(file, 'LC_MESSAGES/django.po');
var translations = {};
try {
var jsTranslations = po2icu.poFileToICUSync(lang, jsFile);
translations = mergeNewTranslations(translations, jsTranslations, icuWithIds);
} catch (err) {
process.stdout.write(lang + ': ' + err + '\n');
}
try {
var pyTranslations = po2icu.poFileToICUSync(lang, pyFile);
translations = mergeNewTranslations(translations, pyTranslations, icuWithIds);
} catch (err) {
process.stdout.write(lang + ': ' + err + '\n');
}
locales[lang] = translations;
});
fs.writeFileSync(outputFile, JSON.stringify(locales, null, 4));
});

View file

@ -1,8 +1,10 @@
var React = require('react');
var render = require('../../lib/render.jsx');
require('./about.scss');
var View = React.createClass({
var About = React.createClass({
type: 'About',
render: function () {
return (
<div>
@ -12,4 +14,4 @@ var View = React.createClass({
}
});
React.render(<View />, document.getElementById('view'));
render(<About />, document.getElementById('view'));

View file

@ -1,5 +1,7 @@
var React = require('react');
var render = require('../../lib/render.jsx');
var Activity = require('../../components/activity/activity.jsx');
var Box = require('../../components/box/box.jsx');
var Button = require('../../components/forms/button.jsx');
var Carousel = require('../../components/carousel/carousel.jsx');
@ -8,7 +10,8 @@ var Input = require('../../components/forms/input.jsx');
require('./components.scss');
var View = React.createClass({
var Components = React.createClass({
type: 'Components',
render: function () {
return (
<div className="inner">
@ -30,9 +33,13 @@ var View = React.createClass({
title="Carousel component in a box!">
<Carousel />
</Box>
<h1>{'What\'s Happening??'}</h1>
<Activity />
<h1>{'Nothing!!!'}</h1>
<Activity items={[]} />
</div>
);
}
});
React.render(<View />, document.getElementById('view'));
render(<Components />, document.getElementById('view'));

147
src/views/hoc/hoc.jsx Normal file
View file

@ -0,0 +1,147 @@
var classNames = require('classnames');
var React = require('react');
var render = require('../../lib/render.jsx');
require('./hoc.scss');
var Button = require('../../components/forms/button.jsx');
var Box = require('../../components/box/box.jsx');
var Hoc = React.createClass({
type: 'Hoc',
getInitialState: function () {
return {
bgClass: ''
};
},
onCardEnter: function (bgClass) {
this.setState({
bgClass: bgClass
});
},
render: function () {
var classes = classNames(
'top-banner',
this.state.bgClass
);
return (
<div>
<div className={classes}>
<h1>Get Creative with Coding</h1>
<p>
With Scratch, you can program your own stories, games, and animations
and share them online.
</p>
<div className="card-deck">
<div className="card">
<a href="/projects/editor/?tip_bar=name">
<div className="card-info" onMouseEnter={this.onCardEnter.bind(this, 'name-bg')}>
<img src="/images/name-tutorial.jpg" />
<Button>Animate Your Name</Button>
</div>
</a>
</div>
<div className="card" onMouseEnter={this.onCardEnter.bind(this, 'wbb-bg')}>
<a href="/hide">
<div className="card-info">
<img src="/images/hide-seek-tutorial.jpg" />
<Button> Hide-and-Seek Game</Button>
</div>
</a>
</div>
<div className="card" onMouseEnter={this.onCardEnter.bind(this, 'dance-bg')}>
<a href="/projects/editor/?tip_bar=dance">
<div className="card-info">
<img src="/images/dance-tutorial.jpg" />
<Button>Dance, Dance, Dance</Button>
</div>
</a>
</div>
</div>
<ul className="sub-nav">
<li className="info">Find out more:</li>
<a href="/about"><li className="link">About Scratch</li></a>
<a href="/parents"><li className="link">For Parents</li></a>
<a href="/educators"><li className="link">For Educators</li></a>
</ul>
</div>
<div className="inner">
<Box>
<section className="one-up">
<div className="column">
<h3>Activity Cards and Guides</h3>
<p>
Want tips and ideas for your Hour-of-Code activities?&nbsp;
View and print activity cards and facilitator guides.
<br />
For more resources, see <a href="/help">Scratch Help</a>.
</p>
</div>
<div className="resource">
<img src="/svgs/tips-card.svg" />
<div className="resource-info">
<h5>Animate Your Name</h5>
<a href="#">Activity Cards</a>
<a href="#">Facilitator Guide</a>
</div>
</div>
<div className="resource">
<img src="/svgs/tips-card.svg" />
<div className="resource-info">
<h5>Hide-and-Seek</h5>
<a href="#">Activity Cards</a>
<a href="#">Facilitator Guide</a>
</div>
</div>
<div className="resource">
<img src="/svgs/tips-card.svg" />
<div className="resource-info">
<h5>Dance, Dance, Dance</h5>
<a href="#">Activity Cards</a>
<a href="#">Facilitator Guide</a>
</div>
</div>
</section>
<section className="two-up">
<div className="column">
<h3>Tips Window</h3>
<p>
Need help getting started? Looking for ideas?&nbsp;
You can find tutorials and helpful hints in the
<br />
<a href="/projects/editor/?tip_bar=home">Tips Window</a>
</p>
</div>
<div className="column">
<img src="/images/tips-test-animation.gif" />
</div>
</section>
</Box>
<section className="one-up">
<h3>Collaborators</h3>
<div className="logos">
<img src="/images/code-org-logo.png" />
<img src="/images/cn-logo.png" />
<img src="/images/paa-logo.png" />
<img src="/images/pocketcode-logo.png" />
</div>
</section>
</div>
</div>
);
}
});
render(<Hoc />, document.getElementById('view'));

245
src/views/hoc/hoc.scss Normal file
View file

@ -0,0 +1,245 @@
@import "../../colors";
$base-bg: $ui-white;
#view {
padding: 0;
// To be integrated into the Global Typography standards
p {
line-height: 2em;
}
// To be revamped in Global Grids standards
.inner {
margin: 0 auto;
width: 80%;
max-width: 960px;
.box {
margin-bottom: 10px;
}
}
.top-banner {
transition: background-image .5s ease, background-color .5s ease;
margin-top: 10px;
margin-bottom: 40px;
background-color: $ui-aqua;
background-position: center;
background-size: cover;
padding: 10px 0;
width: 100%;
&.wbb-bg {
background-image: url("/images/hide-bg.jpg");
}
&.dance-bg {
background-image: url("/images/dance-bg.jpg");
}
&.name-bg {
background-image: url("/images/name-bg.jpg");
}
h1,
p {
margin: 0 auto;
padding-top: 10px;
max-width: 500px;
text-align: center;
color: $type-white;
}
.card-deck,
.sub-nav {
display: flex;
margin: 20px auto;
width: 80%;
max-width: 960px;
justify-content: center;
flex-wrap: wrap;
}
.card-deck {
.card {
display: inline-block;
margin: 10px;
border-radius: 7px;
background-color: $active-gray;
padding: 2px;
width: 30%;
min-width: 200px;
max-width: 230px;
.card-info {
border-radius: 5px;
background-color: $base-bg;
width: 100%;
height: 100%;
button,
img {
width: calc(100% - 20px);
}
img {
margin: 10px 10px 5px 10px;
border-radius: 5px;
}
button {
margin: 0 10px 10px 10px;
}
}
}
}
.sub-nav {
color: $type-white;
font-size: .8em;
font-weight: bold;
li {
display: inline-block;
margin: 5px;
padding: .75em 1em;
list-style-type: none;
}
a .link {
border: 2px solid $active-gray;
border-radius: 50px;
text-decoration: none;
color: $type-white;
&:hover {
transition: background-color .25s ease;
border-color: transparent;
background-color: $active-gray;
}
&:active {
border: 0 solid transparent;
box-shadow: inset 0 0 5px $box-shadow-gray;
background-color: $active-dark-gray;
padding: calc(.75em + 2px) calc(1em + 2px);
}
}
}
}
section {
display: flex;
margin: 0 auto;
border-bottom: 1px solid $ui-border;
padding: 30px 0;
width: 95%;
justify-content: center;
flex-wrap: wrap;
align-items: center;
&:last-child {
border-bottom: 0;
}
h3,
p {
font-weight: 300;
}
.logos {
margin: 10px 0;
width: 100%;
img {
margin: 0 20px;
max-width: 200px;
max-height: 75px;
vertical-align: middle;
}
}
.resource {
display: flex;
margin: 10px;
border-radius: 5px;
padding: 10px 15px;
width: 30%;
min-width: 200px;
max-width: 230px;
text-align: left;
justify-content: center;
align-items: center;
img {
margin-right: 15px;
}
h5 {
margin: 8px 0;
font-weight: 500;
}
a {
display: block;
margin: 5px 0;
font-size: .8em;
}
}
&.one-up {
text-align: center;
.column {
margin: 10px;
width: 100%;
}
.logo {
display: block;
}
}
&.two-up {
.column {
margin: 10px;
min-width: 200px;
max-width: 40%;
img {
border-radius: 5px;
width: 100%;
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,26 @@
var injectIntl = require('react-intl').injectIntl;
var omit = require('lodash.omit');
var React = require('react');
var render = require('../../lib/render.jsx');
var Api = require('../../mixins/api.jsx');
var Session = require('../../mixins/session.jsx');
var Activity = require('../../components/activity/activity.jsx');
var AdminPanel = require('../../components/adminpanel/adminpanel.jsx');
var Banner = require('../../components/banner/banner.jsx');
var Box = require('../../components/box/box.jsx');
var Button = require('../../components/forms/button.jsx');
var Carousel = require('../../components/carousel/carousel.jsx');
var Intro = require('../../components/intro/intro.jsx');
var Modal = require('../../components/modal/modal.jsx');
var News = require('../../components/news/news.jsx');
var Welcome = require('../../components/welcome/welcome.jsx');
require('./splash.scss');
var View = React.createClass({
var Splash = injectIntl(React.createClass({
type: 'Splash',
mixins: [
Api,
Session
@ -21,34 +30,336 @@ var View = React.createClass({
projectCount: 10569070,
activity: [],
news: [],
featured: require('./featured.json')
featuredCustom: {},
featuredGlobal: {},
showEmailConfirmationModal: false
};
},
componentDidUpdate: function (prevProps, prevState) {
if (this.state.session.user != prevState.session.user) {
if (this.state.session.user) {
this.getActivity();
this.getFeaturedCustom();
this.getNews();
} else {
this.setState({featuredCustom: []});
this.setState({activity: []});
this.setState({news: []});
this.getProjectCount();
}
if (this.shouldShowEmailConfirmation()) {
window.addEventListener('message', this.onMessage);
} else {
window.removeEventListener('message', this.onMessage);
}
}
},
componentDidMount: function () {
// @todo API request for News
// @todo API request for Activity
// @todo API request for Featured
this.getFeaturedGlobal();
if (this.state.session.user) {
this.getActivity();
this.getFeaturedCustom();
this.getNews();
} else {
this.getProjectCount();
}
if (this.shouldShowEmailConfirmation()) window.addEventListener('message', this.onMessage);
},
componentWillUnmount: function () {
window.removeEventListener('message', this.onMessage);
},
onMessage: function (e) {
if (e.origin != window.location.origin) return;
if (e.source != this.refs.emailConfirmationiFrame.contentWindow) return;
if (e.data == 'resend-done') {
this.hideEmailConfirmationModal();
} else {
var data = JSON.parse(e.data);
if (data['action'] === 'leave-page') {
window.location.href = data['uri'];
}
}
},
getActivity: function () {
this.api({
uri: '/proxy/users/' + this.state.session.user.username + '/activity?limit=5'
}, function (err, body) {
if (!err) this.setState({activity: body});
}.bind(this));
},
getFeaturedGlobal: function () {
this.api({
uri: '/proxy/featured'
}, function (err, body) {
if (!err) this.setState({featuredGlobal: body});
}.bind(this));
},
getFeaturedCustom: function () {
this.api({
uri: '/proxy/users/' + this.state.session.user.id + '/featured'
}, function (err, body) {
if (!err) this.setState({featuredCustom: body});
}.bind(this));
},
getNews: function () {
this.api({
uri: '/news?limit=3'
}, function (err, body) {
if (!err) this.setState({news: body});
}.bind(this));
},
getProjectCount: function () {
this.api({
uri: '/projects/count/all'
}, function (err, body) {
if (!err) this.setState({projectCount: body.count});
}.bind(this));
},
showEmailConfirmationModal: function () {
this.setState({emailConfirmationModalOpen: true});
},
hideEmailConfirmationModal: function () {
this.setState({emailConfirmationModalOpen: false});
},
handleDismiss: function (cue) {
this.api({
host: '',
uri: '/site-api/users/set-template-cue/',
method: 'post',
useCsrf: true,
json: {cue: cue, value: false}
}, function (err) {
if (!err) window.refreshSession();
});
},
shouldShowWelcome: function () {
if (!this.state.session.user || !this.state.session.flags.show_welcome) return false;
return (
new Date(this.state.session.user.dateJoined) >
new Date(new Date - 2*7*24*60*60*1000) // Two weeks ago
);
},
shouldShowEmailConfirmation: function () {
return (
this.state.session.user && this.state.session.flags.has_outstanding_email_confirmation &&
this.state.session.flags.confirm_email_banner);
},
renderHomepageRows: function () {
var formatMessage = this.props.intl.formatMessage;
var rows = [
<Box
title={formatMessage({
id: 'splash.featuredProjects',
defaultMessage: 'Featured Projects'})}
key="community_featured_projects">
<Carousel items={this.state.featuredGlobal.community_featured_projects} />
</Box>,
<Box
title={formatMessage({
id: 'splash.featuredStudios',
defaultMessage: 'Featured Studios'})}
key="community_featured_studios">
<Carousel items={this.state.featuredGlobal.community_featured_studios}
settings={{slidesToShow: 4, slidesToScroll: 4, lazyLoad: false}} />
</Box>
];
if (this.state.featuredGlobal.curator_top_projects &&
this.state.featuredGlobal.curator_top_projects.length > 4) {
rows.push(
<Box
key="curator_top_projects"
title={
'Projects Curated by ' +
this.state.featuredGlobal.curator_top_projects[0].curator_name}
moreTitle={formatMessage({id: 'general.learnMore', defaultMessage: 'Learn More'})}
moreHref="/studios/386359/">
<Carousel
items={this.state.featuredGlobal.curator_top_projects} />
</Box>
);
}
if (this.state.featuredGlobal.scratch_design_studio &&
this.state.featuredGlobal.scratch_design_studio.length > 4) {
rows.push(
<Box
key="scratch_design_studio"
title={
formatMessage({
id: 'splash.scratchDesignStudioTitle',
defaultMessage: 'Scratch Design Studio' })
+ ' - ' + this.state.featuredGlobal.scratch_design_studio[0].gallery_title}
moreTitle={formatMessage({id: 'splash.visitTheStudio', defaultMessage: 'Visit the studio'})}
moreHref={'/studios/' + this.state.featuredGlobal.scratch_design_studio[0].gallery_id + '/'}>
<Carousel
items={this.state.featuredGlobal.scratch_design_studio} />
</Box>
);
}
if (this.state.session.user &&
this.state.featuredGlobal.community_newest_projects &&
this.state.featuredGlobal.community_newest_projects.length > 0) {
rows.push(
<Box
title={
formatMessage({
id: 'splash.recentlySharedProjects',
defaultMessage: 'Recently Shared Projects' })}
key="community_newest_projects">
<Carousel
items={this.state.featuredGlobal.community_newest_projects} />
</Box>
);
}
if (this.state.featuredCustom.custom_projects_by_following &&
this.state.featuredCustom.custom_projects_by_following.length > 0) {
rows.push(
<Box title={
formatMessage({
id: 'splash.projectsByScratchersFollowing',
defaultMessage: 'Projects by Scratchers I\'m Following'})}
key="custom_projects_by_following">
<Carousel items={this.state.featuredCustom.custom_projects_by_following} />
</Box>
);
}
if (this.state.featuredCustom.custom_projects_loved_by_following &&
this.state.featuredCustom.custom_projects_loved_by_following.length > 0) {
rows.push(
<Box title={
formatMessage({
id: 'splash.projectsLovedByScratchersFollowing',
defaultMessage: 'Projects Loved by Scratchers I\'m Following'})}
key="custom_projects_loved_by_following">
<Carousel items={this.state.featuredCustom.custom_projects_loved_by_following} />
</Box>
);
}
if (this.state.featuredCustom.custom_projects_in_studios_following &&
this.state.featuredCustom.custom_projects_in_studios_following.length > 0) {
rows.push(
<Box title={
formatMessage({
id:'splash.projectsInStudiosFollowing',
defaultMessage: 'Projects in Studios I\'m Following'})}
key="custom_projects_in_studios_following">
<Carousel items={this.state.featuredCustom.custom_projects_in_studios_following} />
</Box>
);
}
rows.push(
<Box title={
formatMessage({
id: 'splash.communityRemixing',
defaultMessage: 'What the Community is Remixing' })}
key="community_most_remixed_projects">
<Carousel items={this.state.featuredGlobal.community_most_remixed_projects} showRemixes={true} />
</Box>,
<Box title={
formatMessage({
id: 'splash.communityLoving',
defaultMessage: 'What the Community is Loving' })}
key="community_most_loved_projects">
<Carousel items={this.state.featuredGlobal.community_most_loved_projects} showLoves={true} />
</Box>
);
return rows;
},
render: function () {
var featured = this.renderHomepageRows();
var emailConfirmationStyle = {width: 500, height: 330, padding: 1};
return (
<div className="inner">
<Intro projectCount={this.state.projectCount} />
<div className="splash-header">
<Activity />
<News />
<div className="splash">
{this.shouldShowEmailConfirmation() ? [
<Banner key="confirmedEmail"
className="warning"
onRequestDismiss={this.handleDismiss.bind(this, 'confirmed_email')}>
<a href="#" onClick={this.showEmailConfirmationModal}>Confirm your email</a>
{' '}to enable sharing.{' '}
<a href="/info/faq/#accounts">Having trouble?</a>
</Banner>,
<Modal key="emailConfirmationModal"
isOpen={this.state.emailConfirmationModalOpen}
onRequestClose={this.hideEmailConfirmationModal}
style={{content: emailConfirmationStyle}}>
<iframe ref="emailConfirmationiFrame"
src="/accounts/email_resend_standalone/"
{...omit(emailConfirmationStyle, 'padding')} />
</Modal>
] : []}
<div key="inner" className="inner">
{this.state.session.user ? [
<div key="header" className="splash-header">
{this.shouldShowWelcome() ? [
<Welcome key="welcome" onDismiss={this.handleDismiss.bind(this, 'welcome')}/>
] : [
<Activity key="activity" items={this.state.activity} />
]}
<News items={this.state.news} />
</div>
] : [
<Intro projectCount={this.state.projectCount} key="intro"/>
]}
{featured}
<AdminPanel>
<dt>Tools</dt>
<dd>
<ul>
<li>
<a href="/scratch_admin/tickets">Ticket Queue</a>
</li>
<li>
<a href="/scratch_admin/ip-search/">IP Search</a>
</li>
<li>
<a href="/scratch_admin/email-search/">Email Search</a>
</li>
</ul>
</dd>
<dt>Homepage Cache</dt>
<dd>
<ul className="cache-list">
<li>
<form
id="homepage-refresh-form"
method="post"
action="/scratch_admin/homepage/clear-cache/">
<div className="button-row">
<span>Refresh row data:</span>
<Button type="submit">
<span>Refresh</span>
</Button>
</div>
</form>
</li>
</ul>
</dd>
</AdminPanel>
</div>
{this.state.featured.map(function (set) {
return (
<Box
className="featured"
title={set.title}>
<Carousel items={set.items} />
</Box>
);
})}
</div>
);
}
});
}));
React.render(<View />, document.getElementById('view'));
render(<Splash />, document.getElementById('view'));

View file

@ -5,7 +5,7 @@
flex-wrap: nowrap;
justify-content: space-between;
.activity {
.box {
display: inline-block;
width: calc(60% - 20px);
}

BIN
static/images/cn-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
static/images/dance-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
static/images/dropdown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/images/hide-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/images/name-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
static/images/og_image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

BIN
static/images/paa-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

42
static/js/lib/react-dom.js vendored Normal file
View file

@ -0,0 +1,42 @@
/**
* ReactDOM v0.14.0
*
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
// Based off https://github.com/ForbesLindesay/umd/blob/master/template.js
;(function(f) {
// CommonJS
if (typeof exports === "object" && typeof module !== "undefined") {
module.exports = f(require('react'));
// RequireJS
} else if (typeof define === "function" && define.amd) {
define(['react'], f);
// <script>
} else {
var g
if (typeof window !== "undefined") {
g = window;
} else if (typeof global !== "undefined") {
g = global;
} else if (typeof self !== "undefined") {
g = self;
} else {
// works providing we're not in "use strict";
// needed for Java 8 Nashorn
// see https://github.com/facebook/react/issues/3037
g = this;
}
g.ReactDOM = f(g.React);
}
})(function(React) {
return React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
});

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