Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
Italy 2018-08-20 13:11:59 -04:00
commit 9d171a6880
278 changed files with 9745 additions and 2543 deletions

View file

@ -5,3 +5,4 @@ intl/*
locales/* locales/*
**/*.min.js **/*.min.js
**/node_modules/* **/node_modules/*
scratch-gui/*

1
.gitignore vendored
View file

@ -28,3 +28,4 @@ ENV
/test/integration-cypress/cypress/screenshots /test/integration-cypress/cypress/screenshots
/test/integration-cypress/cypress/videos /test/integration-cypress/cypress/videos
/test/integration-cypress/package-lock.json /test/integration-cypress/package-lock.json
/test/integration/node_modules/*

View file

@ -18,10 +18,27 @@ env:
- API_HOST_VAR=API_HOST_$TRAVIS_BRANCH - API_HOST_VAR=API_HOST_$TRAVIS_BRANCH
- API_HOST=${!API_HOST_VAR} - API_HOST=${!API_HOST_VAR}
- API_HOST=${API_HOST:-$API_HOST_STAGING} - API_HOST=${API_HOST:-$API_HOST_STAGING}
# EB_AWS_ACCESS_KEY_ID - ASSET_HOST_master=https://assets.scratch.mit.edu
- secure: A138rYuXDsOmpEwYxZ31WyXEeq5fgr9qyqsQh1nTFsjBKpFtNM+CN9e0QJQFT3PLs4wH/lWTRSyHxakxKQS1sxq828f9gHed+f15REKk/fRUplcCYIexT9xKVtU3D8CRNn/KBFWk75fZyZt20eyOVIv4h3pInKQz7y84J6PWzB1BCrAFvADrzS1X68Z3NJJLyxnz0YEurzz8mC2v4D0s/XifKTWvRtefD4QM6pE0C2iYyk+ThrLwg7i9FDHVfo0MrkgcdX7mz37SnTr7p7mHWnGXrGngi/NiDRQ+Uwwq/sr2UIww0rCwS1xsOcS//dC4NNqrrt1kUTsoC1Yt87Ny+gI0nUplsfEpdKajAkOYdANC5bJUGqPdSlOds1v9aJs9Hx48uGamWkm/3cFmoJ5uA2ZzUwbSGjTkWbnhwzT0YRvcLGhP1WE/EswaIyK5qMp522E79mP1yH6M750iUvi4N39+QW1BNX3ADkOwyAI67ArX5on5gWP83RXcJ15im7XsBpsmVn/KXi6AouWPb8jmSmKCj0QZCzfLY7ivM42IugYpK2NV7kFB38DpXQamJ5eskgwYa3elRmednIFUuwb1QDnONvJogVjk4CLmoSxssC2mJnnrUItM7l8G6As81GMI+6lTtl86hAuXBjUk60FMbgTAQDX9ll26LgpBy8jHSx8= - ASSET_HOST_STAGING=https://assets.scratch.ly
# EB_AWS_SECRET_ACCESS_KEY - ASSET_HOST_VAR=ASSET_HOST_$TRAVIS_BRANCH
- secure: EX1fyov+f6ytWN2ZSL4dLslwrVkp6Ho/uoSLO38/qNG3XdGmBN4VprxddcQiWfo+Mrg3GdWcfcM/VazhhStBi1uLfZiw3RHZaSGuWbiuD2EtzqtlC+OVvoajgy91QFajh9Zzuwa0rYbEPd/sw01R53NoWJYl0GSteWk7C8Wv6anl4FUJCqgvvTV2ZEcyTtGcVJgUhKi1MfNpTSM6JWBy0DWszcyxj7C8LSs1+l9ZjAtnlUBWY13HsrNu8G5d+FwqGHZLUAjdu2O602wxV897/xLARLduZ+01ALpVefNEEGMB1Wd+xMw4dm2B0Uk86a4TBRCeOgJZ1yoJoPpGPOHTo+dgNXcU8ReszGVoy7uOjFWwu82FQq8gzfcf75yzaRJgG8/BJ6BkJfa0EmFg3iO5CwixQyHR5+CqsedtoLAWVT8zlOfQ/Z6yx4Pm7jXQSOkyvo09YJ2QIn4IFGPvwOVS7Firzi+fLl8GYApeSV9G10e1IzA4pPrKdJMRA4qRMPt9zJGq7ZO1J/d9aW/5KIsJUDnodnl7yXJyDMOyNeljT9I82ciHZcURxRRY080vrW6dgNJE1V9jxBhWEvr2iCeWMMedWaGuC41I7K9L79eW8lmaE+cQ+OZrzpOJP4GbfmIiXrh+0M4ChL/xBpjtiFwpNdkCXXhzWMnjJ4wCrii4yuc= - ASSET_HOST=${!ASSET_HOST_VAR}
- ASSET_HOST=${ASSET_HOST:-$ASSET_HOST_STAGING}
- BACKPACK_HOST_master=https://backpack.scratch.mit.edu
- BACKPACK_HOST_STAGING=https://backpack.scratch.ly
- BACKPACK_HOST_VAR=BACKPACK_HOST_$TRAVIS_BRANCH
- BACKPACK_HOST=${!BACKPACK_HOST_VAR}
- BACKPACK_HOST=${BACKPACK_HOST:-$BACKPACK_HOST_STAGING}
- ROOT_URL_master=https://scratch.mit.edu
- ROOT_URL_STAGING=https://scratch.ly
- ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH
- ROOT_URL=${!ROOT_URL_VAR}
- ROOT_URL=${ROOT_URL:-$ROOT_URL_STAGING}
- PROJECT_HOST_master=https://projects.scratch.mit.edu
- PROJECT_HOST_STAGING=https://projects.scratch.ly
- PROJECT_HOST_VAR=PROJECT_HOST_$TRAVIS_BRANCH
- PROJECT_HOST=${!PROJECT_HOST_VAR}
- PROJECT_HOST=${PROJECT_HOST:-$PROJECT_HOST_STAGING}
- PATH=$PATH:$PWD/test/integration/node_modules/chromedriver/bin
- AWS_ACCESS_KEY_ID=$EB_AWS_ACCESS_KEY_ID - AWS_ACCESS_KEY_ID=$EB_AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY=$EB_AWS_SECRET_ACCESS_KEY - AWS_SECRET_ACCESS_KEY=$EB_AWS_SECRET_ACCESS_KEY
- FASTLY_ACTIVATE_CHANGES=true - FASTLY_ACTIVATE_CHANGES=true
@ -58,6 +75,7 @@ env:
- NODE_ENV=production - NODE_ENV=production
- WWW_VERSION=${TRAVIS_COMMIT:0:5} - WWW_VERSION=${TRAVIS_COMMIT:0:5}
addons: addons:
chrome: stable
apt: apt:
sources: sources:
- ubuntu-toolchain-r-test - ubuntu-toolchain-r-test
@ -66,20 +84,34 @@ addons:
install: install:
- sudo -H pip install -r requirements.txt - sudo -H pip install -r requirements.txt
- npm --production=false install - npm --production=false install
deploy: - npm --production=false update
- provider: script jobs:
skip_cleanup: $SKIP_CLEANUP include:
script: env make sync - stage: test
on: deploy:
repo: LLK/scratch-www - provider: script
branch: skip_cleanup: $SKIP_CLEANUP
- develop script: env make sync
- hotfix/* on:
- release/* repo: LLK/scratch-www
- provider: script branch:
skip_cleanup: $SKIP_CLEANUP - develop
script: env make sync - hotfix/*
on: - release/*
repo: LLK/scratch-www - provider: script
branch: skip_cleanup: $SKIP_CLEANUP
- master script: env make sync
on:
repo: LLK/scratch-www
branch:
- master
- stage: smoke
install:
- cd test/integration
- npm install
- cd -
script: npm run smoke-sauce
stages:
- test
- name: smoke
if: type != pull_request

View file

@ -128,14 +128,31 @@ source_file = src/views/messages/l10n.json
source_lang = en source_lang = en
type = KEYVALUEJSON type = KEYVALUEJSON
[scratch-website.conference-index-l10njson]
file_filter = localizations/conference-index/<lang>.json
source_file = src/views/conference/2018/index/l10n.json
source_lang = en
type = KEYVALUEJSON
[scratch-website.preview-faq-l10njson] [scratch-website.preview-faq-l10njson]
file_filter = localizations/preview-faq/<lang>.json file_filter = localizations/preview-faq/<lang>.json
source_file = src/views/preview-faq/l10n.json source_file = src/views/preview-faq/l10n.json
source_lang = en source_lang = en
type = KEYVALUEJSON type = KEYVALUEJSON
[scratch-website.research-l10njson]
file_filter = localizations/research/<lang>.json
source_file = src/views/research/l10n.json
source_lang = en
type = KEYVALUEJSON
[scratch-website.preview-l10njson]
file_filter = localizations/preview/<lang>.json
source_file = src/views/preview/l10n.json
source_lang = en
type = KEYVALUEJSON
[scratch-website.ev3-l10njson]
source_file = src/views/ev3/l10n.json
source_lang = en
type = KEYVALUEJSON
[scratch-website.microbit-l10njson]
source_file = src/views/microbit/l10n.json
source_lang = en
type = KEYVALUEJSON

10
Dockerfile Normal file
View file

@ -0,0 +1,10 @@
FROM node:8
RUN mkdir -p /var/app/current
WORKDIR /var/app/current
COPY . ./
RUN rm -rf ./node_modules
RUN npm install
EXPOSE 8333

View file

@ -1,10 +1,10 @@
ESLINT=./node_modules/.bin/eslint ESLINT=./node_modules/.bin/eslint
NODE=node NODE= NODE_OPTIONS=--max_old_space_size=8000 node
SASSLINT=./node_modules/.bin/sass-lint -v SASSLINT=./node_modules/.bin/sass-lint -v
S3CMD=s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600 S3CMD=s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600
TAP=./node_modules/.bin/tap TAP=./node_modules/.bin/tap
WATCH=./node_modules/.bin/watch WATCH= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/watch
WEBPACK=./node_modules/.bin/webpack WEBPACK= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/webpack
# ------------------------------------ # ------------------------------------
@ -71,6 +71,9 @@ integration:
smoke: smoke:
$(TAP) ./test/integration/smoke-testing/*.js --timeout=3600 $(TAP) ./test/integration/smoke-testing/*.js --timeout=3600
smoke-verbose:
$(TAP) ./test/integration/smoke-testing/*.js --timeout=3600 -R spec
localization: localization:
$(TAP) ./test/localization/*.js $(TAP) ./test/localization/*.js

View file

@ -9,7 +9,7 @@
### Where am I? ### Where am I?
Physically? No idea. Physically? No idea.
Digitally? Youre at Scratchs open source web client! Digitally? Youre at Scratchs open source web client!
Were working to update the [Scratch website](https://scratch.mit.edu) to use a new codebase, contained in this repository. Were working to update the [Scratch website](https://scratch.mit.edu) to use a new codebase, contained in this repository.
@ -23,7 +23,7 @@ Were currently building Scratch using [React](https://facebook.github.io/reac
### Before Getting Started ### Before Getting Started
* Make sure you have node (v4.2 or higher) and npm [installed](https://docs.npmjs.com/getting-started/installing-node) * Make sure you have node (v4.2 or higher) and npm [installed](https://docs.npmjs.com/getting-started/installing-node)
We use npm (Node Package Manager) to maintain and update packages required to build the site. We use npm (Node Package Manager) to maintain and update packages required to build the site.
### Update Packages ### Update Packages
It's important to make sure that all of the dependencies are up to date because the scratch-www code only works with specific versions of the dependencies. You can update the packages by running this command: It's important to make sure that all of the dependencies are up to date because the scratch-www code only works with specific versions of the dependencies. You can update the packages by running this command:
@ -81,14 +81,17 @@ To stop the process that is making the site available to your web browser (creat
`npm start` can be configured with the following environment variables `npm start` can be configured with the following environment variables
| Variable | Default | Description | | Variable | Default | Description |
| ------------- | ----------------------------- | ---------------------------------------------- | | --------------- | ---------------------------------- | ---------------------------------------------- |
| `API_HOST` | `https://api.scratch.mit.edu` | Hostname for API requests | | `API_HOST` | `https://api.scratch.mit.edu` | Hostname for API requests |
| `SENTRY_DSN` | `''` | DSN for Sentry | | `ASSETS_HOST` | `https://assets.scratch.mit.edu` | Hostname for asset requests |
| `FALLBACK` | `''` | Pass-through location for old site | | `BACKPACK_HOST` | `https://backpack.scratch.mit.edu` | Hostname for backpack requests |
| `GA_TRACKER` | `''` | Where to log Google Analytics data | | `PROJECTS_HOST` | `https://projects.scratch.mit.edu` | Hostname for project requests |
| `NODE_ENV` | `null` | If not `production`, app acts like development | | `SENTRY_DSN` | `''` | DSN for Sentry |
| `PORT` | `8333` | Port for devserver (http://localhost:XXXX) | | `FALLBACK` | `''` | Pass-through location for old site |
| `GA_TRACKER` | `''` | Where to log Google Analytics data |
| `NODE_ENV` | `null` | If not `production`, app acts like development |
| `PORT` | `8333` | Port for devserver (http://localhost:XXXX) |
**NOTE:** Because by default `API_HOST=https://api.scratch.mit.edu`, please be aware that, by default, you will be seeing and interacting with real data on the Scratch website. **NOTE:** Because by default `API_HOST=https://api.scratch.mit.edu`, please be aware that, by default, you will be seeing and interacting with real data on the Scratch website.
@ -98,6 +101,9 @@ npm test
``` ```
### To Deploy ### To Deploy
Deploying to staging or production will upload code to S3 and configure Fastly.
```bash ```bash
npm install npm install
virtualenv ENV virtualenv ENV
@ -132,3 +138,35 @@ Additionally, if you set `FALLBACK=https://scratch.mit.edu`, be aware that click
#### Windows #### Windows
Some users have experienced difficulties when trying to get our web client to work on Windows. One solution could be to use [Cygwin](https://www.cygwin.com/). If that doesn't work, you might want to use [Wubi](https://wiki.ubuntu.com/WubiGuide) (Windows XP, Vista, 7) or [Wubiuefi](https://github.com/hakuna-m/wubiuefi) (Windows 8 or higher). Wubi(uefi) is a Windows Installer for Ubuntu that allows you to have Ubuntu and Windows on one disk, without the need of an extra partition. Some users have experienced difficulties when trying to get our web client to work on Windows. One solution could be to use [Cygwin](https://www.cygwin.com/). If that doesn't work, you might want to use [Wubi](https://wiki.ubuntu.com/WubiGuide) (Windows XP, Vista, 7) or [Wubiuefi](https://github.com/hakuna-m/wubiuefi) (Windows 8 or higher). Wubi(uefi) is a Windows Installer for Ubuntu that allows you to have Ubuntu and Windows on one disk, without the need of an extra partition.
#### Docker
_This section is only relevant to the Scratch Team since it requires access to private repositories, so is not usable by 3rd party contributors._
A set of [Docker](https://www.docker.com/what-docker) related files are provided to create isolated [container](https://www.docker.com/what-container) environments suitable for end-to-end local development:
* Dockerfile
* docker-compose.yml
* docker_entrypoint.sh
##### Docker Quick Start (CLI)
Make sure you already have the Scratch REST API running locally in its docker environment.
```
$ docker-compose up
```
After this has launched you will be able to access a running copy of `scratch-www` on port 8333 via `http://localhost:8333`
##### Docker Configuration
`Dockerfile` defines how a `scratch-www` docker image is created.
`docker-compose.yml` takes care of launching `scratch-www` into a development environment that is composed of other components, such as the Scratch REST API server and the legacy Scratch code. If you have not already setup the Scratch REST API in your local environment, you will need to modify `docker-compose.yml` by removing `external: true` from:
```yaml
networks:
scratchapi_scratch_network:
external: true
```

View file

@ -1,7 +1 @@
The Scratch trademarks, including the Scratch name, logo, the Scratch Cat, Gobo, Pico, Nano, Tera and Giga graphics (the "Marks"), are property of the Massachusetts Institute of Technology (MIT), and the use of the Marks is governed by this policy. The Scratch trademarks, including the Scratch name, logo, the Scratch Cat, Gobo, Pico, Nano, Tera and Giga graphics (the "Marks"), are property of the Massachusetts Institute of Technology (MIT). Marks may not be used to endorse or promote products derived from this software without specific prior written permission.
You may use the Marks to refer to Scratch in Substantially Unmodified form.
"Substantially Unmodified" means the source code provided by MIT, possibly with minor modifications including but not limited to: bug fixes (including security), changing the locations of files for better integration with the host operating system, adding documentation, and changes to the dynamic linking of libraries.
A version is not "Substantially Unmodified" if it incorporates features not present in a release of Scratch by MIT. If you do make a substantial modification, to avoid confusion with versions of Scratch produced by MIT you must remove all Marks from your version of the software and refrain from using any of the Marks to refer to your version.

View file

@ -176,7 +176,7 @@ async.auto({
if (err) throw new Error(err); if (err) throw new Error(err);
if (process.env.FASTLY_ACTIVATE_CHANGES) { if (process.env.FASTLY_ACTIVATE_CHANGES) {
fastly.activateVersion(results.version, function (e, resp) { fastly.activateVersion(results.version, function (e, resp) {
if (err) throw new Error(e); if (e) throw new Error(e);
process.stdout.write('Successfully configured and activated version ' + resp.number + '\n'); process.stdout.write('Successfully configured and activated version ' + resp.number + '\n');
if (process.env.FASTLY_PURGE_ALL) { if (process.env.FASTLY_PURGE_ALL) {
fastly.purgeAll(FASTLY_SERVICE_ID, function (error) { fastly.purgeAll(FASTLY_SERVICE_ID, function (error) {

View file

@ -21,7 +21,18 @@ routes.forEach(route => {
app.get(route.pattern, handler(route)); app.get(route.pattern, handler(route));
}); });
app.use(webpackDevMiddleware(compiler)); var middlewareOptions = {};
if (process.env.USE_DOCKER_WATCHOPTIONS) {
middlewareOptions = {
watchOptions: {
aggregateTimeout: 500,
poll: 2500,
ignored: ['node_modules', 'build']
}
};
}
app.use(webpackDevMiddleware(compiler, middlewareOptions));
var proxyHost = process.env.FALLBACK || ''; var proxyHost = process.env.FALLBACK || '';
if (proxyHost !== '') { if (proxyHost !== '') {

39
docker-compose.yml Normal file
View file

@ -0,0 +1,39 @@
version: '3.4'
volumes:
npm_data:
runtime_data:
networks:
scratch-api_scratch_network:
external: true
services:
app:
container_name: scratch-www-app
hostname: scratch-www-app
environment:
- API_HOST=http://localhost:8491
- FALLBACK=http://localhost:8080
- USE_DOCKER_WATCHOPTIONS=true
build:
context: ./
dockerfile: Dockerfile
image: scratch-www:latest
command: ./docker_entrypoint.sh npm start
volumes:
- type: bind
source: ./
target: /var/app/current
volume:
nocopy: true
- type: bind
source: ../scratch-gui
target: /var/app/current/scratch-gui
volume:
nocopy: true
- npm_data:/var/app/current/node_modules
- runtime_data:/runtime
ports:
- "8333:8333"
networks:
- scratch-api_scratch_network

11
docker_entrypoint.sh Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
echo "App Entrypoint"
if [ ! -f /runtime/.translations ]; then
echo "Generating intl/translations"
make translations
touch /runtime/.translations
fi
exec "$@"

View file

@ -34,6 +34,7 @@
"is": "Íslenska", "is": "Íslenska",
"it": "Italiano", "it": "Italiano",
"kn": "ಭಾಷೆ-ಹೆಸರು", "kn": "ಭಾಷೆ-ಹೆಸರು",
"kk": "Қазақша",
"rw": "Kinyarwanda", "rw": "Kinyarwanda",
"ht": "Kreyòl", "ht": "Kreyòl",
"ku": "Kurdî", "ku": "Kurdî",

View file

@ -6,7 +6,9 @@
"start": "make start", "start": "make start",
"stop": "make stop", "stop": "make stop",
"test": "make test", "test": "make test",
"smoke": "make smoke", "smoke": "tap ./test/integration/smoke-testing/*.js --timeout=3600",
"smoke-verbose": "tap ./test/integration/smoke-testing/*.js --timeout=3600 -R spec",
"smoke-sauce": "SMOKE_REMOTE=true tap ./test/integration/smoke-testing/*.js --timeout=60000",
"watch": "make watch", "watch": "make watch",
"build": "make build", "build": "make build",
"dev": "make watch && make start &" "dev": "make watch && make start &"
@ -28,10 +30,13 @@
"express-http-proxy": "1.1.0", "express-http-proxy": "1.1.0",
"lodash.defaults": "4.0.1", "lodash.defaults": "4.0.1",
"newrelic": "1.25.4", "newrelic": "1.25.4",
"raven": "0.10.0" "raven": "0.10.0",
"scratch-parser": "^4.2.0",
"scratch-storage": "^0.5.1"
}, },
"devDependencies": { "devDependencies": {
"ajv": "6.4.0", "ajv": "6.4.0",
"approximate-number": "2.0.0",
"async": "1.5.2", "async": "1.5.2",
"autoprefixer": "6.3.6", "autoprefixer": "6.3.6",
"babel-cli": "6.26.0", "babel-cli": "6.26.0",
@ -72,6 +77,7 @@
"lodash.merge": "3.3.2", "lodash.merge": "3.3.2",
"lodash.omit": "3.1.0", "lodash.omit": "3.1.0",
"lodash.range": "3.0.1", "lodash.range": "3.0.1",
"lodash.truncate": "4.4.2",
"minilog": "2.0.8", "minilog": "2.0.8",
"node-dir": "0.1.16", "node-dir": "0.1.16",
"node-sass": "4.6.1", "node-sass": "4.6.1",
@ -88,11 +94,13 @@
"react-redux": "5.0.7", "react-redux": "5.0.7",
"react-responsive": "3.0.0", "react-responsive": "3.0.0",
"react-slick": "0.16.0", "react-slick": "0.16.0",
"react-string-replace": "0.4.1",
"react-telephone-input": "4.3.4", "react-telephone-input": "4.3.4",
"redux": "3.5.2", "redux": "3.5.2",
"redux-thunk": "2.0.1", "redux-thunk": "2.0.1",
"sass-lint": "1.5.1", "sass-lint": "1.5.1",
"sass-loader": "6.0.6", "sass-loader": "6.0.6",
"scratch-gui": "latest",
"scratchr2_translations": "git://github.com/LLK/scratchr2_translations.git#master", "scratchr2_translations": "git://github.com/LLK/scratchr2_translations.git#master",
"slick-carousel": "1.6.0", "slick-carousel": "1.6.0",
"source-map-support": "0.3.2", "source-map-support": "0.3.2",

View file

@ -1,45 +1,54 @@
/* UI Primary Colors */ /* UI Primary Colors */
$ui-blue: hsla(200, 90, 55, 1); // #25AFF4 $ui-blue: hsla(215, 100, 65, 1); // #4C97FF Motion Primary
$ui-orange: hsla(35, 90, 55, 1); // #F49D25 $ui-blue-dark: hsla(215, 65, 55, 1); // #3373CC Motion Secondary
$ui-blue-10percent: hsla(215, 100, 65, .1);
$ui-blue-25percent: hsla(215, 100, 65, .25);
$ui-orange: hsla(38, 100, 55, 1); // #FFAB19 Control Primary
$ui-orange-10percent: hsla(35, 90, 55, .1);
$ui-orange-25percent: hsla(35, 90, 55, .25);
$ui-light-gray: hsla(0, 0, 98, 1); //#FAFAFA $ui-light-gray: hsla(0, 0, 98, 1); //#FAFAFA
$ui-gray: hsla(0, 0, 95, 1); //#F2F2F2 $ui-gray: hsla(0, 0, 95, 1); //#F2F2F2
$ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3 $ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3
$background-color: hsla(0, 0, 99, 1); //#FDFDFD $background-color: hsla(0, 0, 99, 1); //#FDFDFD
/* UI Secondary Colors */ /* UI Secondary Colors */
$ui-aqua: hsla(170, 70, 50, 1); //#26D9BB /* 3.0 colors */
$ui-purple: hsla(265, 55, 55, 1); //#824DCB /* Using www naming convention for now, should be consistent with gui */
$ui-yellow: hsla(45, 100, 50, 1); //#FFBF00 $ui-aqua: hsla(163, 85, 40, 1); // #0FBD8C Extension Primary
$ui-white: #fff; $ui-purple: hsla(260, 100, 70, 1); // #9966FF Looks Primary
$ui-purple-dark: hsla(260, 60, 60, 1); // #774DCB Looks Secondary
$ui-yellow: hsla(45, 100, 50, 1); // #FFBF00 Control Primary
$ui-coral: hsla(350, 100, 70, 1); // #FF6680 More Blocks Primary
$ui-coral-dark: hsla(350, 100, 60, 1); // #FF3355 More Blocks tertiary
$ui-white: hsla(0, 100%, 100%, 1); //#FFF
$ui-white-15percent: hsla(0, 100%, 100%, .15); //#FFF
$ui-light-primary: hsl(215, 100, 95);
$ui-border: hsla(0, 0, 85, 1); //#D9D9D9 $ui-border: hsla(0, 0, 85, 1); //#D9D9D9
/* 3.0 colors */ /* modals */
/* Using www naming convention for now, should be consistent with gui */ $ui-mint-green: hsl(163, 69, 44);
$ui-green: hsla(163, 83, 40, 1); //#0fbd8c Pen Primary $ui-light-mint: hsl(163, 53, 67);
$ui-coral: hsla(350, 100, 70, 1); //#FF6680 More Priamry
$ui-blue-10percent: hsla(215, 100, 65, .1);
$ui-orange-25percent: hsla(35, 90, 55, .25);
/* Overlay UI Gray Colors */ /* Overlay UI Gray Colors */
$active-gray: hsla(0, 0, 0, .1); $active-gray: hsla(0, 0, 0, .1);
$active-dark-gray: hsla(0, 0, 0, .2); $active-dark-gray: hsla(0, 0, 0, .2);
$box-shadow-gray: hsla(0, 0, 0, .25); $box-shadow-gray: hsla(0, 0, 0, .25);
$overlay-gray: hsla(0, 0, 0, .75); $overlay-gray: hsla(0, 0, 0, .75);
$transparent-light-blue: rgba(229, 240, 254, 0);
/* Typography Colors */ /* Typography Colors */
$header-gray: hsla(0, 0, 42, 1); //#6B6B6B $header-gray: hsla(225, 15, 40, 1); //#575E75
$type-gray: hsla(0, 0, 42, 1); //#6B6B6B $type-gray: hsla(225, 15, 40, 1); //#575E75
$type-white: #fff; $type-gray-75percent: hsla(225, 15, 40, .75);
$type-white: hsla(0, 100, 100, 1); //#FFF
$link-blue: $ui-blue; $link-blue: $ui-blue;
/* Component colors */
$splash-green: #9c0;
$splash-pink: #c2479d;
$splash-blue: #199ed7;
/* Down Deep */ /* Down Deep */
$dd-darkblue: hsla(195, 72.4, 17.1, 1); $dd-darkblue: hsla(195, 72.4, 17.1, 1);

View file

@ -0,0 +1,30 @@
const PropTypes = require('prop-types');
const React = require('react');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
require('./extension-landing.scss');
const ExtensionHeader = props => (
<div className="extension-header">
<FlexRow className="inner">
<FlexRow className="column extension-info">
{props.children}
</FlexRow>
<div className="extension-image">
<img
alt={props.imageAlt}
src={props.imageSrc}
/>
</div>
</FlexRow>
</div>
);
ExtensionHeader.propTypes = {
children: PropTypes.node,
imageAlt: PropTypes.string,
imageSrc: PropTypes.string
};
module.exports = ExtensionHeader;

View file

@ -0,0 +1,25 @@
const bindAll = require('lodash.bindall');
const React = require('react');
const OS_ENUM = require('./os-enum.js');
class ExtensionLanding extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'onSetOS'
]);
this.state = {
OS: OS_ENUM.WINDOWS
};
}
onSetOS (os) {
this.setState({
OS: os
});
}
}
module.exports = ExtensionLanding;

View file

@ -0,0 +1,261 @@
@import "../../colors";
@import "../../frameless";
#view {
padding: 0;
}
.extension-landing {
&>div {
padding: 4rem 0;
}
h2 {
margin-bottom: 1rem;
}
h3 {
margin-bottom: 2rem;
}
span {
line-height: 1.7rem;
}
hr {
margin: 4rem 0;
border-width: 1px 0 0 0;
border-style: solid;
border-color: $ui-border;
width: 100%;
}
.download {
display: inline-block;
&::after {
display: inline-block;
margin-left: .5rem;
background-image: url("/svgs/extensions/download.svg");
background-repeat: no-repeat;
width: 20px;
height: 20px;
vertical-align: text-top;
content: "";
}
}
.screenshot {
border-radius: .5rem;
}
.tip-box {
margin-top: 4rem;
border: 1px solid $ui-blue-25percent;
border-radius: 1rem;
background-color: $ui-blue-10percent;
padding: 2rem 3rem;
width: 100%;
box-sizing: border-box;
.tip-content {
align-items: flex-start;
p {
margin-top: 0;
}
}
}
.extension-header {
background-size: cover;
color: $ui-white;
.inner {
justify-content: space-between;
flex-wrap: nowrap;
}
.extension-info {
max-width: $cols7;
align-items: flex-start;
.extension-copy {
margin-bottom: 5rem;
align-items: flex-start;
h2 {
display: flex;
margin-bottom: 2rem;
color: $ui-white;
}
h2 img {
padding-right: .5rem;
max-height: 100%;
}
span {
font-size: 1.2rem;
}
a {
border-bottom: 1px solid $ui-white;
color: $ui-white;
}
}
.extension-requirements-container {
font-weight: 500;
align-items: flex-start;
.requirements-header {
margin-bottom: 1.5rem;
}
.extension-requirements {
justify-content: space-between;
}
.extension-requirements span {
display: flex;
margin-right: 1rem;
font-size: 15px; // TODO: change to rem later
align-items: center;
}
.extension-requirements span img {
padding-right: .5rem;
}
}
}
.extension-image {
width: 100%;
max-width: $cols5;
img {
max-width: 100%;
max-height: 100%;
}
}
}
.os-chooser {
padding: 0;
}
.install-scratch-link {
padding: 2rem 0;
.inner {
align-items: flex-start;
}
.step-image.badge {
height: initial;
}
.download-button {
display: flex;
align-items: center;
img {
margin-left: .5rem;
}
}
}
.extension-section {
.inner {
align-items: flex-start;
}
}
.getting-started {
.getting-started-section {
width: 100%;
align-items: flex-start;
a {
margin: 1rem 0;
}
}
}
.things-to-try .inner {
align-items: center;
}
.project-card {
margin: 0 1.5rem;
border: 1px solid $ui-border;
border-radius: .5rem;
background-color: $ui-white;
overflow: hidden;
flex-basis: 0;
flex-grow: 1;
}
.project-card-image {
img {
max-width: 100%;
}
}
.project-card-info {
padding: 1rem;
p {
margin: .2rem 0;
}
}
.faq {
p {
margin-bottom: 1.25rem;
margin-left: 0;
max-width: $cols8;
text-align: left;
}
.faq-title {
margin-bottom: 0;
font-size: 1.4rem;
}
ul {
max-width: $cols8;
}
section {
ul {
max-width: $cols8;
}
.nav-spacer {
display: block;
visibility: hidden;
margin-top: -50px; // height of nav bar
height: 50px;
}
}
ul,
ol {
&.indented {
padding-left: $cols1 + (20px / $em);
}
}
}
.blue {
background-color: $ui-blue-10percent;
}
.inner {
max-width: $cols12;
}
}

View file

@ -0,0 +1,24 @@
const PropTypes = require('prop-types');
const FormattedMessage = require('react-intl').FormattedMessage;
const React = require('react');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
require('./extension-landing.scss');
const ExtensionRequirements = props => (
<FlexRow className="column extension-requirements-container">
<span className="requirements-header">
<FormattedMessage id="extensionHeader.requirements" />
</span>
<FlexRow className="extension-requirements">
{props.children}
</FlexRow>
</FlexRow>
);
ExtensionRequirements.propTypes = {
children: PropTypes.node
};
module.exports = ExtensionRequirements;

View file

@ -0,0 +1,22 @@
const PropTypes = require('prop-types');
const classNames = require('classnames');
const React = require('react');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
require('./extension-landing.scss');
const ExtensionSection = props => (
<div className={classNames('extension-section', props.className)}>
<FlexRow className="inner column">
{props.children}
</FlexRow>
</div>
);
ExtensionSection.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};
module.exports = ExtensionSection;

View file

@ -0,0 +1,69 @@
const PropTypes = require('prop-types');
const FormattedMessage = require('react-intl').FormattedMessage;
const React = require('react');
const OS_ENUM = require('./os-enum.js');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const Steps = require('../../components/steps/steps.jsx');
const Step = require('../../components/steps/step.jsx');
require('./extension-landing.scss');
const InstallScratchLink = ({
currentOS
}) => (
<div className="blue install-scratch-link">
<FlexRow className="inner column">
<h2><FormattedMessage id="installScratchLink.installHeaderTitle" /></h2>
<Steps>
<div className="step">
<Step
compact
number={1}
>
<span className="step-description">
<FormattedMessage id="installScratchLink.downloadAndInstall" />
</span>
<a
className="step-image badge"
href={`https://downloads.scratch.mit.edu/link/${
currentOS === OS_ENUM.WINDOWS ? 'windows' : 'mac'
}.zip`}
>
<button className="button download-button">
{currentOS === OS_ENUM.WINDOWS ?
<FormattedMessage id="installScratchLink.windowsDownload" /> :
<FormattedMessage id="installScratchLink.macosDownload" />
}
<img src="/svgs/extensions/download-white.svg" />
</button>
</a>
</Step>
</div>
<Step
compact
number={2}
>
<span className="step-description">
<FormattedMessage id="installScratchLink.startScratchLink" />
</span>
<div className="step-image">
<img
className="screenshot"
src={`/images/scratchlink/${
currentOS === OS_ENUM.WINDOWS ? 'windows' : 'mac'
}-toolbar.png`}
/>
</div>
</Step>
</Steps>
</FlexRow>
</div>
);
InstallScratchLink.propTypes = {
currentOS: PropTypes.string
};
module.exports = InstallScratchLink;

View file

@ -0,0 +1,6 @@
const OS_ENUM = {
WINDOWS: 'Windows',
MACOS: 'macOS'
};
module.exports = OS_ENUM;

View file

@ -0,0 +1,27 @@
const PropTypes = require('prop-types');
const React = require('react');
const ProjectCard = props => (
<a
download
className="project-card"
href={props.cardUrl}
>
<div className="project-card-image">
<img src={props.imageSrc} />
</div>
<div className="project-card-info">
<h4>{props.title}</h4>
<p>{props.description}</p>
</div>
</a>
);
ProjectCard.propTypes = {
cardUrl: PropTypes.string,
description: PropTypes.string,
imageSrc: PropTypes.string,
title: PropTypes.string
};
module.exports = ProjectCard;

View file

@ -0,0 +1,20 @@
const PropTypes = require('prop-types');
const React = require('react');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const TipBox = props => (
<div className="tip-box">
<h4>{props.title}</h4>
<FlexRow className="column tip-content">
{props.children}
</FlexRow>
</div>
);
TipBox.propTypes = {
children: PropTypes.node,
title: PropTypes.string
};
module.exports = TipBox;

View file

@ -11,6 +11,75 @@ require('../footer.scss');
const ConferenceFooter = props => ( const ConferenceFooter = props => (
<FooterBox> <FooterBox>
<div className="collaborators">
<h4>Sponsors</h4>
<FlexRow as="ul">
<li className="odl">
<a href="https://odl.mit.edu/">
<img
alt="MIT Office of Digital Learning"
src="/images/conference/footer/2018/mit-ol-logo.png"
/>
</a>
</li>
<li className="google">
<a href="http://www.google.com/">
<img
alt="Google"
src="/images/conference/footer/2018/google.png"
/>
</a>
</li>
<li className="epam">
<a href="https://www.epam.com/">
<img
alt="EPAM Systems"
src="/images/conference/footer/2018/epam.png"
/>
</a>
</li>
<li className="intel">
<a href="http://www.intel.com/content/www/us/en/homepage.html">
<img
alt="Intel"
src="/images/conference/footer/2018/intel.png"
/>
</a>
</li>
<li className="lego">
<a href="http://www.legofoundation.com/">
<img
alt="The LEGO Foundation"
src="/images/conference/footer/2018/lego-foundation.png"
/>
</a>
</li>
<li className="siegel">
<a href="http://www.siegelendowment.org/">
<img
alt="Siegel Family Endowment"
src="/images/conference/footer/2018/siegel.png"
/>
</a>
</li>
<li className="cartoon-network">
<a href="https://www.cartoonnetwork.com/">
<img
alt="Cartoon Network"
src="/images/conference/footer/2018/cartoon-network.png"
/>
</a>
</li>
<li className="scratchfoundation">
<a href="http://www.scratchfoundation.org/">
<img
alt="Scratch Foundation"
src="/images/conference/footer/2018/scratch-foundation.png"
/>
</a>
</li>
</FlexRow>
</div>
<FlexRow className="scratch-links"> <FlexRow className="scratch-links">
<div className="family"> <div className="family">
<h4><FormattedMessage id="footer.scratchFamily" /></h4> <h4><FormattedMessage id="footer.scratchFamily" /></h4>
@ -121,16 +190,28 @@ const ConferenceFooter = props => (
</li> </li>
<li> <li>
<a <a
href="http://medium.com/scratchfoundation-blog" href="https://medium.com/scratchteam-blog"
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
<img <img
alt="scratch foundation blog" alt="scratch team blog"
src="/images/conference/footer/medium.png" src="/images/conference/footer/medium.png"
/> />
</a> </a>
</li> </li>
<li>
<a
href="https://scratch-foundation.myshopify.com/"
rel="noopener noreferrer"
target="_blank"
>
<img
alt="scratch store"
src="/images/conference/footer/shopify-white.svg"
/>
</a>
</li>
</FlexRow> </FlexRow>
</div> </div>
</div> </div>

View file

@ -17,19 +17,14 @@
width: 100%; width: 100%;
ul { ul {
justify-content: space-between; margin: 5px 0;
align-items: center; align-items: center;
justify-content: space-between;
img { img {
margin: 20px 0; margin: 20px 0;
max-width: 180px; max-width: 180px;
max-height: 25px; max-height: 30px;
}
}
.nostarch {
img {
max-height: 40px;
} }
} }
@ -38,6 +33,17 @@
max-height: 50px; max-height: 50px;
} }
} }
.siegel {
img {
max-height: 60px;
}
}
.cartoon-network {
img {
min-height: 50px;
}
}
} }
.scratch-links { .scratch-links {

View file

@ -108,7 +108,7 @@ const Footer = props => (
</a> </a>
</dd> </dd>
<dd> <dd>
<a href="https://wiki.scratch.mit.edu/"> <a href="https://en.scratch-wiki.info/">
<FormattedMessage id="general.wiki" /> <FormattedMessage id="general.wiki" />
</a> </a>
</dd> </dd>

View file

@ -8,31 +8,21 @@ $pass-bg: $ui-aqua;
display: inline-block; display: inline-block;
margin: .5em 0; margin: .5em 0;
border: 0; border: 0;
border-radius: 5px; border-radius: .5rem;
box-shadow: 0 1px 1px $box-shadow-gray;
background-color: $ui-blue; background-color: $ui-blue;
cursor: pointer; cursor: pointer;
padding: .75em 1em; padding: 1em 1.25em;
color: $type-white; color: $type-white;
font-size: .8rem; font-size: .8rem;
font-weight: bold; font-weight: bold;
/* USER BUTTON STATES */ /* USER BUTTON STATES */
&:hover {
box-shadow: 0 2px 2px $box-shadow-gray;
}
&:active {
box-shadow: inset 0 1px 2px $box-shadow-gray;
}
&:focus { &:focus {
outline: none; outline: none;
} }
/* DATA BUTTON STATES */ /* DATA BUTTON STATES */
&.white { &.white {
border-top: 1px inset $active-gray;
background-color: $base-bg; background-color: $base-bg;
color: $ui-blue; color: $ui-blue;
} }

View file

@ -2,7 +2,7 @@
.char-count { .char-count {
letter-spacing: 1px; letter-spacing: 1px;
color: lighten($type-gray, 30%); color: $type-gray-75percent;
font-weight: 500; font-weight: 500;
&.overmax { &.overmax {

View file

@ -0,0 +1,72 @@
const bindAll = require('lodash.bindall');
const React = require('react');
const PropTypes = require('prop-types');
const FRCInput = require('formsy-react-components').Input;
const FRCTextarea = require('formsy-react-components').Textarea;
const classNames = require('classnames');
require('./row.scss');
require('./inplace-input.scss');
class InplaceInput extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleBlur',
'setRef'
]);
}
handleBlur (name, value) {
if (this.inputRef.props.errorMessages.length === 0) {
const jsonData = {};
jsonData[name] = value;
this.props.handleUpdate(jsonData);
}
}
setRef (input) {
this.inputRef = input;
}
render () {
const {
className,
type,
handleUpdate, // eslint-disable-line no-unused-vars
...props
} = this.props;
return (
(type === 'textarea') ?
<FRCTextarea
className="inplace-textarea"
componentRef={this.setRef}
elementWrapperClassName="grow"
label={null}
rowClassName={classNames('textarea-row no-label', className)}
onBlur={this.handleBlur}
{...props}
/> :
<FRCInput
className="inplace-input"
componentRef={this.setRef}
rowClassName={classNames(
className,
'no-label'
)}
onBlur={this.handleBlur}
{...props}
/>
);
}
}
InplaceInput.propTypes = {
className: PropTypes.string,
handleUpdate: PropTypes.func.isRequired,
type: PropTypes.string
};
InplaceInput.defaultProps = {
type: 'text',
value: ''
};
module.exports = InplaceInput;

View file

@ -0,0 +1,69 @@
@import "../../colors";
@import "../../frameless";
.inplace-input {
transition: all .5s ease;
border: 2px dashed $ui-blue-25percent;
border-radius: 8px;
background-color: transparent;
padding: 0 1rem;
width: calc(100% - 2.25rem);
color: $type-gray;
&:focus {
transition: all .5s ease;
outline: none;
border: 2px solid $ui-blue;
box-shadow: 0 0 0 4px $ui-blue-25percent;
}
&.fail {
border: 1px solid $ui-orange;
}
&.pass {
border: 1px solid $active-dark-gray;
}
/* IE10/11-specific style resets */
&::-ms-reveal, &::-ms-clear {
display: none;
}
&::placeholder {
font-style: italic;
}
}
.inplace-textarea {
transition: all 1s ease;
border: 2px dashed $ui-blue-25percent;
border-radius: 8px;
background-color: $ui-light-gray;
padding: .75rem 1rem;
width: 100%;
line-height: 1.75em;
color: $type-gray;
font-size: 1rem;
box-sizing: border-box;
resize: none;
&:focus {
transition: all 1s ease;
outline: none;
border: 2px solid $ui-blue;
box-shadow: 0 0 0 4px $ui-blue-25percent;
}
&.fail {
border: 1px solid $ui-orange;
}
&::placeholder {
padding-top: 1rem;
text-align: center;
font-style: italic;
}
}

View file

@ -2,7 +2,6 @@
@import "../../frameless"; @import "../../frameless";
$base-bg: $ui-light-gray; $base-bg: $ui-light-gray;
$pass-bg: lighten($ui-aqua, 35%);
.row { .row {
label { label {
@ -32,8 +31,7 @@ $pass-bg: lighten($ui-aqua, 35%);
} }
&.pass { &.pass {
border: 1px solid $active-dark-gray; border: 1px solid $ui-aqua;
background-color: $pass-bg;
} }
/* IE10/11-specific style resets */ /* IE10/11-specific style resets */

View file

@ -22,4 +22,8 @@
&.fail { &.fail {
border: 1px solid $ui-orange; border: 1px solid $ui-orange;
} }
&::placeholder {
font-style: italic;
}
} }

View file

@ -28,8 +28,8 @@ module.exports.validationHOCFactory = defaultValidationErrors => (Component => {
<Component <Component
validationErrors={defaults( validationErrors={defaults(
{}, {},
defaultValidationErrors, props.validationErrors,
props.validationErrors defaultValidationErrors
)} )}
{...omit(props, ['validationErrors'])} {...omit(props, ['validationErrors'])}
/> />

View file

@ -21,7 +21,7 @@
.thumbnail { .thumbnail {
margin: 7px; margin: 7px;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 0 3px $box-shadow-gray; box-shadow: 0 0 0 1px $active-gray;
background-color: $ui-white; background-color: $ui-white;
padding-bottom: 4px; padding-bottom: 4px;
width: $thumbnail-width; width: $thumbnail-width;

View file

@ -6,11 +6,9 @@
@import "../../colors"; @import "../../colors";
@import "../../frameless"; @import "../../frameless";
$ui-secondary: darken($ui-blue, 10%);
.title-banner { .title-banner {
&.masthead { &.masthead {
background-color: $ui-secondary; background-color: $ui-blue-dark;
padding-bottom: .5rem; padding-bottom: .5rem;
h1 { h1 {

View file

@ -94,37 +94,37 @@
&.sprite-1 { &.sprite-1 {
.circle { .circle {
background-color: $splash-green; background-color: $ui-aqua;
} }
.text { .text {
top: 60px; top: 60px;
left: 50px; left: 50px;
color: $splash-green; color: $ui-aqua;
} }
} }
&.sprite-2 { &.sprite-2 {
.circle { .circle {
background-color: $splash-pink; background-color: $ui-purple;
} }
.text { .text {
top: 77px; top: 77px;
left: 50px; left: 50px;
color: $splash-pink; color: $ui-purple;
} }
} }
&.sprite-3 { &.sprite-3 {
.circle { .circle {
background-color: $splash-blue; background-color: $ui-blue;
} }
.text { .text {
top: 37px; top: 37px;
left: 45px; left: 45px;
color: $splash-blue; color: $ui-blue;
} }
.subtext { .subtext {
@ -145,19 +145,19 @@
&.sprite-1 { &.sprite-1 {
.circle { .circle {
box-shadow: 0 0 10px 2px $splash-green; box-shadow: 0 0 10px 2px $ui-aqua;
} }
} }
&.sprite-2 { &.sprite-2 {
.circle { .circle {
box-shadow: 0 0 10px 2px $splash-pink; box-shadow: 0 0 10px 2px $ui-purple;
} }
} }
&.sprite-3 { &.sprite-3 {
.circle { .circle {
box-shadow: 0 0 10px 2px $splash-blue; box-shadow: 0 0 10px 2px $ui-blue;
} }
} }
} }

View file

@ -0,0 +1,72 @@
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
const AddToStudioModalPresentation = require('./presentation.jsx');
class AddToStudioModal extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleRequestClose',
'handleSubmit'
]);
this.state = {
waitingToClose: false
};
}
componentWillUpdate () {
this.closeIfFinishedUpdating();
}
hasOutstandingUpdates () {
return (this.props.studios.some(studio => (studio.hasRequestOutstanding === true)));
}
closeIfFinishedUpdating () {
if (this.state.waitingToClose === true && this.hasOutstandingUpdates() === false) {
this.closeAndStopWaiting();
}
}
// before closing, set waitingToClose to false. That way, if user reopens
// modal, it won't unexpectedly close.
closeAndStopWaiting () {
this.setState({waitingToClose: false}, () => {
this.props.onRequestClose();
});
}
handleRequestClose () {
this.closeAndStopWaiting();
}
handleSubmit () {
this.setState({waitingToClose: true}, () => {
this.closeIfFinishedUpdating();
});
}
render () {
return (
<AddToStudioModalPresentation
isOpen={this.props.isOpen}
studios={this.props.studios}
waitingToClose={this.state.waitingToClose}
onRequestClose={this.handleRequestClose}
onSubmit={this.handleSubmit}
onToggleStudio={this.props.onToggleStudio}
/>
);
}
}
AddToStudioModal.propTypes = {
isOpen: PropTypes.bool,
onRequestClose: PropTypes.func,
onToggleStudio: PropTypes.func,
studios: PropTypes.arrayOf(PropTypes.object)
};
module.exports = AddToStudioModal;

View file

@ -0,0 +1,189 @@
@import "../../../colors";
@import "../../../frameless";
.mod-addToStudio * {
box-sizing: border-box;
}
.mod-addToStudio {
margin: 100px auto;
outline: none;
padding: 0;
width: 36.25rem; /* 580px; */
height: 388px; /* 24.25rem; */
overflow: hidden;
user-select: none;
}
.addToStudio-modal-header {
box-shadow: inset 0 -1px 0 0 $ui-blue-dark;
background-color: $ui-blue;
padding-top: .75rem;
width: 100%;
height: 3rem;
box-sizing: border-box;
}
.addToStudio-content-label {
text-align: center;
color: $type-white;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1rem;
font-weight: bold;
}
.addToStudio-modal-content {
margin: 0 auto;
width: 100%;
font-size: .875rem;
}
.studio-list-outer-scrollbox {
position: relative;
background-color: $ui-blue-10percent;
}
.studio-list-inner-scrollbox {
margin-right: .5rem;
padding-right: .5rem;
height: 16.9375rem;
overflow: scroll;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: $active-dark-gray;
height: 92px;
}
&::-webkit-scrollbar-track {
margin-top: 8px;
margin-bottom: 10px;
}
}
.studio-list-container {
display: flex;
padding: .40625rem 0 0 1.46875rem;
justify-content: flex-start;
flex-flow: row wrap;
}
/* NOTE: force scrolling: add to above:
min-height: 30rem;
*/
.studio-list-bottom-gradient {
position: absolute;
right: 1rem;
bottom: 0;
left: 0;
background: linear-gradient(
$transparent-light-blue,
$ui-light-primary
);
height: 32px;
pointer-events: none; /* pass clicks through to buttons underneath */
}
.studio-selector-button {
display: flex;
position: relative;
margin: .21875rem .21875rem;
border-radius: .5rem;
background-color: $ui-white;
padding: 0;
width: 16.1875rem; /* 259px */
height: 2.5rem;
box-sizing: border-box;
justify-content: space-between;
}
.studio-selector-button-text {
position: absolute;
/* per spec, should be:
margin: .375rem 2.18375rem .375rem .6875rem
but in practice, our css seems to vertically align text to top, where
invision spec aligned to middle.
*/
margin: .575rem 2.18375rem .175rem .6875rem;
width: 13.3125rem;
height: 1rem; /* diff from spec, in case we ever do valign to middle */
line-height: 1.25rem;
font-family: "Helvetica Neue";
font-size: .875rem;
font-weight: regular;
}
.studio-selector-button-selected {
background-color: $ui-mint-green;
color: $ui-white;
}
.studio-selector-button-waiting {
background-color: $ui-light-mint;
color: $ui-white;
}
.studio-selector-button-text-selected {
color: $ui-white;
}
.studio-selector-button-text-unselected {
color: $type-gray;
}
.studio-status-icon {
position: absolute;
margin: .5rem .625rem .5rem 14.0625rem;
border-radius: .75rem;
padding: .0625rem .075rem;
width: 1.5rem;
height: 1.5rem;
color: $ui-white;
box-sizing: border-box;
}
.studio-status-icon-unselected {
background-color: $ui-blue;
}
.submit-button {
background-color: $ui-blue;
}
.submit-button-waiting {
background-color: $ui-blue;
}
.studio-status-icon-plus-img {
width: 1.4rem;
height: 1.4rem;
}
.studio-status-icon--img {
width: 1.4rem;
height: 1.4rem;
}
.action-button-text .spinner-smooth {
margin: .2125rem auto;
width: 1.875rem;
height: 1rem;
}
.studio-status-icon .spinner-smooth {
position: unset; /* don't understand why neither relative nor absolute work */
}
.studio-status-icon .spinner-smooth .circle {
/* overlay spinner on circle */
position: absolute;
margin: .1875rem; /* stay within boundaries of circle */
width: 75%; /* stay within boundaries of circle */
height: 75%; /* stay within boundaries of circle */
}

View file

@ -0,0 +1,119 @@
const PropTypes = require('prop-types');
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx');
const Form = require('../../forms/form.jsx');
const Button = require('../../forms/button.jsx');
const Spinner = require('../../spinner/spinner.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx');
const StudioButton = require('./studio-button.jsx');
require('../../forms/button.scss');
require('./modal.scss');
const AddToStudioModalPresentation = ({
intl,
isOpen,
studios,
waitingToClose,
onToggleStudio,
onRequestClose,
onSubmit
}) => {
const contentLabel = intl.formatMessage({id: 'addToStudio.title'});
const studioButtons = studios.map(studio => (
<StudioButton
hasRequestOutstanding={studio.hasRequestOutstanding}
id={studio.id}
includesProject={studio.includesProject}
key={studio.id}
title={studio.title}
onToggleStudio={onToggleStudio}
/>
));
return (
<Modal
className="mod-addToStudio"
contentLabel={contentLabel}
isOpen={isOpen}
onRequestClose={onRequestClose}
>
<div>
<div className="addToStudio-modal-header">
<div className="addToStudio-content-label">
{contentLabel}
</div>
</div>
<div className="addToStudio-modal-content">
<div className="studio-list-outer-scrollbox">
<div className="studio-list-inner-scrollbox">
<div className="studio-list-container">
{studioButtons}
</div>
<div className="studio-list-bottom-gradient" />
</div>
</div>
<Form
className="add-to-studio"
onSubmit={onSubmit}
>
<FlexRow className="action-buttons">
<Button
className="action-button close-button white"
key="closeButton"
name="closeButton"
type="button"
onClick={onRequestClose}
>
<div className="action-button-text">
<FormattedMessage id="general.close" />
</div>
</Button>
{waitingToClose ? [
<Button
className="action-button submit-button submit-button-waiting"
disabled="disabled"
key="submitButton"
type="submit"
>
<div className="action-button-text">
<Spinner mode="smooth" />
<FormattedMessage id="addToStudio.finishing" />
</div>
</Button>
] : [
<Button
className="action-button submit-button"
key="submitButton"
type="submit"
>
<div className="action-button-text">
<FormattedMessage id="general.okay" />
</div>
</Button>
]}
</FlexRow>
</Form>
</div>
</div>
</Modal>
);
};
AddToStudioModalPresentation.propTypes = {
intl: intlShape,
isOpen: PropTypes.bool,
onRequestClose: PropTypes.func,
onSubmit: PropTypes.func,
onToggleStudio: PropTypes.func,
studios: PropTypes.arrayOf(PropTypes.object),
waitingToClose: PropTypes.bool
};
module.exports = injectIntl(AddToStudioModalPresentation);

View file

@ -0,0 +1,72 @@
const truncateAtWordBoundary = require('../../../lib/truncate').truncateAtWordBoundary;
const PropTypes = require('prop-types');
const React = require('react');
const classNames = require('classnames');
const Spinner = require('../../spinner/spinner.jsx');
require('./modal.scss');
const StudioButton = ({
hasRequestOutstanding,
id,
includesProject,
title,
onToggleStudio
}) => {
const checkmark = (
<img
alt="checkmark-icon"
className="studio-status-icon-checkmark-img"
src="/svgs/modal/confirm.svg"
/>
);
const plus = (
<img
alt="plus-icon"
className="studio-status-icon-plus-img"
src="/svgs/modal/add.svg"
/>
);
return (
<div
className={classNames(
'studio-selector-button',
{'studio-selector-button-waiting': hasRequestOutstanding},
{'studio-selector-button-selected':
includesProject && !hasRequestOutstanding}
)}
data-id={id}
onClick={onToggleStudio}
>
<div
className={classNames(
'studio-selector-button-text',
{'studio-selector-button-text-selected': includesProject || hasRequestOutstanding},
{'studio-selector-button-text-unselected': !includesProject && !hasRequestOutstanding}
)}
>
{truncateAtWordBoundary(title, 25)}
</div>
<div
className={classNames(
'studio-status-icon',
{'studio-status-icon-unselected': !includesProject}
)}
>
{(hasRequestOutstanding ?
(<Spinner mode="smooth" />) :
(includesProject ? checkmark : plus))}
</div>
</div>
);
};
StudioButton.propTypes = {
hasRequestOutstanding: PropTypes.bool,
id: PropTypes.number,
includesProject: PropTypes.bool,
onToggleStudio: PropTypes.func,
title: PropTypes.string
};
module.exports = StudioButton;

View file

@ -5,10 +5,14 @@
position: relative; position: relative;
margin: 3.75rem auto; margin: 3.75rem auto;
border-radius: 1rem; border-radius: 1rem;
box-shadow: 0 0 0 1px $active-gray; box-shadow: 0 0 0 4px $ui-white-15percent;
background-color: $ui-white; background-color: $ui-white;
padding: 0; padding: 0;
width: 48.75rem; width: 48.75rem;
&:focus {
outline: none;
}
} }
.modal-overlay { .modal-overlay {
@ -21,10 +25,6 @@
background-color: transparentize($ui-blue, .3); background-color: transparentize($ui-blue, .3);
} }
.modal-content:focus {
outline: none;
}
$modal-close-size: 1rem; $modal-close-size: 1rem;
.modal-content-close { .modal-content-close {
position: absolute; position: absolute;
@ -59,3 +59,52 @@ $modal-close-size: 1rem;
position: fixed; position: fixed;
} }
} }
/* Close button, Submit button, etc. */
.action-buttons {
display: flex;
margin: 1.125rem .8275rem .9375rem .8275rem;
line-height: 1.5rem;
justify-content: flex-end !important;
align-items: flex-start;
flex-wrap: nowrap;
}
/* setting overall modal to contain overflow looks good, but isn't
compatible with elements (like validation popups) that need to bleed
past modal boundary. This class can be used to force modal button
row to appear to contain overflow. */
.action-buttons-overflow-fix {
margin-bottom: .9375rem;
}
.action-button {
margin: 0 0 0 .54625rem;
border-radius: .25rem;
padding: 6px 1.25rem 14px 1.25rem;
height: 36px;
}
.action-button.close-button {
border: 1px solid $active-gray;
}
.action-button-text {
display: flex;
}
.action-button.disabled {
background-color: $active-dark-gray;
}
.error-text
{
display: block;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
padding: 1rem;
min-height: 1rem;
overflow: visible;
color: $type-white;
}

View file

@ -0,0 +1,270 @@
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
const connect = require('react-redux').connect;
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx');
const classNames = require('classnames');
const Form = require('../../forms/form.jsx');
const Button = require('../../forms/button.jsx');
const Select = require('../../forms/select.jsx');
const Spinner = require('../../spinner/spinner.jsx');
const TextArea = require('../../forms/textarea.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx');
const previewActions = require('../../../redux/preview.js');
require('../../forms/button.scss');
require('./modal.scss');
const REPORT_OPTIONS = [
{
value: '',
label: {id: 'report.reasonPlaceHolder'},
prompt: {id: 'report.promptPlaceholder'}
},
{
value: '0',
label: {id: 'report.reasonCopy'},
prompt: {id: 'report.promptCopy'}
},
{
value: '1',
label: {id: 'report.reasonUncredited'},
prompt: {id: 'report.promptUncredited'}
},
{
value: '2',
label: {id: 'report.reasonScary'},
prompt: {id: 'report.promptScary'}
},
{
value: '3',
label: {id: 'report.reasonLanguage'},
prompt: {id: 'report.promptLanguage'}
},
{
value: '4',
label: {id: 'report.reasonMusic'},
prompt: {id: 'report.promptMusic'}
},
{
value: '8',
label: {id: 'report.reasonImage'},
prompt: {id: 'report.promptImage'}
},
{
value: '5',
label: {id: 'report.reasonPersonal'},
prompt: {id: 'report.promptPersonal'}
},
{
value: '6',
label: {id: 'general.other'},
prompt: {id: 'report.promptGuidelines'}
}
];
class ReportModal extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleCategorySelect',
'handleValid',
'handleInvalid'
]);
this.state = {
category: '',
notes: '',
valid: false
};
}
handleCategorySelect (name, value) {
this.setState({category: value});
}
handleValid () {
this.setState({valid: true});
}
handleInvalid () {
this.setState({valid: false});
}
lookupPrompt (value) {
const prompt = REPORT_OPTIONS.find(item => item.value === value).prompt;
return this.props.intl.formatMessage(prompt);
}
render () {
const {
intl,
isConfirmed,
isError,
isOpen,
isWaiting,
onReport, // eslint-disable-line no-unused-vars
onRequestClose,
type,
...modalProps
} = this.props;
const submitEnabled = this.state.valid && !isWaiting;
const submitDisabledParam = submitEnabled ? {} : {disabled: 'disabled'};
const contentLabel = intl.formatMessage({id: `report.${type}`});
return (
<Modal
className="mod-report"
contentLabel={contentLabel}
isOpen={isOpen}
onRequestClose={onRequestClose}
{...modalProps}
>
<div>
<div className="report-modal-header">
<div className="report-content-label">
{contentLabel}
</div>
</div>
<Form
className="report"
onInvalid={this.handleInvalid}
onValid={this.handleValid}
onValidSubmit={onReport}
>
<div className="report-modal-content">
{isConfirmed ? (
<div className="received">
<div className="received-header">
<FormattedMessage id="report.receivedHeader" />
</div>
<FormattedMessage id="report.receivedBody" />
</div>
) : (
<div>
<div className="instructions">
<FormattedMessage
id={`report.${type}Instructions`}
key={`report.${type}Instructions`}
values={{
CommunityGuidelinesLink: (
<a href="/community_guidelines">
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
</a>
)
}}
/>
</div>
<Select
required
elementWrapperClassName="report-modal-field"
label={null}
name="report_category"
options={REPORT_OPTIONS.map(option => ({
value: option.value,
label: this.props.intl.formatMessage(option.label),
key: option.value
}))}
validationErrors={{
isDefaultRequiredValue: this.props.intl.formatMessage({
id: 'report.reasonMissing'
})
}}
value={this.state.category}
onChange={this.handleCategorySelect}
/>
<TextArea
required
className="report-text"
elementWrapperClassName="report-modal-field"
label={null}
name="notes"
placeholder={this.lookupPrompt(this.state.category)}
validationErrors={{
isDefaultRequiredValue: this.props.intl.formatMessage({
id: 'report.textMissing'
}),
maxLength: this.props.intl.formatMessage({id: 'report.tooLongError'}),
minLength: this.props.intl.formatMessage({id: 'report.tooShortError'})
}}
validations={{
maxLength: 500,
minLength: 20
}}
value={this.state.notes}
/>
</div>
)}
{isError && (
<div className="error-text">
<FormattedMessage id="report.error" />
</div>
)}
</div>
<FlexRow className="action-buttons">
<div className="action-buttons-overflow-fix">
{isConfirmed ? (
<Button
className="action-button submit-button"
type="button"
onClick={onRequestClose}
>
<div className="action-button-text">
<FormattedMessage id="general.close" />
</div>
</Button>
) : (
<Button
className={classNames(
'action-button',
'submit-button',
{disabled: !submitEnabled}
)}
{...submitDisabledParam}
key="submitButton"
type="submit"
>
{isWaiting ? (
<div className="action-button-text">
<Spinner mode="smooth" />
<FormattedMessage id="report.sending" />
</div>
) : (
<div className="action-button-text">
<FormattedMessage id="report.send" />
</div>
)}
</Button>
)}
</div>
</FlexRow>
</Form>
</div>
</Modal>
);
}
}
ReportModal.propTypes = {
intl: intlShape,
isConfirmed: PropTypes.bool,
isError: PropTypes.bool,
isOpen: PropTypes.bool,
isWaiting: PropTypes.bool,
onReport: PropTypes.func,
onRequestClose: PropTypes.func,
type: PropTypes.string
};
const mapStateToProps = state => ({
isConfirmed: state.preview.status.report === previewActions.Status.FETCHED,
isError: state.preview.status.report === previewActions.Status.ERROR,
isWaiting: state.preview.status.report === previewActions.Status.FETCHING
});
const mapDispatchToProps = () => ({});
const ConnectedReportModal = connect(
mapStateToProps,
mapDispatchToProps
)(ReportModal);
module.exports = injectIntl(ConnectedReportModal);

View file

@ -0,0 +1,109 @@
@import "../../../colors";
@import "../../../frameless";
.mod-report * {
box-sizing: border-box;
}
.mod-report {
margin: 100px auto;
outline: none;
padding: 0;
width: 36.25rem; /* 580px; */
user-select: none;
}
.report-modal-header {
border-radius: 1rem 1rem 0 0;
box-shadow: inset 0 -1px 0 0 $ui-coral-dark;
background-color: $ui-coral;
padding-top: .75rem;
width: 100%;
height: 3rem;
box-sizing: border-box;
}
.report-content-label {
text-align: center;
color: $type-white;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1rem;
font-weight: bold;
}
.report-modal-content {
margin: 1rem auto;
width: 80%;
font-size: .875rem;
.instructions {
line-height: 1.5rem;
}
.received {
margin: 0 auto;
width: 90%;
text-align: center;
line-height: 1.65rem;
.received-header {
font-weight: bold;
}
}
.error-text {
margin-top: .9375rem;
}
.validation-message {
$arrow-border-width: 1rem;
display: block;
position: absolute;
top: 0;
left: 100%; /* position to the right of parent */
margin-left: $arrow-border-width;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
padding: 1rem;
min-width: 12rem;
max-width: 18.75rem;
min-height: 1rem;
overflow: visible;
color: $type-white;
/* arrow on box that points to the left */
&:before {
display: block;
position: absolute;
top: 1rem;
left: -$arrow-border-width / 2;
transform: rotate(45deg);
border-bottom: 1px solid $active-gray;
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
width: $arrow-border-width;
height: $arrow-border-width;
content: "";
}
}
}
.report-modal-field {
position: relative;
}
.form-group.has-error {
.textarea, select {
border: 1px solid $ui-orange;
}
}
.report-text .textarea {
margin-bottom: 0;
}

View file

@ -33,20 +33,28 @@ const Navigation = () => (
</li> </li>
<li className="li-right mod-2018"> <li className="li-right mod-2018">
<ul className="li-right-ul mod-2018"> <ul className="li-right-ul mod-2018">
<li className="link info"> <li className="link expect">
<a <a
className="link-a" className="link-a"
href="#info" href="/conference/2018/expect"
> >
Registration Info What to Expect
</a> </a>
</li> </li>
<li className="link questions"> <li className="link plan">
<a <a
className="link-a" className="link-a"
href="#questions" href="/conference/2018/plan"
> >
Questions Plan Your Visit
</a>
</li>
<li className="link schedule">
<a
className="link-a"
href="/conference/2018/schedule"
>
Schedule
</a> </a>
</li> </li>
</ul> </ul>

View file

@ -2,7 +2,7 @@
{ {
"id": 128283902498, "id": 128283902498,
"headline": "Update to Scratch Offline Editor", "headline": "Update to Scratch Offline Editor",
"copy": "Weve released an update to Offline Editor which fixed bugs affecting Linux users.", "copy": "Weve released an update to Offline Editor which fixed bugs affecting Windows users.",
"url": "https://scratch.mit.edu/news#128283902498", "url": "https://scratch.mit.edu/news#128283902498",
"image": "https://33.media.tumblr.com/695b93f4ab74c68feaef1fe03baebdd5/tumblr_inline_n0xubtT0vU1szpavb.png" "image": "https://33.media.tumblr.com/695b93f4ab74c68feaef1fe03baebdd5/tumblr_inline_n0xubtT0vU1szpavb.png"
}, },

View file

@ -0,0 +1,48 @@
const classNames = require('classnames');
const injectIntl = require('react-intl').injectIntl;
const FormattedMessage = require('react-intl').FormattedMessage;
const PropTypes = require('prop-types');
const React = require('react');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const Button = require('../../components/forms/button.jsx');
const OS_ENUM = require('../../components/extension-landing/os-enum.js');
require('./os-chooser.scss');
const OSChooser = props => (
<div className="os-chooser">
<FlexRow className="inner">
<FormattedMessage id="oschooser.choose" />
<Button
className={classNames({active: props.currentOS === OS_ENUM.WINDOWS})}
onClick={() => // eslint-disable-line react/jsx-no-bind
props.handleSetOS(OS_ENUM.WINDOWS)
}
>
<img src="/svgs/extensions/windows.svg" />
Windows
</Button>
<Button
className={classNames({active: props.currentOS === OS_ENUM.MACOS})}
onClick={() => // eslint-disable-line react/jsx-no-bind
props.handleSetOS(OS_ENUM.MACOS)
}
>
<img src="/svgs/extensions/mac.svg" />
macOS
</Button>
</FlexRow>
</div>
);
OSChooser.propTypes = {
currentOS: PropTypes.string,
handleSetOS: PropTypes.func
};
const wrappedOSChooser = injectIntl(OSChooser);
module.exports = wrappedOSChooser;

View file

@ -0,0 +1,40 @@
@import "../../colors";
.os-chooser {
display: flex;
position: sticky;
top: 50px;
z-index: 9;
box-shadow: 0 0 3px $box-shadow-gray;
background-color: $ui-white;
padding: 0;
height: 5rem;
.inner {
justify-content: flex-start;
}
span {
margin-right: 1rem;
font-weight: 600;
}
.button {
display: flex;
margin-right: 1rem;
border-radius: 1.6rem;
background-color: $active-gray;
padding: .5rem 1.1rem;
align-items: center;
box-sizing: border-box;
img {
margin-right: .3rem;
height: 1.5rem;
}
&.active {
background-color: $ui-blue;
}
}
}

View file

@ -1243,9 +1243,9 @@ class EmailStep extends React.Component {
validations="equalsField:user.email" validations="equalsField:user.email"
/> />
<Checkbox <Checkbox
value
help={null} help={null}
name="subscribe" name="subscribe"
value={false}
valueLabel={ valueLabel={
this.props.intl.formatMessage({id: 'registration.optIn'}) this.props.intl.formatMessage({id: 'registration.optIn'})
} }

View file

@ -1,37 +0,0 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
const ThumbnailColumn = require('../../components/thumbnailcolumn/thumbnailcolumn.jsx');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
require('./remixlist.scss');
/*
* Container for a list of project remixes
*/
const RemixList = props => (
<FlexRow className={classNames('remix-list', props.className)}>
<h1>Remixes</h1>
{props.items.length === 0 ? (
<span>No remixes</span>
) : (
<ThumbnailColumn
cards
showAvatar
itemType="preview"
items={props.items.slice(0, 5)}
showFavorites={false}
showLoves={false}
showViews={false}
/>
)}
</FlexRow>
);
RemixList.propTypes = {
className: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
module.exports = RemixList;

View file

@ -1,17 +0,0 @@
.remix-list {
flex-direction: column;
.project {
margin-bottom: 1.5rem;
}
.creator-image img {
max-width: 2rem;
max-height: 2rem;
}
.thumbnail-column {
display: inline-block;
width: 100%;
}
}

View file

@ -1,20 +0,0 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
require('./share-banner.scss');
const ShareBanner = props => (
<div className={classNames('shareBanner', props.className)}>
<div className="inner">
{props.children}
</div>
</div>
);
ShareBanner.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};
module.exports = ShareBanner;

View file

@ -1,10 +0,0 @@
@import "../../colors";
$navigation-height: 50px;
.shareBanner {
background-color: $ui-orange-25percent;
width: 100%;
overflow: hidden;
color: $ui-orange;
}

View file

@ -13,7 +13,7 @@
} }
.social-message.mod-unread { .social-message.mod-unread {
background-color: lighten($ui-blue, 40); background-color: $ui-blue-10percent;
} }
.social-message.mod-unread .social-message-icon { .social-message.mod-unread .social-message-icon {
@ -41,7 +41,7 @@ a.social-messages-profile-link {
color: $type-gray; color: $type-gray;
&:hover { &:hover {
color: darken($type-gray, 10); color: $link-blue;
} }
} }

View file

@ -1,18 +1,29 @@
const range = require('lodash.range'); const range = require('lodash.range');
const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
require('./spinner.scss'); require('./spinner.scss');
// Adapted from http://tobiasahlin.com/spinkit/ // Adapted from http://tobiasahlin.com/spinkit/
const Spinner = () => ( const Spinner = ({
<div className="spinner"> mode
{range(1, 13).map(id => ( }) => {
<div const spinnerClassName = (mode === 'smooth' ? 'spinner-smooth' : 'spinner');
className={`circle${id} circle`} const spinnerDivCount = (mode === 'smooth' ? 24 : 12);
key={`circle${id}`} return (
/> <div className={spinnerClassName}>
))} {range(1, spinnerDivCount + 1).map(id => (
</div> <div
); className={`circle${id} circle`}
key={`circle${id}`}
/>
))}
</div>
);
};
Spinner.propTypes = {
mode: PropTypes.string
};
module.exports = Spinner; module.exports = Spinner;

View file

@ -18,13 +18,13 @@
animation: circleFadeDelay 1.2s infinite ease-in-out both; animation: circleFadeDelay 1.2s infinite ease-in-out both;
margin: 0 auto; margin: 0 auto;
border-radius: 100%; border-radius: 100%;
background-color: darken($ui-white, 8%); background-color: $ui-gray;
width: 15%; width: 15%;
height: 15%; height: 15%;
content: ""; content: "";
.white & { .white & {
background-color: darken($ui-blue, 8%); background-color: $ui-blue-dark;
} }
} }
} }
@ -37,7 +37,7 @@
transform: rotate($rotation); transform: rotate($rotation);
&:before { &:before {
animation-delay: $delay; animation-delay: $delay;
} }
} }
@ -54,5 +54,65 @@
40% { 40% {
opacity: 1; opacity: 1;
} }
}
/*********************/
/* type === "smooth" */
/*********************/
.spinner-smooth {
position: relative;
margin: 0 auto;
width: 20px;
height: 20px;
.circle {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
&:before {
display: block;
animation: circleFadeDelaySmooth 1.8s infinite ease-in-out both;
margin: 0 auto;
border-radius: 100%;
background-color: $ui-white;
width: 30%;
height: 20%;
content: "";
.white & {
background-color: darken($ui-blue, 8%);
}
}
}
@for $i from 1 through 24 {
$rotation: 15deg * ($i - 1);
$delay: -1.9s + $i * .075;
.circle#{$i} {
transform: rotate($rotation);
&:before {
animation-delay: $delay;
}
}
}
}
@keyframes circleFadeDelaySmooth {
0%,
35% {
opacity: 0;
},
40% {
opacity: 1;
}
} }

View file

@ -0,0 +1,30 @@
const PropTypes = require('prop-types');
const React = require('react');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
require('./steps.scss');
const Step = props => (
<div className="step">
{(props.compact || props.number) &&
<FlexRow className="step-number-row">
{props.number && <div className="step-number">{props.number}</div>}
{props.compact && <FlexRow className="step-content">{props.children}</FlexRow>}
</FlexRow>
}
{!props.compact &&
<div className="step-content">
{props.children}
</div>
}
</div>
);
Step.propTypes = {
children: PropTypes.node,
compact: PropTypes.bool,
number: PropTypes.number
};
module.exports = Step;

View file

@ -0,0 +1,21 @@
const PropTypes = require('prop-types');
const React = require('react');
const classNames = require('classnames');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
require('./steps.scss');
const Steps = props => (
<FlexRow className={classNames('steps', props.className)}>
{/* TODO: Should this component do something with automatically numbering individual steps? */}
{props.children}
</FlexRow>
);
Steps.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};
module.exports = Steps;

View file

@ -0,0 +1,59 @@
@import "../../colors";
.steps {
display: flex;
width: 100%;
justify-content: space-between;
align-items: flex-start;
}
.step {
flex-basis: 0;
flex-grow: 1;
.step-number-row {
padding-bottom: 1.5rem;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
.step-content {
text-align: left;
align-items: flex-start;
.step-description {
margin-bottom: 1rem;
}
}
.step-number {
display: inline-flex;
border-radius: 2rem;
background-color: $ui-blue;
width: 2rem;
height: 2rem;
color: $ui-white;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
}
.step-content {
display: flex;
padding: 0 2rem;
text-align: center;
flex-flow: column;
align-items: center;
box-sizing: border-box;
.step-image {
height: 10rem;
img {
width: auto;
height: 100%;
}
}
}
}

View file

@ -14,7 +14,7 @@ const ThumbnailColumn = props => (
if (props.itemType === 'preview') { if (props.itemType === 'preview') {
return ( return (
<Thumbnail <Thumbnail
avatar={`https://cdn2.scratch.mit.edu/get_image/user/${item.author.i}_32x32.png`} avatar={`https://cdn2.scratch.mit.edu/get_image/user/${item.author.id}_32x32.png`}
creator={item.author.username} creator={item.author.username}
favorites={item.stats.favorites} favorites={item.stats.favorites}
href={href} href={href}

View file

@ -17,7 +17,7 @@
.thumbnail { .thumbnail {
margin: 7px; margin: 7px;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 0 3px $box-shadow-gray; box-shadow: 0 0 0 1px $active-gray;
background-color: $ui-white; background-color: $ui-white;
padding-bottom: 4px; padding-bottom: 4px;
width: $thumbnail-width; width: $thumbnail-width;

View file

@ -106,7 +106,7 @@
font-weight: 500; font-weight: 500;
&:hover { &:hover {
background-color: lighten($link-blue, 40%); background-color: $ui-blue-10percent;
} }
} }

View file

@ -37,31 +37,31 @@
&.blue { &.blue {
#{$color-bars} { #{$color-bars} {
background-color: $splash-blue; background-color: $ui-blue;
} }
a { a {
color: $splash-blue; color: $ui-blue;
} }
} }
&.green { &.green {
#{$color-bars} { #{$color-bars} {
background-color: $splash-green; background-color: $ui-aqua;
} }
a { a {
color: $splash-green; color: $ui-aqua;
} }
} }
&.pink { &.pink {
#{$color-bars} { #{$color-bars} {
background-color: $splash-pink; background-color: $ui-purple;
} }
a { a {
color: $splash-pink; color: $ui-purple;
} }
} }
} }

View file

@ -36,3 +36,25 @@ const Raven = require('raven-js');
window._locale = updateLocale(); window._locale = updateLocale();
})(); })();
/**
* -----------------------------------------------------------------------------
* Console warning
* -----------------------------------------------------------------------------
*/
(() => {
window.onload = function () {
/* eslint-disable no-console */
console.log('%cStop!', 'color: #F00; font-size: 30px; -webkit-text-stroke: 1px black; font-weight:bold');
console.log(
'This is part of your browser intended for developers. ' +
'If someone told you to copy-and-paste something here, ' +
'don\'t do it! It could allow them to take over your ' +
'Scratch account, delete all of your projects, or do many ' +
'other harmful things. If you don\'t understand what exactly ' +
'you are doing here, you should close this window without doing ' +
'anything.'
);
/* eslint-enable no-console */
};
})();

View file

@ -6,6 +6,7 @@
"general.birthMonth": "Birth Month", "general.birthMonth": "Birth Month",
"general.birthYear": "Birth Year", "general.birthYear": "Birth Year",
"general.donate": "Donate", "general.donate": "Donate",
"general.close": "Close",
"general.collaborators": "Collaborators", "general.collaborators": "Collaborators",
"general.community": "Community", "general.community": "Community",
"general.confirmEmail": "Confirm Email", "general.confirmEmail": "Confirm Email",
@ -52,6 +53,7 @@
"general.noDeletionDescription": "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 {resetLink} to make sure your account is secure.", "general.noDeletionDescription": "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 {resetLink} to make sure your account is secure.",
"general.noDeletionLink": "change your password", "general.noDeletionLink": "change your password",
"general.notRequired": "Not Required", "general.notRequired": "Not Required",
"general.okay": "Okay",
"general.other": "Other", "general.other": "Other",
"general.offlineEditor": "Offline Editor", "general.offlineEditor": "Offline Editor",
"general.password": "Password", "general.password": "Password",
@ -103,6 +105,16 @@
"navigation.signOut": "Sign out", "navigation.signOut": "Sign out",
"extensionHeader.requirements": "Requirements",
"oschooser.choose": "Choose your OS:",
"installScratchLink.installHeaderTitle": "Install Scratch Link",
"installScratchLink.downloadAndInstall": "Download and install Scratch Link.",
"installScratchLink.windowsDownload": "Download for Windows",
"installScratchLink.macosDownload": "Download for macOS",
"installScratchLink.startScratchLink": "Start Scratch Link and make sure it is running. It should appear in your toolbar.",
"parents.FaqAgeRangeA": "While Scratch is primarily designed for 8 to 16 year olds, it is also used by people of all ages, including younger children with their parents.", "parents.FaqAgeRangeA": "While Scratch is primarily designed for 8 to 16 year olds, it is also used by people of all ages, including younger children with their parents.",
"parents.FaqAgeRangeQ": "What is the age range for Scratch?", "parents.FaqAgeRangeQ": "What is the age range for Scratch?",
"parents.FaqResourcesQ": "What resources are available for learning Scratch?", "parents.FaqResourcesQ": "What resources are available for learning Scratch?",
@ -159,5 +171,34 @@
"registration.welcomeStepPrompt": "To get started, click on the button below.", "registration.welcomeStepPrompt": "To get started, click on the button below.",
"registration.welcomeStepTitle": "Hurray! Welcome to Scratch!", "registration.welcomeStepTitle": "Hurray! Welcome to Scratch!",
"thumbnail.by": "by" "thumbnail.by": "by",
} "report.error": "Something went wrong when trying to send your message. Please try again.",
"report.project": "Report Project",
"report.projectInstructions": "From the dropdown below, please select the reason why you feel this project is disrespectful or inappropriate or otherwise breaks the {CommunityGuidelinesLink}.",
"report.CommunityGuidelinesLinkText": "Scratch Community Guidelines",
"report.reasonPlaceHolder": "Select a reason",
"report.reasonCopy": "Exact Copy of Project",
"report.reasonUncredited": "Uses Image/Music Without Credit",
"report.reasonScary": "Too Violent or Scary",
"report.reasonLanguage": "Inappropriate Language",
"report.reasonMusic": "Inappropriate Music",
"report.reasonMissing": "Please select a reason",
"report.reasonImage": "Inappropriate Images",
"report.reasonPersonal": "Sharing Personal Contact Information",
"report.receivedHeader": "We have received your report!",
"report.receivedBody": "The Scratch Team will review the project based on the Scratch community guidelines.",
"report.promptPlaceholder": "Select a reason why above.",
"report.promptCopy": "Please provide a link to the original project",
"report.promptUncredited": "Please provide links to the uncredited content",
"report.promptScary": "Please say why the project is too violent or scary",
"report.promptLanguage": "Please say where the inappropriate language occurs in the project (For example: Notes & Credits, sprite name, project text, etc.)",
"report.promptMusic": "Please say the name of the audio file with the inappropriate music",
"report.promptPersonal": "Please say where the personal contact information is shared (For example: Notes & Credits, sprite name, project text, etc.)",
"report.promptGuidelines": "Please be specific about why this project does not follow our Community Guidelines",
"report.promptImage": "Please say the name of the sprite or the backdrop with the inappropriate image",
"report.tooLongError": "That's too long! Please find a way to shorten your text.",
"report.tooShortError": "That's too short. Please describe in detail what's inappropriate or disrespectful about the project.",
"report.send": "Send",
"report.sending": "Sending...",
"report.textMissing": "Please tell us why you are reporting this project"
}

30
src/lib/decorate-text.jsx Normal file
View file

@ -0,0 +1,30 @@
const React = require('react'); // eslint-disable-line
const reactStringReplace = require('react-string-replace');
/**
* Helper method that replaces @mentions and #hashtags in plain text
*
* @param {string} text string to convert
* @return {string} string with links for @mentions and #hashtags
*/
module.exports = text => {
let replacedText;
// Match @-mentions (username is alphanumeric, underscore and dash)
replacedText = reactStringReplace(text, /@([\w-]+)/g, (match, i) => (
<a
href={`/users/${match}`}
key={match + i}
>@{match}</a>
));
// Match hashtags
replacedText = reactStringReplace(replacedText, /(#[\w-]+)/g, (match, i) => (
<a
href={`/search/projects?q=${match}`}
key={match + i}
>{match}</a>
));
return replacedText;
};

36
src/lib/extensions.js Normal file
View file

@ -0,0 +1,36 @@
const EXTENSION_INFO = {
microbit: {
name: 'micro:bit',
icon: 'extension-microbit.svg',
hasStatus: true
},
music: {
l10nId: 'preview.musicExtensionChip',
icon: 'extension-music.svg'
},
pen: {
l10nId: 'preview.penExtensionChip',
icon: 'extension-pen.svg'
},
speak: {
name: 'Amazon Polly'
},
speech: {
l10nId: 'preview.speechExtensionChip'
},
translate: {
l10nId: 'preview.translateExtensionChip',
icon: 'extension-translate.svg'
},
videoSensing: {
l10nId: 'preview.videoMotionChip',
icon: 'extension-videomotion.svg'
},
wedo2: {
name: 'LEGO WeDo 2.0',
icon: 'extension-wedo2.svg',
hasStatus: true
}
};
export default EXTENSION_INFO;

View file

@ -14,11 +14,13 @@ require('../main.scss');
/** /**
* Function to render views into a full page * Function to render views into a full page
* @param {object} jsx jsx component of the view * @param {object} jsx jsx component of the view
* @param {object} element html element to render to on the template * @param {object} element html element to render to on the template
* @param {array} reducers list of view-specific reducers * @param {array} reducers list of view-specific reducers
* @param {object} initialState optional initialState for store
* @param {bool} enhancer whether or not to apply redux-throttle middleware
*/ */
const render = (jsx, element, reducers) => { const render = (jsx, element, reducers, initialState, enhancer) => {
// Get locale and messages from global namespace (see "init.js") // Get locale and messages from global namespace (see "init.js")
let locale = window._locale || 'en'; let locale = window._locale || 'en';
let messages = {}; let messages = {};
@ -35,9 +37,20 @@ const render = (jsx, element, reducers) => {
} }
const allReducers = reducer(reducers); const allReducers = reducer(reducers);
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || redux.compose;
const enhancers = enhancer ?
composeEnhancers(
redux.applyMiddleware(thunk),
enhancer
) :
composeEnhancers(
redux.applyMiddleware(thunk)
);
const store = redux.createStore( const store = redux.createStore(
allReducers, allReducers,
redux.applyMiddleware(thunk) initialState || {},
enhancers
); );
// Render view component // Render view component

26
src/lib/storage.js Normal file
View file

@ -0,0 +1,26 @@
import ScratchStorage from 'scratch-storage';
const PROJECT_SERVER = 'https://projects.scratch.mit.edu';
/**
* Wrapper for ScratchStorage which adds default web sources.
* @todo make this more configurable
*/
class Storage extends ScratchStorage {
constructor () {
super();
this.addWebSource(
[this.AssetType.Project],
projectAsset => {
const [projectId, revision] = projectAsset.assetId.split('.');
return revision ?
`${PROJECT_SERVER}/internalapi/project/${projectId}/get/${revision}` :
`${PROJECT_SERVER}/internalapi/project/${projectId}/get/`;
}
);
}
}
const storage = new Storage();
export default storage;

9
src/lib/truncate.js Normal file
View file

@ -0,0 +1,9 @@
const lodashTruncate = require('lodash.truncate');
/*
* Function that applies regex for word boundaries, replaces removed string
* with indication of ellipsis (...)
*/
module.exports.truncateAtWordBoundary = (str, length) => (
lodashTruncate(str, {length: length, separator: /[.,:;]*\s+/})
);

View file

@ -6,7 +6,7 @@ html,
body { body {
display: block; display: block;
margin: 0; margin: 0;
background-color: darken($ui-blue, 8%); background-color: $ui-blue-dark;
padding: 0; padding: 0;
color: $type-gray; color: $type-gray;
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif; font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
@ -22,34 +22,34 @@ h4 {
padding: 0; padding: 0;
color: $header-gray; color: $header-gray;
font-weight: 700; font-weight: bold;
} }
h1 { h1 {
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 900; font-weight: bold;
} }
h2 { h2 {
font-size: 2rem; font-size: 2rem;
font-weight: 600; font-weight: bold;
} }
h3 { h3 {
font-size: 1.4rem; font-size: 1.4rem;
font-weight: 500; font-weight: bold;
} }
h4 { h4 {
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: bold;
} }
h5 { h5 {
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 2px; letter-spacing: 2px;
font-size: .85rem; font-size: .85rem;
font-weight: 700; font-weight: bold;
} }
p { p {
@ -69,12 +69,12 @@ p {
margin: 1.5em 0; margin: 1.5em 0;
border: 1px solid $active-gray; border: 1px solid $active-gray;
border-radius: 5px; border-radius: .5rem;
background-color: lighten($ui-blue, 40); background-color: $ui-blue-10percent;
padding: 1.25em; padding: 1.25em;
&.orange { &.orange {
background-color: lighten($ui-orange, 30); background-color: $ui-orange-10percent;
} }
} }
@ -85,14 +85,14 @@ p {
b, b,
strong { strong {
font-weight: 500; font-weight: bold;
} }
/* Links */ /* Links */
a { a {
cursor: pointer; cursor: pointer;
color: $ui-blue; color: $ui-blue;
font-weight: 500; font-weight: bold;
&:link, &:link,
&:visited, &:visited,
@ -103,17 +103,15 @@ a {
&:hover { &:hover {
text-decoration: none; text-decoration: none;
color: darken($ui-blue, 15); color: $ui-blue-dark;
} }
} }
/* Classes */ /* Classes */
.empty { .empty {
$bg-blue: #d9edf7; border: 1px solid $active-gray;
$bg-blue-accent: #bce8f1;
border: 1px solid $bg-blue-accent;
border-radius: 5px; border-radius: 5px;
background-color: $bg-blue; background-color: $ui-blue-10percent;
padding: 10px; padding: 10px;
text-align: center; text-align: center;
line-height: 2rem; line-height: 2rem;
@ -136,11 +134,11 @@ p {
p { p {
font-size: 1rem; font-size: 1rem;
font-weight: 300; font-weight: normal;
} }
::selection { ::selection {
background-color: lighten($ui-blue, 30); background-color: $ui-blue-25percent;
} }
ol, ol,
@ -149,7 +147,7 @@ ul {
line-height: 1.5em; line-height: 1.5em;
font-size: 1rem; font-size: 1rem;
font-weight: 300; font-weight: normal;
li { li {
margin: .75em 0; margin: .75em 0;
@ -159,10 +157,10 @@ ul {
dl { dl {
line-height: 1.5rem; line-height: 1.5rem;
font-size: 1rem; font-size: 1rem;
font-weight: 300; font-weight: normal;
dt { dt {
font-weight: 700; font-weight: bold;
} }
dd { dd {

View file

@ -88,7 +88,7 @@ module.exports.getDaySchedule = day => (dispatch => {
cleanedRow[columns[i]] = cur[i]; cleanedRow[columns[i]] = cur[i];
} }
} }
cleanedRow.uri = `/conference/2016/${cleanedRow.rowid}/details`; cleanedRow.uri = `/conference/2018/${cleanedRow.rowid}/details`;
const timeSlot = cleanedRow.Chunk + cleanedRow.Start; const timeSlot = cleanedRow.Chunk + cleanedRow.Start;
if (typeof prev.timeSlots[timeSlot] === 'undefined') { if (typeof prev.timeSlots[timeSlot] === 'undefined') {
prev.timeSlots[timeSlot] = [cleanedRow]; prev.timeSlots[timeSlot] = [cleanedRow];

View file

@ -1,4 +1,7 @@
const defaults = require('lodash.defaults');
const keyMirror = require('keymirror'); const keyMirror = require('keymirror');
const async = require('async');
const merge = require('lodash.merge');
const api = require('../lib/api'); const api = require('../lib/api');
const log = require('../lib/log'); const log = require('../lib/log');
@ -13,16 +16,28 @@ module.exports.Status = keyMirror({
module.exports.getInitialState = () => ({ module.exports.getInitialState = () => ({
status: { status: {
project: module.exports.Status.NOT_FETCHED, project: module.exports.Status.NOT_FETCHED,
credit: module.exports.Status.NOT_FETCHED,
comments: module.exports.Status.NOT_FETCHED, comments: module.exports.Status.NOT_FETCHED,
faved: module.exports.Status.NOT_FETCHED,
loved: module.exports.Status.NOT_FETCHED,
original: module.exports.Status.NOT_FETCHED,
parent: module.exports.Status.NOT_FETCHED,
remixes: module.exports.Status.NOT_FETCHED, remixes: module.exports.Status.NOT_FETCHED,
studios: module.exports.Status.NOT_FETCHED report: module.exports.Status.NOT_FETCHED,
projectStudios: module.exports.Status.NOT_FETCHED,
curatedStudios: module.exports.Status.NOT_FETCHED,
studioRequests: {}
}, },
projectInfo: {}, projectInfo: {},
remixes: [], remixes: [],
credit: {},
comments: [], comments: [],
studios: [] replies: {},
faved: false,
loved: false,
original: {},
parent: {},
projectStudios: [],
curatedStudios: [],
currentStudioIds: []
}); });
module.exports.previewReducer = (state, action) => { module.exports.previewReducer = (state, action) => {
@ -39,18 +54,58 @@ module.exports.previewReducer = (state, action) => {
return Object.assign({}, state, { return Object.assign({}, state, {
remixes: action.items remixes: action.items
}); });
case 'SET_CREDIT': case 'SET_ORIGINAL':
return Object.assign({}, state, { return Object.assign({}, state, {
credit: action.info original: action.info
});
case 'SET_PARENT':
return Object.assign({}, state, {
parent: action.info
});
case 'SET_PROJECT_STUDIOS':
// also initialize currentStudioIds, to keep track of which studios
// the project is currently in.
return Object.assign({}, state, {
projectStudios: action.items,
currentStudioIds: action.items.map(item => item.id)
});
case 'SET_CURATED_STUDIOS':
return Object.assign({}, state, {curatedStudios: action.items});
case 'ADD_PROJECT_TO_STUDIO':
// add studio id to our studios-that-this-project-belongs-to set.
return Object.assign({}, state, {
currentStudioIds: state.currentStudioIds.concat(action.studioId)
});
case 'REMOVE_PROJECT_FROM_STUDIO':
return Object.assign({}, state, {
currentStudioIds: state.currentStudioIds.filter(item => (
item !== action.studioId
))
}); });
case 'SET_COMMENTS': case 'SET_COMMENTS':
return Object.assign({}, state, { return Object.assign({}, state, {
comments: action.items comments: [...state.comments, ...action.items] // TODO: consider a different way of doing this?
});
case 'SET_REPLIES':
return Object.assign({}, state, {
replies: merge({}, state.replies, action.replies)
});
case 'SET_LOVED':
return Object.assign({}, state, {
loved: action.info
});
case 'SET_FAVED':
return Object.assign({}, state, {
faved: action.info
}); });
case 'SET_FETCH_STATUS': case 'SET_FETCH_STATUS':
state = JSON.parse(JSON.stringify(state)); state = JSON.parse(JSON.stringify(state));
state.status[action.infoType] = action.status; state.status[action.infoType] = action.status;
return state; return state;
case 'SET_STUDIO_FETCH_STATUS':
state = JSON.parse(JSON.stringify(state));
state.status.studioRequests[action.studioId] = action.status;
return state;
case 'ERROR': case 'ERROR':
log.error(action.error); log.error(action.error);
return state; return state;
@ -69,8 +124,23 @@ module.exports.setProjectInfo = info => ({
info: info info: info
}); });
module.exports.setCreditInfo = info => ({ module.exports.setOriginalInfo = info => ({
type: 'SET_CREDIT', type: 'SET_ORIGINAL',
info: info
});
module.exports.setParentInfo = info => ({
type: 'SET_PARENT',
info: info
});
module.exports.setFaved = info => ({
type: 'SET_FAVED',
info: info
});
module.exports.setLoved = info => ({
type: 'SET_LOVED',
info: info info: info
}); });
@ -79,17 +149,57 @@ module.exports.setRemixes = items => ({
items: items items: items
}); });
module.exports.setProjectStudios = items => ({
type: 'SET_PROJECT_STUDIOS',
items: items
});
module.exports.setComments = items => ({
type: 'SET_COMMENTS',
items: items
});
module.exports.setReplies = replies => ({
type: 'SET_REPLIES',
replies: replies
});
module.exports.setCuratedStudios = items => ({
type: 'SET_CURATED_STUDIOS',
items: items
});
module.exports.addProjectToStudio = studioId => ({
type: 'ADD_PROJECT_TO_STUDIO',
studioId: studioId
});
module.exports.removeProjectFromStudio = studioId => ({
type: 'REMOVE_PROJECT_FROM_STUDIO',
studioId: studioId
});
module.exports.setFetchStatus = (type, status) => ({ module.exports.setFetchStatus = (type, status) => ({
type: 'SET_FETCH_STATUS', type: 'SET_FETCH_STATUS',
infoType: type, infoType: type,
status: status status: status
}); });
module.exports.getProjectInfo = id => (dispatch => { module.exports.setStudioFetchStatus = (studioId, status) => ({
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING)); type: 'SET_STUDIO_FETCH_STATUS',
api({ studioId: studioId,
status: status
});
module.exports.getProjectInfo = (id, token) => (dispatch => {
const opts = {
uri: `/projects/${id}` uri: `/projects/${id}`
}, (err, body) => { };
if (token) {
Object.assign(opts, {authentication: token});
}
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING));
api(opts, (err, body) => {
if (err) { if (err) {
dispatch(module.exports.setFetchStatus('project', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('project', module.exports.Status.ERROR));
dispatch(module.exports.setError(err)); dispatch(module.exports.setError(err));
@ -105,26 +215,215 @@ module.exports.getProjectInfo = id => (dispatch => {
}); });
}); });
module.exports.getCreditInfo = id => (dispatch => { module.exports.getOriginalInfo = id => (dispatch => {
dispatch(module.exports.setFetchStatus('credit', module.exports.Status.FETCHING)); dispatch(module.exports.setFetchStatus('original', module.exports.Status.FETCHING));
api({ api({
uri: `/projects/${id}` uri: `/projects/${id}`
}, (err, body) => { }, (err, body) => {
if (err) { if (err) {
dispatch(module.exports.setFetchStatus('credit', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('original', module.exports.Status.ERROR));
dispatch(module.exports.setError(err)); dispatch(module.exports.setError(err));
return; return;
} }
if (typeof body === 'undefined') { if (typeof body === 'undefined') {
dispatch(module.exports.setFetchStatus('credit', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('original', module.exports.Status.ERROR));
dispatch(module.exports.setError('No credit info')); dispatch(module.exports.setError('No original info'));
return; return;
} }
dispatch(module.exports.setFetchStatus('credit', module.exports.Status.FETCHED)); dispatch(module.exports.setFetchStatus('original', module.exports.Status.FETCHED));
dispatch(module.exports.setCreditInfo(body)); dispatch(module.exports.setOriginalInfo(body));
}); });
}); });
module.exports.getParentInfo = id => (dispatch => {
dispatch(module.exports.setFetchStatus('parent', module.exports.Status.FETCHING));
api({
uri: `/projects/${id}`
}, (err, body) => {
if (err) {
dispatch(module.exports.setFetchStatus('parent', module.exports.Status.ERROR));
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setFetchStatus('parent', module.exports.Status.ERROR));
dispatch(module.exports.setError('No parent info'));
return;
}
dispatch(module.exports.setFetchStatus('parent', module.exports.Status.FETCHED));
dispatch(module.exports.setParentInfo(body));
});
});
module.exports.getFavedStatus = (id, username, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHING));
api({
uri: `/projects/${id}/favorites/user/${username}`,
authentication: token
}, (err, body) => {
if (err) {
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.ERROR));
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.ERROR));
dispatch(module.exports.setError('No faved info'));
return;
}
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHED));
dispatch(module.exports.setFaved(body.userFavorite));
});
});
module.exports.getTopLevelComments = (id, offset) => (dispatch => {
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHING));
api({
uri: `/comments/project/${id}`,
params: {offset: offset || 0}
}, (err, body) => {
if (err) {
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
dispatch(module.exports.setError('No comment info'));
return;
}
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHED));
dispatch(module.exports.setComments(body));
dispatch(module.exports.getReplies(id, body.map(comment => comment.id)));
});
});
module.exports.getReplies = (projectId, commentIds) => (dispatch => {
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.FETCHING));
const fetchedReplies = {};
async.eachLimit(commentIds, 10, (parentId, callback) => {
api({
uri: `/comments/project/${projectId}/${parentId}`
}, (err, body) => {
if (err) {
return callback(`Error fetching comment replies: ${err}`);
}
if (typeof body === 'undefined') {
return callback('No comment reply information');
}
fetchedReplies[parentId] = body;
callback(null, body);
});
}, err => {
if (err) {
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.ERROR));
dispatch(module.exports.setError(err));
return;
}
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.FETCHED));
dispatch(module.exports.setReplies(fetchedReplies));
});
});
module.exports.setFavedStatus = (faved, id, username, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHING));
if (faved) {
api({
uri: `/projects/${id}/favorites/user/${username}`,
authentication: token,
method: 'POST'
}, (err, body) => {
if (err) {
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setError('Set favorites returned no data'));
return;
}
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHED));
dispatch(module.exports.setFaved(body.userFavorite));
});
} else {
api({
uri: `/projects/${id}/favorites/user/${username}`,
authentication: token,
method: 'DELETE'
}, (err, body) => {
if (err) {
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setError('Set favorites returned no data'));
return;
}
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHED));
dispatch(module.exports.setFaved(false));
});
}
});
module.exports.getLovedStatus = (id, username, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.FETCHING));
api({
uri: `/projects/${id}/loves/user/${username}`,
authentication: token
}, (err, body) => {
if (err) {
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
dispatch(module.exports.setError('No loved info'));
return;
}
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.FETCHED));
dispatch(module.exports.setLoved(body.userLove));
});
});
module.exports.setLovedStatus = (loved, id, username, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.FETCHING));
if (loved) {
api({
uri: `/projects/${id}/loves/user/${username}`,
authentication: token,
method: 'POST'
}, (err, body) => {
if (err) {
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setError('Set loved returned no data'));
return;
}
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.FETCHED));
dispatch(module.exports.setLoved(body.userLove));
});
} else {
api({
uri: `/projects/${id}/loves/user/${username}`,
authentication: token,
method: 'DELETE'
}, (err, body) => {
if (err) {
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setError('Set loved returned no data'));
return;
}
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.FETCHED));
dispatch(module.exports.setLoved(body.userLove));
});
}
});
module.exports.getRemixes = id => (dispatch => { module.exports.getRemixes = id => (dispatch => {
dispatch(module.exports.setFetchStatus('remixes', module.exports.Status.FETCHING)); dispatch(module.exports.setFetchStatus('remixes', module.exports.Status.FETCHING));
api({ api({
@ -148,3 +447,141 @@ module.exports.getRemixes = id => (dispatch => {
dispatch(module.exports.setRemixes(body)); dispatch(module.exports.setRemixes(body));
}); });
}); });
module.exports.getProjectStudios = id => (dispatch => {
dispatch(module.exports.setFetchStatus('projectStudios', module.exports.Status.FETCHING));
api({
uri: `/projects/${id}/studios`
}, (err, body, res) => {
if (err) {
dispatch(module.exports.setFetchStatus('projectStudios', module.exports.Status.ERROR));
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setFetchStatus('projectStudios', module.exports.Status.ERROR));
dispatch(module.exports.setError('No projectStudios info'));
return;
}
if (res.statusCode === 404) { // NotFound
body = [];
}
dispatch(module.exports.setFetchStatus('projectStudios', module.exports.Status.FETCHED));
dispatch(module.exports.setProjectStudios(body));
});
});
module.exports.getCuratedStudios = username => (dispatch => {
dispatch(module.exports.setFetchStatus('curatedStudios', module.exports.Status.FETCHING));
api({
uri: `/users/${username}/studios/curate`
}, (err, body, res) => {
if (err) {
dispatch(module.exports.setFetchStatus('curatedStudios', module.exports.Status.ERROR));
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setFetchStatus('curatedStudios', module.exports.Status.ERROR));
dispatch(module.exports.setError('No curated studios info'));
return;
}
if (res.statusCode === 404) { // NotFound
body = [];
}
dispatch(module.exports.setFetchStatus('curatedStudios', module.exports.Status.FETCHED));
dispatch(module.exports.setCuratedStudios(body));
});
});
module.exports.addToStudio = (studioId, projectId, token) => (dispatch => {
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHING));
api({
uri: `/studios/${studioId}/project/${projectId}`,
authentication: token,
method: 'POST'
}, (err, body) => {
if (err) {
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setError('Add to studio returned no data'));
return;
}
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED));
dispatch(module.exports.addProjectToStudio(studioId));
});
});
module.exports.leaveStudio = (studioId, projectId, token) => (dispatch => {
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHING));
api({
uri: `/studios/${studioId}/project/${projectId}`,
authentication: token,
method: 'DELETE'
}, (err, body) => {
if (err) {
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setError('Leave studio returned no data'));
return;
}
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED));
dispatch(module.exports.removeProjectFromStudio(studioId));
});
});
module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING));
api({
uri: `/projects/${id}`,
authentication: token,
method: 'PUT',
json: jsonData
}, (err, body, res) => {
if (err) {
dispatch(module.exports.setFetchStatus('project', module.exports.Status.ERROR));
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setFetchStatus('project', module.exports.Status.ERROR));
dispatch(module.exports.setError('No project info'));
return;
}
if (res.statusCode === 500) { // InternalServer
dispatch(module.exports.setFetchStatus('project', module.exports.Status.ERROR));
dispatch(module.exports.setError('API Internal Server Error'));
return;
}
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHED));
dispatch(module.exports.setProjectInfo(body));
});
});
module.exports.reportProject = (id, jsonData) => (dispatch => {
dispatch(module.exports.setFetchStatus('report', module.exports.Status.FETCHING));
// scratchr2 will fail if no thumbnail base64 string provided. We don't yet have
// a way to get the actual project thumbnail in www/gui, so for now just submit
// a minimal base64 png string.
defaults(jsonData, {
thumbnail: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC' +
'0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII='
});
api({
host: '',
uri: `/site-api/projects/all/${id}/report/`,
method: 'POST',
json: jsonData,
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
dispatch(module.exports.setFetchStatus('report', module.exports.Status.ERROR));
return;
}
dispatch(module.exports.setFetchStatus('report', module.exports.Status.FETCHED));
});
});

View file

@ -27,20 +27,6 @@
"view": "communityblocks-interviews/communityblocks-interviews", "view": "communityblocks-interviews/communityblocks-interviews",
"title": "Community Blocks Beta Tester Interviews" "title": "Community Blocks Beta Tester Interviews"
}, },
{
"name": "conference-details-2016",
"pattern": "^/conference/2016/:id/details/?$",
"routeAlias": "/conference(?!/201[4-5])",
"view": "conference/2016/details/details",
"title": "Event Details"
},
{
"name": "conference-expectations-2016",
"pattern": "^/conference/2016/expect/?$",
"routeAlias": "/conference(?!/201[4-5])",
"view": "conference/2016/expect/expect",
"title": "What to Expect"
},
{ {
"name": "conference-index", "name": "conference-index",
"pattern": "^/conference/?(\\?.*)?$", "pattern": "^/conference/?(\\?.*)?$",
@ -58,27 +44,47 @@
"viewportWidth": "device-width" "viewportWidth": "device-width"
}, },
{ {
"name": "conference-index-2016", "name": "conference-details-2018",
"pattern": "^/conference/2016/?$", "pattern": "^/conference/2018/:id/details/?$",
"routeAlias": "/conference(?!/201[4-5])", "routeAlias": "/conference(?!/201[4-5])",
"view": "conference/2016/index/index", "view": "conference/2018/details/details",
"title": "Event Details"
},
{
"name": "conference-expectations-2018",
"pattern": "^/conference/2018/expect/?$",
"routeAlias": "/conference(?!/201[4-5])",
"view": "conference/2018/expect/expect",
"title": "What to Expect"
},
{
"name": "conference-index-2018",
"pattern": "^/conference/2018/?$",
"routeAlias": "/conference(?!/201[4-5])",
"view": "conference/2018/index/index",
"title": "Scratch Conference", "title": "Scratch Conference",
"viewportWidth": "device-width" "viewportWidth": "device-width"
}, },
{ {
"name": "conference-plan-2016", "name": "conference-plan-2018",
"pattern": "^/conference/2016/plan/?$", "pattern": "^/conference/2018/plan/?$",
"routeAlias": "/conference(?!/201[4-5])", "routeAlias": "/conference(?!/201[4-5])",
"view": "conference/2016/plan/plan", "view": "conference/2018/plan/plan",
"title": "Plan Your Visit" "title": "Plan Your Visit"
}, },
{ {
"name": "conference-schedule-2016", "name": "conference-schedule-2018",
"pattern": "^/conference/2016/schedule/?$", "pattern": "^/conference/2018/schedule/?$",
"routeAlias": "/conference(?!/201[4-5])", "routeAlias": "/conference(?!/201[4-5])",
"view": "conference/2016/schedule/schedule", "view": "conference/2018/schedule/schedule",
"title": "Conference Schedule" "title": "Conference Schedule"
}, },
{
"name": "connect",
"pattern": "^/connect/?$",
"routeAlias": "/connect/?$",
"redirect": "https://eepurl.com/cws7_f"
},
{ {
"name": "credits", "name": "credits",
"pattern": "^/info/credits/?$", "pattern": "^/info/credits/?$",
@ -193,17 +199,23 @@
}, },
{ {
"name": "preview", "name": "preview",
"pattern": "^/preview/?(\\d+)?/?$", "pattern": "^/preview(/editor|(/\\d+(/editor|/fullscreen)?)?)?/?$",
"routeAlias": "/preview/?$", "routeAlias": "/preview/?$",
"view": "preview/preview", "view": "preview/preview",
"title": "Scratch 3.0 Preview" "title": "Scratch 3.0 Preview"
}, },
{ {
"name": "preview-faq", "name": "3faq",
"pattern": "^/preview-faq/?$", "pattern": "^/3faq/?$",
"routeAlias": "/preview-faq/?$", "routeAlias": "/3faq/?$",
"view": "preview-faq/preview-faq", "view": "preview-faq/preview-faq",
"title": "Scratch 3.0 Preview FAQ" "title": "Scratch 3.0 FAQ"
},
{
"name": "preview-faq-redirect",
"pattern": "^/preview-faq/?$",
"routeAlias": "/preview-faq",
"redirect": "/3faq"
}, },
{ {
"name": "privacypolicy", "name": "privacypolicy",
@ -289,12 +301,32 @@
"view": "wedo2/wedo2", "view": "wedo2/wedo2",
"title": "LEGO WeDo 2.0" "title": "LEGO WeDo 2.0"
}, },
{
"name": "ev3",
"pattern": "^/ev3/?$",
"routeAlias": "/ev3/?$",
"view": "ev3/ev3",
"title": "LEGO MINDSTORMS EV3"
},
{
"name": "microbit",
"pattern": "^/microbit/?$",
"routeAlias": "/microbit/?$",
"view": "microbit/microbit",
"title": "micro:bit"
},
{ {
"name": "donate-redirect", "name": "donate-redirect",
"pattern": "^/info/donate/?", "pattern": "^/info/donate/?",
"routeAlias": "/info/(cards|communityblocks-interviews|credits|faq|donate)/?$", "routeAlias": "/info/(cards|communityblocks-interviews|credits|faq|donate)/?$",
"redirect": "https://secure.donationpay.org/scratchfoundation/" "redirect": "https://secure.donationpay.org/scratchfoundation/"
}, },
{
"name": "donate-redirect2",
"pattern": "^/donate/?$",
"routeAlias": "/donate/?$",
"redirect": "https://secure.donationpay.org/scratchfoundation/"
},
{ {
"name": "download-redirect", "name": "download-redirect",
"pattern": "^/scratch2download/?$", "pattern": "^/scratch2download/?$",

View file

@ -67,7 +67,7 @@ const About = () => (
id="about.aroundTheWorldDescription" id="about.aroundTheWorldDescription"
values={{ values={{
translationLink: ( translationLink: (
<a href="http://wiki.scratch.mit.edu/wiki/How_to_Translate_Scratch"> <a href="https://en.scratch-wiki.info/wiki/How_to_Translate_Scratch">
<FormattedMessage id="about.translationLinkText" /> <FormattedMessage id="about.translationLinkText" />
</a> </a>
) )
@ -119,7 +119,7 @@ const About = () => (
id="about.researchDescription" id="about.researchDescription"
values={{ values={{
researchLink: ( researchLink: (
<a href="/info/research"> <a href="/research">
<FormattedMessage id="about.researchLinkText" /> <FormattedMessage id="about.researchLinkText" />
</a> </a>
), ),
@ -141,7 +141,7 @@ const About = () => (
<h3><FormattedMessage id="about.learnMore" /></h3> <h3><FormattedMessage id="about.learnMore" /></h3>
<ul className="list"> <ul className="list">
<li> <li>
<a href="/help"><FormattedMessage id="about.learnMoreHelp" /></a> <a href="/tips"><FormattedMessage id="about.learnMoreHelp" /></a>
</li> </li>
<li> <li>
<a href="/info/faq"><FormattedMessage id="about.learnMoreFaq" /></a> <a href="/info/faq"><FormattedMessage id="about.learnMoreFaq" /></a>

View file

@ -57,9 +57,9 @@ const Components = () => (
<span className="type-gray">$type-gray</span> <span className="type-gray">$type-gray</span>
<span className="type-white">$type-white</span> <span className="type-white">$type-white</span>
<span className="link-blue">$link-blue</span> <span className="link-blue">$link-blue</span>
<span className="splash-green">$splash-green</span> <span className="splash-green">$ui-aqua</span>
<span className="splash-pink">$splash-pink</span> <span className="splash-pink">$ui-purple</span>
<span className="splash-blue">$splash-blue</span> <span className="splash-blue">$ui-blue</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -101,15 +101,15 @@
} }
.splash-green { .splash-green {
background-color: $splash-green; background-color: $ui-aqua;
} }
.splash-pink { .splash-pink {
background-color: $splash-pink; background-color: $ui-purple;
} }
.splash-blue { .splash-blue {
background-color: $splash-blue; background-color: $ui-blue;
} }
} }
} }

View file

@ -67,7 +67,7 @@
display: block; display: block;
&:hover { &:hover {
background-color: lighten($ui-blue, 40); background-color: $ui-blue-25percent;
} }
} }
} }

View file

@ -27,7 +27,7 @@
} }
.conf2017-title-band { .conf2017-title-band {
background-color: lighten($ui-blue, 10%); background-color: $ui-blue-dark;
padding: 1.5rem; padding: 1.5rem;
text-align: center; text-align: center;
color: $type-white; color: $type-white;

View file

@ -0,0 +1,121 @@
const classNames = require('classnames');
const connect = require('react-redux').connect;
const PropTypes = require('prop-types');
const React = require('react');
const render = require('../../../../lib/render.jsx');
const detailsActions = require('../../../../redux/conference-details.js');
const Page = require('../../../../components/page/conference/2018/page.jsx');
require('./details.scss');
class ConferenceDetails extends React.Component {
componentDidMount () {
let pathname = window.location.pathname.toLowerCase();
if (pathname[pathname.length - 1] === '/') {
pathname = pathname.substring(0, pathname.length - 1);
}
const path = pathname.split('/');
const detailsId = path[path.length - 2];
this.props.dispatch(detailsActions.startGetDetails(detailsId));
}
render () {
let backUri = '/conference/2018/schedule';
if (!this.props.conferenceDetails.error && !this.props.conferenceDetails.fetching) {
backUri = `${backUri}#${this.props.conferenceDetails.Day}`;
}
const classes = classNames(
'inner',
'details',
{fetching: this.props.conferenceDetails.fetching}
);
return (
<div className={classes}>
<div className="back">
<a href={backUri}>
&larr; Back to Full Schedule
</a>
</div>
{this.props.conferenceDetails.error ? [
<h2 key="not-found">Agenda Item Not Found</h2>
] : [
<h2 key="details-title">{this.props.conferenceDetails.Title}</h2>,
<ul
className="logistics"
key="details-logistics"
>
{this.props.conferenceDetails.fetching ? [] : [
<li key="presenter">
<img
alt="presenter icon"
src="/svgs/conference/schedule/presenter-icon.svg"
/>
{this.props.conferenceDetails.Presenter}
</li>,
<li key="start">
<img
alt="time icon"
src="/svgs/conference/schedule/time-icon.svg"
/>
{this.props.conferenceDetails.Start} &ndash; {this.props.conferenceDetails.End}
</li>,
<li key="type">
<img
alt="event icon"
src="/svgs/conference/schedule/event-icon.svg"
/>
{this.props.conferenceDetails.Type}
</li>,
<li key="location">
<img
alt="location icon"
src="/svgs/conference/schedule/location-icon.svg"
/>
{this.props.conferenceDetails.Location}
</li>
]}
</ul>,
<div
className="description"
key="details-desc"
>
<p>
{this.props.conferenceDetails.Description}
</p>
</div>,
<div
className="back"
key="details-back"
>
{this.props.conferenceDetails.fetching ? [] : [
<a
href={backUri}
key="details-back-uri"
>
&larr; Back to Full Schedule
</a>
]}
</div>
]}
</div>
);
}
}
ConferenceDetails.propTypes = {
conferenceDetails: PropTypes.object, // eslint-disable-line react/forbid-prop-types
dispatch: PropTypes.func
};
const mapStateToProps = state => ({
conferenceDetails: state.conferenceDetails
});
const ConnectedDetails = connect(mapStateToProps)(ConferenceDetails);
render(
<Page><ConnectedDetails /></Page>,
document.getElementById('app'),
{conferenceDetails: detailsActions.detailsReducer}
);

View file

@ -0,0 +1,56 @@
@import "../../../../frameless";
#view {
@media only screen and (max-width: $tablet - 1) {
margin-top: 100px;
}
@media only screen and (max-width: $desktop - 1) {
text-align: left;
}
}
.details {
width: $cols8;
&.inner {
margin-top: 2rem;
&.fetching {
opacity: .6;
}
}
.back {
margin: 1rem 0;
}
ul {
&.logistics {
margin: .25rem 0 2.5rem;
padding-left: 0;
list-style-type: none;
}
li {
margin: .25rem 0;
img {
margin-right: .5rem;
width: 1rem;
height: 1rem;
}
}
}
.description {
margin: 2rem 0;
}
}
//8 columns
@media only screen and (max-width: $desktop - 1) {
.details {
width: 100%;
}
}

View file

@ -0,0 +1,330 @@
const React = require('react');
const render = require('../../../../lib/render.jsx');
const FlexRow = require('../../../../components/flex-row/flex-row.jsx');
const Page = require('../../../../components/page/conference/2018/page.jsx');
const TitleBanner = require('../../../../components/title-banner/title-banner.jsx');
require('./expect.scss');
const ConferenceExpectations = () => (
<div className="expect">
<TitleBanner className="mod-conference">
<h1>
What to Expect
</h1>
<div className="title-icon">
<img
alt="expect-image"
src="/images/conference/expect/what-to-expect.png"
/>
</div>
</TitleBanner>
<section className="inner profile">
<FlexRow className="uneven">
<div className="short">
<img src="/images/conference/expect/2018/mitch.jpg" />
<h4>Mitchel Resnick</h4>
<p>
Professor of Learning Research
<br />
Founder, MIT Scratch Team
<br />
MIT Media Lab
</p>
</div>
<div className="long">
<h2>Welcome to Scratch@MIT 2018!</h2>
<p className="intro">
The theme of this years Scratch conference is The Next{' '}
Generation. In choosing this phrase, we had two different{' '}
meanings in mind.
</p>
<p className="intro">
The theme is motivated, in part, by our work on the next generation{' '}
of Scratch. We plan to release this new version, called Scratch 3.0,{' '}
later this year. Scratch 3.0 will expand how, what, and where children{' '}
can create and learn with Scratch. At the conference, youll have lots{' '}
of opportunities to experiment and explore with prototype versions of{' '}
Scratch 3.0.
</p>
<p className="intro">
But even as we develop the next generation of software, our top{' '}
priority is always the next generation of children.
</p>
<p className="intro">
We continue to be amazed and delighted by all of the ways that{' '}
children around the world are creating and collaborating with Scratch.{' '}
As we see the outpouring of creativity in the Scratch community, we{' '}
become even more committed to developing a next generation of Scratch{' '}
that is truly worthy of the next generation of children.
</p>
<p className="intro">
At this summers Scratch conference, we look forward to hearing{' '}
your stories of how children are creating and learning with Scratch,{' '}
and how you are supporting them. Lets work together to expand{' '}
opportunities for all children, from all backgrounds, to imagine,{' '}
create, and collaborate &mdash; so they can shape the world of tomorrow.
</p>
</div>
</FlexRow>
</section>
<section className="keynote">
<div className="inner">
<div className="section-header">
<h2>Keynotes</h2>
</div>
<FlexRow>
<div className="card">
<div className="date">
<b>Thursday</b>
</div>
<h3>The Next Generation</h3>
<img
alt="Scratch Team Photo"
src="/images/conference/expect/scratch-team.jpg"
/>
<p>
<b>MIT Scratch Team</b>
<br />
<b>Mitchel Resnick (moderator)</b>
</p>
<p>
Join us for an inside look at the next generation of Scratch &mdash;{' '}
and a discussion of how Scratch is opening new opportunities for the next{' '}
generation of children around the world.
</p>
</div>
<div className="card">
<div className="date">
<b>Friday</b>
</div>
<h3>Creative Is Not A Noun</h3>
<img
alt="Austin Kleon Photo"
src="/images/conference/expect/2018/austin_kleon.png"
/>
<p>
<b>Austin Kleon</b>
</p>
<img
alt="Karen Photo"
className="moderator"
src="/images/conference/expect/2018/karen.jpg"
/>
<p>
<b>Karen Brennan (moderator)</b>
</p>
<p>
Writer and artist Austin Kleon (author of the bestsellers Steal Like An Artist{' '}
and Show Your Work!) discusses his practice and shares 10 principles for anyone{' '}
who wants to do more creative work in a connected world.
</p>
</div>
<div className="card">
<div className="date">
<b>Saturday</b>
</div>
<h3>Growing Up With Scratch</h3>
<img
alt="Isabella, JT, and Jocelyn Photo"
src="/images/conference/expect/2018/growing-up-with-scratch-presenters.png"
/>
<p>
<b>Isabella Bruyere, JT Galla, &amp; Jocelyn Marencik</b>
</p>
<img
alt="Ricarose Photo"
className="moderator"
src="/images/conference/expect/2018/ricarose.png"
/>
<p>
<b>Ricarose Roque (moderator)</b>
</p>
<p>
What is it like to grow up with Scratch? Three long-time Scratch community{' '}
members share how they have used Scratch to express their interests, to make{' '}
friends, and to lead initiatives in their communities.
</p>
</div>
</FlexRow>
</div>
</section>
<section className="inner schedule">
<div className="section-header">
<div className="title">
<h2>Daily Schedules</h2>
</div>
<p className="callout">
<img
alt="July 25th Icon"
src="/svgs/conference/expect/july25-icon.svg"
/>
<b>Wednesday at 6:00p</b>&nbsp;&nbsp;Early check-in and opening reception
</p>
</div>
<FlexRow>
<table>
<tbody>
<tr>
<th>
<img
alt="July 26th Icon"
src="/svgs/conference/expect/july26-icon.svg"
/>
<h3>Thursday</h3>
</th>
</tr>
<tr>
<td>
<b>8:30a-9:30a</b>
<p>Breakfast (provided)</p>
</td>
</tr>
<tr>
<td>
<b>9:30a-10:30a</b>
<p>Keynote Presentation</p>
</td>
</tr>
<tr>
<td>
<b>11:00a-12:30p</b>
<p>Morning Workshops</p>
</td>
</tr>
<tr>
<td>
<b>12:30p-1:30p</b>
<p>Lunch (provided)</p>
</td>
</tr>
<tr>
<td>
<b>2:00p-3:30p</b>
<p>Afternoon Workshops</p>
</td>
</tr>
<tr>
<td>
<b>4:00p-5:30p</b>
<p>Poster Sessions</p>
</td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<th>
<img
alt="July 27th Icon"
src="/svgs/conference/expect/july27-icon.svg"
/>
<h3>Friday</h3>
</th>
</tr>
<tr>
<td>
<b>8:30a-9:30a</b>
<p>Breakfast (provided)</p>
</td>
</tr>
<tr>
<td>
<b>9:30a-10:30a</b>
<p>Keynote Presentation</p>
</td>
</tr>
<tr>
<td>
<b>11:00a-12:00p</b>
<p>Morning Workshops, Panels, and Ignite Talks</p>
</td>
</tr>
<tr>
<td>
<b>12:00p-1:00p</b>
<p>Lunch (provided)</p>
</td>
</tr>
<tr>
<td>
<b>1:30p-2:30p</b>
<p>Early Afternoon Workshops, Panels, and Ignite Talks</p>
</td>
</tr>
<tr>
<td>
<b>3:00p-4:00p</b>
<p>Late Afternoon Workshops, Panels, and Ignite Talks</p>
</td>
</tr>
<tr>
<td>
<b>4:30p-6:00p</b>
<p>Poster Sessions</p>
</td>
</tr>
<tr>
<td>
<b>6:00p-7:30p</b>
<p>Conference Dinner (provided)</p>
</td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<th>
<img
alt="July 28th Icon"
src="/svgs/conference/expect/july28-icon.svg"
/>
<h3>Saturday</h3>
</th>
</tr>
<tr>
<td>
<b>8:30a-9:30a</b>
<p>Breakfast (provided)</p>
</td>
</tr>
<tr>
<td>
<b>9:30a-10:30a</b>
<p>Keynote Presentation</p>
</td>
</tr>
<tr>
<td>
<b>11:00a-12:00p</b>
<p>Morning Workshops, Panels and Ignite Talks</p>
</td>
</tr>
<tr>
<td>
<b>12:00p-1:30p</b>
<p>Closing Ceremony and Lunch (provided)</p>
</td>
</tr>
<tr>
<td>
<b>1:30p</b>
<p>End of Conference</p>
</td>
</tr>
</tbody>
</table>
</FlexRow>
</section>
</div>
);
render(<Page><ConferenceExpectations /></Page>, document.getElementById('app'));

View file

@ -0,0 +1,198 @@
@import "../../../../colors";
@import "../../../../frameless";
.expect {
.flex-row {
align-items: flex-start;
justify-content: space-between;
.card {
width: $cols4;
p {
text-align: left;
}
}
}
.profile {
img {
border-radius: 8em;
width: 80%;
}
h4 {
margin-top: 1.2rem;
}
@media only screen and (max-width: $tablet - 1) {
img {
width: 50%;
}
h2 {
margin: 20px 0;
text-align: center;
font-size: 1.7rem;
}
}
@media only screen and (max-width: $desktop - 1) {
.uneven {
flex-direction: column;
align-items: center;
}
}
}
.keynote {
background-color: $ui-purple;
padding: 48px 0 64px 0;
width: 100%;
h2,
h3,
b,
p {
color: $ui-white;
}
h2 {
margin-bottom: 32px;
}
h3 {
margin: 15px 0;
}
img {
width: 100%;
&.moderator {
width: 50%;
}
}
.date {
b {
border-radius: 20px;
background-color: $ui-orange;
padding: 5px 15px;
font-size: .85rem;
}
margin: 15px 0;
}
@media only screen and (max-width: $desktop - 1) {
.flex-row {
flex-direction: column;
align-items: center;
.card {
margin-top: 25px;
text-align: left;
}
}
}
}
.schedule {
.title {
display: flex;
margin-top: 20px;
justify-content: space-between;
align-items: center;
h2 {
margin: 0;
}
}
img {
width: 30px;
}
.callout {
display: flex;
padding: .85rem;
align-items: center;
img {
margin-right: 30px;
}
}
table {
width: $cols4;
th {
display: flex;
border-bottom: thin solid $ui-border;
padding: 2.5%;
align-items: center;
justify-content: flex-start;
h3 {
margin: 0;
}
img {
margin-right: 30px;
}
}
td {
display: flex;
border-bottom: thin solid $ui-border;
padding: 2.5%;
height: 60px;
align-items: center;
b {
width: 40%;
line-height: 1.7em;
}
p {
margin: 0;
padding-left: 20px;
width: 60%;
}
}
}
@media only screen and (max-width: $mobile - 1) {
.flex-row {
table {
width: 100%;
}
}
}
@media only screen and (min-width: $mobile) and (max-width: $desktop - 1) {
.flex-row {
table {
width: $cols6;
}
}
}
@media only screen and (max-width: $desktop - 1) {
.flex-row {
flex-direction: column;
align-items: center;
table {
margin-top: 50px;
text-align: left;
th {
justify-content: center;
}
}
}
}
}
}

View file

@ -1,5 +1,3 @@
const FormattedDate = require('react-intl').FormattedDate;
const FormattedMessage = require('react-intl').FormattedMessage;
const React = require('react'); const React = require('react');
const render = require('../../../../lib/render.jsx'); const render = require('../../../../lib/render.jsx');
@ -8,246 +6,76 @@ const FlexRow = require('../../../../components/flex-row/flex-row.jsx');
const Page = require('../../../../components/page/conference/2018/page.jsx'); const Page = require('../../../../components/page/conference/2018/page.jsx');
const TitleBanner = require('../../../../components/title-banner/title-banner.jsx'); const TitleBanner = require('../../../../components/title-banner/title-banner.jsx');
require('../../../../components/forms/button.scss');
require('./index.scss'); require('./index.scss');
const ConferenceSplash = () => ( const ConferenceSplash = () => (
<div className="index mod-2018"> <div className="index mod-2016">
<TitleBanner className="mod-conference"> <TitleBanner className="mod-conference">
<h1> <h1>
<FormattedMessage id="conference-2018.subtitle" /> Scratch: The Next Generation
</h1> </h1>
<h3> <h3>
<FormattedMessage id="conference-2018.dateDesc" /> July 26-28, 2018 | Cambridge, MA, USA
</h3>
<h3>
<a href="http://www.media.mit.edu/events/medialabtalk/">
Watch the keynotes live 7/26-7/28 at 9:30 a.m. EDT
</a>
</h3> </h3>
<p> <p>
<a href="https://scratch2018.eventbrite.com"> <a href="/conference/2018/schedule">
<Button className="mod-register"> <Button>
<FormattedMessage id="conference-2018.registerNow" /> See the Schedule
</Button> </Button>
</a> </a>
</p> </p>
</TitleBanner> </TitleBanner>
<div className="inner"> <section className="inner">
<section className="info"> <FlexRow>
<FlexRow className="uneven"> <div>
<div className="long"> <h3>
<p> <a href="/conference/2018/expect">
<FormattedMessage id="conference-2018.desc1" /> <img
</p> alt="expect-image"
src="/images/conference/expect/what-to-expect.png"
<p>
<FormattedMessage id="conference-2018.desc2" />
</p>
</div>
<div className="short">
<p>
<b><FormattedMessage id="conference-2018.date" /></b>{' '}
{/* eslint-disable react/jsx-sort-props */}
<FormattedDate
value={new Date(2018, 6, 26)}
year="numeric"
month="long"
day="2-digit"
/> />
{' - '} What to Expect
<FormattedDate </a>
value={new Date(2018, 6, 28)} </h3>
year="numeric" <p>
month="long" Learn more about participating in Scratch@MIT
day="2-digit" </p>
</div>
<div>
<h3>
<a href="/conference/2018/plan">
<img
alt="plan-image"
src="/images/conference/plan/plan-your-visit.png"
/> />
{/* eslint-enable react/jsx-sort-props */} Plan Your Visit
<br /> </a>
<FormattedMessage id="conference-2018.dateDescMore" /> </h3>
<br /> <p>
<b><FormattedMessage id="conference-2018.location" /></b>{' '} Information on traveling, staying, and exploring around the Media Lab
<FormattedMessage id="conference-2018.locationDetails" /> </p>
</p> </div>
</div> <div>
</FlexRow> <h3>
<FlexRow className="uneven"> <a href="/conference/2018/schedule">
<div className="long"> <img
<h3 id="info"><FormattedMessage id="conference-2018.registrationTitle" /></h3> alt="schedule"
<p className="conf2018-panel-desc"> src="/images/conference/schedule/2018/schedule.png"
<b><FormattedMessage id="conference-2018.registrationEarly" /></b> />
<br /> Schedule
<b><FormattedMessage id="conference-2018.registrationStandard" /></b> </a>
</p> </h3>
<p> <p>
<a href="https://scratch2018.eventbrite.com"> Full schedule of events and sessions
<Button className="mod-register"> </p>
<FormattedMessage id="conference-2018.registerNow" /> </div>
</Button> </FlexRow>
</a> </section>
</p>
<h3 id="questions"><FormattedMessage id="conference-2018.questionsTitle" /></h3>
<div>
<p className="conf2018-question">
<FormattedMessage id="conference-2018.submissionQ" />
</p>
<p className="conf2018-answer">
<FormattedMessage id="conference-2018.submissionAns" />
</p>
</div>
<div>
<p className="conf2018-question">
<FormattedMessage id="conference-2018.regQ" />
</p>
<p className="conf2018-answer">
<FormattedMessage id="conference-2018.regAns" />
</p>
</div>
<div>
<p className="conf2018-question">
<FormattedMessage id="conference-2018.accommodationsQ" />
</p>
<p className="conf2018-answer">
<FormattedMessage
id="conference-2018.accommodationsAns1"
values={{
marriottLink: (
<a href="http://www.marriott.com/hotels/travel/boscb-boston-marriott-cambridge/">
Boston Marriott Cambridge
</a>
),
holidayinnLink: (
<a href="http://www.hiexpress.com/hotels/us/en/reservation/searchresult?qAdlt=1&qBrs=6c.hi.ex.rs.ic.cp.in.sb.cw.cv.ul.vn&qChld=0&qDest=CAMBRIDGE%2CMA%2CUnited+States&qFRA=1&qGRM=0&qIta=99504425&qPSt=0&qRRSrt=rt&qRef=df&qRms=1&qRpn=1&qRpp=12&qSHp=1&qSmP=3&qSrt=sBR&qWch=0&srb_u=1&icdv=99504425&dp=true">
Holiday Inn Express and Suites
</a>
),
residenceinnLink: (
<a href="http://www.marriott.com/hotels/travel/boscm-residence-inn-boston-cambridge/">
Residence Inn
</a>
),
lemeridienLink: (
<a href="http://www.starwoodhotels.com/lemeridien/property/overview/index.html?propertyID=3253&language=en_US">
Le Meridien
</a>
)
}}
/>
</p>
<p className="conf2018-answer">
<FormattedMessage
id="conference-2018.accommodationsAns2"
values={{
acLink: (
<a href="http://www.marriott.com/meeting-event-hotels/group-corporate-travel/groupCorp.mi?resLinkData=Scratch%20Conference%5EBOSAR%60sccscca%7Csccsccb%60229%60USD%60false%604%607/25/18%607/28/18%607/4/18&app=resvlink&stop_mobi=yes">
AC Hotel Boston Cambridge
</a>
),
doubletreeLink: (
<a href="https://secure3.hilton.com/en_US/dt/reservation/book.htm?inputModule=HOTEL&ctyhocn=BOSCODT&spec_plan=CDTMIT&arrival=20180725&departure=20180728&cid=OM,WW,HILTONLINK,EN,DirectLink&fromId=HILTONLINKDIRECT">
DoubleTree by Hilton Hotel Boston - Downtown
</a>
),
hotelbostonLink: (
<a href="https://www.hotelboston.com/">
Hotel Boston
</a>
),
mitLink: (
<a href="http://www.media.mit.edu/contact/accommodations">
<FormattedMessage id="conference-2018.here" />
</a>
)
}}
/>
</p>
<p className="conf2018-answer">
<FormattedMessage
id="conference-2018.accommodationsAns3"
values={{
neuLink: (
<a href="http://www.northeastern.edu/">
Northeastern University
</a>
)
}}
/>
</p>
<p className="conf2018-question">
<FormattedMessage id="conference-2018.suite" />
</p>
<p className="conf2018-answer">
<FormattedMessage id="conference-2018.single" />
{' - $80.00'}
<FormattedMessage id="conference-2018.pp" />
<br />
<FormattedMessage id="conference-2018.double" />
{' - $65.00'}
<FormattedMessage id="conference-2018.pp" />
</p>
<p className="conf2018-answer">
<FormattedMessage
id="conference-2018.accommodationsAns4"
values={{
dormrequestLink: (
<a href="https://docs.google.com/forms/d/e/1FAIpQLSd8LRQyz9ZLXcpvjmYrnpAlN0_RVyYsgObUXQveI9_WpoDabw/viewform?usp=sf_link">
<FormattedMessage id="conference-2018.dormRequestText" />
</a>
)
}}
/>
</p>
</div>
<div>
<p className="conf2018-question">
<FormattedMessage id="conference-2018.letterQ" />
</p>
<p className="conf2018-answer">
<FormattedMessage
id="conference-2018.letterAns"
values={{
emailLink: (
<a href="mailto:conference@scratch.mit.edu">
conference@scratch.mit.edu
</a>
)
}}
/>
</p>
</div>
<div>
<p className="conf2018-question">
<FormattedMessage id="conference-2018.preConfQ" />
</p>
<p className="conf2018-answer">
<FormattedMessage id="conference-2018.preConfAns" />
</p>
</div>
<div>
<p className="conf2018-question">
<FormattedMessage id="conference-2018.bringQ" />
</p>
<p className="conf2018-answer">
<FormattedMessage id="conference-2018.bringAns" />
</p>
</div>
<div>
<p className="conf2018-question">
<FormattedMessage id="conference-2018.moreQ" />
</p>
<p className="conf2018-answer">
<FormattedMessage
id="conference-2018.moreAns"
values={{
emailLink: (
<a href="mailto:conference@scratch.mit.edu">
conference@scratch.mit.edu
</a>
)
}}
/>
</p>
</div>
</div>
</FlexRow>
</section>
</div>
</div> </div>
); );

View file

@ -1,6 +1,11 @@
@import "../../../../colors"; @import "../../../../colors";
@import "../../../../frameless"; @import "../../../../frameless";
#view {
background-color: $ui-light-gray;
min-height: initial;
}
.index { .index {
.title-banner { .title-banner {
margin-bottom: 0; margin-bottom: 0;
@ -18,8 +23,19 @@
color: $type-white; color: $type-white;
} }
h1 {
font-size: 4rem;
}
h3 {
a {
text-decoration: underline;
color: $type-white;
}
}
p { p {
margin-top: 3rem; margin-top: 1rem;
&.sub-button { &.sub-button {
margin-top: 1rem; margin-top: 1rem;
@ -37,7 +53,9 @@
a { a {
button { button {
background-color: $ui-white;
color: $ui-blue;
font-size: 1rem;
} }
} }
} }
@ -65,46 +83,23 @@
} }
} }
.flex-row { .flex-row {
align-items: flex-start; align-items: flex-start;
justify-content: space-between;
&.uneven { div {
width: 28%;
text-align: center;
img { img {
width: 100%; display: block;
margin: auto;
max-width: 125px;
} }
@media only screen and (max-width: $tablet - 1) { @media only screen and (max-width: $tablet - 1) {
img { margin: .5rem;
width: 30%; width: 125px;
}
}
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
img {
width: 70%;
}
}
}
}
.info {
text-align: left;
.conf2018-question {
margin-bottom: 0;
font-weight: 500;
}
.conf2018-answer {
margin-top: 0;
}
@media only screen and (max-width: $desktop - 1) {
.uneven {
.short {
width: 70%;
}
} }
} }
} }
@ -117,133 +112,3 @@
} }
} }
} }
.conf2018-panel {
border-bottom: 1px solid $ui-border;
}
.conf2018-panel.mod-last {
border-bottom: 0;
}
.flex-row.conf2018-panel-title {
justify-content: flex-start;
align-items: center;
}
.conf2018-panel-desc {
margin: 2rem 0;
}
td {
padding: .75rem 1.25rem;
vertical-align: top;
}
.conf2018-panel-row-icon-image {
margin-top: .125rem;
width: 1rem;
height: 1rem;
}
.button.mod-register {
padding: .75em 3.5em;
text-align: center;
color: $type-white;
font-size: 1rem;
font-weight: 500;
}
@media only screen and (max-width: $mobile - 1) {
.index.mod-2018 {
text-align: left;
}
.title-banner-image.mod-2018 {
height: 10rem;
}
.conf2018-panel,
.title-banner-h3.mod-2018 {
width: initial;
}
.conf2018-panel {
margin: auto .5rem;
}
.title-banner-h3.mod-2018 {
margin: 1rem .5rem .5rem;
font-size: 1.1rem;
}
.flex-row.conf2018-panel-title {
flex-direction: row;
}
.conf2018-panel-title-text {
max-width: 14rem;
}
.conf2018-panel-row > td {
padding: .75rem .375rem .75rem 0;
}
}
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
.index.mod-2018 {
text-align: left;
}
.title-banner-image.mod-2018 {
height: 10rem;
}
.conf2018-panel,
.title-banner-h3.mod-2018 {
margin: auto .5rem ;
width: initial;
}
.title-banner-h3.mod-2018 {
font-size: 1.1rem;
}
.flex-row.conf2018-panel-title {
flex-direction: row;
}
.conf2018-panel-title-text {
max-width: 18.75rem;
}
.button.mod-register {
padding: .75em 2em;
}
}
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
.index.mod-2018 {
text-align: left;
}
.title-banner-image.mod-2018 {
height: 15rem;
}
.conf2018-panel,
.title-banner-h3.mod-2018 {
margin: auto;
width: 38.75rem;
}
.title-banner-h3.mod-2018 {
font-size: 1.1rem;
}
.button.mod-register {
padding: .75em 1.25em;
}
}

View file

@ -1,60 +0,0 @@
{
"conference-2018.title": "Scratch Conference 2018:",
"conference-2018.subtitle": "The Next Generation",
"conference-2018.dateDesc": "July 26-28, 2018 | Cambridge, MA, USA",
"conference-2018.dateDescMore": " (with opening reception the evening of July 25)",
"conference-2018.locationDetails": "MIT Media Lab, Cambridge, MA",
"conference-2018.seeBelow": "Learn more about conference dates and locations below.",
"conference-2018.date": "When:",
"conference-2018.location": "Where:",
"conference-2018.desc1": "Join us for the Scratch@MIT conference, a playful gathering of educators, researchers, developers, and other members of the worldwide Scratch community.",
"conference-2018.desc2": "We're planning a very participatory conference, with an entire day of hands-on workshops and lots of opportunities for peer-to-peer discussion and collaboration. The conference is intended primarily for adults who support young people learning Scratch.",
"conference-2018.registrationDate": "Registration opens March 1, 2018.",
"conference-2018.registerNow": "Register Now!",
"conference-2018.sessionDesc": "Interested in offering a session? We invite four types of proposals:",
"conference-2018.sessionItem1Title": "Poster/demonstration (90 minutes).",
"conference-2018.sessionItem1Desc": "Show off your project in an exhibition setting, alongside other presenters. You will be provided with display space for a poster and table space for a computer or handouts.",
"conference-2018.sessionItem2Title": "Hands-on workshop (90 minutes).",
"conference-2018.sessionItem2Desc": "Engage participants in hands-on activities, highlighting new ways of creating and collaborating with Scratch.",
"conference-2018.sessionItem3Title": "Interactive panel (60 minutes).",
"conference-2018.sessionItem3Desc": "Discuss a Scratch-related topic in a panel with three or more people. Your proposal should describe how you will engage the audience during the session.",
"conference-2018.sessionItem4Title": "Ignite talk (5 minutes).",
"conference-2018.sessionItem4Desc": "Share what you've been doing in a short, lively presentation.",
"conference-2018.deadline": "Deadline for proposals is February 5, 2018.",
"conference-2018.proposal": " Submit Your Proposal",
"conference-2018.proposalDeadline": "Deadline for proposals: February 5",
"conference-2018.proposalAccept": "Notification of acceptance: March 1",
"conference-2018.registrationTitle": "Registration:",
"conference-2018.registrationEarly": "Early Bird Registration (March 1-May 1): $200",
"conference-2018.registrationStandard": "Standard Registration (after May 1): $300",
"conference-2018.questions": "Questions? Contact the Scratch Team at {emailLink}",
"conference-2018.questionsTitle": "Questions:",
"conference-2018.submissionQ": "I missed the submission deadline. Can I still submit a proposal for the conference?",
"conference-2018.submissionAns": "We are no longer accepting proposal submissions.",
"conference-2018.regQ": "I can only attend one day of the conference. Do you offer single-day registration?",
"conference-2018.regAns": "Sorry, we are not offering single-day tickets.",
"conference-2018.accommodationsQ": "I want to plan my visit. Do you have suggestions for accommodations?",
"conference-2018.accommodationsAns1": "Yes, MIT partners with several hotels in the area who offer discounts to participants attending MIT events, including: {marriottLink} (0.4 miles from the MIT Media Lab), {holidayinnLink} (1.6 miles), {residenceinnLink} (0.3 miles), and {lemeridienLink} (0.9 miles). To reserve a room at one of these hotels, call the hotel and request the MIT discount. Advance booking is strongly recommended, as summer is a busy time in Boston. All MIT rates are subject to availability.",
"conference-2018.accommodationsAns2": "If you are looking for additional accommodation options, we also recommend the {acLink} (7.1 miles), {doubletreeLink} (3.3 miles), and {hotelbostonLink} with the code MITSC2018 (5.3 mile). You might also consider home-share options such as Airbnb. Find an extended list of accommodations {mitLink}.",
"conference-2018.here": "here",
"conference-2018.accommodationsAns3": "Limited lodging is available in {neuLink} dorm rooms at the following rates:",
"conference-2018.suite": "Suite",
"conference-2018.single": "Single",
"conference-2018.double": "Double",
"conference-2018.pp": "/person/night",
"conference-2018.accommodationsAns4": "Northeastern University dorms are open ONLY to individuals over 18 years of age. To request a dorm room, please complete the {dormrequestLink}. Northeastern is located in Boston, two miles from the conference site at MIT. It is a half-hour commute via public transportation, accessible by subway via the Green Line (the Northeastern stop on the E line) or the Orange Line (Ruggles Station stop).",
"conference-2018.dormRequestText": "Dorm Room Request Form",
"conference-2018.letterQ": "Can I get a visa letter?",
"conference-2018.letterAns": "Yes. Contact us at {emailLink}, and we can email you a letter.",
"conference-2018.preConfQ": "In previous years, there was an event on Wednesday evening before the conference. Will you be hosting something similar this year?",
"conference-2018.preConfAns": "There will be an informal, optional reception the evening of Wednesday, July 25. Participants may register early at this time as well.",
"conference-2018.bringQ": "What should I bring?",
"conference-2018.bringAns": "Plan to bring your personal device (laptops are preferred) and power cord. Presenters should plan to bring all additional presentation materials (we will provide projectors and screens). Snacks and beverages will be available throughout the day.",
"conference-2018.moreQ": "Have additional questions?",
"conference-2018.moreAns": "Contact the Scratch Team at {emailLink}."
}

View file

@ -0,0 +1,343 @@
const bindAll = require('lodash.bindall');
const React = require('react');
const Button = require('../../../../components/forms/button.jsx');
const FlexRow = require('../../../../components/flex-row/flex-row.jsx');
const TitleBanner = require('../../../../components/title-banner/title-banner.jsx');
const render = require('../../../../lib/render.jsx');
const Page = require('../../../../components/page/conference/2018/page.jsx');
require('./plan.scss');
class ConferencePlan extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'toggleQuestion'
]);
this.state = {
dorm: false
};
}
toggleQuestion (element) {
this.setState({element: !this.state[element]});
}
render () {
return (
<div className="plan">
<TitleBanner className="mod-conference">
<h1>
Plan Your Visit
</h1>
<div className="title-icon">
<img
alt="plan-image"
src="/images/conference/plan/plan-your-visit.png"
/>
</div>
</TitleBanner>
<div className="inner">
<section className="lodging">
<FlexRow className="uneven">
<div className="long">
<h2>Lodging</h2>
<p>
MIT partners with several hotels in the area who offer discounts to{' '}
participants attending MIT events, including:
</p>
<FlexRow>
<FlexRow className="column">
<p>
<a href="http://bit.ly/P0kTKy">
Boston Marriott Cambridge
</a>
<br />
<span>(Kendall Square, 0.4 miles from the MIT Media Lab)</span>
</p>
<p>
<a href="http://bit.ly/2459rhL">
Holiday Inn Express and Suites
</a>
<br />
<span>(Lechmere Station, 1.6 miles)</span>
</p>
</FlexRow>
<FlexRow className="column">
<p>
<a href="http://bit.ly/1qbQNmO">
Residence Inn
</a>
<br />
<span>(Kendall Square, 0.3 miles)</span>
</p>
<p>
<a href="http://lemerid.ie/1Kt3TDF">
Le Meridien
</a>
<br />
<span>(between Central and Kendall Squares, 0.9 miles)</span>
</p>
</FlexRow>
</FlexRow>
<p>
To reserve a room at one of these hotels, call the hotel and request the{' '}
&#34;MIT discount&#34;. Advance booking is strongly recommended, as{' '}
summer is a busy time in Boston. All MIT rates are subject to availability.
</p>
<p>
If you are looking for additional accommodation options, we also recommend the {' '}
<a href="http://www.marriott.com/meeting-event-hotels/group-corporate-travel/groupCorp.mi?resLinkData=Scratch%20Conference%5EBOSAR%60sccscca%7Csccsccb%60229%60USD%60false%604%607/25/18%607/28/18%607/4/18&app=resvlink&stop_mobi=yes">
AC Hotel Boston Cambridge
</a> (7.1 miles from the MIT Media Lab),{' '}
<a href="https://secure3.hilton.com/en_US/dt/reservation/book.htm?inputModule=HOTEL&ctyhocn=BOSCODT&spec_plan=CDTMIT&arrival=20180725&departure=20180728&cid=OM,WW,HILTONLINK,EN,DirectLink&fromId=HILTONLINKDIRECT">
DoubleTree by Hilton Hotel Boston - Downtown
</a> (3.3 miles),{' '}
and <a href="https://www.hotelboston.com/">
Hotel Boston
</a> with the code MITSC2018 (5.3 miles).{' '}
</p>
<p>
You might also consider home-share options such as Airbnb.
</p>
</div>
<div className="short">
<img
alt="Lodging Illustration"
src="/images/conference/plan/lodging.png"
/>
</div>
</FlexRow>
</section>
<section className="transportation">
<FlexRow className="uneven">
<div className="long">
<h2>Transportation</h2>
<p>
The <a href="https://whereis.mit.edu/?go=E14">MIT Media Lab</a> is located{' '}
in Kendall Square, Cambridge, MA, a 5-minute walk from the Kendall/MIT stop of{' '}
MBTA Red Line subway. Cambridge is a bike-friendly, walkable city, and{' '}
public transportation is encouraged. The MBTA provides free services from{' '}
Boston Logan Airport to the South Station subway stop as well.
</p>
<p>
Learn about{' '}
<a href="https://www.media.mit.edu/posts/directions-and-parking/">
driving, parking, and public transportation options
</a> around the MIT Media Lab.
</p>
<p>
<a href="http://web.mit.edu/facilities/transportation/parking/visitors/public_parking.html">
Public parking facilities
</a> are available near campus for a fee.
</p>
<p>
Learn about additional{' '}
<a href="http://www.cityofboston.gov/transportation/modes.asp">
transportation options in Cambridge and Boston
</a>.
</p>
</div>
<div className="short">
<img
alt="Transportation Illustration"
src="/images/conference/plan/transportation.png"
/>
</div>
</FlexRow>
</section>
<section className="explore">
<h2>Exploring Cambridge</h2>
<div>
<p>
Boston is a city full of history and diverse neighborhoods. Check some{' '}
of these attractions to experience the citys rich cultural offerings:
</p>
<ul>
<li>
<a href="http://www.trolleytours.com/boston/">
Beantown Trolley Tour
</a>
</li>
<li>
<a href="http://www.bostonducktours.com/">
Boston Duck Tours
</a>
</li>
<li>
<a href="http://www.bostonteapartyship.com/">
Boston Tea Party Ship &amp; Museum
</a>
</li>
<li>
<a href="http://www.faneuilhallmarketplace.com/">
Faneuil Hall Marketplace
</a>
</li>
<li>
<a href="http://www.thefreedomtrail.org/">
Freedom Trail Walking Tours
</a>
</li>
<li>
<a href="http://www.hmnh.harvard.edu/">
Harvard Museum of Natural History
</a>
</li>
<li>
<a href="https://www.icaboston.org/">
Institute of Contemporary Art
</a>
</li>
<li>
<a href="http://www.gardnermuseum.org/home">
Isabella Stewart Gardner Museum
</a>
</li>
<li>
<a href="http://www.jfklibrary.org/">
John F. Kennedy Library &amp; Museum
</a>
</li>
<li>
<a href="http://web.mit.edu/museum/">
MIT Museum
</a>
</li>
<li>
<a href="http://www.mfa.org/">
Museum of Fine Arts
</a>
</li>
<li>
<a href="http://www.mos.org/">
Museum Of Science
</a>
</li>
<li>
<a href="http://www.neaq.org/index.php">
New England Aquarium
</a>
</li>
<li>
<a href="https://ussconstitutionmuseum.org/">
USS Constitution
</a>
</li>
</ul>
</div>
<div>
<p>
Try some Scratch Team favorites for snacking and dining around the Lab:
</p>
<ul>
<li>
A4 Pizza
</li>
<li>
Abigails
</li>
<li>
Bailey and Sage
</li>
<li>
Clover
</li>
<li>
Commonwealth
</li>
<li>
Legal Seafood
</li>
<li>
Meadhall
</li>
<li>
Sebastians
</li>
<li>
Tatte
</li>
<li>
Za
</li>
</ul>
</div>
</section>
<section className="faq last">
<FlexRow className="uneven">
<div className="long">
<h2>FAQ</h2>
<dl>
<dt>
The conference is sold out. What can I do?
</dt>
<dd>
Scratch@MIT is sold out and at capacity. Regrettably, we are{' '}
unable to add any additional guests. Please keep in mind that{' '}
you must have registered on Eventbrite to attend Scratch@MIT;{' '}
people who are not registered / do not have a ticket will not be{' '}
able to attend the conference.
</dd>
<dt>
I missed the submission deadline. Can I still submit a proposal for{' '}
the conference?
</dt>
<dd>
We are no longer accepting proposal submissions.
</dd>
<dt>
I can only attend one day of the conference. Do you offer single-day{' '}
registration?
</dt>
<dd>
Sorry, we are not offering single-day tickets.
</dd>
<dt>
Can I receive a visa letter?
</dt>
<dd>
Yes. Contact us at{' '}
<a href="mailto:conference@scratch.mit.edu">conference@scratch.mit.edu</a>{' '}
and we can email you a letter.
</dd>
<dt>
In previous years, there was an event on Wednesday evening before the{' '}
conference. Will you be hosting something similar this year?
</dt>
<dd>
There will be an informal, optional reception the evening of Wednesday,{' '}
July 25. Participants may register early at this time as well.
</dd>
<dt>
What should I bring?
</dt>
<dd>
Plan to bring your personal device (laptops are preferred) and power cord.{' '}
Presenters should plan to bring all additional presentation materials{' '}
(we will provide projectors and screens). Snacks and beverages will be{' '}
available throughout the day.
</dd>
</dl>
</div>
<div className="short">
<h3>Have Additional Questions?</h3>
<a href="mailto:conference@scratch.mit.edu">
<Button>Email Us</Button>
</a>
</div>
</FlexRow>
</section>
</div>
</div>
);
}
}
render(<Page><ConferencePlan /></Page>, document.getElementById('app'));

View file

@ -0,0 +1,113 @@
@import "../../../../colors";
@import "../../../../frameless";
.plan {
section {
border-bottom: 2px solid $ui-border;
&.last {
border-bottom: 0;
}
}
.flex-row {
align-items: flex-start;
justify-content: space-between;
&.uneven {
img {
width: 100%;
}
@media only screen and (max-width: $tablet - 1) {
img {
width: 30%;
}
}
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
img {
width: 70%;
}
}
}
}
.lodging {
text-align: left;
@media only screen and (max-width: $desktop - 1) {
.uneven {
.short {
display: none;
}
}
}
}
.transportation {
.uneven {
align-items: center;
}
@media only screen and (max-width: $desktop - 1) {
.flex-row {
flex-direction: column-reverse;
}
}
}
.explore {
div {
margin-top: 30px;
}
ul {
display: flex;
max-height: 23rem;
flex-flow: column wrap;
justify-content: flex-start;
}
@media only screen and (max-width: $tablet - 1) {
ul {
max-height: 100%;
}
}
@media only screen and (max-width: $desktop - 1) {
div {
text-align: left;
}
}
}
.faq {
dl {
dt {
font-weight: bold;
}
dd {
margin: 8px 0 32px 0;
}
}
.short {
margin-top: 64px;
border: 2px solid $ui-border;
border-radius: 4px;
background-color: $ui-white;
padding: 16px;
text-align: center;
h3 {
margin: 0;
}
@media only screen and (max-width: $tablet - 1) {
margin-top: 0;
}
}
}
}

View file

@ -0,0 +1,205 @@
const bindAll = require('lodash.bindall');
const classNames = require('classnames');
const connect = require('react-redux').connect;
const PropTypes = require('prop-types');
const React = require('react');
const scheduleActions = require('../../../../redux/conference-schedule.js');
const FlexRow = require('../../../../components/flex-row/flex-row.jsx');
const SubNavigation = require('../../../../components/subnavigation/subnavigation.jsx');
const TitleBanner = require('../../../../components/title-banner/title-banner.jsx');
const render = require('../../../../lib/render.jsx');
const Page = require('../../../../components/page/conference/2018/page.jsx');
require('./schedule.scss');
class ConferenceSchedule extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleScheduleChange',
'renderChunkItems'
]);
}
componentDidMount () {
const day = window.location.hash.substr(1) || 'thursday';
this.handleScheduleChange(day);
}
handleScheduleChange (day) {
window.history.replaceState(history.state, '', `#${day}`);
this.props.dispatch(scheduleActions.startGetSchedule(day));
}
renderChunkItems (timeSlot) {
return timeSlot.map(item => {
if (item.Presenter) {
return (
<a
className="item-url"
href={item.uri}
>
<div
className="agenda-item"
key={item.rowid}
>
<h3>{item.Title}</h3>
<FlexRow>
<p>
<img
alt="time icon"
src="/svgs/conference/schedule/time-icon.svg"
/>
{item.Start} &ndash; {item.End}
</p>
<p>
<img
alt="location icon"
src="/svgs/conference/schedule/location-icon.svg"
/>
{item.Location}
</p>
</FlexRow>
<FlexRow>
<p>
<img
alt="presenter icon"
src="/svgs/conference/schedule/presenter-icon.svg"
/>
{item.Presenter}
</p>
<p>
<img
alt="event icon"
src="/svgs/conference/schedule/event-icon.svg"
/>
{item.Type}
</p>
</FlexRow>
</div>
</a>
);
}
return (
<div
className="agenda-item no-click"
key={item.rowid}
>
<h3>{item.Title}</h3>
<FlexRow>
<p>
<img
alt="time icon"
src="/svgs/conference/schedule/time-icon.svg"
/>
{item.Start} &ndash; {item.End}
</p>
<p>
<img
alt="location icon"
src="/svgs/conference/schedule/location-icon.svg"
/>
{item.Location}
</p>
</FlexRow>
</div>
);
});
}
render () {
const tabClasses = {
thursday: classNames({
selected: (this.props.conferenceSchedule.day === 'thursday')
}),
friday: classNames({
selected: (this.props.conferenceSchedule.day === 'friday')
}),
saturday: classNames({
last: true,
selected: (this.props.conferenceSchedule.day === 'saturday')
})
};
const handleScheduleMethods = {
thursday: () => {
this.handleScheduleChange('thursday');
},
friday: () => {
this.handleScheduleChange('friday');
},
saturday: () => {
this.handleScheduleChange('saturday');
}
};
return (
<div className="schedule">
<TitleBanner className="mod-conference">
<h1>
Schedule
</h1>
</TitleBanner>
<SubNavigation>
<li
className={tabClasses.thursday}
onClick={handleScheduleMethods.thursday}
>
<img
alt="July 26th Icon"
src="/svgs/conference/expect/july26-icon.svg"
/>
<span>Thursday</span>
</li>
<li
className={tabClasses.friday}
onClick={handleScheduleMethods.friday}
>
<img
alt="July 27th Icon"
src="/svgs/conference/expect/july27-icon.svg"
/>
<span>Friday</span>
</li>
<li
className={tabClasses.saturday}
onClick={handleScheduleMethods.saturday}
>
<img
alt="July 28th Icon"
src="/svgs/conference/expect/july28-icon.svg"
/>
<span>Saturday</span>
</li>
</SubNavigation>
<div className="inner">
{this.props.conferenceSchedule.timeSlots.map(timeSlot => ([
<h2
className="breaking-title"
key={timeSlot.info.name}
>
<span>{timeSlot.info.name} {timeSlot.info.time}</span>
</h2>,
this.renderChunkItems(timeSlot.items)
]))}
</div>
</div>
);
}
}
ConferenceSchedule.propTypes = {
conferenceSchedule: PropTypes.object, // eslint-disable-line react/forbid-prop-types
dispatch: PropTypes.func
};
const mapStateToProps = state => ({
conferenceSchedule: state.conferenceSchedule
});
const ConnectedSchedule = connect(mapStateToProps)(ConferenceSchedule);
render(
<Page><ConnectedSchedule /></Page>,
document.getElementById('app'),
{conferenceSchedule: scheduleActions.scheduleReducer}
);

View file

@ -0,0 +1,150 @@
@import "../../../../colors";
@import "../../../../frameless";
.schedule {
.title-banner {
margin-bottom: 0;
}
.sub-nav {
z-index: -1;
box-shadow: 0 2px 5px $ui-dark-gray;
padding: 0;
li {
margin: 0;
border: 0;
border-top: 4px solid transparent;
border-left: 2px solid $active-gray;
border-radius: 0;
padding: .75em 1em;
color: $type-gray;
font-size: 1rem;
&.last {
border-right: 2px solid $active-gray;
}
&:hover,
&:active,
&.selected {
border-top: 4px solid $ui-orange;
border-left: 2px solid $active-gray;
box-shadow: none;
background-color: inherit;
}
&.selected {
font-weight: 700;
}
}
img {
margin-right: .5em;
width: 2em;
vertical-align: middle;
}
}
.inner {
h2 {
&.breaking-title {
margin: 4rem 0 2rem 0;
border-bottom: 1px solid $ui-dark-gray;
width: 100%;
height: 1.7rem; // match the line-height for h2
text-align: center;
}
span {
background-color: $background-color;
padding: 0 10px;
}
}
a {
&.item-url {
display: block;
&:hover {
background-color: $ui-blue-10percent;
}
}
}
.agenda-item {
margin: 1rem 0;
border: 1px solid $active-gray;
border-radius: 5px;
padding: 1.25rem 2.25rem;
&.no-click {
background-color: $ui-gray;
}
.flex-row {
margin: .5rem 0;
justify-content: space-between;
align-items: flex-start;
p {
margin: 0;
width: 48%;
text-align: left;
}
img {
margin-right: .5rem;
width: 1rem;
height: 1rem;
}
}
}
}
@media only screen and (max-width: $mobile - 1) {
.sub-nav {
flex-wrap: nowrap;
}
.inner {
h2 {
&.breaking-title {
margin: 2rem 0 2rem;
height: 100%;
font-size: 1.5rem;
}
}
.agenda-item {
h3 {
font-size: 1.2rem;
}
}
}
}
@media only screen and (max-width: $tablet - 1) {
.inner {
h2 {
&.breaking-title {
border-bottom: 0;
}
span {
background-color: transparent;
padding: 0;
}
}
.agenda-item {
.flex-row {
p {
margin: .5rem 0;
width: 100%;
}
}
}
}
}
}

View file

@ -174,6 +174,14 @@ const Credits = () => (
<span className="name">Tracy Tang</span> <span className="name">Tracy Tang</span>
</li> </li>
<li>
<img
alt="Bryce Avatar"
src="//cdn.scratch.mit.edu/get_image/user/2029640_170x170.png"
/>
<span className="name">Bryce Taylor</span>
</li>
<li> <li>
<img <img
alt="Matthew Avatar" alt="Matthew Avatar"
@ -198,6 +206,14 @@ const Credits = () => (
<span className="name">Chris Willis-Ford</span> <span className="name">Chris Willis-Ford</span>
</li> </li>
<li>
<img
alt="Kathy Avatar"
src="//cdn.scratch.mit.edu/get_image/user/26779669_170x170.png"
/>
<span className="name">Kathy Wu</span>
</li>
<li> <li>
<img <img
alt="Julia Avatar" alt="Julia Avatar"

View file

@ -55,8 +55,7 @@ class Download extends React.Component {
downloadUrls = { downloadUrls = {
mac: `${downloadPath}${this.state.swfVersion}.dmg`, mac: `${downloadPath}${this.state.swfVersion}.dmg`,
mac105: `${downloadPath}${this.state.swfVersion}.air`, mac105: `${downloadPath}${this.state.swfVersion}.air`,
windows: `${downloadPath}${this.state.swfVersion}.exe`, windows: `${downloadPath}${this.state.swfVersion}.exe`
linux: `${downloadPath}${this.state.swfVersion}.air`
}; };
} }
@ -143,12 +142,6 @@ class Download extends React.Component {
<FormattedMessage id="download.download" /> <FormattedMessage id="download.download" />
</a> </a>
</li> </li>
<li className="installation-downloads-item">
<FormattedMessage id="download.linux" /> -
{' '}<a href="http://airdownload.adobe.com/air/lin/download/2.6/AdobeAIRInstaller.bin">
<FormattedMessage id="download.download" />
</a>
</li>
</ul> </ul>
</div> </div>
<div className="installation-column"> <div className="installation-column">
@ -180,12 +173,6 @@ class Download extends React.Component {
<FormattedMessage id="download.download" /> <FormattedMessage id="download.download" />
</a> </a>
</li> </li>
<li className="installation-downloads-item">
<FormattedMessage id="download.linux" /> -
{' '}<a href={downloadUrls.linux}>
<FormattedMessage id="download.download" />
</a>
</li>
</ul> </ul>
]} ]}
{this.state.swfVersion === -1 ? [ {this.state.swfVersion === -1 ? [

View file

@ -1,8 +1,6 @@
@import "../../colors"; @import "../../colors";
@import "../../frameless"; @import "../../frameless";
$developer-spot: $splash-blue;
#view { #view {
padding: 0; padding: 0;
} }
@ -11,7 +9,7 @@ $developer-spot: $splash-blue;
.title-banner { .title-banner {
&.masthead { &.masthead {
margin-bottom: 0; margin-bottom: 0;
background-color: $developer-spot; background-color: $ui-blue-dark;
padding-bottom: 0; padding-bottom: 0;
h1 { h1 {
@ -32,10 +30,8 @@ $developer-spot: $splash-blue;
} }
.band { .band {
$band-color: #3baddd;
margin-top: 2rem; margin-top: 2rem;
background-color: $band-color; background-color: $ui-white-15percent;
padding: 1rem 0; padding: 1rem 0;
} }
@ -74,13 +70,17 @@ $developer-spot: $splash-blue;
.installation-column { .installation-column {
max-width: $cols4; max-width: $cols4;
text-align: center; text-align: center;
p {
margin-right: .5rem;
margin-left: .5rem;
}
} }
.installation-column-number { .installation-column-number {
margin: 2rem auto; margin: 2rem auto;
border: 2px solid $active-gray; border: 2px solid $active-gray;
border-radius: 2rem; border-radius: 2rem;
box-shadow: 0 0 0 .5rem lighten($ui-blue, 35);
background-color: $ui-blue; background-color: $ui-blue;
width: 3.75rem; width: 3.75rem;
height: 3.75rem; height: 3.75rem;

View file

@ -1,6 +1,6 @@
{ {
"download.title": "Scratch 2.0 Offline Editor", "download.title": "Scratch 2.0 Offline Editor",
"download.intro": "You can install the Scratch 2.0 editor to work on projects without an internet connection. This version will work on Mac, Windows, and some versions of Linux (32 bit).", "download.intro": "You can install the Scratch 2.0 editor to work on projects without an internet connection. This version will work on Windows and MacOS.",
"download.introMac": "<b>Note for Mac Users:</b> the latest version of Scratch 2.0 Offline requires Adobe AIR 20. To upgrade to Adobe AIR 20 manually, go <a href=\"https://get.adobe.com/air/\">here</a>.", "download.introMac": "<b>Note for Mac Users:</b> the latest version of Scratch 2.0 Offline requires Adobe AIR 20. To upgrade to Adobe AIR 20 manually, go <a href=\"https://get.adobe.com/air/\">here</a>.",
"download.installation": "Installation", "download.installation": "Installation",
"download.airTitle": "Adobe AIR", "download.airTitle": "Adobe AIR",
@ -8,7 +8,6 @@
"download.macOSX": "Mac OS X", "download.macOSX": "Mac OS X",
"download.macOlder": "Mac OS 10.5 & Older", "download.macOlder": "Mac OS 10.5 & Older",
"download.windows": "Windows", "download.windows": "Windows",
"download.linux": "Linux",
"download.download": "Download", "download.download": "Download",
"download.offlineEditorTitle": "Scratch Offline Editor", "download.offlineEditorTitle": "Scratch Offline Editor",
"download.offlineEditorBody": "Next download and install the Scratch 2.0 Offline Editor", "download.offlineEditorBody": "Next download and install the Scratch 2.0 Offline Editor",

274
src/views/ev3/ev3.jsx Normal file
View file

@ -0,0 +1,274 @@
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const FormattedMessage = require('react-intl').FormattedMessage;
const React = require('react');
const Page = require('../../components/page/www/page.jsx');
const render = require('../../lib/render.jsx');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const OSChooser = require('../../components/os-chooser/os-chooser.jsx');
const ExtensionLanding = require('../../components/extension-landing/extension-landing.jsx');
const ExtensionHeader = require('../../components/extension-landing/extension-header.jsx');
const ExtensionRequirements = require('../../components/extension-landing/extension-requirements.jsx');
const ExtensionSection = require('../../components/extension-landing/extension-section.jsx');
const InstallScratchLink = require('../../components/extension-landing/install-scratch-link.jsx');
const TipBox = require('../../components/extension-landing/tip-box.jsx');
const ProjectCard = require('../../components/extension-landing/project-card.jsx');
const Steps = require('../../components/steps/steps.jsx');
const Step = require('../../components/steps/step.jsx');
const OS_ENUM = require('../../components/extension-landing/os-enum.js');
require('../../components/extension-landing/extension-landing.scss');
require('./ev3.scss');
class EV3 extends ExtensionLanding {
render () {
return (
<div className="extension-landing ev3">
<ExtensionHeader imageSrc="/images/ev3/ev3-illustration.png">
<FlexRow className="column extension-copy">
<h2><img src="/images/ev3/ev3.svg" />LEGO MINDSTORMS EV3</h2>
<FormattedMessage
id="ev3.headerText"
values={{
ev3Link: (
<a
href="https://shop.lego.com/en-US/LEGO-MINDSTORMS-EV3-31313"
rel="noopener noreferrer"
target="_blank"
>
LEGO MINDSTORMS EV3
</a>
)
}}
/>
</FlexRow>
<ExtensionRequirements>
<span>
<img src="/svgs/extensions/windows.svg" />
Windows 10+
</span>
<span>
<img src="/svgs/extensions/mac.svg" />
macOS 10.13+
</span>
<span>
<img src="/svgs/extensions/bluetooth.svg" />
Bluetooth
</span>
<span>
<img src="/svgs/extensions/scratch-link.svg" />
Scratch Link
</span>
</ExtensionRequirements>
</ExtensionHeader>
<OSChooser
currentOS={this.state.OS}
handleSetOS={this.onSetOS}
/>
<InstallScratchLink
currentOS={this.state.OS}
/>
<ExtensionSection className="getting-started">
<h2><FormattedMessage id="ev3.gettingStarted" /></h2>
<FlexRow className="column getting-started-section">
<h3><FormattedMessage id="ev3.connectingEV3" /></h3>
<Steps>
<Step number={1}>
<div className="step-image">
<img src="/images/ev3/ev3-connect-1.png" />
</div>
<p><FormattedMessage id="ev3.turnOnEV3" /></p>
</Step>
<Step number={2}>
<div className="step-image">
<img src="/images/ev3/ev3-connect-2.png" />
</div>
<p>
<FormattedMessage
id="ev3.useScratch3"
values={{
scratch3Link: (
<a
href="https://beta.scratch.mit.edu/"
rel="noopener noreferrer"
target="_blank"
>
Scratch 3.0
</a>
)
}}
/>
</p>
</Step>
<Step number={3}>
<div className="step-image">
<img src="/images/ev3/ev3-connect-3.png" />
</div>
<p><FormattedMessage id="ev3.addExtension" /></p>
</Step>
</Steps>
<TipBox title={this.props.intl.formatMessage({id: 'ev3.firstTimeConnecting'})}>
<p><FormattedMessage id="ev3.pairingDescription" /></p>
<Steps>
<Step>
<div className="step-image">
<img src="/images/ev3/ev3-accept-connection.png" />
</div>
<p><FormattedMessage id="ev3.acceptConnection" /></p>
</Step>
<Step>
<div className="step-image">
<img src="/images/ev3/ev3-pin.png" />
</div>
<p><FormattedMessage id="ev3.acceptPasscode" /></p>
</Step>
<Step>
<div className="step-image">
<img
className="screenshot"
src={`/images/ev3/${
this.state.OS === OS_ENUM.WINDOWS ?
'win-device-ready.png' :
'mac-enter-passcode.png'
}`}
/>
</div>
<p>
{this.state.OS === OS_ENUM.WINDOWS ?
<FormattedMessage id="ev3.windowsFinalizePairing" /> :
<FormattedMessage id="ev3.macosFinalizePairing" />
}
</p>
</Step>
</Steps>
</TipBox>
</FlexRow>
</ExtensionSection>
<ExtensionSection className="blue things-to-try">
<h2><FormattedMessage id="ev3.thingsToTry" /></h2>
<h3><FormattedMessage id="ev3.makeMotorMove" /></h3>
<Steps>
<Step
compact
number={1}
>
<span className="step-description">
<FormattedMessage
id="ev3.plugMotorIn"
values={{
portA: (
<strong><FormattedMessage id="ev3.portA" /></strong>
)
}}
/>
</span>
<div className="step-image">
<img src="/images/ev3/ev3-motor-port-a.png" />
</div>
</Step>
<Step
compact
number={2}
>
<span className="step-description">
<FormattedMessage
id="ev3.clickMotorBlock"
values={{
motorBlockText: (
<strong><FormattedMessage id="ev3.motorBlockText" /></strong>
)
}}
/>
</span>
<div className="step-image">
<img src="/images/ev3/motor-turn-block.png" />
</div>
</Step>
</Steps>
<hr />
<h3><FormattedMessage id="ev3.starterProjects" /></h3>
<Steps>
<ProjectCard
cardUrl="https://downloads.scratch.mit.edu/ev3/ev3-wave-hello.sb3"
description={this.props.intl.formatMessage({id: 'ev3.waveHelloDescription'})}
imageSrc="/images/ev3/starter-wave-hello.png"
title={this.props.intl.formatMessage({id: 'ev3.waveHelloTitle'})}
/>
<ProjectCard
cardUrl="https://downloads.scratch.mit.edu/ev3/ev3-distance-instrument.sb3"
description={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentDescription'})}
imageSrc="/images/ev3/starter-distance-instrument.png"
title={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentTitle'})}
/>
<ProjectCard
cardUrl="https://downloads.scratch.mit.edu/ev3/ev3-space-tacos.sb3"
description={this.props.intl.formatMessage({id: 'ev3.spaceTacosDescription'})}
imageSrc="/images/ev3/starter-flying-game.png"
title={this.props.intl.formatMessage({id: 'ev3.spaceTacosTitle'})}
/>
</Steps>
</ExtensionSection>
<ExtensionSection className="faq">
<h2><FormattedMessage id="ev3.troubleshootingTitle" /></h2>
<h3 className="faq-title"><FormattedMessage id="ev3.makeSurePairedTitle" /></h3>
<p>
<FormattedMessage
id="ev3.makeSurePairedText"
values={{
pairingInstructionLink: (
<a
href="https://www.lego.com/en-us/service/help/products/themes-sets/mindstorms/connecting-your-lego-mindstorms-ev3-to-bluetooth-408100000007886"
rel="noopener noreferrer"
target="_blank"
>
<FormattedMessage id="ev3.pairingInstructionText" />
</a>
)
}}
/>
</p>
<h3 className="faq-title"><FormattedMessage id="ev3.closeScratchCopiesTitle" /></h3>
<p>
<FormattedMessage id="ev3.closeScratchCopiesText" />
</p>
<h3 className="faq-title"><FormattedMessage id="ev3.otherComputerConnectedTitle" /></h3>
<p>
<FormattedMessage id="ev3.otherComputerConnectedText" />
</p>
<h3 className="faq-title"><FormattedMessage id="ev3.updateFirmwareTitle" /></h3>
<p>
<FormattedMessage
id="ev3.updateFirmwareText"
values={{
firmwareUpdateLink: (
<a
href="https://education.lego.com/en-us/support/mindstorms-ev3/firmware-update"
rel="noopener noreferrer"
target="_blank"
>
<FormattedMessage id="ev3.firmwareUpdateText" />
</a>
)
}}
/>
</p>
</ExtensionSection>
</div>
);
}
}
EV3.propTypes = {
intl: intlShape.isRequired
};
const WrappedEV3 = injectIntl(EV3);
render(<Page><WrappedEV3 /></Page>, document.getElementById('app'));

8
src/views/ev3/ev3.scss Normal file
View file

@ -0,0 +1,8 @@
@import "../../colors";
.ev3 {
.extension-header {
background-color: $ui-aqua;
background-image: url("/images/ev3/ev3-pattern.svg");
}
}

38
src/views/ev3/l10n.json Normal file
View file

@ -0,0 +1,38 @@
{
"ev3.headerText": "{ev3Link} is an invention kit with motors and sensors you can use to build interactive robotic creations. Connecting it to Scratch expands the possibilities: build a robotic puppet and tell stories, make your own musical instruments and game controllers, or whatever else you can imagine.",
"ev3.gettingStarted": "Getting Started",
"ev3.connectingEV3": "Connecting EV3 to Scratch",
"ev3.turnOnEV3": "Turn on your EV3 by holding down the center button.",
"ev3.useScratch3": "Use the {scratch3Link} editor.",
"ev3.addExtension": "Add the EV3 extension.",
"ev3.firstTimeConnecting": "First time connecting your EV3?",
"ev3.pairingDescription": "After clicking the connect button in Scratch, you will need to pair it with your computer:",
"ev3.acceptConnection": "Accept the connection.",
"ev3.acceptPasscode": "Accept the passcode.",
"ev3.windowsFinalizePairing": "Wait for your device to be ready.",
"ev3.macosFinalizePairing": "Enter the passcode on your computer.",
"ev3.thingsToTry": "Things to Try",
"ev3.makeMotorMove": "Make a motor move",
"ev3.plugMotorIn": "Plug a motor into {portA} on the EV3 hub",
"ev3.portA": "port A",
"ev3.clickMotorBlock": "Find the {motorBlockText} block and click on it.",
"ev3.motorBlockText": "\"motor A turn this way\"",
"ev3.starterProjects": "Starter Projects",
"ev3.waveHelloTitle": "Wave Hello",
"ev3.waveHelloDescription": "Make a puppet robot and have a friendly chat.",
"ev3.distanceInstrumentTitle": "Distance Instrument",
"ev3.distanceInstrumentDescription": "Move your body in front of the sensor to make music.",
"ev3.spaceTacosTitle": "Space Tacos",
"ev3.spaceTacosDescription": "Build your own controller to catch tacos in space.",
"ev3.troubleshootingTitle": "Troubleshooting",
"ev3.makeSurePairedTitle": "Make sure your computer is paired with your EV3",
"ev3.makeSurePairedText": "Your computer needs to be paired with your EV3 before it can connect to Scratch. We try to do this automatically the first time you add the EV3 extension, but if it isn't working you can try these {pairingInstructionLink}.",
"ev3.pairingInstructionText": "bluetooth pairing instructions from LEGO",
"ev3.closeScratchCopiesTitle": "Close other copies of Scratch",
"ev3.closeScratchCopiesText": "Only one copy of Scratch can connect with the EV3 at a time. If you have Scratch open in other browser tabs, close it and try again.",
"ev3.otherComputerConnectedTitle": "Make sure no other computer is connected to your EV3",
"ev3.otherComputerConnectedText": "Only one computer can be connected to an EV3 at a time. If you have another computer connected to your EV3, disconnect the EV3 or close Scratch on that computer and try again.",
"ev3.updateFirmwareTitle": "Try updating your EV3 firmware",
"ev3.updateFirmwareText": "We recommend updating to EV3 firmware version 1.10E or above. See {firmwareUpdateLink}. We recommend following the instructions for \"Manual Firmware Update\".",
"ev3.firmwareUpdateText": "firmware update instructions from LEGO"
}

View file

@ -209,7 +209,6 @@ class Explore extends React.Component {
showViews={false} showViews={false}
/> />
<Button <Button
className="white"
onClick={this.handleGetExploreMore} onClick={this.handleGetExploreMore}
> >
<FormattedMessage id="general.loadMore" /> <FormattedMessage id="general.loadMore" />

View file

@ -114,6 +114,7 @@ $base-bg: $ui-white;
.button { .button {
display: block; display: block;
margin: 0 auto; margin: 0 auto;
width: 58.75rem;
} }
} }
} }
@ -128,6 +129,12 @@ $base-bg: $ui-white;
.sort-controls { .sort-controls {
width: $cols4; width: $cols4;
} }
#projectBox {
.button {
width: $cols4;
}
}
} }
} }
@ -141,6 +148,12 @@ $base-bg: $ui-white;
.sort-controls { .sort-controls {
width: $cols6; width: $cols6;
} }
#projectBox {
.button {
width: $cols6;
}
}
} }
} }
@ -161,6 +174,9 @@ $base-bg: $ui-white;
width: $cols9; width: $cols9;
} }
} }
.button {
width: $cols9;
}
} }
} }
} }

View file

@ -30,7 +30,7 @@
"faq.privacyCountry":"country", "faq.privacyCountry":"country",
"faq.privacyBirthdate":"birth month and year - We use this to confirm ownership of the account if the owner loses the password and email or asks to close an account.", "faq.privacyBirthdate":"birth month and year - We use this to confirm ownership of the account if the owner loses the password and email or asks to close an account.",
"faq.privacyGender":"gender", "faq.privacyGender":"gender",
"faq.privacyEmail":"contact email address - If the account holder is younger than 13, we ask for the email address of their parent or guardian. We do not send email to this address except when someone requests to have the account password reset.", "faq.privacyEmail":"contact email address - If the account holder is younger than 16, we ask for the email address of their parent or guardian. We do not send email to this address except when someone requests to have the account password reset.",
"faq.accountPublicInfo":"The username and country of the account holder are displayed publicly on their profile page. The birth month / year, email address, and gender associated with the account are not displayed publicly. We collect this info so we can know the age and gender of our users in aggregate, and for research purposes. We do not sell or rent information about our users to anyone.", "faq.accountPublicInfo":"The username and country of the account holder are displayed publicly on their profile page. The birth month / year, email address, and gender associated with the account are not displayed publicly. We collect this info so we can know the age and gender of our users in aggregate, and for research purposes. We do not sell or rent information about our users to anyone.",
"faq.dataCollectionTitle":"What data is collected from people while they use the website?", "faq.dataCollectionTitle":"What data is collected from people while they use the website?",
"faq.dataCollectionOne":"When a user logs in, the Scratch website asks their browser to put an <a href=\"http://en.wikipedia.org/wiki/HTTP_cookie\">http cookie</a> on their computer in order to remember that they are logged in while they browse different pages. We collect some data on where users click and which parts of the site they visit using Google Analytics. This \"click data\" helps us figure out ways to improve the website.", "faq.dataCollectionOne":"When a user logs in, the Scratch website asks their browser to put an <a href=\"http://en.wikipedia.org/wiki/HTTP_cookie\">http cookie</a> on their computer in order to remember that they are logged in while they browse different pages. We collect some data on where users click and which parts of the site they visit using Google Analytics. This \"click data\" helps us figure out ways to improve the website.",

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