mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-23 07:38:07 -05:00
Merge branch 'develop' into add-parents
This commit is contained in:
commit
9cb2c34a1d
166 changed files with 5053 additions and 1948 deletions
18
.travis.yml
18
.travis.yml
|
@ -14,15 +14,30 @@ env:
|
||||||
global:
|
global:
|
||||||
- CXX=g++-4.8
|
- CXX=g++-4.8
|
||||||
- API_HOST_master=https://api.scratch.mit.edu
|
- API_HOST_master=https://api.scratch.mit.edu
|
||||||
- API_HOST_STAGING=https://api-staging.scratch.mit.edu
|
- API_HOST_STAGING=https://api.scratch.ly
|
||||||
- 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}
|
||||||
|
- ASSET_HOST_master=https://assets.scratch.mit.edu
|
||||||
|
- ASSET_HOST_STAGING=https://assets.scratch.ly
|
||||||
|
- ASSET_HOST_VAR=ASSET_HOST_$TRAVIS_BRANCH
|
||||||
|
- 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_master=https://scratch.mit.edu
|
||||||
- ROOT_URL_STAGING=https://scratch.ly
|
- ROOT_URL_STAGING=https://scratch.ly
|
||||||
- ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH
|
- ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH
|
||||||
- ROOT_URL=${!ROOT_URL_VAR}
|
- ROOT_URL=${!ROOT_URL_VAR}
|
||||||
- ROOT_URL=${ROOT_URL:-$ROOT_URL_STAGING}
|
- 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
|
- 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
|
||||||
|
@ -69,6 +84,7 @@ 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
|
||||||
|
- npm --production=false update
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- stage: test
|
- stage: test
|
||||||
|
|
19
.tx/config
19
.tx/config
|
@ -1,6 +1,6 @@
|
||||||
[main]
|
[main]
|
||||||
host = https://www.transifex.com
|
host = https://www.transifex.com
|
||||||
lang_map = zh_CN:zh-cn, zh_TW:zh-tw, pt_BR:pt-br
|
lang_map = zh_CN:zh-cn, zh_TW:zh-tw, pt_BR:pt-br, es_419:es-419, aa_DJ:aa-dj
|
||||||
|
|
||||||
[scratch-website.explore-l10njson]
|
[scratch-website.explore-l10njson]
|
||||||
file_filter = localizations/explore/<lang>.json
|
file_filter = localizations/explore/<lang>.json
|
||||||
|
@ -156,6 +156,23 @@ source_file = src/views/microbit/l10n.json
|
||||||
source_lang = en
|
source_lang = en
|
||||||
type = KEYVALUEJSON
|
type = KEYVALUEJSON
|
||||||
|
|
||||||
|
[scratch-website.3faq-l10njson]
|
||||||
|
file_filter = localizations/preview-faq/<lang>.json
|
||||||
|
source_file = src/views/preview-faq/l10n.json
|
||||||
|
source_lang = en
|
||||||
|
type = KEYVALUEJSON
|
||||||
|
|
||||||
|
[scratch-website.search-l10njson]
|
||||||
|
file_filter = localizations/search/<lang>.json
|
||||||
|
source_file = src/views/search/l10n.json
|
||||||
|
source_lang = en
|
||||||
|
type = KEYVALUEJSON
|
||||||
|
|
||||||
|
[scratch-website.wedo2-legacy-l10njson]
|
||||||
|
source_file = src/views/wedo2-legacy/l10n.json
|
||||||
|
source_lang = en
|
||||||
|
type = KEYVALUEJSON
|
||||||
|
|
||||||
[scratch-website.parents-l10njson]
|
[scratch-website.parents-l10njson]
|
||||||
source_file = src/views/parents/l10n.json
|
source_file = src/views/parents/l10n.json
|
||||||
source_lang = en
|
source_lang = en
|
||||||
|
|
14
Makefile
14
Makefile
|
@ -1,11 +1,25 @@
|
||||||
ESLINT=./node_modules/.bin/eslint
|
ESLINT=./node_modules/.bin/eslint
|
||||||
NODE= NODE_OPTIONS=--max_old_space_size=8000 node
|
NODE= NODE_OPTIONS=--max_old_space_size=8000 node
|
||||||
SASSLINT=./node_modules/.bin/sass-lint -v
|
SASSLINT=./node_modules/.bin/sass-lint -v
|
||||||
|
SCRATCH_DOCKER_CONFIG=./node_modules/.bin/docker_config.sh
|
||||||
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_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/watch
|
WATCH= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/watch
|
||||||
WEBPACK= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/webpack
|
WEBPACK= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/webpack
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------
|
||||||
|
|
||||||
|
$(SCRATCH_DOCKER_CONFIG):
|
||||||
|
npm install scratch-docker
|
||||||
|
|
||||||
|
docker-up: $(SCRATCH_DOCKER_CONFIG)
|
||||||
|
$(SCRATCH_DOCKER_CONFIG) network create
|
||||||
|
docker-compose up
|
||||||
|
|
||||||
|
docker-down:
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
# ------------------------------------
|
# ------------------------------------
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|
19
README.md
19
README.md
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,9 @@ volumes:
|
||||||
runtime_data:
|
runtime_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
scratch-api_scratch_network:
|
default:
|
||||||
external: true
|
external:
|
||||||
|
name: scratchapi_scratch_network
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
|
@ -13,7 +14,7 @@ services:
|
||||||
hostname: scratch-www-app
|
hostname: scratch-www-app
|
||||||
environment:
|
environment:
|
||||||
- API_HOST=http://localhost:8491
|
- API_HOST=http://localhost:8491
|
||||||
- FALLBACK=http://localhost:8080
|
- FALLBACK=http://scratchr2-app:8080
|
||||||
- USE_DOCKER_WATCHOPTIONS=true
|
- USE_DOCKER_WATCHOPTIONS=true
|
||||||
build:
|
build:
|
||||||
context: ./
|
context: ./
|
||||||
|
@ -35,5 +36,3 @@ services:
|
||||||
- runtime_data:/runtime
|
- runtime_data:/runtime
|
||||||
ports:
|
ports:
|
||||||
- "8333:8333"
|
- "8333:8333"
|
||||||
networks:
|
|
||||||
- scratch-api_scratch_network
|
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"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-docker": "^1.0.2",
|
||||||
"scratch-parser": "^4.2.0",
|
"scratch-parser": "^4.2.0",
|
||||||
"scratch-storage": "^0.5.1"
|
"scratch-storage": "^0.5.1"
|
||||||
},
|
},
|
||||||
|
@ -77,7 +78,6 @@
|
||||||
"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",
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
"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",
|
"scratch-gui": "develop",
|
||||||
"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",
|
||||||
|
|
|
@ -28,6 +28,7 @@ $ui-coral-dark: hsla(350, 100, 60, 1); // #FF3355 More Blocks tertiary
|
||||||
$ui-white: hsla(0, 100%, 100%, 1); //#FFF
|
$ui-white: hsla(0, 100%, 100%, 1); //#FFF
|
||||||
$ui-white-15percent: hsla(0, 100%, 100%, .15); //#FFF
|
$ui-white-15percent: hsla(0, 100%, 100%, .15); //#FFF
|
||||||
$ui-light-primary: hsl(215, 100, 95);
|
$ui-light-primary: hsl(215, 100, 95);
|
||||||
|
$ui-light-primary-transparent: hsla(215, 100, 95, 0);
|
||||||
|
|
||||||
$ui-border: hsla(0, 0, 85, 1); //#D9D9D9
|
$ui-border: hsla(0, 0, 85, 1); //#D9D9D9
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,31 @@ $desktop: 942px;
|
||||||
$tablet: 640px;
|
$tablet: 640px;
|
||||||
$mobile: 480px;
|
$mobile: 480px;
|
||||||
|
|
||||||
|
/* Media Queries */
|
||||||
|
|
||||||
|
/* Width */
|
||||||
|
/*
|
||||||
|
* ... small | medium | intermediate | big ...
|
||||||
|
* ... medium-and-smaller |
|
||||||
|
* ... intermediate-and-smaller |
|
||||||
|
*/
|
||||||
|
|
||||||
|
$small: "only screen and (max-width : #{$mobile}-1)";
|
||||||
|
$medium: "only screen and (min-width : #{$mobile}) and (max-width : #{$tablet}-1)";
|
||||||
|
$intermediate: "only screen and (min-width : #{$tablet}) and (max-width : #{$desktop}-1)";
|
||||||
|
$big: "only screen and (min-width : #{$desktop})";
|
||||||
|
|
||||||
|
$medium-and-smaller: "only screen and (max-width : #{$tablet}-1)";
|
||||||
|
$intermediate-and-smaller: "only screen and (max-width : #{$desktop}-1)";
|
||||||
|
|
||||||
|
$medium-and-intermediate: "only screen and (min-width : #{$mobile}) and (max-width : #{$desktop}-1)";
|
||||||
|
|
||||||
|
/* Height */
|
||||||
|
|
||||||
|
$small-height: "only screen and (max-height : #{$mobile} - 1)";
|
||||||
|
$medium-height: "only screen and (min-height : #{$mobile}) and (max-height : #{$tablet} - 1)";
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Column-widths in a function, in ems
|
// Column-widths in a function, in ems
|
||||||
//
|
//
|
||||||
|
@ -48,7 +73,7 @@ $mobile: 480px;
|
||||||
|
|
||||||
//4 columns
|
//4 columns
|
||||||
@mixin submobile ($parent-selector, $child-selector) {
|
@mixin submobile ($parent-selector, $child-selector) {
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
#{$parent-selector} {
|
#{$parent-selector} {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -64,7 +89,7 @@ $mobile: 480px;
|
||||||
|
|
||||||
//6 columns
|
//6 columns
|
||||||
@mixin mobile ($parent-selector, $child-selector) {
|
@mixin mobile ($parent-selector, $child-selector) {
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
#{$parent-selector} {
|
#{$parent-selector} {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -80,7 +105,7 @@ $mobile: 480px;
|
||||||
|
|
||||||
//8 columns
|
//8 columns
|
||||||
@mixin tablet ($parent-selector, $child-selector) {
|
@mixin tablet ($parent-selector, $child-selector) {
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
#{$parent-selector} {
|
#{$parent-selector} {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -94,7 +119,7 @@ $mobile: 480px;
|
||||||
|
|
||||||
//12 columns
|
//12 columns
|
||||||
@mixin desktop ($parent-selector, $child-selector) {
|
@mixin desktop ($parent-selector, $child-selector) {
|
||||||
@media only screen and (min-width: $desktop) {
|
@media #{$big} {
|
||||||
#{$child-selector} {
|
#{$child-selector} {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: $desktop;
|
width: $desktop;
|
||||||
|
|
|
@ -7,9 +7,9 @@ $base-bg: $ui-white;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: 1px solid $ui-border;
|
border: 1px solid $ui-border;
|
||||||
border-radius: 10px 10px 0 0;
|
border-radius: 10px 10px 0 0;
|
||||||
|
|
||||||
//4 columns
|
//4 columns
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
width: $cols4;
|
width: $cols4;
|
||||||
|
|
||||||
.box-header {
|
.box-header {
|
||||||
|
@ -22,7 +22,7 @@ $base-bg: $ui-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
//6 columns
|
//6 columns
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
width: $cols6;
|
width: $cols6;
|
||||||
|
|
||||||
.box-header {
|
.box-header {
|
||||||
|
@ -35,7 +35,7 @@ $base-bg: $ui-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
//8 columns
|
//8 columns
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
width: $cols8;
|
width: $cols8;
|
||||||
|
|
||||||
.box-header {
|
.box-header {
|
||||||
|
@ -48,7 +48,7 @@ $base-bg: $ui-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
//12 columns
|
//12 columns
|
||||||
@media only screen and (min-width: $desktop) {
|
@media #{$big} {
|
||||||
width: $cols12;
|
width: $cols12;
|
||||||
|
|
||||||
.box-header {
|
.box-header {
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
margin: 0 0 -3rem -4rem;
|
margin: 0 0 -3rem -4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
margin-bottom: 1.2rem;
|
margin-bottom: 1.2rem;
|
||||||
|
|
||||||
&.has-error {
|
&.has-error {
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.card {
|
.card {
|
||||||
width: 22.5rem;
|
width: 22.5rem;
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
.card {
|
.card {
|
||||||
.input {
|
.input {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
|
@ -103,7 +103,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.card {
|
.card {
|
||||||
.validation-message {
|
.validation-message {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
border-radius: 0 0 5px 5px;
|
border-radius: 0 0 5px 5px;
|
||||||
background-color: $ui-blue;
|
background-color: $ui-blue;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
max-width: 260px;
|
min-width: 9rem;
|
||||||
|
max-width: 16.25rem;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
color: $type-white;
|
color: $type-white;
|
||||||
font-size: .8125rem;
|
font-size: .8125rem;
|
||||||
|
@ -33,8 +34,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
// 100% minus border and padding
|
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
// 100% minus border and padding
|
||||||
width: calc(100% - 30px);
|
width: calc(100% - 30px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,8 +89,4 @@
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
|
||||||
min-width: 160px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,16 @@ class ExtensionLanding extends React.Component {
|
||||||
'onSetOS'
|
'onSetOS'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// @todo use bowser for browser detection
|
||||||
|
let detectedOS = OS_ENUM.WINDOWS;
|
||||||
|
if (window.navigator && window.navigator.platform) {
|
||||||
|
if (window.navigator.platform === 'MacIntel') {
|
||||||
|
detectedOS = OS_ENUM.MACOS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
OS: OS_ENUM.WINDOWS
|
OS: detectedOS
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,11 @@
|
||||||
padding: 4rem 0;
|
padding: 4rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2 {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +50,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.screenshot {
|
.screenshot {
|
||||||
|
border: 1px solid $ui-border;
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,15 +89,15 @@
|
||||||
margin-bottom: 5rem;
|
margin-bottom: 5rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
h2 {
|
h1, h2 {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
}
|
|
||||||
|
|
||||||
h2 img {
|
img {
|
||||||
padding-right: .5rem;
|
padding-right: .5rem;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
|
|
|
@ -35,7 +35,10 @@ const InstallScratchLink = ({
|
||||||
<FormattedMessage id="installScratchLink.windowsDownload" /> :
|
<FormattedMessage id="installScratchLink.windowsDownload" /> :
|
||||||
<FormattedMessage id="installScratchLink.macosDownload" />
|
<FormattedMessage id="installScratchLink.macosDownload" />
|
||||||
}
|
}
|
||||||
<img src="/svgs/extensions/download-white.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/download-white.svg"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</Step>
|
</Step>
|
||||||
|
@ -50,6 +53,7 @@ const InstallScratchLink = ({
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
className="screenshot"
|
className="screenshot"
|
||||||
src={`/images/scratchlink/${
|
src={`/images/scratchlink/${
|
||||||
currentOS === OS_ENUM.WINDOWS ? 'windows' : 'mac'
|
currentOS === OS_ENUM.WINDOWS ? 'windows' : 'mac'
|
||||||
|
|
|
@ -3,12 +3,15 @@ const React = require('react');
|
||||||
|
|
||||||
const ProjectCard = props => (
|
const ProjectCard = props => (
|
||||||
<a
|
<a
|
||||||
download
|
|
||||||
className="project-card"
|
className="project-card"
|
||||||
href={props.cardUrl}
|
href={props.cardUrl}
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div className="project-card-image">
|
<div className="project-card-image">
|
||||||
<img src={props.imageSrc} />
|
<img
|
||||||
|
alt={props.imageAlt}
|
||||||
|
src={props.imageSrc}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="project-card-info">
|
<div className="project-card-info">
|
||||||
<h4>{props.title}</h4>
|
<h4>{props.title}</h4>
|
||||||
|
@ -20,6 +23,7 @@ const ProjectCard = props => (
|
||||||
ProjectCard.propTypes = {
|
ProjectCard.propTypes = {
|
||||||
cardUrl: PropTypes.string,
|
cardUrl: PropTypes.string,
|
||||||
description: PropTypes.string,
|
description: PropTypes.string,
|
||||||
|
imageAlt: PropTypes.string,
|
||||||
imageSrc: PropTypes.string,
|
imageSrc: PropTypes.string,
|
||||||
title: PropTypes.string
|
title: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
&.uneven {
|
&.uneven {
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.family {
|
.family {
|
||||||
|
@ -122,7 +122,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
ul {
|
ul {
|
||||||
li {
|
li {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.inplace-textarea {
|
.inplace-textarea {
|
||||||
transition: all 1s ease;
|
transition: all .2s ease;
|
||||||
border: 2px dashed $ui-blue-25percent;
|
border: 2px dashed $ui-blue-25percent;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: $ui-light-gray;
|
background-color: $ui-light-gray;
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
resize: none;
|
resize: none;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
transition: all 1s ease;
|
transition: all .2s ease;
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 2px solid $ui-blue;
|
border: 2px solid $ui-blue;
|
||||||
box-shadow: 0 0 0 4px $ui-blue-25percent;
|
box-shadow: 0 0 0 4px $ui-blue-25percent;
|
||||||
|
|
|
@ -13,9 +13,9 @@
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: $ui-light-gray url("../../../static/svgs/forms/carot.svg") no-repeat right center;
|
background: $ui-light-gray url("../../../static/svgs/forms/carot.svg") no-repeat right center;
|
||||||
padding-right: 4rem;
|
padding-right: 4rem;
|
||||||
|
padding-left: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
text-indent: 1rem;
|
|
||||||
color: $type-gray;
|
color: $type-gray;
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|
|
@ -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'])}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
$thumbnail-width: 220px;
|
$thumbnail-width: 220px;
|
||||||
$thumbnail-inner-width: 204px;
|
$thumbnail-inner-width: 204px;
|
||||||
|
|
||||||
$project-height: 208px;
|
$project-height: 208px;
|
||||||
$gallery-height: 164px;
|
$gallery-height: 164px;
|
||||||
|
|
||||||
|
@ -94,21 +94,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
//4 columns
|
//4 columns
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
width: $cols4;
|
width: $cols4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//6 columns
|
//6 columns
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
width: $cols6;
|
width: $cols6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8 columns
|
// 8 columns
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
width: $cols9;
|
width: $cols9;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ const connect = require('react-redux').connect;
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
const sessionActions = require('../../redux/session.js');
|
const navigationActions = require('../../redux/navigation.js');
|
||||||
|
|
||||||
const IframeModal = require('../modal/iframe/modal.jsx');
|
const IframeModal = require('../modal/iframe/modal.jsx');
|
||||||
const Registration = require('../registration/registration.jsx');
|
const Registration = require('../registration/registration.jsx');
|
||||||
|
@ -15,10 +15,7 @@ class Intro extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'handleShowVideo',
|
'handleShowVideo',
|
||||||
'handleCloseVideo',
|
'handleCloseVideo'
|
||||||
'handleJoinClick',
|
|
||||||
'handleCloseRegistration',
|
|
||||||
'handleCompleteRegistration'
|
|
||||||
]);
|
]);
|
||||||
this.state = {
|
this.state = {
|
||||||
videoOpen: false
|
videoOpen: false
|
||||||
|
@ -30,17 +27,6 @@ class Intro extends React.Component {
|
||||||
handleCloseVideo () {
|
handleCloseVideo () {
|
||||||
this.setState({videoOpen: false});
|
this.setState({videoOpen: false});
|
||||||
}
|
}
|
||||||
handleJoinClick (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({registrationOpen: true});
|
|
||||||
}
|
|
||||||
handleCloseRegistration () {
|
|
||||||
this.setState({registrationOpen: false});
|
|
||||||
}
|
|
||||||
handleCompleteRegistration () {
|
|
||||||
this.props.dispatch(sessionActions.refreshSession());
|
|
||||||
this.closeRegistration();
|
|
||||||
}
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className="intro">
|
<div className="intro">
|
||||||
|
@ -92,7 +78,7 @@ class Intro extends React.Component {
|
||||||
<a
|
<a
|
||||||
className="sprite sprite-3"
|
className="sprite sprite-3"
|
||||||
href="#"
|
href="#"
|
||||||
onClick={this.handleJoinClick}
|
onClick={this.props.handleOpenRegistration}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Gobo"
|
alt="Gobo"
|
||||||
|
@ -111,10 +97,7 @@ class Intro extends React.Component {
|
||||||
<div className="text subtext">{this.props.messages['intro.itsFree']}</div>
|
<div className="text subtext">{this.props.messages['intro.itsFree']}</div>
|
||||||
</a>
|
</a>
|
||||||
<Registration
|
<Registration
|
||||||
isOpen={this.state.registrationOpen}
|
|
||||||
key="registration"
|
key="registration"
|
||||||
onRegistrationDone={this.handleCompleteRegistration}
|
|
||||||
onRequestClose={this.handleCloseRegistration}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -160,7 +143,7 @@ class Intro extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
Intro.propTypes = {
|
Intro.propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
handleOpenRegistration: PropTypes.func,
|
||||||
messages: PropTypes.shape({
|
messages: PropTypes.shape({
|
||||||
'intro.aboutScratch': PropTypes.string,
|
'intro.aboutScratch': PropTypes.string,
|
||||||
'intro.forEducators': PropTypes.string,
|
'intro.forEducators': PropTypes.string,
|
||||||
|
@ -194,6 +177,17 @@ const mapStateToProps = state => ({
|
||||||
session: state.session
|
session: state.session
|
||||||
});
|
});
|
||||||
|
|
||||||
const ConnectedIntro = connect(mapStateToProps)(Intro);
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
handleOpenRegistration: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.handleOpenRegistration());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const ConnectedIntro = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Intro);
|
||||||
|
|
||||||
module.exports = ConnectedIntro;
|
module.exports = ConnectedIntro;
|
||||||
|
|
60
src/components/login/canceled-deletion-modal.jsx
Normal file
60
src/components/login/canceled-deletion-modal.jsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
const React = require('react');
|
||||||
|
const connect = require('react-redux').connect;
|
||||||
|
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const injectIntl = require('react-intl').injectIntl;
|
||||||
|
const intlShape = require('react-intl').intlShape;
|
||||||
|
|
||||||
|
const navigationActions = require('../../redux/navigation.js');
|
||||||
|
const Modal = require('../modal/base/modal.jsx');
|
||||||
|
|
||||||
|
const CanceledDeletionModal = ({
|
||||||
|
canceledDeletionOpen,
|
||||||
|
handleCloseCanceledDeletion,
|
||||||
|
intl
|
||||||
|
}) => (
|
||||||
|
<Modal
|
||||||
|
isOpen={canceledDeletionOpen}
|
||||||
|
style={{
|
||||||
|
content: {
|
||||||
|
padding: 15
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRequestClose={handleCloseCanceledDeletion}
|
||||||
|
>
|
||||||
|
<h4><FormattedMessage id="general.noDeletionTitle" /></h4>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="general.noDeletionDescription"
|
||||||
|
values={{
|
||||||
|
resetLink: <a href="/accounts/password_reset/">
|
||||||
|
{intl.formatMessage({id: 'general.noDeletionLink'})}
|
||||||
|
</a>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
|
||||||
|
CanceledDeletionModal.propTypes = {
|
||||||
|
canceledDeletionOpen: PropTypes.bool,
|
||||||
|
handleCloseCanceledDeletion: PropTypes.func,
|
||||||
|
intl: intlShape
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
canceledDeletionOpen: state.navigation && state.navigation.canceledDeletionOpen
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
handleCloseCanceledDeletion: () => {
|
||||||
|
dispatch(navigationActions.setCanceledDeletionOpen(false));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ConnectedCanceledDeletionModal = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(CanceledDeletionModal);
|
||||||
|
|
||||||
|
module.exports = injectIntl(ConnectedCanceledDeletionModal);
|
34
src/components/login/connected-login.jsx
Normal file
34
src/components/login/connected-login.jsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const React = require('react');
|
||||||
|
const connect = require('react-redux').connect;
|
||||||
|
|
||||||
|
const Login = require('./login.jsx');
|
||||||
|
|
||||||
|
require('./login-dropdown.scss');
|
||||||
|
|
||||||
|
const ConnectedLogin = ({
|
||||||
|
error,
|
||||||
|
onLogIn
|
||||||
|
}) => (
|
||||||
|
<Login
|
||||||
|
error={error}
|
||||||
|
key="login-dropdown-presentation"
|
||||||
|
onLogIn={onLogIn}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
ConnectedLogin.propTypes = {
|
||||||
|
error: PropTypes.string,
|
||||||
|
onLogIn: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
error: state.navigation && state.navigation.loginError
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
module.exports = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ConnectedLogin);
|
50
src/components/login/login-dropdown.jsx
Normal file
50
src/components/login/login-dropdown.jsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const React = require('react');
|
||||||
|
const connect = require('react-redux').connect;
|
||||||
|
|
||||||
|
const navigationActions = require('../../redux/navigation.js');
|
||||||
|
const Dropdown = require('../dropdown/dropdown.jsx');
|
||||||
|
const ConnectedLogin = require('./connected-login.jsx');
|
||||||
|
|
||||||
|
require('./login-dropdown.scss');
|
||||||
|
|
||||||
|
const LoginDropdown = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onLogIn
|
||||||
|
}) => (
|
||||||
|
<Dropdown
|
||||||
|
className={'with-arrow'}
|
||||||
|
isOpen={isOpen}
|
||||||
|
key="login-dropdown"
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<ConnectedLogin
|
||||||
|
onLogIn={onLogIn}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
|
||||||
|
LoginDropdown.propTypes = {
|
||||||
|
isOpen: PropTypes.bool,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
onLogIn: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
isOpen: state.navigation && state.navigation.loginOpen
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onClose: () => {
|
||||||
|
dispatch(navigationActions.setLoginOpen(false));
|
||||||
|
},
|
||||||
|
onLogIn: (formData, callback) => {
|
||||||
|
dispatch(navigationActions.handleLogIn(formData, callback));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(LoginDropdown);
|
0
src/components/login/login-dropdown.scss
Normal file
0
src/components/login/login-dropdown.scss
Normal file
|
@ -3,8 +3,6 @@ const FormattedMessage = require('react-intl').FormattedMessage;
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
const log = require('../../lib/log.js');
|
|
||||||
|
|
||||||
const Form = require('../forms/form.jsx');
|
const Form = require('../forms/form.jsx');
|
||||||
const Input = require('../forms/input.jsx');
|
const Input = require('../forms/input.jsx');
|
||||||
const Button = require('../forms/button.jsx');
|
const Button = require('../forms/button.jsx');
|
||||||
|
@ -24,8 +22,7 @@ class Login extends React.Component {
|
||||||
}
|
}
|
||||||
handleSubmit (formData) {
|
handleSubmit (formData) {
|
||||||
this.setState({waiting: true});
|
this.setState({waiting: true});
|
||||||
this.props.onLogIn(formData, err => {
|
this.props.onLogIn(formData, () => {
|
||||||
if (err) log.error(err);
|
|
||||||
this.setState({waiting: false});
|
this.setState({waiting: false});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -48,9 +45,6 @@ class Login extends React.Component {
|
||||||
key="usernameInput"
|
key="usernameInput"
|
||||||
maxLength="30"
|
maxLength="30"
|
||||||
name="username"
|
name="username"
|
||||||
ref={input => {
|
|
||||||
this.username = input;
|
|
||||||
}}
|
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
|
@ -63,9 +57,6 @@ class Login extends React.Component {
|
||||||
required
|
required
|
||||||
key="passwordInput"
|
key="passwordInput"
|
||||||
name="password"
|
name="password"
|
||||||
ref={input => {
|
|
||||||
this.password = input;
|
|
||||||
}}
|
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
{this.state.waiting ? [
|
{this.state.waiting ? [
|
||||||
|
@ -75,7 +66,10 @@ class Login extends React.Component {
|
||||||
key="submitButton"
|
key="submitButton"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<Spinner />
|
<Spinner
|
||||||
|
className="spinner"
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
] : [
|
] : [
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -2,6 +2,26 @@
|
||||||
|
|
||||||
.login {
|
.login {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
width: 200px;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
white-space: normal; // override any parent, such as in gui, who sets nowrap
|
||||||
|
color: $type-white;
|
||||||
|
font-size: .8125rem;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: .75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
// 100% minus border and padding
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
height: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
|
@ -15,7 +35,7 @@
|
||||||
.spinner {
|
.spinner {
|
||||||
margin: 0 .8rem;
|
margin: 0 .8rem;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-button {
|
.submit-button {
|
||||||
|
@ -24,13 +44,19 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:link,
|
||||||
|
&:visited,
|
||||||
|
&:active {
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
border: 1px solid $active-dark-gray;
|
border: 1px solid $active-dark-gray;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
|
@ -9,14 +9,14 @@
|
||||||
|
|
||||||
// column-count required for Firefox, IE and Edge
|
// column-count required for Firefox, IE and Edge
|
||||||
//4 columns
|
//4 columns
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.masonry {
|
.masonry {
|
||||||
column-count: 1;
|
column-count: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//6 columns
|
//6 columns
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
.masonry {
|
.masonry {
|
||||||
column-count: 1;
|
column-count: 1;
|
||||||
}
|
}
|
||||||
|
@ -24,14 +24,14 @@
|
||||||
|
|
||||||
|
|
||||||
//8 columns
|
//8 columns
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
.masonry {
|
.masonry {
|
||||||
column-count: 2;
|
column-count: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12 columns
|
// 12 columns
|
||||||
@media only screen and (min-width: $desktop) {
|
@media #{$big} {
|
||||||
.masonry {
|
.masonry {
|
||||||
column-count: 3;
|
column-count: 3;
|
||||||
}
|
}
|
||||||
|
|
48
src/components/modal/addtostudio/animate-hoc.jsx
Normal file
48
src/components/modal/addtostudio/animate-hoc.jsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
const React = require('react');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Higher-order component for building an animated studio button
|
||||||
|
* it is used to decorate the onToggleStudio function with noticing
|
||||||
|
* when the button has first been clicked.
|
||||||
|
* This is needed so the buttons don't play the animation when they are
|
||||||
|
* first rendered but when they are first clicked.
|
||||||
|
* @param {React.Component} Component a studio button component
|
||||||
|
* @return {React.Component} a wrapped studio button component
|
||||||
|
*/
|
||||||
|
|
||||||
|
const AnimateHOC = Component => {
|
||||||
|
class WrappedComponent extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
wasClicked: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
}
|
||||||
|
handleClick () {
|
||||||
|
this.setState({ // else tell the state that the button has been clicked
|
||||||
|
wasClicked: true
|
||||||
|
}, () => this.props.onClick(this.props.id)); // callback after state has been updated
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
const {wasClicked} = this.state;
|
||||||
|
return (<Component
|
||||||
|
{...this.props}
|
||||||
|
wasClicked={wasClicked}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WrappedComponent.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
onClick: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
return WrappedComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = AnimateHOC;
|
|
@ -1,53 +1,41 @@
|
||||||
@import "../../../colors";
|
@import "../../../colors";
|
||||||
@import "../../../frameless";
|
@import "../../../frameless";
|
||||||
|
|
||||||
.mod-addToStudio * {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mod-addToStudio {
|
.mod-addToStudio {
|
||||||
margin: 100px auto;
|
|
||||||
outline: none;
|
|
||||||
padding: 0;
|
|
||||||
width: 36.25rem; /* 580px; */
|
|
||||||
height: 388px; /* 24.25rem; */
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
user-select: none;
|
|
||||||
|
@media #{$small}, #{$small-height} {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.addToStudio-modal-header {
|
.addToStudio-modal-header {
|
||||||
box-shadow: inset 0 -1px 0 0 $ui-blue-dark;
|
box-shadow: inset 0 -1px 0 0 $ui-blue-dark;
|
||||||
background-color: $ui-blue;
|
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 {
|
.addToStudio-modal-content {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
box-shadow: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 1.5rem;
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-list-outer-scrollbox {
|
.studio-list-outer-scrollbox {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: $ui-blue-10percent;
|
background-color: $ui-blue-10percent;
|
||||||
|
min-height: 15rem;
|
||||||
|
max-height: calc(100% - 8rem);
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
@media #{$small-height} {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-list-inner-scrollbox {
|
.studio-list-inner-scrollbox {
|
||||||
margin-right: .5rem;
|
margin-right: .5rem;
|
||||||
padding-right: .5rem;
|
padding-right: .5rem;
|
||||||
height: 16.9375rem;
|
height: 100%;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
@ -90,34 +78,36 @@
|
||||||
pointer-events: none; /* pass clicks through to buttons underneath */
|
pointer-events: none; /* pass clicks through to buttons underneath */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.studio-selector-button {
|
.studio-selector-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: .21875rem .21875rem;
|
transition: all .5s;
|
||||||
|
margin: .21875rem;
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
background-color: $ui-white;
|
background-color: $ui-white;
|
||||||
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 16.1875rem; /* 259px */
|
width: 48%;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
box-sizing: border-box;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media #{$small} {
|
||||||
|
min-width: 98%;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-selector-button-text {
|
.studio-selector-button-text {
|
||||||
position: absolute;
|
margin: auto 2.18375rem auto .6875rem;
|
||||||
/* per spec, should be:
|
min-width: 0;
|
||||||
margin: .375rem 2.18375rem .375rem .6875rem
|
overflow: hidden;
|
||||||
but in practice, our css seems to vertically align text to top, where
|
text-overflow: ellipsis;
|
||||||
invision spec aligned to middle.
|
white-space: nowrap;
|
||||||
*/
|
|
||||||
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-family: "Helvetica Neue";
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
font-weight: regular;
|
font-weight: regular;
|
||||||
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-selector-button-selected {
|
.studio-selector-button-selected {
|
||||||
|
@ -140,7 +130,7 @@
|
||||||
|
|
||||||
.studio-status-icon {
|
.studio-status-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin: .5rem .625rem .5rem 14.0625rem;
|
right: .625rem;
|
||||||
border-radius: .75rem;
|
border-radius: .75rem;
|
||||||
padding: .0625rem .075rem;
|
padding: .0625rem .075rem;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
|
@ -161,30 +151,30 @@
|
||||||
background-color: $ui-blue;
|
background-color: $ui-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-status-icon-plus-img {
|
.studio-status-icon-plus-img,
|
||||||
|
.studio-status-icon-checkmark-img {
|
||||||
|
animation-direction: normal;
|
||||||
width: 1.4rem;
|
width: 1.4rem;
|
||||||
height: 1.4rem;
|
height: 1.4rem;
|
||||||
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-status-icon--img {
|
.studio-status-icon-with-animation {
|
||||||
width: 1.4rem;
|
animation-name: bump;
|
||||||
height: 1.4rem;
|
animation-duration: .25s;
|
||||||
|
animation-timing-function: cubic-bezier(.3, -3, .6, 3);
|
||||||
|
animation-iteration-count: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button-text .spinner-smooth {
|
@keyframes bump {
|
||||||
margin: .2125rem auto;
|
0% {
|
||||||
width: 1.875rem;
|
transform: scale(0);
|
||||||
height: 1rem;
|
opacity: 0;
|
||||||
}
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
.studio-status-icon .spinner-smooth {
|
100% {
|
||||||
position: unset; /* don't understand why neither relative nor absolute work */
|
transform: scale(1);
|
||||||
}
|
opacity: 1;
|
||||||
|
-webkit-transform: scale(1);
|
||||||
.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 */
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,76 +31,75 @@ const AddToStudioModalPresentation = ({
|
||||||
includesProject={studio.includesProject}
|
includesProject={studio.includesProject}
|
||||||
key={studio.id}
|
key={studio.id}
|
||||||
title={studio.title}
|
title={studio.title}
|
||||||
onToggleStudio={onToggleStudio}
|
onClick={onToggleStudio}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
useStandardSizes
|
||||||
className="mod-addToStudio"
|
className="mod-addToStudio"
|
||||||
contentLabel={contentLabel}
|
contentLabel={contentLabel}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
>
|
>
|
||||||
<div>
|
<div className="addToStudio-modal-header modal-header">
|
||||||
<div className="addToStudio-modal-header">
|
<div className="addToStudio-content-label content-label">
|
||||||
<div className="addToStudio-content-label">
|
{contentLabel}
|
||||||
{contentLabel}
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="addToStudio-modal-content 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>
|
||||||
</div>
|
</div>
|
||||||
<div className="addToStudio-modal-content">
|
|
||||||
<div className="studio-list-outer-scrollbox">
|
|
||||||
<div className="studio-list-inner-scrollbox">
|
<Form
|
||||||
<div className="studio-list-container">
|
className="add-to-studio"
|
||||||
{studioButtons}
|
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>
|
</div>
|
||||||
<div className="studio-list-bottom-gradient" />
|
</Button>
|
||||||
</div>
|
{waitingToClose ? [
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<Form
|
|
||||||
className="add-to-studio"
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
<FlexRow className="action-buttons">
|
|
||||||
<Button
|
<Button
|
||||||
className="action-button close-button white"
|
className="action-button submit-button submit-button-waiting"
|
||||||
key="closeButton"
|
disabled="disabled"
|
||||||
name="closeButton"
|
key="submitButton"
|
||||||
type="button"
|
type="submit"
|
||||||
onClick={onRequestClose}
|
|
||||||
>
|
>
|
||||||
<div className="action-button-text">
|
<div className="action-button-text">
|
||||||
<FormattedMessage id="general.close" />
|
<Spinner />
|
||||||
|
<FormattedMessage id="addToStudio.finishing" />
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
{waitingToClose ? [
|
] : [
|
||||||
<Button
|
<Button
|
||||||
className="action-button submit-button submit-button-waiting"
|
className="action-button submit-button"
|
||||||
disabled="disabled"
|
key="submitButton"
|
||||||
key="submitButton"
|
type="submit"
|
||||||
type="submit"
|
>
|
||||||
>
|
<div className="action-button-text">
|
||||||
<div className="action-button-text">
|
<FormattedMessage id="general.okay" />
|
||||||
<Spinner mode="smooth" />
|
</div>
|
||||||
<FormattedMessage id="addToStudio.finishing" />
|
</Button>
|
||||||
</div>
|
]}
|
||||||
</Button>
|
</FlexRow>
|
||||||
] : [
|
</Form>
|
||||||
<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>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,29 +1,36 @@
|
||||||
const truncateAtWordBoundary = require('../../../lib/truncate').truncateAtWordBoundary;
|
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const classNames = require('classnames');
|
const classNames = require('classnames');
|
||||||
|
|
||||||
const Spinner = require('../../spinner/spinner.jsx');
|
const Spinner = require('../../spinner/spinner.jsx');
|
||||||
|
const AnimateHOC = require('./animate-hoc.jsx');
|
||||||
|
|
||||||
require('./modal.scss');
|
require('./modal.scss');
|
||||||
|
|
||||||
const StudioButton = ({
|
const StudioButton = ({
|
||||||
hasRequestOutstanding,
|
hasRequestOutstanding,
|
||||||
id,
|
|
||||||
includesProject,
|
includesProject,
|
||||||
title,
|
title,
|
||||||
onToggleStudio
|
onClick,
|
||||||
|
wasClicked
|
||||||
}) => {
|
}) => {
|
||||||
const checkmark = (
|
const checkmark = (
|
||||||
<img
|
<img
|
||||||
alt="checkmark-icon"
|
alt="checkmark-icon"
|
||||||
className="studio-status-icon-checkmark-img"
|
className={classNames(
|
||||||
|
'studio-status-icon-checkmark-img',
|
||||||
|
{'studio-status-icon-with-animation': wasClicked}
|
||||||
|
)}
|
||||||
src="/svgs/modal/confirm.svg"
|
src="/svgs/modal/confirm.svg"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const plus = (
|
const plus = (
|
||||||
<img
|
<img
|
||||||
alt="plus-icon"
|
alt="plus-icon"
|
||||||
className="studio-status-icon-plus-img"
|
className={classNames(
|
||||||
|
'studio-status-icon-plus-img',
|
||||||
|
{'studio-status-icon-with-animation': wasClicked}
|
||||||
|
)}
|
||||||
src="/svgs/modal/add.svg"
|
src="/svgs/modal/add.svg"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -35,8 +42,7 @@ const StudioButton = ({
|
||||||
{'studio-selector-button-selected':
|
{'studio-selector-button-selected':
|
||||||
includesProject && !hasRequestOutstanding}
|
includesProject && !hasRequestOutstanding}
|
||||||
)}
|
)}
|
||||||
data-id={id}
|
onClick={onClick}
|
||||||
onClick={onToggleStudio}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -44,17 +50,18 @@ const StudioButton = ({
|
||||||
{'studio-selector-button-text-selected': includesProject || hasRequestOutstanding},
|
{'studio-selector-button-text-selected': includesProject || hasRequestOutstanding},
|
||||||
{'studio-selector-button-text-unselected': !includesProject && !hasRequestOutstanding}
|
{'studio-selector-button-text-unselected': !includesProject && !hasRequestOutstanding}
|
||||||
)}
|
)}
|
||||||
|
title={title}
|
||||||
>
|
>
|
||||||
{truncateAtWordBoundary(title, 25)}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'studio-status-icon',
|
'studio-status-icon',
|
||||||
{'studio-status-icon-unselected': !includesProject}
|
{'studio-status-icon-unselected': !includesProject && !hasRequestOutstanding}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{(hasRequestOutstanding ?
|
{(hasRequestOutstanding ?
|
||||||
(<Spinner mode="smooth" />) :
|
<Spinner /> :
|
||||||
(includesProject ? checkmark : plus))}
|
(includesProject ? checkmark : plus))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,10 +70,10 @@ const StudioButton = ({
|
||||||
|
|
||||||
StudioButton.propTypes = {
|
StudioButton.propTypes = {
|
||||||
hasRequestOutstanding: PropTypes.bool,
|
hasRequestOutstanding: PropTypes.bool,
|
||||||
id: PropTypes.number,
|
|
||||||
includesProject: PropTypes.bool,
|
includesProject: PropTypes.bool,
|
||||||
onToggleStudio: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
title: PropTypes.string
|
title: PropTypes.string,
|
||||||
|
wasClicked: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = StudioButton;
|
module.exports = AnimateHOC(StudioButton);
|
||||||
|
|
|
@ -7,7 +7,7 @@ const ReactModal = require('react-modal');
|
||||||
|
|
||||||
require('./modal.scss');
|
require('./modal.scss');
|
||||||
|
|
||||||
ReactModal.setAppElement(document.getElementById('view'));
|
ReactModal.setAppElement(document.getElementById('app'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Container for pop up windows (See: registration window)
|
* Container for pop up windows (See: registration window)
|
||||||
|
@ -23,11 +23,19 @@ class Modal extends React.Component {
|
||||||
return this.modal.portal.requestClose();
|
return this.modal.portal.requestClose();
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
|
// bodyOpenClassName prop cannot be blank string or null here; both cause
|
||||||
|
// an error, because ReactModal does not correctly handle them.
|
||||||
|
// If we're not setting it to a class name, we must omit the prop entirely.
|
||||||
|
const bodyOpenClassNameProp = this.props.useStandardSizes ?
|
||||||
|
{bodyOpenClassName: classNames('overflow-hidden')} : {};
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<ReactModal
|
||||||
appElement={document.getElementById('view')}
|
appElement={document.getElementById('app')}
|
||||||
|
{...bodyOpenClassNameProp}
|
||||||
className={{
|
className={{
|
||||||
base: classNames('modal-content', this.props.className),
|
base: classNames('modal-content', this.props.className, {
|
||||||
|
'modal-sizes': this.props.useStandardSizes
|
||||||
|
}),
|
||||||
afterOpen: classNames('modal-content', this.props.className),
|
afterOpen: classNames('modal-content', this.props.className),
|
||||||
beforeClose: classNames('modal-content', this.props.className)
|
beforeClose: classNames('modal-content', this.props.className)
|
||||||
}}
|
}}
|
||||||
|
@ -60,7 +68,8 @@ class Modal extends React.Component {
|
||||||
Modal.propTypes = {
|
Modal.propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
overlayClassName: PropTypes.string
|
overlayClassName: PropTypes.string,
|
||||||
|
useStandardSizes: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Modal;
|
module.exports = Modal;
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
@import "../../../colors";
|
@import "../../../colors";
|
||||||
@import "../../../frameless";
|
@import "../../../frameless";
|
||||||
|
|
||||||
|
.overflow-hidden {
|
||||||
|
/* to avoid double scroll bars this
|
||||||
|
gets added to body while modal is open */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 3.75rem auto;
|
margin: 3.75rem auto;
|
||||||
|
@ -10,9 +16,27 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 48.75rem;
|
width: 48.75rem;
|
||||||
|
|
||||||
|
.modal-content { /* content inside of content */
|
||||||
|
display: flex;
|
||||||
|
border-radius: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media #{$intermediate-and-smaller} {
|
||||||
|
margin-top: 0;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media #{$small}, #{$small-height} {
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
|
@ -43,30 +67,27 @@ $modal-close-size: 1rem;
|
||||||
padding-top: $modal-close-size / 2;
|
padding-top: $modal-close-size / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
|
||||||
.modal-content {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content-close {
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Close button, Submit button, etc. */
|
/* Close button, Submit button, etc. */
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 1.125rem .8275rem .9375rem .8275rem;
|
margin: 1.125rem .8275rem .9375rem .8275rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
justify-content: flex-end !important;
|
justify-content: flex-end !important;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
@media #{$intermediate-and-smaller} {
|
||||||
|
justify-content: center !important; //overwriting flex row properties
|
||||||
|
flex-direction: row !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
.action-button {
|
||||||
|
@ -83,3 +104,62 @@ $modal-close-size: 1rem;
|
||||||
.action-button-text {
|
.action-button-text {
|
||||||
display: flex;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-sizes * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-sizes {
|
||||||
|
margin: 100px auto;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 36.25rem; /* 580px; */
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
@media #{$medium}, #{$medium-height} {
|
||||||
|
margin: 40px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media #{$small}, #{$small-height} {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding-top: .75rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 3rem;
|
||||||
|
|
||||||
|
@media #{$small}, #{$small-height} {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-label {
|
||||||
|
text-align: center;
|
||||||
|
color: $type-white;
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
81
src/components/modal/comments/delete-comment.jsx
Normal file
81
src/components/modal/comments/delete-comment.jsx
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
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 Button = require('../../forms/button.jsx');
|
||||||
|
const FlexRow = require('../../flex-row/flex-row.jsx');
|
||||||
|
|
||||||
|
require('../../forms/button.scss');
|
||||||
|
require('./modal.scss');
|
||||||
|
|
||||||
|
const DeleteModal = ({
|
||||||
|
intl,
|
||||||
|
onDelete,
|
||||||
|
onReport,
|
||||||
|
onRequestClose,
|
||||||
|
...modalProps
|
||||||
|
}) => (
|
||||||
|
<Modal
|
||||||
|
useStandardSizes
|
||||||
|
className="mod-report"
|
||||||
|
contentLabel={intl.formatMessage({id: 'comments.deleteModal.title'})}
|
||||||
|
onRequestClose={onRequestClose}
|
||||||
|
{...modalProps}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="report-modal-header">
|
||||||
|
<div className="report-content-label">
|
||||||
|
<FormattedMessage id="comments.deleteModal.title" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="report-modal-content">
|
||||||
|
<div>
|
||||||
|
<div className="instructions">
|
||||||
|
<FormattedMessage id="comments.deleteModal.body" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FlexRow className="action-buttons">
|
||||||
|
<div className="action-buttons-overflow-fix">
|
||||||
|
<Button
|
||||||
|
className="action-button submit-button"
|
||||||
|
type="button"
|
||||||
|
onClick={onRequestClose}
|
||||||
|
>
|
||||||
|
<div className="action-button-text">
|
||||||
|
<FormattedMessage id="general.close" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="action-button submit-button"
|
||||||
|
type="button"
|
||||||
|
onClick={onReport}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="comments.report" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="action-button submit-button"
|
||||||
|
type="button"
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="comments.delete" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FlexRow>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
DeleteModal.propTypes = {
|
||||||
|
intl: intlShape,
|
||||||
|
onDelete: PropTypes.func,
|
||||||
|
onReport: PropTypes.func,
|
||||||
|
onRequestClose: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = injectIntl(DeleteModal);
|
44
src/components/modal/comments/modal.scss
Normal file
44
src/components/modal/comments/modal.scss
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
@import "../../../colors";
|
||||||
|
@import "../../../frameless";
|
||||||
|
|
||||||
|
$medium-and-small: "screen and (max-width : #{$tablet}-1)";
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
84
src/components/modal/comments/report-comment.jsx
Normal file
84
src/components/modal/comments/report-comment.jsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
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 Button = require('../../forms/button.jsx');
|
||||||
|
const FlexRow = require('../../flex-row/flex-row.jsx');
|
||||||
|
|
||||||
|
require('../../forms/button.scss');
|
||||||
|
require('./modal.scss');
|
||||||
|
|
||||||
|
const ReportModal = ({
|
||||||
|
intl,
|
||||||
|
isConfirmed,
|
||||||
|
onReport,
|
||||||
|
onRequestClose,
|
||||||
|
...modalProps
|
||||||
|
}) => (
|
||||||
|
<Modal
|
||||||
|
useStandardSizes
|
||||||
|
className="mod-report"
|
||||||
|
contentLabel={intl.formatMessage({id: 'comments.reportModal.title'})}
|
||||||
|
onRequestClose={onRequestClose}
|
||||||
|
{...modalProps}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="report-modal-header">
|
||||||
|
<div className="report-content-label">
|
||||||
|
<FormattedMessage id="comments.reportModal.title" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="report-modal-content">
|
||||||
|
<div>
|
||||||
|
<div className="instructions">
|
||||||
|
{isConfirmed ? (
|
||||||
|
<FormattedMessage id="comments.reportModal.reported" />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id="comments.reportModal.prompt" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FlexRow className="action-buttons">
|
||||||
|
<div className="action-buttons-overflow-fix">
|
||||||
|
<Button
|
||||||
|
className="action-button submit-button"
|
||||||
|
type="button"
|
||||||
|
onClick={onRequestClose}
|
||||||
|
>
|
||||||
|
<div className="action-button-text">
|
||||||
|
<FormattedMessage id="general.close" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
{isConfirmed ? null : (
|
||||||
|
<Button
|
||||||
|
className="action-button submit-button"
|
||||||
|
type="button"
|
||||||
|
onClick={onReport}
|
||||||
|
>
|
||||||
|
<div className="action-button-text">
|
||||||
|
<FormattedMessage id="comments.report" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FlexRow>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ReportModal.propTypes = {
|
||||||
|
intl: intlShape,
|
||||||
|
isConfirmed: PropTypes.bool,
|
||||||
|
isOwnSpace: PropTypes.bool,
|
||||||
|
onReport: PropTypes.func,
|
||||||
|
onRequestClose: PropTypes.func,
|
||||||
|
type: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = injectIntl(ReportModal);
|
|
@ -1,10 +1,12 @@
|
||||||
const bindAll = require('lodash.bindall');
|
const bindAll = require('lodash.bindall');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
const connect = require('react-redux').connect;
|
||||||
const FormattedMessage = require('react-intl').FormattedMessage;
|
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||||
const injectIntl = require('react-intl').injectIntl;
|
const injectIntl = require('react-intl').injectIntl;
|
||||||
const intlShape = require('react-intl').intlShape;
|
const intlShape = require('react-intl').intlShape;
|
||||||
const Modal = require('../base/modal.jsx');
|
const Modal = require('../base/modal.jsx');
|
||||||
|
const classNames = require('classnames');
|
||||||
|
|
||||||
const Form = require('../../forms/form.jsx');
|
const Form = require('../../forms/form.jsx');
|
||||||
const Button = require('../../forms/button.jsx');
|
const Button = require('../../forms/button.jsx');
|
||||||
|
@ -12,6 +14,7 @@ const Select = require('../../forms/select.jsx');
|
||||||
const Spinner = require('../../spinner/spinner.jsx');
|
const Spinner = require('../../spinner/spinner.jsx');
|
||||||
const TextArea = require('../../forms/textarea.jsx');
|
const TextArea = require('../../forms/textarea.jsx');
|
||||||
const FlexRow = require('../../flex-row/flex-row.jsx');
|
const FlexRow = require('../../flex-row/flex-row.jsx');
|
||||||
|
const previewActions = require('../../../redux/preview.js');
|
||||||
|
|
||||||
require('../../forms/button.scss');
|
require('../../forms/button.scss');
|
||||||
require('./modal.scss');
|
require('./modal.scss');
|
||||||
|
@ -68,12 +71,24 @@ class ReportModal extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'handleReportCategorySelect'
|
'handleCategorySelect',
|
||||||
|
'handleValid',
|
||||||
|
'handleInvalid'
|
||||||
]);
|
]);
|
||||||
this.state = {reportCategory: this.props.report.category};
|
this.state = {
|
||||||
|
category: '',
|
||||||
|
notes: '',
|
||||||
|
valid: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
handleReportCategorySelect (name, value) {
|
handleCategorySelect (name, value) {
|
||||||
this.setState({reportCategory: value});
|
this.setState({category: value});
|
||||||
|
}
|
||||||
|
handleValid () {
|
||||||
|
this.setState({valid: true});
|
||||||
|
}
|
||||||
|
handleInvalid () {
|
||||||
|
this.setState({valid: false});
|
||||||
}
|
}
|
||||||
lookupPrompt (value) {
|
lookupPrompt (value) {
|
||||||
const prompt = REPORT_OPTIONS.find(item => item.value === value).prompt;
|
const prompt = REPORT_OPTIONS.find(item => item.value === value).prompt;
|
||||||
|
@ -82,90 +97,145 @@ class ReportModal extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
intl,
|
intl,
|
||||||
|
isConfirmed,
|
||||||
|
isError,
|
||||||
|
isOpen,
|
||||||
|
isWaiting,
|
||||||
onReport, // eslint-disable-line no-unused-vars
|
onReport, // eslint-disable-line no-unused-vars
|
||||||
report,
|
onRequestClose,
|
||||||
type,
|
type,
|
||||||
...modalProps
|
...modalProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const submitEnabled = this.state.valid && !isWaiting;
|
||||||
|
const submitDisabledParam = submitEnabled ? {} : {disabled: 'disabled'};
|
||||||
const contentLabel = intl.formatMessage({id: `report.${type}`});
|
const contentLabel = intl.formatMessage({id: `report.${type}`});
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
useStandardSizes
|
||||||
className="mod-report"
|
className="mod-report"
|
||||||
contentLabel={contentLabel}
|
contentLabel={contentLabel}
|
||||||
isOpen={report.open}
|
isOpen={isOpen}
|
||||||
|
onRequestClose={onRequestClose}
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="report-modal-header">
|
<div className="report-modal-header modal-header">
|
||||||
<div className="report-content-label">
|
<div className="report-content-label content-label">
|
||||||
{contentLabel}
|
{contentLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
className="report"
|
className="report"
|
||||||
onSubmit={onReport}
|
onInvalid={this.handleInvalid}
|
||||||
|
onValid={this.handleValid}
|
||||||
|
onValidSubmit={onReport}
|
||||||
>
|
>
|
||||||
<div className="report-modal-content">
|
<div className="report-modal-content modal-content">
|
||||||
<FormattedMessage
|
{isConfirmed ? (
|
||||||
id={`report.${type}Instructions`}
|
<div className="received">
|
||||||
values={{
|
<div className="received-header">
|
||||||
CommunityGuidelinesLink: (
|
<FormattedMessage id="report.receivedHeader" />
|
||||||
<a href="/community_guidelines">
|
</div>
|
||||||
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
|
<FormattedMessage id="report.receivedBody" />
|
||||||
</a>
|
</div>
|
||||||
)
|
) : (
|
||||||
}}
|
<div>
|
||||||
/>
|
<div className="instructions">
|
||||||
<Select
|
<FormattedMessage
|
||||||
required
|
id={`report.${type}Instructions`}
|
||||||
elementWrapperClassName="report-modal-field"
|
key={`report.${type}Instructions`}
|
||||||
label={null}
|
values={{
|
||||||
name="report_category"
|
CommunityGuidelinesLink: (
|
||||||
options={REPORT_OPTIONS.map(option => ({
|
<a href="/community_guidelines">
|
||||||
value: option.value,
|
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
|
||||||
label: this.props.intl.formatMessage(option.label)
|
</a>
|
||||||
}))}
|
)
|
||||||
value={this.state.reportCategory}
|
}}
|
||||||
onChange={this.handleReportCategorySelect}
|
/>
|
||||||
/>
|
</div>
|
||||||
<TextArea
|
<Select
|
||||||
required
|
required
|
||||||
className="report-text"
|
elementWrapperClassName="report-modal-field"
|
||||||
elementWrapperClassName="report-modal-field"
|
label={null}
|
||||||
label={null}
|
name="report_category"
|
||||||
name="notes"
|
options={REPORT_OPTIONS.map(option => ({
|
||||||
placeholder={this.lookupPrompt(this.state.reportCategory)}
|
value: option.value,
|
||||||
validationErrors={{
|
label: this.props.intl.formatMessage(option.label),
|
||||||
maxLength: this.props.intl.formatMessage({id: 'report.tooLongError'}),
|
key: option.value
|
||||||
minLength: this.props.intl.formatMessage({id: 'report.tooShortError'})
|
}))}
|
||||||
}}
|
validationErrors={{
|
||||||
validations={{
|
isDefaultRequiredValue: this.props.intl.formatMessage({
|
||||||
maxLength: 500,
|
id: 'report.reasonMissing'
|
||||||
minLength: 20
|
})
|
||||||
}}
|
}}
|
||||||
value={report.notes}
|
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>
|
</div>
|
||||||
<FlexRow className="action-buttons">
|
<FlexRow className="action-buttons">
|
||||||
{report.waiting ? [
|
<div className="action-buttons-overflow-fix">
|
||||||
<Button
|
{isConfirmed ? (
|
||||||
className="submit-button"
|
<Button
|
||||||
disabled="disabled"
|
className="action-button submit-button"
|
||||||
key="submitButton"
|
type="button"
|
||||||
type="submit"
|
onClick={onRequestClose}
|
||||||
>
|
>
|
||||||
<Spinner />
|
<div className="action-button-text">
|
||||||
</Button>
|
<FormattedMessage id="general.close" />
|
||||||
] : [
|
</div>
|
||||||
<Button
|
</Button>
|
||||||
className="submit-button"
|
) : (
|
||||||
key="submitButton"
|
<Button
|
||||||
type="submit"
|
className={classNames(
|
||||||
>
|
'action-button',
|
||||||
<FormattedMessage id="report.send" />
|
'submit-button',
|
||||||
</Button>
|
{disabled: !submitEnabled}
|
||||||
]}
|
)}
|
||||||
|
{...submitDisabledParam}
|
||||||
|
key="submitButton"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{isWaiting ? (
|
||||||
|
<div className="action-button-text">
|
||||||
|
<Spinner />
|
||||||
|
<FormattedMessage id="report.sending" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="action-button-text">
|
||||||
|
<FormattedMessage id="report.send" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -176,15 +246,26 @@ class ReportModal extends React.Component {
|
||||||
|
|
||||||
ReportModal.propTypes = {
|
ReportModal.propTypes = {
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
|
isConfirmed: PropTypes.bool,
|
||||||
|
isError: PropTypes.bool,
|
||||||
|
isOpen: PropTypes.bool,
|
||||||
|
isWaiting: PropTypes.bool,
|
||||||
onReport: PropTypes.func,
|
onReport: PropTypes.func,
|
||||||
onRequestClose: PropTypes.func,
|
onRequestClose: PropTypes.func,
|
||||||
report: PropTypes.shape({
|
|
||||||
category: PropTypes.string,
|
|
||||||
notes: PropTypes.string,
|
|
||||||
open: PropTypes.bool,
|
|
||||||
waiting: PropTypes.bool
|
|
||||||
}),
|
|
||||||
type: PropTypes.string
|
type: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = injectIntl(ReportModal);
|
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);
|
||||||
|
|
|
@ -1,66 +1,65 @@
|
||||||
@import "../../../colors";
|
@import "../../../colors";
|
||||||
@import "../../../frameless";
|
@import "../../../frameless";
|
||||||
|
|
||||||
.mod-report * {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mod-report {
|
|
||||||
margin: 100px auto;
|
|
||||||
outline: none;
|
|
||||||
padding: 0;
|
|
||||||
width: 30rem;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-modal-header {
|
.report-modal-header {
|
||||||
border-radius: 1rem 1rem 0 0;
|
|
||||||
box-shadow: inset 0 -1px 0 0 $ui-coral-dark;
|
box-shadow: inset 0 -1px 0 0 $ui-coral-dark;
|
||||||
background-color: $ui-coral;
|
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 {
|
.report-modal-content {
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
line-height: 1.5rem;
|
|
||||||
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 {
|
.validation-message {
|
||||||
$arrow-border-width: 1rem;
|
$arrow-border-width: 1rem;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: 0;
|
z-index: 1;
|
||||||
left: 0;
|
margin-top: $arrow-border-width;
|
||||||
transform: translate(23.5rem, 0);
|
|
||||||
margin-left: $arrow-border-width;
|
|
||||||
border: 1px solid $active-gray;
|
border: 1px solid $active-gray;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: $ui-orange;
|
background-color: $ui-orange;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
max-width: 18.75rem;
|
min-width: 12rem;
|
||||||
min-height: 1rem;
|
min-height: 1rem;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
color: $type-white;
|
color: $type-white;
|
||||||
|
|
||||||
|
@media #{$medium-and-smaller} {
|
||||||
|
position: relative;
|
||||||
|
margin-top: calc($arrow-border-width / 2);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* arrow on box that points to the left */
|
||||||
&:before {
|
&:before {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1rem;
|
top: -.5rem;
|
||||||
left: -$arrow-border-width / 2;
|
left: calc(50% - calc(#{$arrow-border-width} / 2));
|
||||||
|
|
||||||
transform: rotate(45deg);
|
transform: rotate(135deg);
|
||||||
|
|
||||||
border-bottom: 1px solid $active-gray;
|
border-bottom: 1px solid $active-gray;
|
||||||
border-left: 1px solid $active-gray;
|
border-left: 1px solid $active-gray;
|
||||||
|
@ -71,6 +70,10 @@
|
||||||
height: $arrow-border-width;
|
height: $arrow-border-width;
|
||||||
|
|
||||||
content: "";
|
content: "";
|
||||||
|
|
||||||
|
@media #{$medium-and-smaller} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,3 +81,16 @@
|
||||||
.report-modal-field {
|
.report-modal-field {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group.has-error {
|
||||||
|
.textarea, select {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid $ui-orange;
|
||||||
|
}
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-text .textarea {
|
||||||
|
margin-bottom: 0;
|
||||||
|
min-height: 8rem;
|
||||||
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.modal-content.mod-ttt {
|
.modal-content.mod-ttt {
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
.modal-content.mod-ttt {
|
.modal-content.mod-ttt {
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
.modal-content.mod-ttt {
|
.modal-content.mod-ttt {
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.li-right-ul.mod-2016 {
|
.li-right-ul.mod-2016 {
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
|
|
||||||
.ul.mod-2016 {
|
.ul.mod-2016 {
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.li-left-ul.mod-2018 {
|
.li-left-ul.mod-2018 {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.li-right-ul.mod-2018 {
|
.li-right-ul.mod-2018 {
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
|
|
||||||
.ul.mod-2018 {
|
.ul.mod-2018 {
|
||||||
|
|
102
src/components/navigation/www/accountnav.jsx
Normal file
102
src/components/navigation/www/accountnav.jsx
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
const classNames = require('classnames');
|
||||||
|
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||||
|
const injectIntl = require('react-intl').injectIntl;
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const React = require('react');
|
||||||
|
|
||||||
|
const Avatar = require('../../avatar/avatar.jsx');
|
||||||
|
const Dropdown = require('../../dropdown/dropdown.jsx');
|
||||||
|
|
||||||
|
require('./accountnav.scss');
|
||||||
|
|
||||||
|
const AccountNav = ({
|
||||||
|
classroomId,
|
||||||
|
isEducator,
|
||||||
|
isOpen,
|
||||||
|
isStudent,
|
||||||
|
profileUrl,
|
||||||
|
thumbnailUrl,
|
||||||
|
username,
|
||||||
|
onClick,
|
||||||
|
onClickLogout,
|
||||||
|
onClose
|
||||||
|
}) => (
|
||||||
|
<div className="account-nav">
|
||||||
|
<a
|
||||||
|
className={classNames([
|
||||||
|
'ignore-react-onclickoutside',
|
||||||
|
'user-info',
|
||||||
|
{open: isOpen}
|
||||||
|
])}
|
||||||
|
href="#"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
alt=""
|
||||||
|
src={thumbnailUrl}
|
||||||
|
/>
|
||||||
|
<span className="profile-name">
|
||||||
|
{username}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<Dropdown
|
||||||
|
as="ul"
|
||||||
|
className={process.env.SCRATCH_ENV}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a href={profileUrl}>
|
||||||
|
<FormattedMessage id="general.profile" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/mystuff/">
|
||||||
|
<FormattedMessage id="general.myStuff" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{isEducator ? [
|
||||||
|
<li key="my-classes-li">
|
||||||
|
<a href="/educators/classes/">
|
||||||
|
<FormattedMessage id="general.myClasses" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
] : []}
|
||||||
|
{isStudent ? [
|
||||||
|
<li key="my-class-li">
|
||||||
|
<a href={`/classes/${classroomId}/`}>
|
||||||
|
<FormattedMessage id="general.myClass" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
] : []}
|
||||||
|
<li>
|
||||||
|
<a href="/accounts/settings/">
|
||||||
|
<FormattedMessage id="general.accountSettings" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="divider">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={onClickLogout}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="navigation.signOut" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
AccountNav.propTypes = {
|
||||||
|
classroomId: PropTypes.string,
|
||||||
|
isEducator: PropTypes.bool,
|
||||||
|
isOpen: PropTypes.bool,
|
||||||
|
isStudent: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onClickLogout: PropTypes.func,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
profileUrl: PropTypes.string,
|
||||||
|
thumbnailUrl: PropTypes.string,
|
||||||
|
username: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = injectIntl(AccountNav);
|
98
src/components/navigation/www/accountnav.scss
Normal file
98
src/components/navigation/www/accountnav.scss
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
@import "../../../colors";
|
||||||
|
@import "../../../frameless";
|
||||||
|
|
||||||
|
.account-nav {
|
||||||
|
.user-info {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 14px 15px 4px 15px;
|
||||||
|
max-width: 260px;
|
||||||
|
height: 33px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: $type-white;
|
||||||
|
font-size: .8125rem;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $active-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
background-color: $active-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
background-image: url("/images/dropdown.png");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
content: " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
top: 50px;
|
||||||
|
padding: 0;
|
||||||
|
padding-top: 5px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//4 columns
|
||||||
|
@media #{$small} {
|
||||||
|
.account-nav {
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
.avatar {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//6 columns
|
||||||
|
@media #{$medium} {
|
||||||
|
.account-nav {
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
.avatar {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//8 columns
|
||||||
|
@media #{$intermediate} {
|
||||||
|
.account-nav {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,19 +8,17 @@ const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
const messageCountActions = require('../../../redux/message-count.js');
|
const messageCountActions = require('../../../redux/message-count.js');
|
||||||
|
const navigationActions = require('../../../redux/navigation.js');
|
||||||
const sessionActions = require('../../../redux/session.js');
|
const sessionActions = require('../../../redux/session.js');
|
||||||
|
|
||||||
const api = require('../../../lib/api');
|
|
||||||
const Avatar = require('../../avatar/avatar.jsx');
|
|
||||||
const Button = require('../../forms/button.jsx');
|
const Button = require('../../forms/button.jsx');
|
||||||
const Dropdown = require('../../dropdown/dropdown.jsx');
|
|
||||||
const Form = require('../../forms/form.jsx');
|
const Form = require('../../forms/form.jsx');
|
||||||
const Input = require('../../forms/input.jsx');
|
const Input = require('../../forms/input.jsx');
|
||||||
const log = require('../../../lib/log.js');
|
const LoginDropdown = require('../../login/login-dropdown.jsx');
|
||||||
const Login = require('../../login/login.jsx');
|
const CanceledDeletionModal = require('../../login/canceled-deletion-modal.jsx');
|
||||||
const Modal = require('../../modal/base/modal.jsx');
|
|
||||||
const NavigationBox = require('../base/navigation.jsx');
|
const NavigationBox = require('../base/navigation.jsx');
|
||||||
const Registration = require('../../registration/registration.jsx');
|
const Registration = require('../../registration/registration.jsx');
|
||||||
|
const AccountNav = require('./accountnav.jsx');
|
||||||
|
|
||||||
require('./navigation.scss');
|
require('./navigation.scss');
|
||||||
|
|
||||||
|
@ -29,34 +27,16 @@ class Navigation extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'getProfileUrl',
|
'getProfileUrl',
|
||||||
'handleJoinClick',
|
|
||||||
'handleLoginClick',
|
|
||||||
'handleCloseLogin',
|
|
||||||
'handleLogIn',
|
|
||||||
'handleLogOut',
|
|
||||||
'handleAccountNavClick',
|
|
||||||
'handleCloseAccountNav',
|
|
||||||
'showCanceledDeletion',
|
|
||||||
'handleCloseCanceledDeletion',
|
|
||||||
'handleCloseRegistration',
|
|
||||||
'handleCompleteRegistration',
|
|
||||||
'handleSearchSubmit'
|
'handleSearchSubmit'
|
||||||
]);
|
]);
|
||||||
this.state = {
|
this.state = {
|
||||||
accountNavOpen: false,
|
|
||||||
canceledDeletionOpen: false,
|
|
||||||
loginOpen: false,
|
|
||||||
loginError: null,
|
|
||||||
registrationOpen: false,
|
|
||||||
messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
|
messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
if (this.props.session.session.user) {
|
if (this.props.user) {
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
this.props.dispatch(
|
this.props.getMessageCount(this.props.user.username);
|
||||||
messageCountActions.getCount(this.props.session.session.user.username)
|
|
||||||
);
|
|
||||||
}, 120000); // check for new messages every 2 mins.
|
}, 120000); // check for new messages every 2 mins.
|
||||||
this.setState({ // eslint-disable-line react/no-did-mount-set-state
|
this.setState({ // eslint-disable-line react/no-did-mount-set-state
|
||||||
messageCountIntervalId: intervalId
|
messageCountIntervalId: intervalId
|
||||||
|
@ -64,16 +44,11 @@ class Navigation extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (prevProps.session.session.user !== this.props.session.session.user) {
|
if (prevProps.user !== this.props.user) {
|
||||||
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
this.props.closeAccountMenus();
|
||||||
loginOpen: false,
|
if (this.props.user) {
|
||||||
accountNavOpen: false
|
|
||||||
});
|
|
||||||
if (this.props.session.session.user) {
|
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
this.props.dispatch(
|
this.props.getMessageCount(this.props.user.username);
|
||||||
messageCountActions.getCount(this.props.session.session.user.username)
|
|
||||||
);
|
|
||||||
}, 120000); // check for new messages every 2 mins.
|
}, 120000); // check for new messages every 2 mins.
|
||||||
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
||||||
messageCountIntervalId: intervalId
|
messageCountIntervalId: intervalId
|
||||||
|
@ -81,7 +56,7 @@ class Navigation extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
// clear message count check, and set to default id.
|
// clear message count check, and set to default id.
|
||||||
clearInterval(this.state.messageCountIntervalId);
|
clearInterval(this.state.messageCountIntervalId);
|
||||||
this.props.dispatch(messageCountActions.setCount(0));
|
this.props.setMessageCount(0);
|
||||||
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
||||||
messageCountIntervalId: -1
|
messageCountIntervalId: -1
|
||||||
});
|
});
|
||||||
|
@ -92,102 +67,25 @@ class Navigation extends React.Component {
|
||||||
// clear message interval if it exists
|
// clear message interval if it exists
|
||||||
if (this.state.messageCountIntervalId !== -1) {
|
if (this.state.messageCountIntervalId !== -1) {
|
||||||
clearInterval(this.state.messageCountIntervalId);
|
clearInterval(this.state.messageCountIntervalId);
|
||||||
this.props.dispatch(messageCountActions.setCount(0));
|
this.props.setMessageCount(0);
|
||||||
this.setState({
|
this.setState({
|
||||||
messageCountIntervalId: -1
|
messageCountIntervalId: -1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getProfileUrl () {
|
getProfileUrl () {
|
||||||
if (!this.props.session.session.user) return;
|
if (!this.props.user) return;
|
||||||
return `/users/${this.props.session.session.user.username}/`;
|
return `/users/${this.props.user.username}/`;
|
||||||
}
|
|
||||||
handleJoinClick (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({registrationOpen: true});
|
|
||||||
}
|
|
||||||
handleLoginClick (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({loginOpen: !this.state.loginOpen});
|
|
||||||
}
|
|
||||||
handleCloseLogin () {
|
|
||||||
this.setState({loginOpen: false});
|
|
||||||
}
|
|
||||||
handleLogIn (formData, callback) {
|
|
||||||
this.setState({loginError: null});
|
|
||||||
formData.useMessages = true;
|
|
||||||
api({
|
|
||||||
method: 'post',
|
|
||||||
host: '',
|
|
||||||
uri: '/accounts/login/',
|
|
||||||
json: formData,
|
|
||||||
useCsrf: true
|
|
||||||
}, (err, body) => {
|
|
||||||
if (err) this.setState({loginError: err.message});
|
|
||||||
if (body) {
|
|
||||||
body = body[0];
|
|
||||||
if (body.success) {
|
|
||||||
this.handleCloseLogin();
|
|
||||||
body.messages.map(message => { // eslint-disable-line array-callback-return
|
|
||||||
if (message.message === 'canceled-deletion') {
|
|
||||||
this.showCanceledDeletion();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.props.dispatch(sessionActions.refreshSession());
|
|
||||||
} else {
|
|
||||||
if (body.redirect) {
|
|
||||||
window.location = body.redirect;
|
|
||||||
}
|
|
||||||
// Update login error message to a friendlier one if it exists
|
|
||||||
this.setState({loginError: body.msg});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// JS error already logged by api mixin
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
handleLogOut (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
api({
|
|
||||||
host: '',
|
|
||||||
method: 'post',
|
|
||||||
uri: '/accounts/logout/',
|
|
||||||
useCsrf: true
|
|
||||||
}, err => {
|
|
||||||
if (err) log.error(err);
|
|
||||||
this.handleCloseLogin();
|
|
||||||
window.location = '/';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
handleAccountNavClick (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({accountNavOpen: true});
|
|
||||||
}
|
|
||||||
handleCloseAccountNav () {
|
|
||||||
this.setState({accountNavOpen: false});
|
|
||||||
}
|
|
||||||
showCanceledDeletion () {
|
|
||||||
this.setState({canceledDeletionOpen: true});
|
|
||||||
}
|
|
||||||
handleCloseCanceledDeletion () {
|
|
||||||
this.setState({canceledDeletionOpen: false});
|
|
||||||
}
|
|
||||||
handleCloseRegistration () {
|
|
||||||
this.setState({registrationOpen: false});
|
|
||||||
}
|
|
||||||
handleCompleteRegistration () {
|
|
||||||
this.props.dispatch(sessionActions.refreshSession());
|
|
||||||
this.handleCloseRegistration();
|
|
||||||
}
|
}
|
||||||
handleSearchSubmit (formData) {
|
handleSearchSubmit (formData) {
|
||||||
window.location.href = `/search/projects?q=${encodeURIComponent(formData.q)}`;
|
window.location.href = `/search/projects?q=${encodeURIComponent(formData.q)}`;
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
const createLink = this.props.session.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
|
const createLink = this.props.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
|
||||||
return (
|
return (
|
||||||
<NavigationBox
|
<NavigationBox
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'logged-in': this.props.session.session.user
|
'logged-in': this.props.user
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -235,7 +133,7 @@ class Navigation extends React.Component {
|
||||||
</Form>
|
</Form>
|
||||||
</li>
|
</li>
|
||||||
{this.props.session.status === sessionActions.Status.FETCHED ? (
|
{this.props.session.status === sessionActions.Status.FETCHED ? (
|
||||||
this.props.session.session.user ? [
|
this.props.user ? [
|
||||||
<li
|
<li
|
||||||
className="link right messages"
|
className="link right messages"
|
||||||
key="messages"
|
key="messages"
|
||||||
|
@ -249,7 +147,7 @@ class Navigation extends React.Component {
|
||||||
'message-count': true,
|
'message-count': true,
|
||||||
'show': this.props.unreadMessageCount > 0
|
'show': this.props.unreadMessageCount > 0
|
||||||
})}
|
})}
|
||||||
>{this.props.unreadMessageCount}</span>
|
>{this.props.unreadMessageCount} </span>
|
||||||
<FormattedMessage id="general.messages" />
|
<FormattedMessage id="general.messages" />
|
||||||
</a>
|
</a>
|
||||||
</li>,
|
</li>,
|
||||||
|
@ -268,66 +166,18 @@ class Navigation extends React.Component {
|
||||||
className="link right account-nav"
|
className="link right account-nav"
|
||||||
key="account-nav"
|
key="account-nav"
|
||||||
>
|
>
|
||||||
<a
|
<AccountNav
|
||||||
className={classNames({
|
classroomId={this.props.user.classroomId}
|
||||||
'user-info': true,
|
isEducator={this.props.permissions.educator}
|
||||||
'open': this.state.accountNavOpen
|
isOpen={this.props.accountNavOpen}
|
||||||
})}
|
isStudent={this.props.permissions.student}
|
||||||
href="#"
|
profileUrl={this.getProfileUrl()}
|
||||||
onClick={this.handleAccountNavClick}
|
thumbnailUrl={this.props.user.thumbnailUrl}
|
||||||
>
|
username={this.props.user.username}
|
||||||
<Avatar
|
onClick={this.props.handleToggleAccountNav}
|
||||||
alt=""
|
onClickLogout={this.props.handleLogOut}
|
||||||
src={this.props.session.session.user.thumbnailUrl}
|
onClose={this.props.handleCloseAccountNav}
|
||||||
/>
|
/>
|
||||||
<span className="profile-name">
|
|
||||||
{this.props.session.session.user.username}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<Dropdown
|
|
||||||
as="ul"
|
|
||||||
className={process.env.SCRATCH_ENV}
|
|
||||||
isOpen={this.state.accountNavOpen}
|
|
||||||
onRequestClose={this.handleCloseAccountNav}
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a href={this.getProfileUrl()}>
|
|
||||||
<FormattedMessage id="general.profile" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/mystuff/">
|
|
||||||
<FormattedMessage id="general.myStuff" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{this.props.permissions.educator ? [
|
|
||||||
<li key="my-classes-li">
|
|
||||||
<a href="/educators/classes/">
|
|
||||||
<FormattedMessage id="general.myClasses" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
] : []}
|
|
||||||
{this.props.permissions.student ? [
|
|
||||||
<li key="my-class-li">
|
|
||||||
<a href={`/classes/${this.props.session.session.user.classroomId}/`}>
|
|
||||||
<FormattedMessage id="general.myClass" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
] : []}
|
|
||||||
<li>
|
|
||||||
<a href="/accounts/settings/">
|
|
||||||
<FormattedMessage id="general.accountSettings" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="divider">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={this.handleLogOut}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="navigation.signOut" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</Dropdown>
|
|
||||||
</li>
|
</li>
|
||||||
] : [
|
] : [
|
||||||
<li
|
<li
|
||||||
|
@ -336,16 +186,13 @@ class Navigation extends React.Component {
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
onClick={this.handleJoinClick}
|
onClick={this.props.handleOpenRegistration}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="general.joinScratch" />
|
<FormattedMessage id="general.joinScratch" />
|
||||||
</a>
|
</a>
|
||||||
</li>,
|
</li>,
|
||||||
<Registration
|
<Registration
|
||||||
isOpen={this.state.registrationOpen}
|
|
||||||
key="registration"
|
key="registration"
|
||||||
onRegistrationDone={this.handleCompleteRegistration}
|
|
||||||
onRequestClose={this.handleCloseRegistration}
|
|
||||||
/>,
|
/>,
|
||||||
<li
|
<li
|
||||||
className="link right login-item"
|
className="link right login-item"
|
||||||
|
@ -355,53 +202,31 @@ class Navigation extends React.Component {
|
||||||
className="ignore-react-onclickoutside"
|
className="ignore-react-onclickoutside"
|
||||||
href="#"
|
href="#"
|
||||||
key="login-link"
|
key="login-link"
|
||||||
onClick={this.handleLoginClick}
|
onClick={this.props.handleToggleLoginOpen}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="general.signIn" />
|
<FormattedMessage id="general.signIn" />
|
||||||
</a>
|
</a>
|
||||||
<Dropdown
|
<LoginDropdown
|
||||||
className="login-dropdown with-arrow"
|
|
||||||
isOpen={this.state.loginOpen}
|
|
||||||
key="login-dropdown"
|
key="login-dropdown"
|
||||||
onRequestClose={this.handleCloseLogin}
|
/>
|
||||||
>
|
|
||||||
<Login
|
|
||||||
error={this.state.loginError}
|
|
||||||
onLogIn={this.handleLogIn}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
</li>
|
</li>
|
||||||
]) : []}
|
]) : []}
|
||||||
</ul>
|
</ul>
|
||||||
<Modal
|
<CanceledDeletionModal />
|
||||||
isOpen={this.state.canceledDeletionOpen}
|
|
||||||
style={{
|
|
||||||
content: {
|
|
||||||
padding: 15
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onRequestClose={this.handleCloseCanceledDeletion}
|
|
||||||
>
|
|
||||||
<h4>Your Account Will Not Be Deleted</h4>
|
|
||||||
<h4><FormattedMessage id="general.noDeletionTitle" /></h4>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id="general.noDeletionDescription"
|
|
||||||
values={{
|
|
||||||
resetLink: <a href="/accounts/password_reset/">
|
|
||||||
{this.props.intl.formatMessage({id: 'general.noDeletionLink'})}
|
|
||||||
</a>
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</Modal>
|
|
||||||
</NavigationBox>
|
</NavigationBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigation.propTypes = {
|
Navigation.propTypes = {
|
||||||
dispatch: PropTypes.func,
|
accountNavOpen: PropTypes.bool,
|
||||||
|
closeAccountMenus: PropTypes.func,
|
||||||
|
getMessageCount: PropTypes.func,
|
||||||
|
handleCloseAccountNav: PropTypes.func,
|
||||||
|
handleLogOut: PropTypes.func,
|
||||||
|
handleOpenRegistration: PropTypes.func,
|
||||||
|
handleToggleAccountNav: PropTypes.func,
|
||||||
|
handleToggleLoginOpen: PropTypes.func,
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
permissions: PropTypes.shape({
|
permissions: PropTypes.shape({
|
||||||
admin: PropTypes.bool,
|
admin: PropTypes.bool,
|
||||||
|
@ -412,16 +237,15 @@ Navigation.propTypes = {
|
||||||
}),
|
}),
|
||||||
searchTerm: PropTypes.string,
|
searchTerm: PropTypes.string,
|
||||||
session: PropTypes.shape({
|
session: PropTypes.shape({
|
||||||
session: PropTypes.shape({
|
|
||||||
user: PropTypes.shape({
|
|
||||||
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
||||||
thumbnailUrl: PropTypes.string,
|
|
||||||
username: PropTypes.string
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
status: PropTypes.string
|
status: PropTypes.string
|
||||||
}),
|
}),
|
||||||
unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
setMessageCount: PropTypes.func,
|
||||||
|
unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
|
user: PropTypes.shape({
|
||||||
|
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
|
thumbnailUrl: PropTypes.string,
|
||||||
|
username: PropTypes.string
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
Navigation.defaultProps = {
|
Navigation.defaultProps = {
|
||||||
|
@ -431,12 +255,48 @@ Navigation.defaultProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
accountNavOpen: state.navigation && state.navigation.accountNavOpen,
|
||||||
session: state.session,
|
session: state.session,
|
||||||
permissions: state.permissions,
|
permissions: state.permissions,
|
||||||
|
searchTerm: state.navigation.searchTerm,
|
||||||
unreadMessageCount: state.messageCount.messageCount,
|
unreadMessageCount: state.messageCount.messageCount,
|
||||||
searchTerm: state.navigation
|
user: state.session && state.session.session && state.session.session.user
|
||||||
});
|
});
|
||||||
|
|
||||||
const ConnectedNavigation = connect(mapStateToProps)(Navigation);
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
closeAccountMenus: () => {
|
||||||
|
dispatch(navigationActions.closeAccountMenus());
|
||||||
|
},
|
||||||
|
getMessageCount: username => {
|
||||||
|
dispatch(messageCountActions.getCount(username));
|
||||||
|
},
|
||||||
|
handleToggleAccountNav: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.handleToggleAccountNav());
|
||||||
|
},
|
||||||
|
handleCloseAccountNav: () => {
|
||||||
|
dispatch(navigationActions.setAccountNavOpen(false));
|
||||||
|
},
|
||||||
|
handleOpenRegistration: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.setRegistrationOpen(true));
|
||||||
|
},
|
||||||
|
handleLogOut: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.handleLogOut());
|
||||||
|
},
|
||||||
|
handleToggleLoginOpen: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.toggleLoginOpen());
|
||||||
|
},
|
||||||
|
setMessageCount: newCount => {
|
||||||
|
dispatch(messageCountActions.setCount(newCount));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ConnectedNavigation = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Navigation);
|
||||||
|
|
||||||
module.exports = injectIntl(ConnectedNavigation);
|
module.exports = injectIntl(ConnectedNavigation);
|
||||||
|
|
|
@ -163,78 +163,10 @@
|
||||||
background-image: url("/images/mystuff.png");
|
background-image: url("/images/mystuff.png");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-dropdown {
|
|
||||||
width: 200px;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
padding: .75em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
.row {
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
|
|
||||||
input {
|
|
||||||
margin: 0;
|
|
||||||
height: 2.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-nav {
|
|
||||||
.user-info {
|
|
||||||
padding-top: 14px;
|
|
||||||
max-width: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> a {
|
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: .8125rem;
|
|
||||||
font-weight: normal;
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
margin-right: 10px;
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
background-color: $active-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 8px;
|
|
||||||
|
|
||||||
background-image: url("/images/dropdown.png");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center center;
|
|
||||||
background-size: 50%;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
vertical-align: middle;
|
|
||||||
content: " ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
top: 50px;
|
|
||||||
padding: 0;
|
|
||||||
padding-top: 5px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//4 columns
|
//4 columns
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
#navigation .inner {
|
#navigation .inner {
|
||||||
width: $cols4;
|
width: $cols4;
|
||||||
|
|
||||||
|
@ -242,20 +174,6 @@
|
||||||
&.login-item {
|
&.login-item {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.account-nav {
|
|
||||||
margin-left: 0;
|
|
||||||
|
|
||||||
> a {
|
|
||||||
.avatar {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.create,
|
.create,
|
||||||
|
@ -272,7 +190,7 @@
|
||||||
|
|
||||||
|
|
||||||
//6 columns
|
//6 columns
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
#navigation .inner {
|
#navigation .inner {
|
||||||
width: $cols6;
|
width: $cols6;
|
||||||
|
|
||||||
|
@ -280,20 +198,6 @@
|
||||||
&.login-item {
|
&.login-item {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.account-nav {
|
|
||||||
margin-left: 0;
|
|
||||||
|
|
||||||
> a {
|
|
||||||
.avatar {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.discuss,
|
.discuss,
|
||||||
|
@ -308,13 +212,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
//8 columns
|
//8 columns
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
#navigation .inner {
|
#navigation .inner {
|
||||||
width: $cols8;
|
width: $cols8;
|
||||||
|
|
||||||
> ul > li {
|
> ul > li {
|
||||||
&.login-item,
|
&.login-item {
|
||||||
&.account-nav {
|
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,19 +6,19 @@
|
||||||
font-size: 4.5rem;
|
font-size: 4.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3.5rem;
|
font-size: 3.5rem;
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
h1,
|
h1,
|
||||||
.title-banner-h1.mod-2017 {
|
.title-banner-h1.mod-2017 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
|
@ -96,7 +96,7 @@
|
||||||
width: 125px;
|
width: 125px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
img {
|
img {
|
||||||
transform: translate(0, 5px);
|
transform: translate(0, 5px);
|
||||||
width: 85px;
|
width: 85px;
|
||||||
|
@ -108,7 +108,7 @@ section {
|
||||||
padding: 64px 0;
|
padding: 64px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
#view {
|
#view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
const bindAll = require('lodash.bindall');
|
const bindAll = require('lodash.bindall');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
const connect = require('react-redux').connect;
|
||||||
|
|
||||||
const IframeModal = require('../modal/iframe/modal.jsx');
|
const IframeModal = require('../modal/iframe/modal.jsx');
|
||||||
|
const navigationActions = require('../../redux/navigation.js');
|
||||||
|
|
||||||
require('./registration.scss');
|
require('./registration.scss');
|
||||||
|
|
||||||
|
@ -26,7 +28,7 @@ class Registration extends React.Component {
|
||||||
handleMessage (e) {
|
handleMessage (e) {
|
||||||
if (e.origin !== window.location.origin) return;
|
if (e.origin !== window.location.origin) return;
|
||||||
if (e.source !== this.registrationIframe.contentWindow) return;
|
if (e.source !== this.registrationIframe.contentWindow) return;
|
||||||
if (e.data === 'registration-done') this.props.onRegistrationDone();
|
if (e.data === 'registration-done') this.props.handleCompleteRegistration();
|
||||||
if (e.data === 'registration-relaunch') {
|
if (e.data === 'registration-relaunch') {
|
||||||
this.registrationIframe.contentWindow.location.reload();
|
this.registrationIframe.contentWindow.location.reload();
|
||||||
}
|
}
|
||||||
|
@ -47,16 +49,32 @@ class Registration extends React.Component {
|
||||||
}}
|
}}
|
||||||
isOpen={this.props.isOpen}
|
isOpen={this.props.isOpen}
|
||||||
src="/accounts/standalone-registration/"
|
src="/accounts/standalone-registration/"
|
||||||
onRequestClose={this.props.onRequestClose}
|
onRequestClose={this.props.handleCloseRegistration}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Registration.propTypes = {
|
Registration.propTypes = {
|
||||||
isOpen: PropTypes.bool,
|
handleCloseRegistration: PropTypes.func,
|
||||||
onRegistrationDone: PropTypes.func,
|
handleCompleteRegistration: PropTypes.func,
|
||||||
onRequestClose: PropTypes.func
|
isOpen: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Registration;
|
const mapStateToProps = state => ({
|
||||||
|
isOpen: state.navigation.registrationOpen
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
handleCloseRegistration: () => {
|
||||||
|
dispatch(navigationActions.setRegistrationOpen(false));
|
||||||
|
},
|
||||||
|
handleCompleteRegistration: () => {
|
||||||
|
dispatch(navigationActions.handleCompleteRegistration());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Registration);
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
min-height: 27.375rem;
|
min-height: 27.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.modal-content.mod-registration {
|
.modal-content.mod-registration {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
|
|
|
@ -444,18 +444,20 @@ class DemographicsStep extends React.Component {
|
||||||
handleChooseGender (name, gender) {
|
handleChooseGender (name, gender) {
|
||||||
this.setState({otherDisabled: gender !== 'other'});
|
this.setState({otherDisabled: gender !== 'other'});
|
||||||
}
|
}
|
||||||
handleValidSubmit (formData, reset, invalidate) {
|
handleValidSubmit (formData) {
|
||||||
|
return this.props.onNextStep(formData);
|
||||||
|
}
|
||||||
|
isValidBirthdate (year, month) {
|
||||||
const birthdate = new Date(
|
const birthdate = new Date(
|
||||||
formData.user.birth.year,
|
year,
|
||||||
formData.user.birth.month - 1,
|
month - 1,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
if (((Date.now() - birthdate) / (24 * 3600 * 1000 * 365.25)) < this.props.birthOffset) {
|
return (((Date.now() - birthdate) / (24 * 3600 * 1000 * 365.25)) >= this.props.birthOffset);
|
||||||
return invalidate({
|
}
|
||||||
'user.birth.year': this.props.intl.formatMessage({id: 'teacherRegistration.validationAge'})
|
birthDateValidator (values) {
|
||||||
});
|
const isValid = this.isValidBirthdate(values['user.birth.year'], values['user.birth.month']);
|
||||||
}
|
return isValid ? true : this.props.intl.formatMessage({id: 'teacherRegistration.validationAge'});
|
||||||
return this.props.onNextStep(formData);
|
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
const countryOptions = getCountryOptions(this.props.intl, DEFAULT_COUNTRY);
|
const countryOptions = getCountryOptions(this.props.intl, DEFAULT_COUNTRY);
|
||||||
|
@ -485,6 +487,9 @@ class DemographicsStep extends React.Component {
|
||||||
}
|
}
|
||||||
name="user.birth.month"
|
name="user.birth.month"
|
||||||
options={this.getMonthOptions()}
|
options={this.getMonthOptions()}
|
||||||
|
validations={{
|
||||||
|
birthDateVal: values => this.birthDateValidator(values)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
required
|
required
|
||||||
|
|
|
@ -155,7 +155,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.registration-step {
|
.registration-step {
|
||||||
&.demographics-step {
|
&.demographics-step {
|
||||||
.radio {
|
.radio {
|
||||||
|
@ -174,7 +174,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.registration-step {
|
.registration-step {
|
||||||
.form {
|
.form {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
.slide {
|
.slide {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ a.social-messages-profile-link {
|
||||||
margin-left: 1.5rem;
|
margin-left: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.social-message {
|
.social-message {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ a.social-messages-profile-link {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
.social-message {
|
.social-message {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,28 @@
|
||||||
const range = require('lodash.range');
|
|
||||||
const PropTypes = require('prop-types');
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const classNames = require('classnames');
|
||||||
|
|
||||||
require('./spinner.scss');
|
require('./spinner.scss');
|
||||||
|
|
||||||
// Adapted from http://tobiasahlin.com/spinkit/
|
// Adapted from http://tobiasahlin.com/spinkit/
|
||||||
const Spinner = ({
|
const Spinner = ({
|
||||||
mode
|
className,
|
||||||
}) => {
|
color
|
||||||
const spinnerClassName = (mode === 'smooth' ? 'spinner-smooth' : 'spinner');
|
}) => (
|
||||||
const spinnerDivCount = (mode === 'smooth' ? 24 : 12);
|
<img
|
||||||
return (
|
alt="loading animation"
|
||||||
<div className={spinnerClassName}>
|
className={classNames('studio-status-icon-spinner', className)}
|
||||||
{range(1, spinnerDivCount + 1).map(id => (
|
src={`/svgs/modal/spinner-${color}.svg`}
|
||||||
<div
|
/>
|
||||||
className={`circle${id} circle`}
|
);
|
||||||
key={`circle${id}`}
|
|
||||||
/>
|
Spinner.defaultProps = {
|
||||||
))}
|
color: 'white'
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Spinner.propTypes = {
|
Spinner.propTypes = {
|
||||||
mode: PropTypes.string
|
className: PropTypes.string,
|
||||||
|
color: PropTypes.oneOf(['white', 'blue', 'transparent-gray'])
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Spinner;
|
module.exports = Spinner;
|
||||||
|
|
|
@ -1,118 +1,44 @@
|
||||||
@import "../../colors";
|
.studio-status-icon-spinner {
|
||||||
|
/* This class can be used on an icon that should spin.
|
||||||
.spinner {
|
It first plays the intro animation, then spins forever. */
|
||||||
position: relative;
|
animation-name: intro, spin;
|
||||||
margin: 0 auto;
|
animation-duration: .25s, .5s;
|
||||||
width: 20px;
|
animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear;
|
||||||
height: 20px;
|
animation-delay: 0s, .25s;
|
||||||
|
animation-iteration-count: 1, infinite;
|
||||||
.circle {
|
animation-direction: normal;
|
||||||
position: absolute;
|
width: 1.4rem; /* standard is 1.4 rem but can be overwritten by parent */
|
||||||
top: 0;
|
height: 1.4rem;
|
||||||
left: 0;
|
-webkit-animation-name: intro, spin;
|
||||||
width: 100%;
|
-webkit-animation-duration: .25s, .5s;
|
||||||
height: 100%;
|
-webkit-animation-iteration-count: 1, infinite;
|
||||||
|
-webkit-animation-delay: 0s, .25s;
|
||||||
&:before {
|
-webkit-animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear;
|
||||||
display: block;
|
transform-origin: center;
|
||||||
animation: circleFadeDelay 1.2s infinite ease-in-out both;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: $ui-gray;
|
|
||||||
width: 15%;
|
|
||||||
height: 15%;
|
|
||||||
content: "";
|
|
||||||
|
|
||||||
.white & {
|
|
||||||
background-color: $ui-blue-dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@for $i from 1 through 12 {
|
|
||||||
$rotation: 30deg * ($i - 1);
|
|
||||||
$delay: -1.3s + $i * .1;
|
|
||||||
|
|
||||||
.circle#{$i} {
|
|
||||||
transform: rotate($rotation);
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
animation-delay: $delay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes circleFadeDelay {
|
@keyframes intro {
|
||||||
0%,
|
0% {
|
||||||
39%,
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
transform: scale(1);
|
||||||
}
|
|
||||||
|
|
||||||
40% {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
-webkit-transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
/*********************/
|
0% {
|
||||||
/* type === "smooth" */
|
transform: rotate(0);
|
||||||
/*********************/
|
-webkit-transform: rotate(0);
|
||||||
|
|
||||||
.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 {
|
100% {
|
||||||
$rotation: 15deg * ($i - 1);
|
transform: rotate(359deg);
|
||||||
$delay: -1.9s + $i * .075;
|
-webkit-transform: rotate(359deg);
|
||||||
|
|
||||||
.circle#{$i} {
|
|
||||||
transform: rotate($rotation);
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
animation-delay: $delay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes circleFadeDelaySmooth {
|
|
||||||
0%,
|
|
||||||
35% {
|
|
||||||
opacity: 0;
|
|
||||||
},
|
|
||||||
40% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,7 @@ const Thumbnail = props => {
|
||||||
<a
|
<a
|
||||||
href={props.href}
|
href={props.href}
|
||||||
key="titleElement"
|
key="titleElement"
|
||||||
|
title={props.title}
|
||||||
>
|
>
|
||||||
{props.title}
|
{props.title}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
|
|
||||||
$thumbnail-width: 220px;
|
$thumbnail-width: 220px;
|
||||||
$thumbnail-inner-width: 204px;
|
$thumbnail-inner-width: 204px;
|
||||||
|
|
||||||
$project-height: 208px;
|
$project-height: 208px;
|
||||||
$gallery-height: 164px;
|
$gallery-height: 164px;
|
||||||
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
@ -16,14 +16,13 @@
|
||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
margin: 7px;
|
margin: 7px;
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 0 0 1px $active-gray;
|
|
||||||
background-color: $ui-white;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
width: $thumbnail-width;
|
width: $thumbnail-width;
|
||||||
|
|
||||||
.thumbnail-image {
|
.thumbnail-image {
|
||||||
margin: 8px auto;
|
margin: 8px auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 0 0 1px $active-gray;
|
||||||
|
background-color: $ui-white;
|
||||||
width: $thumbnail-inner-width;
|
width: $thumbnail-inner-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,10 +44,19 @@
|
||||||
.thumbnail-title {
|
.thumbnail-title {
|
||||||
float: left;
|
float: left;
|
||||||
max-width: 164px;
|
max-width: 164px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
.thumbnail-creator a {
|
.thumbnail-creator a {
|
||||||
color: $type-gray;
|
color: $type-gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.tooltip {
|
.tooltip {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ const Raven = require('raven-js');
|
||||||
};
|
};
|
||||||
|
|
||||||
window._locale = updateLocale();
|
window._locale = updateLocale();
|
||||||
|
document.documentElement.lang = window._locale;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -106,6 +106,8 @@
|
||||||
"navigation.signOut": "Sign out",
|
"navigation.signOut": "Sign out",
|
||||||
|
|
||||||
"extensionHeader.requirements": "Requirements",
|
"extensionHeader.requirements": "Requirements",
|
||||||
|
"extensionInstallation.addExtension": "In the editor, click on the \"Add Extensions\" button on the lower left.",
|
||||||
|
|
||||||
|
|
||||||
"oschooser.choose": "Choose your OS:",
|
"oschooser.choose": "Choose your OS:",
|
||||||
|
|
||||||
|
@ -172,6 +174,7 @@
|
||||||
"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.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.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.CommunityGuidelinesLinkText": "Scratch Community Guidelines",
|
||||||
|
@ -181,8 +184,11 @@
|
||||||
"report.reasonScary": "Too Violent or Scary",
|
"report.reasonScary": "Too Violent or Scary",
|
||||||
"report.reasonLanguage": "Inappropriate Language",
|
"report.reasonLanguage": "Inappropriate Language",
|
||||||
"report.reasonMusic": "Inappropriate Music",
|
"report.reasonMusic": "Inappropriate Music",
|
||||||
|
"report.reasonMissing": "Please select a reason",
|
||||||
"report.reasonImage": "Inappropriate Images",
|
"report.reasonImage": "Inappropriate Images",
|
||||||
"report.reasonPersonal": "Sharing Personal Contact Information",
|
"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.promptPlaceholder": "Select a reason why above.",
|
||||||
"report.promptCopy": "Please provide a link to the original project",
|
"report.promptCopy": "Please provide a link to the original project",
|
||||||
"report.promptUncredited": "Please provide links to the uncredited content",
|
"report.promptUncredited": "Please provide links to the uncredited content",
|
||||||
|
@ -194,5 +200,47 @@
|
||||||
"report.promptImage": "Please say the name of the sprite or the backdrop with the inappropriate image",
|
"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.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.tooShortError": "That's too short. Please describe in detail what's inappropriate or disrespectful about the project.",
|
||||||
"report.send": "Send"
|
"report.send": "Send",
|
||||||
|
"report.sending": "Sending...",
|
||||||
|
"report.textMissing": "Please tell us why you are reporting this project",
|
||||||
|
|
||||||
|
"comments.report": "Report",
|
||||||
|
"comments.delete": "Delete",
|
||||||
|
"comments.restore": "Restore",
|
||||||
|
"comments.reportModal.title": "Report Comment",
|
||||||
|
"comments.reportModal.reported": "The comment has been reported, and the Scratch Team has been notified.",
|
||||||
|
"comments.reportModal.prompt": "Are you sure you want to report this comment?",
|
||||||
|
"comments.deleteModal.title": "Delete Comment",
|
||||||
|
"comments.deleteModal.body": "Delete this comment? If the comment is mean or disrespectful, please click Report instead to let the Scratch Team know about it.",
|
||||||
|
"comments.reply": "reply",
|
||||||
|
"comments.isEmpty": "You can't post an empty comment",
|
||||||
|
"comments.isFlood": "Woah, seems like you're commenting really quickly. Please wait longer between posts.",
|
||||||
|
"comments.isBad": "Hmm...the bad word detector thinks there is a problem with your comment. Please change it and remember to be respectful.",
|
||||||
|
"comments.hasChatSite": "Uh oh! The comment contains a link to a website with unmoderated chat. For safety reasons, please do not link to these sites!",
|
||||||
|
"comments.isSpam": "Hmm, seems like you've posted the same comment a bunch of times. Please don't spam.",
|
||||||
|
"comments.isMuted": "Hmm, the filterbot is pretty sure your recent comments weren't ok for Scratch, so your account has been muted for the rest of the day. :/",
|
||||||
|
"comments.isUnconstructive": "Hmm, the filterbot thinks your comment may be mean or disrespectful. Remember, most projects on Scratch are made by people who are just learning how to program.",
|
||||||
|
"comments.isDisallowed": "Hmm, it looks like comments have been turned off for this page. :/",
|
||||||
|
"comments.isIPMuted": "Sorry, the Scratch Team had to prevent your network from sharing comments or projects because it was used to break our community guidelines too many times. You can still share comments and projects from another network.",
|
||||||
|
"comments.isTooLong": "That comment is too long! Please find a way to shorten your text.",
|
||||||
|
"comments.error": "Oops! Something went wrong posting your comment",
|
||||||
|
"comments.posting": "Posting...",
|
||||||
|
"comments.post": "Post",
|
||||||
|
"comments.cancel": "Cancel",
|
||||||
|
"comments.lengthWarning": "{remainingCharacters, plural, one {1 character left} other {{remainingCharacters} characters left}}",
|
||||||
|
"comments.seeMoreReplies": "{repliesCount, plural, one {See 1 more reply} other {See all {repliesCount} replies}}",
|
||||||
|
"comments.status.delbyusr": "Deleted by project owner",
|
||||||
|
"comments.status.censbyfilter": "Censored by filter",
|
||||||
|
"comments.status.delbyparentcomment": "Parent comment deleted",
|
||||||
|
"comments.status.censbyadmin": "Censored by admin",
|
||||||
|
"comments.status.delbyadmin": "Deleted by admin",
|
||||||
|
"comments.status.parentcommentcensored": "Parent comment censored",
|
||||||
|
"comments.status.delbyclass": "Deleted by class",
|
||||||
|
"comments.status.hiddenduetourl": "Hidden due to URL",
|
||||||
|
"comments.status.markedbyfilter": "Marked by filter",
|
||||||
|
"comments.status.censbyunconstructive": "Censored unconstructive",
|
||||||
|
"comments.status.suspended": "Suspended",
|
||||||
|
"comments.status.acctdel": "Account deleted",
|
||||||
|
"comments.status.deleted": "Deleted",
|
||||||
|
"comments.status.reported": "Reported"
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ const render = (jsx, element, reducers, initialState, enhancer) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const allReducers = reducer(reducers);
|
const allReducers = reducer(reducers);
|
||||||
|
|
||||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || redux.compose;
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || redux.compose;
|
||||||
const enhancers = enhancer ?
|
const enhancers = enhancer ?
|
||||||
composeEnhancers(
|
composeEnhancers(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import ScratchStorage from 'scratch-storage';
|
import ScratchStorage from 'scratch-storage';
|
||||||
|
|
||||||
const PROJECT_SERVER = 'https://projects.scratch.mit.edu';
|
const PROJECT_HOST = process.env.PROJECT_HOST || 'https://projects.scratch.mit.edu';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for ScratchStorage which adds default web sources.
|
* Wrapper for ScratchStorage which adds default web sources.
|
||||||
|
@ -14,8 +14,8 @@ class Storage extends ScratchStorage {
|
||||||
projectAsset => {
|
projectAsset => {
|
||||||
const [projectId, revision] = projectAsset.assetId.split('.');
|
const [projectId, revision] = projectAsset.assetId.split('.');
|
||||||
return revision ?
|
return revision ?
|
||||||
`${PROJECT_SERVER}/internalapi/project/${projectId}/get/${revision}` :
|
`${PROJECT_HOST}/internalapi/project/${projectId}/get/${revision}` :
|
||||||
`${PROJECT_SERVER}/internalapi/project/${projectId}/get/`;
|
`${PROJECT_HOST}/internalapi/project/${projectId}/get/`;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
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+/})
|
|
||||||
);
|
|
|
@ -1,22 +1,153 @@
|
||||||
const keyMirror = require('keymirror');
|
const keyMirror = require('keymirror');
|
||||||
|
const defaults = require('lodash.defaults');
|
||||||
|
|
||||||
|
const api = require('../lib/api');
|
||||||
|
const log = require('../lib/log.js');
|
||||||
|
const sessionActions = require('./session.js');
|
||||||
|
|
||||||
const Types = keyMirror({
|
const Types = keyMirror({
|
||||||
SET_SEARCH_TERM: null
|
SET_SEARCH_TERM: null,
|
||||||
|
SET_ACCOUNT_NAV_OPEN: null,
|
||||||
|
TOGGLE_ACCOUNT_NAV_OPEN: null,
|
||||||
|
SET_LOGIN_ERROR: null,
|
||||||
|
SET_LOGIN_OPEN: null,
|
||||||
|
TOGGLE_LOGIN_OPEN: null,
|
||||||
|
SET_CANCELED_DELETION_OPEN: null,
|
||||||
|
SET_REGISTRATION_OPEN: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports.getInitialState = () => ({
|
||||||
|
accountNavOpen: false,
|
||||||
|
canceledDeletionOpen: false,
|
||||||
|
loginError: null,
|
||||||
|
loginOpen: false,
|
||||||
|
registrationOpen: false,
|
||||||
|
searchTerm: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports.navigationReducer = (state, action) => {
|
module.exports.navigationReducer = (state, action) => {
|
||||||
if (typeof state === 'undefined') {
|
if (typeof state === 'undefined') {
|
||||||
state = '';
|
state = module.exports.getInitialState();
|
||||||
}
|
}
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case Types.SET_SEARCH_TERM:
|
case Types.SET_SEARCH_TERM:
|
||||||
return action.searchTerm;
|
return defaults({searchTerm: action.searchTerm}, state);
|
||||||
|
case Types.SET_ACCOUNT_NAV_OPEN:
|
||||||
|
return defaults({accountNavOpen: action.isOpen}, state);
|
||||||
|
case Types.TOGGLE_ACCOUNT_NAV_OPEN:
|
||||||
|
return defaults({accountNavOpen: !state.accountNavOpen}, state);
|
||||||
|
case Types.SET_LOGIN_ERROR:
|
||||||
|
return defaults({loginError: action.loginError}, state);
|
||||||
|
case Types.SET_LOGIN_OPEN:
|
||||||
|
return defaults({loginOpen: action.isOpen}, state);
|
||||||
|
case Types.TOGGLE_LOGIN_OPEN:
|
||||||
|
return defaults({loginOpen: !state.loginOpen}, state);
|
||||||
|
case Types.SET_CANCELED_DELETION_OPEN:
|
||||||
|
return defaults({canceledDeletionOpen: action.isOpen}, state);
|
||||||
|
case Types.SET_REGISTRATION_OPEN:
|
||||||
|
return defaults({registrationOpen: action.isOpen}, state);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.setAccountNavOpen = isOpen => ({
|
||||||
|
type: Types.SET_ACCOUNT_NAV_OPEN,
|
||||||
|
isOpen: isOpen
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.handleToggleAccountNav = () => ({
|
||||||
|
type: Types.TOGGLE_ACCOUNT_NAV_OPEN
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setCanceledDeletionOpen = isOpen => ({
|
||||||
|
type: Types.SET_CANCELED_DELETION_OPEN,
|
||||||
|
isOpen: isOpen
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setLoginError = loginError => ({
|
||||||
|
type: Types.SET_LOGIN_ERROR,
|
||||||
|
loginError: loginError
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setLoginOpen = isOpen => ({
|
||||||
|
type: Types.SET_LOGIN_OPEN,
|
||||||
|
isOpen: isOpen
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.toggleLoginOpen = () => ({
|
||||||
|
type: Types.TOGGLE_LOGIN_OPEN
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setRegistrationOpen = isOpen => ({
|
||||||
|
type: Types.SET_REGISTRATION_OPEN,
|
||||||
|
isOpen: isOpen
|
||||||
|
});
|
||||||
|
|
||||||
module.exports.setSearchTerm = searchTerm => ({
|
module.exports.setSearchTerm = searchTerm => ({
|
||||||
type: Types.SET_SEARCH_TERM,
|
type: Types.SET_SEARCH_TERM,
|
||||||
searchTerm: searchTerm
|
searchTerm: searchTerm
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports.handleCompleteRegistration = () => (dispatch => {
|
||||||
|
dispatch(sessionActions.refreshSession());
|
||||||
|
dispatch(module.exports.setRegistrationOpen(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.closeAccountMenus = () => (dispatch => {
|
||||||
|
dispatch(module.exports.setAccountNavOpen(false));
|
||||||
|
dispatch(module.exports.setRegistrationOpen(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.handleLogIn = (formData, callback) => (dispatch => {
|
||||||
|
dispatch(module.exports.setLoginError(null));
|
||||||
|
formData.useMessages = true; // NOTE: this may or may not be being used anywhere else
|
||||||
|
api({
|
||||||
|
method: 'post',
|
||||||
|
host: '',
|
||||||
|
uri: '/accounts/login/',
|
||||||
|
json: formData,
|
||||||
|
useCsrf: true
|
||||||
|
}, (err, body) => {
|
||||||
|
if (err) dispatch(module.exports.setLoginError(err.message));
|
||||||
|
if (body) {
|
||||||
|
body = body[0];
|
||||||
|
if (body.success) {
|
||||||
|
dispatch(module.exports.setLoginOpen(false));
|
||||||
|
body.messages.forEach(message => {
|
||||||
|
if (message.message === 'canceled-deletion') {
|
||||||
|
dispatch(module.exports.setCanceledDeletionOpen(true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dispatch(sessionActions.refreshSession());
|
||||||
|
callback({success: true});
|
||||||
|
} else {
|
||||||
|
if (body.redirect) {
|
||||||
|
window.location = body.redirect;
|
||||||
|
}
|
||||||
|
// Update login error message to a friendlier one if it exists
|
||||||
|
dispatch(module.exports.setLoginError(body.msg));
|
||||||
|
// JS error already logged by api mixin
|
||||||
|
callback({success: false});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// JS error already logged by api mixin
|
||||||
|
callback({success: false});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.handleLogOut = () => (dispatch => {
|
||||||
|
api({
|
||||||
|
host: '',
|
||||||
|
method: 'post',
|
||||||
|
uri: '/accounts/logout/',
|
||||||
|
useCsrf: true
|
||||||
|
}, err => {
|
||||||
|
if (err) log.error(err);
|
||||||
|
dispatch(module.exports.setLoginOpen(false));
|
||||||
|
dispatch(module.exports.setAccountNavOpen(false));
|
||||||
|
window.location = '/';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
@ -19,6 +22,7 @@ module.exports.getInitialState = () => ({
|
||||||
original: module.exports.Status.NOT_FETCHED,
|
original: module.exports.Status.NOT_FETCHED,
|
||||||
parent: module.exports.Status.NOT_FETCHED,
|
parent: module.exports.Status.NOT_FETCHED,
|
||||||
remixes: module.exports.Status.NOT_FETCHED,
|
remixes: module.exports.Status.NOT_FETCHED,
|
||||||
|
report: module.exports.Status.NOT_FETCHED,
|
||||||
projectStudios: module.exports.Status.NOT_FETCHED,
|
projectStudios: module.exports.Status.NOT_FETCHED,
|
||||||
curatedStudios: module.exports.Status.NOT_FETCHED,
|
curatedStudios: module.exports.Status.NOT_FETCHED,
|
||||||
studioRequests: {}
|
studioRequests: {}
|
||||||
|
@ -26,6 +30,7 @@ module.exports.getInitialState = () => ({
|
||||||
projectInfo: {},
|
projectInfo: {},
|
||||||
remixes: [],
|
remixes: [],
|
||||||
comments: [],
|
comments: [],
|
||||||
|
replies: {},
|
||||||
faved: false,
|
faved: false,
|
||||||
loved: false,
|
loved: false,
|
||||||
original: {},
|
original: {},
|
||||||
|
@ -79,7 +84,56 @@ module.exports.previewReducer = (state, action) => {
|
||||||
});
|
});
|
||||||
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 'UPDATE_COMMENT':
|
||||||
|
if (action.topLevelCommentId) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
replies: Object.assign({}, state.replies, {
|
||||||
|
[action.topLevelCommentId]: state.replies[action.topLevelCommentId].map(comment => {
|
||||||
|
if (comment.id === action.commentId) {
|
||||||
|
return Object.assign({}, comment, action.comment);
|
||||||
|
}
|
||||||
|
return comment;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
comments: state.comments.map(comment => {
|
||||||
|
if (comment.id === action.commentId) {
|
||||||
|
return Object.assign({}, comment, action.comment);
|
||||||
|
}
|
||||||
|
return comment;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
case 'ADD_NEW_COMMENT':
|
||||||
|
if (action.topLevelCommentId) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
replies: Object.assign({}, state.replies, {
|
||||||
|
// Replies to comments go at the end of the thread
|
||||||
|
[action.topLevelCommentId]: state.replies[action.topLevelCommentId].concat(action.comment)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reply to the top level project, put the reply at the beginning
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
comments: [action.comment, ...state.comments],
|
||||||
|
replies: Object.assign({}, state.replies, {[action.comment.id]: []})
|
||||||
|
});
|
||||||
|
case 'UPDATE_ALL_REPLIES':
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
replies: Object.assign({}, state.replies, {
|
||||||
|
[action.commentId]: state.replies[action.commentId].map(reply =>
|
||||||
|
Object.assign({}, reply, action.comment)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
case 'SET_REPLIES':
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
replies: merge({}, state.replies, action.replies)
|
||||||
});
|
});
|
||||||
case 'SET_LOVED':
|
case 'SET_LOVED':
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
|
@ -145,6 +199,16 @@ module.exports.setProjectStudios = items => ({
|
||||||
items: items
|
items: items
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports.setComments = items => ({
|
||||||
|
type: 'SET_COMMENTS',
|
||||||
|
items: items
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setReplies = replies => ({
|
||||||
|
type: 'SET_REPLIES',
|
||||||
|
replies: replies
|
||||||
|
});
|
||||||
|
|
||||||
module.exports.setCuratedStudios = items => ({
|
module.exports.setCuratedStudios = items => ({
|
||||||
type: 'SET_CURATED_STUDIOS',
|
type: 'SET_CURATED_STUDIOS',
|
||||||
items: items
|
items: items
|
||||||
|
@ -172,6 +236,55 @@ module.exports.setStudioFetchStatus = (studioId, status) => ({
|
||||||
status: status
|
status: status
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports.setCommentDeleted = (commentId, topLevelCommentId) => ({
|
||||||
|
type: 'UPDATE_COMMENT',
|
||||||
|
commentId: commentId,
|
||||||
|
topLevelCommentId: topLevelCommentId,
|
||||||
|
comment: {
|
||||||
|
visibility: 'deleted'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setRepliesDeleted = commentId => ({
|
||||||
|
type: 'UPDATE_ALL_REPLIES',
|
||||||
|
commentId: commentId,
|
||||||
|
comment: {
|
||||||
|
visibility: 'deleted'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setCommentReported = (commentId, topLevelCommentId) => ({
|
||||||
|
type: 'UPDATE_COMMENT',
|
||||||
|
commentId: commentId,
|
||||||
|
topLevelCommentId: topLevelCommentId,
|
||||||
|
comment: {
|
||||||
|
visibility: 'reported'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setCommentRestored = (commentId, topLevelCommentId) => ({
|
||||||
|
type: 'UPDATE_COMMENT',
|
||||||
|
commentId: commentId,
|
||||||
|
topLevelCommentId: topLevelCommentId,
|
||||||
|
comment: {
|
||||||
|
visibility: 'visible'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setRepliesRestored = commentId => ({
|
||||||
|
type: 'UPDATE_ALL_REPLIES',
|
||||||
|
commentId: commentId,
|
||||||
|
comment: {
|
||||||
|
visibility: 'visible'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.addNewComment = (comment, topLevelCommentId) => ({
|
||||||
|
type: 'ADD_NEW_COMMENT',
|
||||||
|
comment: comment,
|
||||||
|
topLevelCommentId: topLevelCommentId
|
||||||
|
});
|
||||||
|
|
||||||
module.exports.getProjectInfo = (id, token) => (dispatch => {
|
module.exports.getProjectInfo = (id, token) => (dispatch => {
|
||||||
const opts = {
|
const opts = {
|
||||||
uri: `/projects/${id}`
|
uri: `/projects/${id}`
|
||||||
|
@ -257,7 +370,59 @@ module.exports.getFavedStatus = (id, username, token) => (dispatch => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports.getTopLevelComments = (id, offset, isAdmin, token) => (dispatch => {
|
||||||
|
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHING));
|
||||||
|
api({
|
||||||
|
uri: `${isAdmin ? '/admin' : ''}/comments/project/${id}`,
|
||||||
|
authentication: isAdmin ? token : null,
|
||||||
|
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), isAdmin, token));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.getReplies = (projectId, commentIds, isAdmin, token) => (dispatch => {
|
||||||
|
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.FETCHING));
|
||||||
|
const fetchedReplies = {};
|
||||||
|
async.eachLimit(commentIds, 10, (parentId, callback) => {
|
||||||
|
api({
|
||||||
|
uri: `${isAdmin ? '/admin' : ''}/comments/project/${projectId}/${parentId}`,
|
||||||
|
authentication: isAdmin ? token : null
|
||||||
|
}, (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 => {
|
module.exports.setFavedStatus = (faved, id, username, token) => (dispatch => {
|
||||||
|
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHING));
|
||||||
if (faved) {
|
if (faved) {
|
||||||
api({
|
api({
|
||||||
uri: `/projects/${id}/favorites/user/${username}`,
|
uri: `/projects/${id}/favorites/user/${username}`,
|
||||||
|
@ -317,6 +482,7 @@ module.exports.getLovedStatus = (id, username, token) => (dispatch => {
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.setLovedStatus = (loved, id, username, token) => (dispatch => {
|
module.exports.setLovedStatus = (loved, id, username, token) => (dispatch => {
|
||||||
|
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.FETCHING));
|
||||||
if (loved) {
|
if (loved) {
|
||||||
api({
|
api({
|
||||||
uri: `/projects/${id}/loves/user/${username}`,
|
uri: `/projects/${id}/loves/user/${username}`,
|
||||||
|
@ -465,6 +631,7 @@ module.exports.leaveStudio = (studioId, projectId, token) => (dispatch => {
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
|
module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
|
||||||
|
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING));
|
||||||
api({
|
api({
|
||||||
uri: `/projects/${id}`,
|
uri: `/projects/${id}`,
|
||||||
authentication: token,
|
authentication: token,
|
||||||
|
@ -490,3 +657,84 @@ module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
|
||||||
dispatch(module.exports.setProjectInfo(body));
|
dispatch(module.exports.setProjectInfo(body));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports.deleteComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
|
||||||
|
/* TODO fetching/fetched/error states updates for comment deleting */
|
||||||
|
api({
|
||||||
|
uri: `/proxy/comments/project/${projectId}/comment/${commentId}`,
|
||||||
|
authentication: token,
|
||||||
|
withCredentials: true,
|
||||||
|
method: 'DELETE',
|
||||||
|
useCsrf: true
|
||||||
|
}, (err, body, res) => {
|
||||||
|
if (err || res.statusCode !== 200) {
|
||||||
|
log.error(err || res.body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(module.exports.setCommentDeleted(commentId, topLevelCommentId));
|
||||||
|
if (!topLevelCommentId) {
|
||||||
|
dispatch(module.exports.setRepliesDeleted(commentId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.reportComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
|
||||||
|
api({
|
||||||
|
uri: `/proxy/project/${projectId}/comment/${commentId}/report`,
|
||||||
|
authentication: token,
|
||||||
|
withCredentials: true,
|
||||||
|
method: 'POST',
|
||||||
|
useCsrf: true
|
||||||
|
}, (err, body, res) => {
|
||||||
|
if (err || res.statusCode !== 200) {
|
||||||
|
log.error(err || res.body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO use the reportId in the response for unreporting functionality
|
||||||
|
dispatch(module.exports.setCommentReported(commentId, topLevelCommentId));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.restoreComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
|
||||||
|
api({
|
||||||
|
uri: `/proxy/admin/project/${projectId}/comment/${commentId}/undelete`,
|
||||||
|
authentication: token,
|
||||||
|
withCredentials: true,
|
||||||
|
method: 'PUT',
|
||||||
|
useCsrf: true
|
||||||
|
}, (err, body, res) => {
|
||||||
|
if (err || res.statusCode !== 200) {
|
||||||
|
log.error(err || res.body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(module.exports.setCommentRestored(commentId, topLevelCommentId));
|
||||||
|
if (!topLevelCommentId) {
|
||||||
|
dispatch(module.exports.setRepliesRestored(commentId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.reportProject = (id, jsonData, token) => (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({
|
||||||
|
uri: `/proxy/projects/${id}/report`,
|
||||||
|
authentication: token,
|
||||||
|
withCredentials: true,
|
||||||
|
method: 'POST',
|
||||||
|
useCsrf: true,
|
||||||
|
json: jsonData
|
||||||
|
}, (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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ const defaults = require('lodash.defaults');
|
||||||
const messageCountReducer = require('./message-count.js').messageCountReducer;
|
const messageCountReducer = require('./message-count.js').messageCountReducer;
|
||||||
const permissionsReducer = require('./permissions.js').permissionsReducer;
|
const permissionsReducer = require('./permissions.js').permissionsReducer;
|
||||||
const sessionReducer = require('./session.js').sessionReducer;
|
const sessionReducer = require('./session.js').sessionReducer;
|
||||||
|
const navigationReducer = require('./navigation.js').navigationReducer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a combined reducer to be used for a page in `render.jsx`.
|
* Returns a combined reducer to be used for a page in `render.jsx`.
|
||||||
|
@ -18,8 +19,9 @@ const sessionReducer = require('./session.js').sessionReducer;
|
||||||
module.exports = opts => {
|
module.exports = opts => {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
return combineReducers(defaults(opts, {
|
return combineReducers(defaults(opts, {
|
||||||
session: sessionReducer,
|
messageCount: messageCountReducer,
|
||||||
|
navigation: navigationReducer,
|
||||||
permissions: permissionsReducer,
|
permissions: permissionsReducer,
|
||||||
messageCount: messageCountReducer
|
session: sessionReducer
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
|
@ -77,13 +77,13 @@ module.exports.getActivity = (username, token) => (dispatch => {
|
||||||
api({
|
api({
|
||||||
uri: `/users/${username}/following/users/activity?limit=5`,
|
uri: `/users/${username}/following/users/activity?limit=5`,
|
||||||
authentication: token
|
authentication: token
|
||||||
}, (err, body) => {
|
}, (err, body, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError(err));
|
dispatch(module.exports.setError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof body === 'undefined') {
|
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||||
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError('No session content'));
|
dispatch(module.exports.setError('No session content'));
|
||||||
return;
|
return;
|
||||||
|
@ -100,13 +100,13 @@ module.exports.getFeaturedGlobal = () => (dispatch => {
|
||||||
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.FETCHING));
|
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.FETCHING));
|
||||||
api({
|
api({
|
||||||
uri: '/proxy/featured'
|
uri: '/proxy/featured'
|
||||||
}, (err, body) => {
|
}, (err, body, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError(err));
|
dispatch(module.exports.setError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof body === 'undefined') {
|
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||||
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError('No session content'));
|
dispatch(module.exports.setError('No session content'));
|
||||||
return;
|
return;
|
||||||
|
@ -126,13 +126,13 @@ module.exports.getSharedByFollowing = (username, token) => (dispatch => {
|
||||||
api({
|
api({
|
||||||
uri: `/users/${username}/following/users/projects`,
|
uri: `/users/${username}/following/users/projects`,
|
||||||
authentication: token
|
authentication: token
|
||||||
}, (err, body) => {
|
}, (err, body, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.Status.ERROR));
|
||||||
dispatch(module.exports.setError(err));
|
dispatch(module.exports.setError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof body === 'undefined') {
|
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||||
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError('No session content'));
|
dispatch(module.exports.setError('No session content'));
|
||||||
return;
|
return;
|
||||||
|
@ -152,13 +152,13 @@ module.exports.getInStudiosFollowing = (username, token) => (dispatch => {
|
||||||
api({
|
api({
|
||||||
uri: `/users/${username}/following/studios/projects`,
|
uri: `/users/${username}/following/studios/projects`,
|
||||||
authentication: token
|
authentication: token
|
||||||
}, (err, body) => {
|
}, (err, body, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError(err));
|
dispatch(module.exports.setError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof body === 'undefined') {
|
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError('No session content'));
|
dispatch(module.exports.setError('No session content'));
|
||||||
return;
|
return;
|
||||||
|
@ -178,13 +178,13 @@ module.exports.getLovedByFollowing = (username, token) => (dispatch => {
|
||||||
api({
|
api({
|
||||||
uri: `/users/${username}/following/users/loves`,
|
uri: `/users/${username}/following/users/loves`,
|
||||||
authentication: token
|
authentication: token
|
||||||
}, (err, body) => {
|
}, (err, body, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError(err));
|
dispatch(module.exports.setError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof body === 'undefined') {
|
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||||
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError('No session content'));
|
dispatch(module.exports.setError('No session content'));
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -308,6 +308,13 @@
|
||||||
"view": "wedo2/wedo2",
|
"view": "wedo2/wedo2",
|
||||||
"title": "LEGO WeDo 2.0"
|
"title": "LEGO WeDo 2.0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "wedo-legacy",
|
||||||
|
"pattern": "^/wedo-legacy/?$",
|
||||||
|
"routeAlias": "/wedo-legacy/?$",
|
||||||
|
"view": "wedo2-legacy/wedo2",
|
||||||
|
"title": "LEGO WeDo"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ev3",
|
"name": "ev3",
|
||||||
"pattern": "^/ev3/?$",
|
"pattern": "^/ev3/?$",
|
||||||
|
|
|
@ -35,8 +35,10 @@ const Components = () => (
|
||||||
<Box title="Carousel component in a box!">
|
<Box title="Carousel component in a box!">
|
||||||
<Carousel />
|
<Carousel />
|
||||||
</Box>
|
</Box>
|
||||||
<h1>This is a Spinner</h1>
|
<h1>This is a blue Spinner</h1>
|
||||||
<Spinner />
|
<Spinner
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
<h1>Colors</h1>
|
<h1>Colors</h1>
|
||||||
<div className="colors">
|
<div className="colors">
|
||||||
<span className="ui-blue">$ui-blue</span>
|
<span className="ui-blue">$ui-blue</span>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
@import "../../../../frameless";
|
@import "../../../../frameless";
|
||||||
|
|
||||||
#view {
|
#view {
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
margin-top: 100px;
|
margin-top: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
//8 columns
|
//8 columns
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.details {
|
.details {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
margin-top: 1.2rem;
|
margin-top: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
img {
|
img {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.uneven {
|
.uneven {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -80,7 +80,7 @@
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -164,7 +164,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $desktop - 1) {
|
@media #{$medium-and-intermediate} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
table {
|
table {
|
||||||
width: $cols6;
|
width: $cols6;
|
||||||
|
@ -172,7 +172,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
h3 {
|
h3 {
|
||||||
display: none;
|
display: none;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
max-width: 125px;
|
max-width: 125px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
margin: .5rem;
|
margin: .5rem;
|
||||||
width: 125px;
|
width: 125px;
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
.index {
|
.index {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -19,13 +19,13 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
img {
|
img {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
img {
|
img {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
.lodging {
|
.lodging {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.uneven {
|
.uneven {
|
||||||
.short {
|
.short {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
}
|
}
|
||||||
|
@ -69,13 +69,13 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
ul {
|
ul {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
div {
|
div {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.sub-nav {
|
.sub-nav {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
.inner {
|
.inner {
|
||||||
h2 {
|
h2 {
|
||||||
&.breaking-title {
|
&.breaking-title {
|
||||||
|
|
|
@ -79,7 +79,7 @@ td {
|
||||||
color: $type-white;
|
color: $type-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.index.mod-2017 {
|
.index.mod-2017 {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@ td {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
.index.mod-2017 {
|
.index.mod-2017 {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
@ -159,7 +159,7 @@ td {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
.index.mod-2017 {
|
.index.mod-2017 {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
@import "../../../../frameless";
|
@import "../../../../frameless";
|
||||||
|
|
||||||
#view {
|
#view {
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
margin-top: 100px;
|
margin-top: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
//8 columns
|
//8 columns
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.details {
|
.details {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
margin-top: 1.2rem;
|
margin-top: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
img {
|
img {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.uneven {
|
.uneven {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -163,7 +163,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -171,7 +171,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $desktop - 1) {
|
@media #{$medium-and-intermediate} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
table {
|
table {
|
||||||
width: $cols6;
|
width: $cols6;
|
||||||
|
@ -179,7 +179,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
h3 {
|
h3 {
|
||||||
display: none;
|
display: none;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
max-width: 125px;
|
max-width: 125px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
margin: .5rem;
|
margin: .5rem;
|
||||||
width: 125px;
|
width: 125px;
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
.index {
|
.index {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -19,13 +19,13 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
img {
|
img {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
img {
|
img {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
.lodging {
|
.lodging {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.uneven {
|
.uneven {
|
||||||
.short {
|
.short {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.flex-row {
|
.flex-row {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
}
|
}
|
||||||
|
@ -69,13 +69,13 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
ul {
|
ul {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
div {
|
div {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.sub-nav {
|
.sub-nav {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1) {
|
@media #{$medium-and-smaller} {
|
||||||
.inner {
|
.inner {
|
||||||
h2 {
|
h2 {
|
||||||
&.breaking-title {
|
&.breaking-title {
|
||||||
|
|
|
@ -29,7 +29,7 @@ const Credits = () => (
|
||||||
/>
|
/>
|
||||||
<span className="name">Carl Bowman</span>
|
<span className="name">Carl Bowman</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<img
|
<img
|
||||||
alt="Karishma Avatar"
|
alt="Karishma Avatar"
|
||||||
|
@ -86,6 +86,14 @@ const Credits = () => (
|
||||||
<span className="name">DD Liu</span>
|
<span className="name">DD Liu</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<img
|
||||||
|
alt="Katelyn Avatar"
|
||||||
|
src="//cdn.scratch.mit.edu/get_image/user/34607790_170x170.png"
|
||||||
|
/>
|
||||||
|
<span className="name">Katelyn Mann</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<img
|
<img
|
||||||
alt="Shruti Avatar"
|
alt="Shruti Avatar"
|
||||||
|
@ -446,6 +454,6 @@ const Credits = () => (
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
render(<Page><Credits /></Page>, document.getElementById('app'));
|
render(<Page><Credits /></Page>, document.getElementById('app'));
|
||||||
|
|
|
@ -170,7 +170,7 @@ $developer-spot: $ui-aqua;
|
||||||
}
|
}
|
||||||
|
|
||||||
//4 columns
|
//4 columns
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
#view {
|
#view {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
@ -196,7 +196,7 @@ $developer-spot: $ui-aqua;
|
||||||
}
|
}
|
||||||
|
|
||||||
//6 columns
|
//6 columns
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
#view {
|
#view {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
@ -216,7 +216,7 @@ $developer-spot: $ui-aqua;
|
||||||
}
|
}
|
||||||
|
|
||||||
//8 columns
|
//8 columns
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
#view {
|
#view {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.inner {
|
.inner {
|
||||||
.installation-column {
|
.installation-column {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -119,7 +119,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $desktop - 1) {
|
@media #{$intermediate-and-smaller} {
|
||||||
.three-col-row {
|
.three-col-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -31,19 +31,25 @@ class EV3 extends ExtensionLanding {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className="extension-landing ev3">
|
<div className="extension-landing ev3">
|
||||||
<ExtensionHeader imageSrc="/images/ev3/ev3-illustration.png">
|
<ExtensionHeader
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltEv3Illustration'})}
|
||||||
|
imageSrc="/images/ev3/ev3-illustration.png"
|
||||||
|
>
|
||||||
<FlexRow className="column extension-copy">
|
<FlexRow className="column extension-copy">
|
||||||
<h2><img src="/images/ev3/ev3.svg" />LEGO MINDSTORMS EV3</h2>
|
<h1><img
|
||||||
|
alt=""
|
||||||
|
src="/images/ev3/ev3.svg"
|
||||||
|
/>LEGO MINDSTORMS EV3</h1>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="ev3.headerText"
|
id="ev3.headerText"
|
||||||
values={{
|
values={{
|
||||||
ev3Link: (
|
ev3Link: (
|
||||||
<a
|
<a
|
||||||
href="https://shop.lego.com/en-US/LEGO-MINDSTORMS-EV3-31313"
|
href="https://education.lego.com/en-us/middle-school/intro/mindstorms-ev3"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
LEGO MINDSTORMS EV3
|
LEGO MINDSTORMS Education EV3
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
@ -51,11 +57,17 @@ class EV3 extends ExtensionLanding {
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
<ExtensionRequirements>
|
<ExtensionRequirements>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/windows.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/windows.svg"
|
||||||
|
/>
|
||||||
Windows 10+
|
Windows 10+
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/mac.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/mac.svg"
|
||||||
|
/>
|
||||||
macOS 10.13+
|
macOS 10.13+
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
|
@ -63,7 +75,10 @@ class EV3 extends ExtensionLanding {
|
||||||
Bluetooth
|
Bluetooth
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/scratch-link.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/scratch-link.svg"
|
||||||
|
/>
|
||||||
Scratch Link
|
Scratch Link
|
||||||
</span>
|
</span>
|
||||||
</ExtensionRequirements>
|
</ExtensionRequirements>
|
||||||
|
@ -82,13 +97,20 @@ class EV3 extends ExtensionLanding {
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step number={1}>
|
<Step number={1}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/ev3-connect-1.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/ev3/ev3-connect-1.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p><FormattedMessage id="ev3.turnOnEV3" /></p>
|
<p><FormattedMessage id="ev3.turnOnEV3" /></p>
|
||||||
</Step>
|
</Step>
|
||||||
<Step number={2}>
|
<Step number={2}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/ev3-connect-2.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
className="screenshot"
|
||||||
|
src="/images/ev3/ev3-connect-2.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -109,7 +131,11 @@ class EV3 extends ExtensionLanding {
|
||||||
</Step>
|
</Step>
|
||||||
<Step number={3}>
|
<Step number={3}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/ev3-connect-3.png" />
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'extensionInstallation.addExtension'})}
|
||||||
|
className="screenshot"
|
||||||
|
src="/images/ev3/ev3-connect-3.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p><FormattedMessage id="ev3.addExtension" /></p>
|
<p><FormattedMessage id="ev3.addExtension" /></p>
|
||||||
</Step>
|
</Step>
|
||||||
|
@ -119,19 +145,30 @@ class EV3 extends ExtensionLanding {
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step>
|
<Step>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/ev3-accept-connection.png" />
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'ev3.imgAltAcceptConnection'})}
|
||||||
|
src="/images/ev3/ev3-accept-connection.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p><FormattedMessage id="ev3.acceptConnection" /></p>
|
<p><FormattedMessage id="ev3.acceptConnection" /></p>
|
||||||
</Step>
|
</Step>
|
||||||
<Step>
|
<Step>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/ev3-pin.png" />
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'ev3.imgAltAcceptPasscode'})}
|
||||||
|
src="/images/ev3/ev3-pin.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p><FormattedMessage id="ev3.acceptPasscode" /></p>
|
<p><FormattedMessage id="ev3.acceptPasscode" /></p>
|
||||||
</Step>
|
</Step>
|
||||||
<Step>
|
<Step>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: `ev3.imgAlt${
|
||||||
|
this.state.OS === OS_ENUM.WINDOWS ?
|
||||||
|
'WaitForWindows' :
|
||||||
|
'EnterPasscodeMac'
|
||||||
|
}`})}
|
||||||
className="screenshot"
|
className="screenshot"
|
||||||
src={`/images/ev3/${
|
src={`/images/ev3/${
|
||||||
this.state.OS === OS_ENUM.WINDOWS ?
|
this.state.OS === OS_ENUM.WINDOWS ?
|
||||||
|
@ -170,7 +207,10 @@ class EV3 extends ExtensionLanding {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/ev3-motor-port-a.png" />
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'ev3.imgAltPlugInMotor'})}
|
||||||
|
src="/images/ev3/ev3-motor-port-a.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Step>
|
</Step>
|
||||||
<Step
|
<Step
|
||||||
|
@ -188,7 +228,10 @@ class EV3 extends ExtensionLanding {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/motor-turn-block.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/ev3/motor-turn-block.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
@ -196,20 +239,23 @@ class EV3 extends ExtensionLanding {
|
||||||
<h3><FormattedMessage id="ev3.starterProjects" /></h3>
|
<h3><FormattedMessage id="ev3.starterProjects" /></h3>
|
||||||
<Steps>
|
<Steps>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://downloads.scratch.mit.edu/ev3/ev3-wave-hello.sb3"
|
cardUrl="https://beta.scratch.mit.edu/#239075992"
|
||||||
description={this.props.intl.formatMessage({id: 'ev3.waveHelloDescription'})}
|
description={this.props.intl.formatMessage({id: 'ev3.waveHelloDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltWaveHello'})}
|
||||||
imageSrc="/images/ev3/starter-wave-hello.png"
|
imageSrc="/images/ev3/starter-wave-hello.png"
|
||||||
title={this.props.intl.formatMessage({id: 'ev3.waveHelloTitle'})}
|
title={this.props.intl.formatMessage({id: 'ev3.waveHelloTitle'})}
|
||||||
/>
|
/>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://downloads.scratch.mit.edu/ev3/ev3-distance-instrument.sb3"
|
cardUrl="https://beta.scratch.mit.edu/#239076020"
|
||||||
description={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentDescription'})}
|
description={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltDistanceInstrument'})}
|
||||||
imageSrc="/images/ev3/starter-distance-instrument.png"
|
imageSrc="/images/ev3/starter-distance-instrument.png"
|
||||||
title={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentTitle'})}
|
title={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentTitle'})}
|
||||||
/>
|
/>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://downloads.scratch.mit.edu/ev3/ev3-space-tacos.sb3"
|
cardUrl="https://beta.scratch.mit.edu/#239076044"
|
||||||
description={this.props.intl.formatMessage({id: 'ev3.spaceTacosDescription'})}
|
description={this.props.intl.formatMessage({id: 'ev3.spaceTacosDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltSpaceTacos'})}
|
||||||
imageSrc="/images/ev3/starter-flying-game.png"
|
imageSrc="/images/ev3/starter-flying-game.png"
|
||||||
title={this.props.intl.formatMessage({id: 'ev3.spaceTacosTitle'})}
|
title={this.props.intl.formatMessage({id: 'ev3.spaceTacosTitle'})}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
.ev3 {
|
.ev3 {
|
||||||
.extension-header {
|
.extension-header {
|
||||||
background-color: $ui-aqua;
|
background-color: $ui-orange;
|
||||||
background-image: url("/images/ev3/ev3-pattern.svg");
|
background-image: url("/images/ev3/ev3-pattern.svg");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,15 @@
|
||||||
"ev3.otherComputerConnectedTitle": "Make sure no other computer is connected to your EV3",
|
"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.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.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.updateFirmwareText": "We recommend updating to EV3 firmware version 1.10E or above. See {firmwareUpdateLink}.",
|
||||||
"ev3.firmwareUpdateText": "firmware update instructions from LEGO"
|
"ev3.firmwareUpdateText": "firmware update instructions from LEGO",
|
||||||
|
"ev3.imgAltEv3Illustration": "Illustration of an EV3 hub, featuring some examples of interacting with it.",
|
||||||
|
"ev3.imgAltAcceptConnection": "Use the buttons on your EV3 to accept the connection.",
|
||||||
|
"ev3.imgAltAcceptPasscode": "Use the center button on your EV3 to accept the passcode.",
|
||||||
|
"ev3.imgAltWaitForWindows": "Windows will notify you when the EV3 is ready.",
|
||||||
|
"ev3.imgAltEnterPasscodeMac": "Enter the passcode into the connection request window opening on your Mac.",
|
||||||
|
"ev3.imgAltPlugInMotor": "To find port A: hold the EV3 with the screen and buttons facing you, with the screen above the buttons. Port A is on top, and it is the left-most one",
|
||||||
|
"ev3.imgAltWaveHello": "A Scratch project with a waving fairy.",
|
||||||
|
"ev3.imgAltDistanceInstrument": "A Scratch project with a guitar.",
|
||||||
|
"ev3.imgAltSpaceTacos": "A Scratch project with Scratch Cat and a taco in space."
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@ $base-bg: $ui-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
//4 columns
|
//4 columns
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.outer {
|
.outer {
|
||||||
.tabs {
|
.tabs {
|
||||||
width: $cols4;
|
width: $cols4;
|
||||||
|
@ -139,7 +139,7 @@ $base-bg: $ui-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
//6 columns
|
//6 columns
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
.outer {
|
.outer {
|
||||||
.tabs {
|
.tabs {
|
||||||
width: $cols6;
|
width: $cols6;
|
||||||
|
@ -158,7 +158,7 @@ $base-bg: $ui-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8 columns
|
// 8 columns
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
.outer {
|
.outer {
|
||||||
.tabs {
|
.tabs {
|
||||||
width: $cols8;
|
width: $cols8;
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $tablet - 1){
|
@media #{$medium-and-smaller}{
|
||||||
.guidelines-footer {
|
.guidelines-footer {
|
||||||
img {
|
img {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -35,14 +35,6 @@ const Jobs = () => (
|
||||||
MIT Media Lab, Cambridge, MA
|
MIT Media Lab, Cambridge, MA
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href="https://scratch.mit.edu/jobs/moderator">
|
|
||||||
Community Moderator
|
|
||||||
</a>
|
|
||||||
<span>
|
|
||||||
Remote
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -129,7 +129,7 @@
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $mobile - 1) {
|
@media #{$small} {
|
||||||
.flex-row.admin-message-header,
|
.flex-row.admin-message-header,
|
||||||
.flex-row.mod-comment-message {
|
.flex-row.mod-comment-message {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -144,7 +144,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
@media #{$medium} {
|
||||||
.flex-row.admin-message-header,
|
.flex-row.admin-message-header,
|
||||||
.flex-row.mod-comment-message {
|
.flex-row.mod-comment-message {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -159,7 +159,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
@media #{$intermediate} {
|
||||||
.comment-text {
|
.comment-text {
|
||||||
max-width: 23.75rem;
|
max-width: 23.75rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,5 +28,11 @@
|
||||||
"microbit.otherComputerConnectedTitle": "Make sure no other computer is connected to your micro:bit",
|
"microbit.otherComputerConnectedTitle": "Make sure no other computer is connected to your micro:bit",
|
||||||
"microbit.otherComputerConnectedText": "Only one computer can be connected to an micro:bit at a time. If you have another computer connected to your micro:bit, disconnect the micro:bit or close Scratch on that computer and try again.",
|
"microbit.otherComputerConnectedText": "Only one computer can be connected to an micro:bit at a time. If you have another computer connected to your micro:bit, disconnect the micro:bit or close Scratch on that computer and try again.",
|
||||||
"microbit.resetButtonTitle": "Make sure you aren’t hitting the “reset” button",
|
"microbit.resetButtonTitle": "Make sure you aren’t hitting the “reset” button",
|
||||||
"microbit.resetButtonText": "Sometimes while using the micro:bit you can accidentally press the “reset” button on the back in-between the USB and power ports. Make sure you keep your fingers (and toes) away from it while using Scratch!"
|
"microbit.resetButtonText": "Sometimes while using the micro:bit you can accidentally press the “reset” button on the back in-between the USB and power ports. Make sure you keep your fingers (and toes) away from it while using Scratch!",
|
||||||
|
"microbit.imgAltMicrobitIllustration": "Illustration of the micro:bit circuit board.",
|
||||||
|
"microbit.imgAltDragDropHex": "Drag and drop the HEX file from the folder you downloaded it to to the micro:bit.",
|
||||||
|
"microbit.imgAltDisplayH": "A micro:bit displaying an H.",
|
||||||
|
"microbit.imgAltHeartBeat" : "A Scratch project with a heart.",
|
||||||
|
"microbit.imgAltTiltGuitar": "A Scratch project with a guitar.",
|
||||||
|
"microbit.imgAltOceanAdventure": "A Scratch project with a clown fish and a saxophone under water."
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,9 +31,15 @@ class MicroBit extends ExtensionLanding {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className="extension-landing microbit">
|
<div className="extension-landing microbit">
|
||||||
<ExtensionHeader imageSrc="/images/microbit/microbit-heart.png">
|
<ExtensionHeader
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltMicrobitIllustration'})}
|
||||||
|
imageSrc="/images/microbit/microbit-heart.png"
|
||||||
|
>
|
||||||
<FlexRow className="column extension-copy">
|
<FlexRow className="column extension-copy">
|
||||||
<h2><img src="/images/microbit/microbit.svg" />micro:bit</h2>
|
<h1><img
|
||||||
|
alt=""
|
||||||
|
src="/images/microbit/microbit.svg"
|
||||||
|
/>micro:bit</h1>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="microbit.headerText"
|
id="microbit.headerText"
|
||||||
values={{
|
values={{
|
||||||
|
@ -51,19 +57,31 @@ class MicroBit extends ExtensionLanding {
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
<ExtensionRequirements>
|
<ExtensionRequirements>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/windows.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/windows.svg"
|
||||||
|
/>
|
||||||
Windows 10+
|
Windows 10+
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/mac.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/mac.svg"
|
||||||
|
/>
|
||||||
macOS 10.13+
|
macOS 10.13+
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/bluetooth.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/bluetooth.svg"
|
||||||
|
/>
|
||||||
Bluetooth 4.0
|
Bluetooth 4.0
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/scratch-link.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/scratch-link.svg"
|
||||||
|
/>
|
||||||
Scratch Link
|
Scratch Link
|
||||||
</span>
|
</span>
|
||||||
</ExtensionRequirements>
|
</ExtensionRequirements>
|
||||||
|
@ -82,7 +100,10 @@ class MicroBit extends ExtensionLanding {
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step number={1}>
|
<Step number={1}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/mbit-usb.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/microbit/mbit-usb.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage id="microbit.connectUSB" />
|
<FormattedMessage id="microbit.connectUSB" />
|
||||||
|
@ -90,7 +111,10 @@ class MicroBit extends ExtensionLanding {
|
||||||
</Step>
|
</Step>
|
||||||
<Step number={2}>
|
<Step number={2}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/mbit-hex-download.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/microbit/mbit-hex-download.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
download
|
download
|
||||||
|
@ -103,6 +127,7 @@ class MicroBit extends ExtensionLanding {
|
||||||
<Step number={3}>
|
<Step number={3}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'microbit.imgAltDragDropHex'})}
|
||||||
src={`/images/microbit/${
|
src={`/images/microbit/${
|
||||||
this.state.OS === OS_ENUM.WINDOWS ? 'win' : 'mac'
|
this.state.OS === OS_ENUM.WINDOWS ? 'win' : 'mac'
|
||||||
}-copy-hex.png`}
|
}-copy-hex.png`}
|
||||||
|
@ -120,13 +145,20 @@ class MicroBit extends ExtensionLanding {
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step number={1}>
|
<Step number={1}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/mbit-connect-1.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/microbit/mbit-connect-1.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p><FormattedMessage id="microbit.powerMicrobit" /></p>
|
<p><FormattedMessage id="microbit.powerMicrobit" /></p>
|
||||||
</Step>
|
</Step>
|
||||||
<Step number={2}>
|
<Step number={2}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/mbit-connect-2.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
className="screenshot"
|
||||||
|
src="/images/microbit/mbit-connect-2.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -147,7 +179,11 @@ class MicroBit extends ExtensionLanding {
|
||||||
</Step>
|
</Step>
|
||||||
<Step number={3}>
|
<Step number={3}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/mbit-connect-3.png" />
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'extensionInstallation.addExtension'})}
|
||||||
|
className="screenshot"
|
||||||
|
src="/images/microbit/mbit-connect-3.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p><FormattedMessage id="microbit.addExtension" /></p>
|
<p><FormattedMessage id="microbit.addExtension" /></p>
|
||||||
</Step>
|
</Step>
|
||||||
|
@ -175,7 +211,10 @@ class MicroBit extends ExtensionLanding {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/display-hello-block.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/microbit/display-hello-block.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Step>
|
</Step>
|
||||||
<Step
|
<Step
|
||||||
|
@ -193,7 +232,10 @@ class MicroBit extends ExtensionLanding {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/mbit-display-h.png" />
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'microbit.imgAltDisplayH'})}
|
||||||
|
src="/images/microbit/mbit-display-h.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
@ -201,20 +243,23 @@ class MicroBit extends ExtensionLanding {
|
||||||
<h3><FormattedMessage id="microbit.starterProjects" /></h3>
|
<h3><FormattedMessage id="microbit.starterProjects" /></h3>
|
||||||
<Steps>
|
<Steps>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://downloads.scratch.mit.edu/microbit/microbit-heartbeat.sb3"
|
cardUrl="https://beta.scratch.mit.edu/#239075756"
|
||||||
description={this.props.intl.formatMessage({id: 'microbit.heartBeatDescription'})}
|
description={this.props.intl.formatMessage({id: 'microbit.heartBeatDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltHeartBeat'})}
|
||||||
imageSrc="/images/microbit/starter-heart.png"
|
imageSrc="/images/microbit/starter-heart.png"
|
||||||
title={this.props.intl.formatMessage({id: 'microbit.heartBeat'})}
|
title={this.props.intl.formatMessage({id: 'microbit.heartBeat'})}
|
||||||
/>
|
/>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://downloads.scratch.mit.edu/microbit/microbit-guitar.sb3"
|
cardUrl="https://beta.scratch.mit.edu/#239075950"
|
||||||
description={this.props.intl.formatMessage({id: 'microbit.tiltGuitarDescription'})}
|
description={this.props.intl.formatMessage({id: 'microbit.tiltGuitarDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltTiltGuitar'})}
|
||||||
imageSrc="/images/microbit/starter-guitar.png"
|
imageSrc="/images/microbit/starter-guitar.png"
|
||||||
title={this.props.intl.formatMessage({id: 'microbit.tiltGuitar'})}
|
title={this.props.intl.formatMessage({id: 'microbit.tiltGuitar'})}
|
||||||
/>
|
/>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://downloads.scratch.mit.edu/microbit/microbit-fish.sb3"
|
cardUrl="https://beta.scratch.mit.edu/#239075973"
|
||||||
description={this.props.intl.formatMessage({id: 'microbit.oceanAdventureDescription'})}
|
description={this.props.intl.formatMessage({id: 'microbit.oceanAdventureDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltOceanAdventure'})}
|
||||||
imageSrc="/images/microbit/starter-fish.png"
|
imageSrc="/images/microbit/starter-fish.png"
|
||||||
title={this.props.intl.formatMessage({id: 'microbit.oceanAdventure'})}
|
title={this.props.intl.formatMessage({id: 'microbit.oceanAdventure'})}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
.microbit {
|
.microbit {
|
||||||
.extension-header {
|
.extension-header {
|
||||||
background-color: $ui-purple;
|
background-color: $ui-mint-green;
|
||||||
background-image: url("/images/microbit/mbit-pattern.svg");
|
background-image: url("/images/microbit/mbit-pattern.svg");
|
||||||
|
|
||||||
.extension-info {
|
.extension-info {
|
||||||
|
|
246
src/views/preview/comment/comment.jsx
Normal file
246
src/views/preview/comment/comment.jsx
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
const React = require('react');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const bindAll = require('lodash.bindall');
|
||||||
|
const classNames = require('classnames');
|
||||||
|
|
||||||
|
const FlexRow = require('../../../components/flex-row/flex-row.jsx');
|
||||||
|
const Avatar = require('../../../components/avatar/avatar.jsx');
|
||||||
|
const EmojiText = require('../../../components/emoji-text/emoji-text.jsx');
|
||||||
|
const FormattedRelative = require('react-intl').FormattedRelative;
|
||||||
|
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||||
|
const ComposeComment = require('./compose-comment.jsx');
|
||||||
|
const DeleteCommentModal = require('../../../components/modal/comments/delete-comment.jsx');
|
||||||
|
const ReportCommentModal = require('../../../components/modal/comments/report-comment.jsx');
|
||||||
|
|
||||||
|
require('./comment.scss');
|
||||||
|
|
||||||
|
class Comment extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleDelete',
|
||||||
|
'handleCancelDelete',
|
||||||
|
'handleConfirmDelete',
|
||||||
|
'handleReport',
|
||||||
|
'handleConfirmReport',
|
||||||
|
'handleCancelReport',
|
||||||
|
'handlePostReply',
|
||||||
|
'handleToggleReplying',
|
||||||
|
'handleRestore'
|
||||||
|
]);
|
||||||
|
this.state = {
|
||||||
|
deleting: false,
|
||||||
|
reporting: false,
|
||||||
|
reportConfirmed: false,
|
||||||
|
replying: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePostReply (comment) {
|
||||||
|
this.setState({replying: false});
|
||||||
|
this.props.onAddComment(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleReplying () {
|
||||||
|
this.setState({replying: !this.state.replying});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDelete () {
|
||||||
|
this.setState({deleting: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfirmDelete () {
|
||||||
|
this.setState({deleting: false});
|
||||||
|
this.props.onDelete(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelDelete () {
|
||||||
|
this.setState({deleting: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReport () {
|
||||||
|
this.setState({reporting: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRestore () {
|
||||||
|
this.props.onRestore(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfirmReport () {
|
||||||
|
this.setState({
|
||||||
|
reporting: false,
|
||||||
|
reportConfirmed: true,
|
||||||
|
deleting: false // To close delete modal if reported from delete modal
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.onReport(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelReport () {
|
||||||
|
this.setState({
|
||||||
|
reporting: false,
|
||||||
|
reportConfirmed: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
author,
|
||||||
|
canDelete,
|
||||||
|
canReply,
|
||||||
|
canReport,
|
||||||
|
canRestore,
|
||||||
|
content,
|
||||||
|
datetimeCreated,
|
||||||
|
id,
|
||||||
|
projectId,
|
||||||
|
replyUsername,
|
||||||
|
visibility
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const visible = visibility === 'visible';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex-row comment"
|
||||||
|
id={`comments-${id}`}
|
||||||
|
>
|
||||||
|
<a href={`/users/${author.username}`}>
|
||||||
|
<Avatar src={author.image} />
|
||||||
|
</a>
|
||||||
|
<FlexRow className="comment-body column">
|
||||||
|
<FlexRow className="comment-top-row">
|
||||||
|
<a
|
||||||
|
className="username"
|
||||||
|
href={`/users/${author.username}`}
|
||||||
|
>{author.username}</a>
|
||||||
|
<div className="action-list">
|
||||||
|
{visible ? (
|
||||||
|
<React.Fragment>
|
||||||
|
{canDelete && (
|
||||||
|
<span
|
||||||
|
className="comment-delete"
|
||||||
|
onClick={this.handleDelete}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="comments.delete" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{canReport && (
|
||||||
|
<span
|
||||||
|
className="comment-report"
|
||||||
|
onClick={this.handleReport}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="comments.report" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<React.Fragment>
|
||||||
|
<span className="comment-visibility">
|
||||||
|
<FormattedMessage id={`comments.status.${visibility}`} />
|
||||||
|
</span>
|
||||||
|
{canRestore && (
|
||||||
|
<span
|
||||||
|
className="comment-restore"
|
||||||
|
onClick={this.handleRestore}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="comments.restore" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FlexRow>
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
'comment-bubble': true,
|
||||||
|
'comment-bubble-reported': !visible
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* TODO: at the moment, comment content does not properly display
|
||||||
|
* emojis/easter eggs
|
||||||
|
* @user links in replies
|
||||||
|
* links to scratch.mit.edu pages
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<span className="comment-content">
|
||||||
|
{replyUsername && (
|
||||||
|
<a href={`/users/${replyUsername}`}>@{replyUsername} </a>
|
||||||
|
)}
|
||||||
|
<EmojiText
|
||||||
|
as="span"
|
||||||
|
text={content}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<FlexRow className="comment-bottom-row">
|
||||||
|
<span className="comment-time">
|
||||||
|
<FormattedRelative value={new Date(datetimeCreated)} />
|
||||||
|
</span>
|
||||||
|
{(canReply && visible) ? (
|
||||||
|
<span
|
||||||
|
className="comment-reply"
|
||||||
|
onClick={this.handleToggleReplying}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="comments.reply" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</FlexRow>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.state.replying ? (
|
||||||
|
<FlexRow className="comment-reply-row">
|
||||||
|
<ComposeComment
|
||||||
|
parentId={id}
|
||||||
|
projectId={projectId}
|
||||||
|
onAddComment={this.handlePostReply}
|
||||||
|
onCancel={this.handleToggleReplying}
|
||||||
|
/>
|
||||||
|
</FlexRow>
|
||||||
|
) : null}
|
||||||
|
</FlexRow>
|
||||||
|
{this.state.deleting ? (
|
||||||
|
<DeleteCommentModal
|
||||||
|
isOpen
|
||||||
|
key="delete-comment-modal"
|
||||||
|
onDelete={this.handleConfirmDelete}
|
||||||
|
onReport={this.handleConfirmReport}
|
||||||
|
onRequestClose={this.handleCancelDelete}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{(this.state.reporting || this.state.reportConfirmed) ? (
|
||||||
|
<ReportCommentModal
|
||||||
|
isOpen
|
||||||
|
isConfirmed={this.state.reportConfirmed}
|
||||||
|
key="report-comment-modal"
|
||||||
|
onReport={this.handleConfirmReport}
|
||||||
|
onRequestClose={this.handleCancelReport}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Comment.propTypes = {
|
||||||
|
author: PropTypes.shape({
|
||||||
|
id: PropTypes.number,
|
||||||
|
image: PropTypes.string,
|
||||||
|
username: PropTypes.string
|
||||||
|
}),
|
||||||
|
canDelete: PropTypes.bool,
|
||||||
|
canReply: PropTypes.bool,
|
||||||
|
canReport: PropTypes.bool,
|
||||||
|
canRestore: PropTypes.bool,
|
||||||
|
content: PropTypes.string,
|
||||||
|
datetimeCreated: PropTypes.string,
|
||||||
|
id: PropTypes.number,
|
||||||
|
onAddComment: PropTypes.func,
|
||||||
|
onDelete: PropTypes.func,
|
||||||
|
onReport: PropTypes.func,
|
||||||
|
onRestore: PropTypes.func,
|
||||||
|
projectId: PropTypes.string,
|
||||||
|
replyUsername: PropTypes.string,
|
||||||
|
visibility: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Comment;
|
276
src/views/preview/comment/comment.scss
Normal file
276
src/views/preview/comment/comment.scss
Normal file
|
@ -0,0 +1,276 @@
|
||||||
|
@import "../../../colors";
|
||||||
|
|
||||||
|
.compose-comment {
|
||||||
|
margin-left: .5rem;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.compose-error-row {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.compose-error-tip {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
border: 1px solid $active-gray;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: $ui-orange;
|
||||||
|
padding: .25rem;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
color: $type-white;
|
||||||
|
font-size: .85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-row {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
&:not(:focus) {
|
||||||
|
border: 1px solid $active-dark-gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-bottom-row {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.compose-post {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-cancel {
|
||||||
|
background-color: $ui-dark-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-limit {
|
||||||
|
margin-left: auto;
|
||||||
|
height: 100%;
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-left: 0;
|
||||||
|
border-radius: .25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.comment-top-row {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.username {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-delete,
|
||||||
|
.comment-report,
|
||||||
|
.comment-restore {
|
||||||
|
opacity: .5;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: .75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: .5rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: contain;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-visibility {
|
||||||
|
opacity: .5;
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-delete {
|
||||||
|
margin-right: 1rem;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
background-image: url("/svgs/project/delete-gray.svg");
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
vertical-align: -.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-report {
|
||||||
|
&:before {
|
||||||
|
margin-right: .25rem;
|
||||||
|
background-image: url("/svgs/project/report-gray.svg");
|
||||||
|
width: .75rem;
|
||||||
|
height: .75rem;
|
||||||
|
vertical-align: -.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-restore {
|
||||||
|
margin-left: 1rem;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
margin-right: .25rem;
|
||||||
|
background-image: url("/svgs/project/restore-gray.svg");
|
||||||
|
width: .75rem;
|
||||||
|
height: .75rem;
|
||||||
|
vertical-align: -.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-body {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
min-width: 50%;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.comment-bubble {
|
||||||
|
position: relative;
|
||||||
|
margin-left: .5rem;
|
||||||
|
border: 1px solid $active-gray;
|
||||||
|
border-radius: 0 .5rem .5rem .5rem;
|
||||||
|
background-color: $ui-white;
|
||||||
|
padding: .75rem;
|
||||||
|
width: calc(100% - .5rem);
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
left: -11px; // width + 1px
|
||||||
|
border-width: 1px 0 1px 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 0 0 0 8px;
|
||||||
|
border-color: $active-gray transparent $active-gray $active-gray;
|
||||||
|
background: $ui-white;
|
||||||
|
width: 10px;
|
||||||
|
height: 9px;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
&.comment-bubble-reported {
|
||||||
|
$reported-outline: #ff6680;
|
||||||
|
$reported-background: rgb(236, 206, 223);
|
||||||
|
|
||||||
|
border-color: $reported-outline;
|
||||||
|
background-color: $reported-background;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
border-color: $reported-outline transparent $reported-outline $reported-outline;
|
||||||
|
background: $reported-background;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-content {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-bottom-row {
|
||||||
|
padding-top: 1rem;
|
||||||
|
font-size: .75rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.comment-time {
|
||||||
|
color: $ui-dark-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-reply {
|
||||||
|
display: inline-flex;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $ui-blue;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
margin-left: .25rem;
|
||||||
|
background-image: url("/svgs/project/comment-reply.svg");
|
||||||
|
background-size: cover;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.replies {
|
||||||
|
width: calc(100% - 4rem);
|
||||||
|
|
||||||
|
&.collapsed .comment {
|
||||||
|
&:last-child {
|
||||||
|
&:after {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
$ui-light-primary-transparent,
|
||||||
|
$ui-light-primary
|
||||||
|
);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments-root-reply {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-reply-row {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-left: .5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-thread {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
background-color: $active-gray;
|
||||||
|
width: 50%;
|
||||||
|
height: 2px;
|
||||||
|
vertical-align: middle;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
right: .5em;
|
||||||
|
margin-left: -50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
left: .5em;
|
||||||
|
margin-right: -50%;
|
||||||
|
}
|
||||||
|
}
|
182
src/views/preview/comment/compose-comment.jsx
Normal file
182
src/views/preview/comment/compose-comment.jsx
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
const React = require('react');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const bindAll = require('lodash.bindall');
|
||||||
|
const classNames = require('classnames');
|
||||||
|
const keyMirror = require('keymirror');
|
||||||
|
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||||
|
|
||||||
|
const FlexRow = require('../../../components/flex-row/flex-row.jsx');
|
||||||
|
const Avatar = require('../../../components/avatar/avatar.jsx');
|
||||||
|
const InplaceInput = require('../../../components/forms/inplace-input.jsx');
|
||||||
|
const Button = require('../../../components/forms/button.jsx');
|
||||||
|
|
||||||
|
const connect = require('react-redux').connect;
|
||||||
|
|
||||||
|
const api = require('../../../lib/api');
|
||||||
|
|
||||||
|
require('./comment.scss');
|
||||||
|
|
||||||
|
const onUpdate = update => update;
|
||||||
|
|
||||||
|
const MAX_COMMENT_LENGTH = 500;
|
||||||
|
|
||||||
|
const ComposeStatus = keyMirror({
|
||||||
|
EDITING: null,
|
||||||
|
SUBMITTING: null,
|
||||||
|
REJECTED: null
|
||||||
|
});
|
||||||
|
|
||||||
|
class ComposeComment extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handlePost',
|
||||||
|
'handleCancel',
|
||||||
|
'handleInput'
|
||||||
|
]);
|
||||||
|
this.state = {
|
||||||
|
message: '',
|
||||||
|
status: ComposeStatus.EDITING,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
handleInput (event) {
|
||||||
|
this.setState({
|
||||||
|
message: event.target.value,
|
||||||
|
status: ComposeStatus.EDITING,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handlePost () {
|
||||||
|
this.setState({status: ComposeStatus.SUBMITTING});
|
||||||
|
api({
|
||||||
|
uri: `/proxy/comments/project/${this.props.projectId}`,
|
||||||
|
authentication: this.props.user.token,
|
||||||
|
withCredentials: true,
|
||||||
|
method: 'POST',
|
||||||
|
useCsrf: true,
|
||||||
|
json: {
|
||||||
|
content: this.state.message,
|
||||||
|
parent_id: this.props.parentId || '',
|
||||||
|
comentee_id: this.props.comenteeId || ''
|
||||||
|
}
|
||||||
|
}, (err, body, res) => {
|
||||||
|
if (err || res.statusCode !== 200) {
|
||||||
|
body = {rejected: 'error'};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.rejected && this.state.status === ComposeStatus.SUBMITTING) {
|
||||||
|
// Note: does not reset the message state
|
||||||
|
this.setState({
|
||||||
|
status: ComposeStatus.REJECTED,
|
||||||
|
error: body.rejected
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the text field and reset status on successful submission
|
||||||
|
this.setState({
|
||||||
|
message: '',
|
||||||
|
status: ComposeStatus.EDITING,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the username, which isn't included right now from scratch-api
|
||||||
|
if (body.author) body.author.username = this.props.user.username;
|
||||||
|
|
||||||
|
this.props.onAddComment(body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleCancel () {
|
||||||
|
this.setState({
|
||||||
|
message: '',
|
||||||
|
status: ComposeStatus.EDITING,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
if (this.props.onCancel) this.props.onCancel();
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex-row comment"
|
||||||
|
>
|
||||||
|
<a href={`/users/${this.props.user.username}`}>
|
||||||
|
<Avatar src={this.props.user.thumbnailUrl} />
|
||||||
|
</a>
|
||||||
|
<FlexRow className="compose-comment column">
|
||||||
|
{this.state.error ? (
|
||||||
|
<FlexRow className="compose-error-row">
|
||||||
|
<div className="compose-error-tip">
|
||||||
|
<FormattedMessage id={`comments.${this.state.error}`} />
|
||||||
|
</div>
|
||||||
|
</FlexRow>
|
||||||
|
) : null}
|
||||||
|
<InplaceInput
|
||||||
|
className={classNames('compose-input',
|
||||||
|
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ? 'compose-valid' : 'compose-invalid')}
|
||||||
|
handleUpdate={onUpdate}
|
||||||
|
name="compose-comment"
|
||||||
|
type="textarea"
|
||||||
|
value={this.state.message}
|
||||||
|
onInput={this.handleInput}
|
||||||
|
/>
|
||||||
|
<FlexRow className="compose-bottom-row">
|
||||||
|
<Button
|
||||||
|
className="compose-post"
|
||||||
|
disabled={this.state.status === ComposeStatus.SUBMITTING}
|
||||||
|
onClick={this.handlePost}
|
||||||
|
>
|
||||||
|
{this.state.status === ComposeStatus.SUBMITTING ? (
|
||||||
|
<FormattedMessage id="comments.posting" />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id="comments.post" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="compose-cancel"
|
||||||
|
onClick={this.handleCancel}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="comments.cancel" />
|
||||||
|
</Button>
|
||||||
|
<span
|
||||||
|
className={classNames('compose-limit',
|
||||||
|
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ?
|
||||||
|
'compose-valid' : 'compose-invalid')}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="comments.lengthWarning"
|
||||||
|
values={{
|
||||||
|
remainingCharacters: MAX_COMMENT_LENGTH - this.state.message.length
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</FlexRow>
|
||||||
|
</FlexRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ComposeComment.propTypes = {
|
||||||
|
comenteeId: PropTypes.number,
|
||||||
|
onAddComment: PropTypes.func,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
parentId: PropTypes.number,
|
||||||
|
projectId: PropTypes.string,
|
||||||
|
user: PropTypes.shape({
|
||||||
|
id: PropTypes.number,
|
||||||
|
username: PropTypes.string,
|
||||||
|
token: PropTypes.string,
|
||||||
|
thumbnailUrl: PropTypes.string
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
user: state.session.session.user
|
||||||
|
});
|
||||||
|
|
||||||
|
const ConnectedComposeComment = connect(
|
||||||
|
mapStateToProps
|
||||||
|
)(ComposeComment);
|
||||||
|
|
||||||
|
module.exports = ConnectedComposeComment;
|
182
src/views/preview/comment/top-level-comment.jsx
Normal file
182
src/views/preview/comment/top-level-comment.jsx
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
const React = require('react');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const bindAll = require('lodash.bindall');
|
||||||
|
const classNames = require('classnames');
|
||||||
|
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||||
|
|
||||||
|
const FlexRow = require('../../../components/flex-row/flex-row.jsx');
|
||||||
|
const Comment = require('./comment.jsx');
|
||||||
|
|
||||||
|
require('./comment.scss');
|
||||||
|
|
||||||
|
class TopLevelComment extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleExpandThread',
|
||||||
|
'handleAddComment',
|
||||||
|
'handleDeleteReply',
|
||||||
|
'handleReportReply',
|
||||||
|
'handleRestoreReply'
|
||||||
|
]);
|
||||||
|
this.state = {
|
||||||
|
expanded: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// A cache of {commentId: username, ...} in order to show reply usernames
|
||||||
|
this.commentUsernameCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleExpandThread () {
|
||||||
|
this.setState({
|
||||||
|
expanded: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteReply (replyId) {
|
||||||
|
// Only apply topLevelCommentId for deleting replies
|
||||||
|
// The top level comment itself just gets passed onDelete directly
|
||||||
|
this.props.onDelete(replyId, this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReportReply (replyId) {
|
||||||
|
// Only apply topLevelCommentId for reporting replies
|
||||||
|
// The top level comment itself just gets passed onReport directly
|
||||||
|
this.props.onReport(replyId, this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRestoreReply (replyId) {
|
||||||
|
this.props.onRestore(replyId, this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddComment (comment) {
|
||||||
|
this.props.onAddComment(comment, this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
commentUsername (parentId) {
|
||||||
|
if (this.commentUsernameCache[parentId]) return this.commentUsernameCache[parentId];
|
||||||
|
|
||||||
|
// If the cache misses, rebuild it. Every reply has a parent id that is
|
||||||
|
// either a reply to this top level comment or to one of the replies.
|
||||||
|
this.commentUsernameCache[this.props.id] = this.props.author.username;
|
||||||
|
const replies = this.props.replies;
|
||||||
|
for (let i = 0; i < replies.length; i++) {
|
||||||
|
this.commentUsernameCache[replies[i].id] = replies[i].author.username;
|
||||||
|
}
|
||||||
|
return this.commentUsernameCache[parentId];
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
author,
|
||||||
|
canDelete,
|
||||||
|
canReply,
|
||||||
|
canReport,
|
||||||
|
canRestore,
|
||||||
|
content,
|
||||||
|
datetimeCreated,
|
||||||
|
id,
|
||||||
|
onDelete,
|
||||||
|
onReport,
|
||||||
|
onRestore,
|
||||||
|
replies,
|
||||||
|
projectId,
|
||||||
|
visibility
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const parentVisible = visibility === 'visible';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlexRow className="comment-container">
|
||||||
|
<Comment
|
||||||
|
projectId={projectId}
|
||||||
|
onAddComment={this.handleAddComment}
|
||||||
|
{...{
|
||||||
|
author,
|
||||||
|
content,
|
||||||
|
datetimeCreated,
|
||||||
|
canDelete,
|
||||||
|
canReply,
|
||||||
|
canReport,
|
||||||
|
canRestore,
|
||||||
|
id,
|
||||||
|
onDelete,
|
||||||
|
onReport,
|
||||||
|
onRestore,
|
||||||
|
visibility
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{replies.length > 0 &&
|
||||||
|
<FlexRow
|
||||||
|
className={classNames(
|
||||||
|
'replies',
|
||||||
|
'column',
|
||||||
|
{collapsed: !this.state.expanded && replies.length > 3}
|
||||||
|
)}
|
||||||
|
key={id}
|
||||||
|
>
|
||||||
|
{(this.state.expanded ? replies : replies.slice(0, 3)).map(reply => (
|
||||||
|
<Comment
|
||||||
|
author={reply.author}
|
||||||
|
canDelete={canDelete}
|
||||||
|
canReply={canReply}
|
||||||
|
canReport={canReport}
|
||||||
|
canRestore={canRestore && parentVisible}
|
||||||
|
content={reply.content}
|
||||||
|
datetimeCreated={reply.datetime_created}
|
||||||
|
id={reply.id}
|
||||||
|
key={reply.id}
|
||||||
|
projectId={projectId}
|
||||||
|
replyUsername={this.commentUsername(reply.parent_id)}
|
||||||
|
visibility={reply.visibility}
|
||||||
|
onAddComment={this.handleAddComment}
|
||||||
|
onDelete={this.handleDeleteReply}
|
||||||
|
onReport={this.handleReportReply}
|
||||||
|
onRestore={this.handleRestoreReply}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FlexRow>
|
||||||
|
}
|
||||||
|
{!this.state.expanded && replies.length > 3 &&
|
||||||
|
<a
|
||||||
|
className="expand-thread"
|
||||||
|
onClick={this.handleExpandThread}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="comments.seeMoreReplies"
|
||||||
|
values={{
|
||||||
|
repliesCount: replies.length
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</FlexRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TopLevelComment.propTypes = {
|
||||||
|
author: PropTypes.shape({
|
||||||
|
id: PropTypes.number,
|
||||||
|
image: PropTypes.string,
|
||||||
|
username: PropTypes.string
|
||||||
|
}),
|
||||||
|
canDelete: PropTypes.bool,
|
||||||
|
canReply: PropTypes.bool,
|
||||||
|
canReport: PropTypes.bool,
|
||||||
|
canRestore: PropTypes.bool,
|
||||||
|
content: PropTypes.string,
|
||||||
|
datetimeCreated: PropTypes.string,
|
||||||
|
deletable: PropTypes.bool,
|
||||||
|
id: PropTypes.number,
|
||||||
|
onAddComment: PropTypes.func,
|
||||||
|
onDelete: PropTypes.func,
|
||||||
|
onReport: PropTypes.func,
|
||||||
|
onRestore: PropTypes.func,
|
||||||
|
parentId: PropTypes.number,
|
||||||
|
projectId: PropTypes.string,
|
||||||
|
replies: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
visibility: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = TopLevelComment;
|
|
@ -26,6 +26,7 @@
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.extension-status {
|
.extension-status {
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
{
|
{
|
||||||
"addToStudio.title": "Add to Studio",
|
"addToStudio.title": "Add to Studio",
|
||||||
"addToStudio.finishing": "Finishing up...",
|
"addToStudio.finishing": "Finishing up...",
|
||||||
|
"preview.titleMaxLength": "Title is too long",
|
||||||
"preview.musicExtensionChip": "Music",
|
"preview.musicExtensionChip": "Music",
|
||||||
"preview.penExtensionChip": "Pen",
|
"preview.penExtensionChip": "Pen",
|
||||||
"preview.speechExtensionChip": "Google Speech",
|
"preview.speechExtensionChip": "Google Speech",
|
||||||
"preview.translateExtensionChip": "Google Translate",
|
"preview.translateExtensionChip": "Google Translate",
|
||||||
"preview.videoMotionChip": "Video Motion"
|
"preview.videoMotionChip": "Video Motion",
|
||||||
|
"preview.comments.header": "Comments",
|
||||||
|
"preview.comments.turnOff": "Turn off commenting",
|
||||||
|
"preview.comments.turnedOff": "Sorry, comment posting has been turned off for this project.",
|
||||||
|
"preview.share.notShared": "This project is not shared — so only you can see it. Click share to let everyone see it!",
|
||||||
|
"preview.share.shareButton": "Share"
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue