mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 01:25:52 -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:
|
||||
- CXX=g++-4.8
|
||||
- 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=${!API_HOST_VAR}
|
||||
- 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_STAGING=https://scratch.ly
|
||||
- ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH
|
||||
- ROOT_URL=${!ROOT_URL_VAR}
|
||||
- ROOT_URL=${ROOT_URL:-$ROOT_URL_STAGING}
|
||||
- PROJECT_HOST_master=https://projects.scratch.mit.edu
|
||||
- PROJECT_HOST_STAGING=https://projects.scratch.ly
|
||||
- PROJECT_HOST_VAR=PROJECT_HOST_$TRAVIS_BRANCH
|
||||
- PROJECT_HOST=${!PROJECT_HOST_VAR}
|
||||
- PROJECT_HOST=${PROJECT_HOST:-$PROJECT_HOST_STAGING}
|
||||
- PATH=$PATH:$PWD/test/integration/node_modules/chromedriver/bin
|
||||
- AWS_ACCESS_KEY_ID=$EB_AWS_ACCESS_KEY_ID
|
||||
- AWS_SECRET_ACCESS_KEY=$EB_AWS_SECRET_ACCESS_KEY
|
||||
|
@ -69,6 +84,7 @@ addons:
|
|||
install:
|
||||
- sudo -H pip install -r requirements.txt
|
||||
- npm --production=false install
|
||||
- npm --production=false update
|
||||
jobs:
|
||||
include:
|
||||
- stage: test
|
||||
|
|
19
.tx/config
19
.tx/config
|
@ -1,6 +1,6 @@
|
|||
[main]
|
||||
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]
|
||||
file_filter = localizations/explore/<lang>.json
|
||||
|
@ -156,6 +156,23 @@ source_file = src/views/microbit/l10n.json
|
|||
source_lang = en
|
||||
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]
|
||||
source_file = src/views/parents/l10n.json
|
||||
source_lang = en
|
||||
|
|
14
Makefile
14
Makefile
|
@ -1,11 +1,25 @@
|
|||
ESLINT=./node_modules/.bin/eslint
|
||||
NODE= NODE_OPTIONS=--max_old_space_size=8000 node
|
||||
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
|
||||
TAP=./node_modules/.bin/tap
|
||||
WATCH= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/watch
|
||||
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:
|
||||
|
|
|
@ -82,8 +82,11 @@ 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
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------- | ----------------------------- | ---------------------------------------------- |
|
||||
| --------------- | ---------------------------------- | ---------------------------------------------- |
|
||||
| `API_HOST` | `https://api.scratch.mit.edu` | Hostname for API requests |
|
||||
| `ASSETS_HOST` | `https://assets.scratch.mit.edu` | Hostname for asset requests |
|
||||
| `BACKPACK_HOST` | `https://backpack.scratch.mit.edu` | Hostname for backpack requests |
|
||||
| `PROJECTS_HOST` | `https://projects.scratch.mit.edu` | Hostname for project requests |
|
||||
| `SENTRY_DSN` | `''` | DSN for Sentry |
|
||||
| `FALLBACK` | `''` | Pass-through location for old site |
|
||||
| `GA_TRACKER` | `''` | Where to log Google Analytics data |
|
||||
|
|
|
@ -4,8 +4,9 @@ volumes:
|
|||
runtime_data:
|
||||
|
||||
networks:
|
||||
scratch-api_scratch_network:
|
||||
external: true
|
||||
default:
|
||||
external:
|
||||
name: scratchapi_scratch_network
|
||||
|
||||
services:
|
||||
app:
|
||||
|
@ -13,7 +14,7 @@ services:
|
|||
hostname: scratch-www-app
|
||||
environment:
|
||||
- API_HOST=http://localhost:8491
|
||||
- FALLBACK=http://localhost:8080
|
||||
- FALLBACK=http://scratchr2-app:8080
|
||||
- USE_DOCKER_WATCHOPTIONS=true
|
||||
build:
|
||||
context: ./
|
||||
|
@ -35,5 +36,3 @@ services:
|
|||
- runtime_data:/runtime
|
||||
ports:
|
||||
- "8333:8333"
|
||||
networks:
|
||||
- scratch-api_scratch_network
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
"lodash.defaults": "4.0.1",
|
||||
"newrelic": "1.25.4",
|
||||
"raven": "0.10.0",
|
||||
"scratch-docker": "^1.0.2",
|
||||
"scratch-parser": "^4.2.0",
|
||||
"scratch-storage": "^0.5.1"
|
||||
},
|
||||
|
@ -77,7 +78,6 @@
|
|||
"lodash.merge": "3.3.2",
|
||||
"lodash.omit": "3.1.0",
|
||||
"lodash.range": "3.0.1",
|
||||
"lodash.truncate": "4.4.2",
|
||||
"minilog": "2.0.8",
|
||||
"node-dir": "0.1.16",
|
||||
"node-sass": "4.6.1",
|
||||
|
@ -100,7 +100,7 @@
|
|||
"redux-thunk": "2.0.1",
|
||||
"sass-lint": "1.5.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"scratch-gui": "latest",
|
||||
"scratch-gui": "develop",
|
||||
"scratchr2_translations": "git://github.com/LLK/scratchr2_translations.git#master",
|
||||
"slick-carousel": "1.6.0",
|
||||
"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-15percent: hsla(0, 100%, 100%, .15); //#FFF
|
||||
$ui-light-primary: hsl(215, 100, 95);
|
||||
$ui-light-primary-transparent: hsla(215, 100, 95, 0);
|
||||
|
||||
$ui-border: hsla(0, 0, 85, 1); //#D9D9D9
|
||||
|
||||
|
|
|
@ -38,6 +38,31 @@ $desktop: 942px;
|
|||
$tablet: 640px;
|
||||
$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
|
||||
//
|
||||
|
@ -48,7 +73,7 @@ $mobile: 480px;
|
|||
|
||||
//4 columns
|
||||
@mixin submobile ($parent-selector, $child-selector) {
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
#{$parent-selector} {
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -64,7 +89,7 @@ $mobile: 480px;
|
|||
|
||||
//6 columns
|
||||
@mixin mobile ($parent-selector, $child-selector) {
|
||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
||||
@media #{$medium} {
|
||||
#{$parent-selector} {
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -80,7 +105,7 @@ $mobile: 480px;
|
|||
|
||||
//8 columns
|
||||
@mixin tablet ($parent-selector, $child-selector) {
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
#{$parent-selector} {
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -94,7 +119,7 @@ $mobile: 480px;
|
|||
|
||||
//12 columns
|
||||
@mixin desktop ($parent-selector, $child-selector) {
|
||||
@media only screen and (min-width: $desktop) {
|
||||
@media #{$big} {
|
||||
#{$child-selector} {
|
||||
margin: 0 auto;
|
||||
width: $desktop;
|
||||
|
|
|
@ -9,7 +9,7 @@ $base-bg: $ui-white;
|
|||
border-radius: 10px 10px 0 0;
|
||||
|
||||
//4 columns
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
width: $cols4;
|
||||
|
||||
.box-header {
|
||||
|
@ -22,7 +22,7 @@ $base-bg: $ui-white;
|
|||
}
|
||||
|
||||
//6 columns
|
||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
||||
@media #{$medium} {
|
||||
width: $cols6;
|
||||
|
||||
.box-header {
|
||||
|
@ -35,7 +35,7 @@ $base-bg: $ui-white;
|
|||
}
|
||||
|
||||
//8 columns
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
width: $cols8;
|
||||
|
||||
.box-header {
|
||||
|
@ -48,7 +48,7 @@ $base-bg: $ui-white;
|
|||
}
|
||||
|
||||
//12 columns
|
||||
@media only screen and (min-width: $desktop) {
|
||||
@media #{$big} {
|
||||
width: $cols12;
|
||||
|
||||
.box-header {
|
||||
|
|
|
@ -81,7 +81,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.card {
|
||||
width: 22.5rem;
|
||||
|
||||
|
@ -95,7 +95,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
.card {
|
||||
.input {
|
||||
width: 90%;
|
||||
|
@ -103,7 +103,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.card {
|
||||
.validation-message {
|
||||
position: relative;
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
border-radius: 0 0 5px 5px;
|
||||
background-color: $ui-blue;
|
||||
padding: 10px;
|
||||
max-width: 260px;
|
||||
min-width: 9rem;
|
||||
max-width: 16.25rem;
|
||||
overflow: visible;
|
||||
color: $type-white;
|
||||
font-size: .8125rem;
|
||||
|
@ -33,8 +34,8 @@
|
|||
}
|
||||
|
||||
input {
|
||||
// 100% minus border and padding
|
||||
margin-bottom: 12px;
|
||||
// 100% minus border and padding
|
||||
width: calc(100% - 30px);
|
||||
}
|
||||
|
||||
|
@ -88,8 +89,4 @@
|
|||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,16 @@ class ExtensionLanding extends React.Component {
|
|||
'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 = {
|
||||
OS: OS_ENUM.WINDOWS
|
||||
OS: detectedOS
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,11 @@
|
|||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
@ -46,6 +50,7 @@
|
|||
}
|
||||
|
||||
.screenshot {
|
||||
border: 1px solid $ui-border;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
|
@ -84,16 +89,16 @@
|
|||
margin-bottom: 5rem;
|
||||
align-items: flex-start;
|
||||
|
||||
h2 {
|
||||
h1, h2 {
|
||||
display: flex;
|
||||
margin-bottom: 2rem;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
h2 img {
|
||||
img {
|
||||
padding-right: .5rem;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 1.2rem;
|
||||
|
|
|
@ -35,7 +35,10 @@ const InstallScratchLink = ({
|
|||
<FormattedMessage id="installScratchLink.windowsDownload" /> :
|
||||
<FormattedMessage id="installScratchLink.macosDownload" />
|
||||
}
|
||||
<img src="/svgs/extensions/download-white.svg" />
|
||||
<img
|
||||
alt=""
|
||||
src="/svgs/extensions/download-white.svg"
|
||||
/>
|
||||
</button>
|
||||
</a>
|
||||
</Step>
|
||||
|
@ -50,6 +53,7 @@ const InstallScratchLink = ({
|
|||
</span>
|
||||
<div className="step-image">
|
||||
<img
|
||||
alt=""
|
||||
className="screenshot"
|
||||
src={`/images/scratchlink/${
|
||||
currentOS === OS_ENUM.WINDOWS ? 'windows' : 'mac'
|
||||
|
|
|
@ -3,12 +3,15 @@ const React = require('react');
|
|||
|
||||
const ProjectCard = props => (
|
||||
<a
|
||||
download
|
||||
className="project-card"
|
||||
href={props.cardUrl}
|
||||
target="_blank"
|
||||
>
|
||||
<div className="project-card-image">
|
||||
<img src={props.imageSrc} />
|
||||
<img
|
||||
alt={props.imageAlt}
|
||||
src={props.imageSrc}
|
||||
/>
|
||||
</div>
|
||||
<div className="project-card-info">
|
||||
<h4>{props.title}</h4>
|
||||
|
@ -20,6 +23,7 @@ const ProjectCard = props => (
|
|||
ProjectCard.propTypes = {
|
||||
cardUrl: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
imageAlt: PropTypes.string,
|
||||
imageSrc: PropTypes.string,
|
||||
title: PropTypes.string
|
||||
};
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
flex-direction: column;
|
||||
|
||||
&.uneven {
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +103,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
text-align: center;
|
||||
|
||||
.family {
|
||||
|
@ -122,7 +122,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
ul {
|
||||
li {
|
||||
margin-left: 0;
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
}
|
||||
|
||||
.inplace-textarea {
|
||||
transition: all 1s ease;
|
||||
transition: all .2s ease;
|
||||
border: 2px dashed $ui-blue-25percent;
|
||||
border-radius: 8px;
|
||||
background-color: $ui-light-gray;
|
||||
|
@ -49,7 +49,7 @@
|
|||
resize: none;
|
||||
|
||||
&:focus {
|
||||
transition: all 1s ease;
|
||||
transition: all .2s ease;
|
||||
outline: none;
|
||||
border: 2px solid $ui-blue;
|
||||
box-shadow: 0 0 0 4px $ui-blue-25percent;
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
border-radius: 5px;
|
||||
background: $ui-light-gray url("../../../static/svgs/forms/carot.svg") no-repeat right center;
|
||||
padding-right: 4rem;
|
||||
padding-left: 1rem;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
text-indent: 1rem;
|
||||
color: $type-gray;
|
||||
font-size: .875rem;
|
||||
appearance: none;
|
||||
|
|
|
@ -28,8 +28,8 @@ module.exports.validationHOCFactory = defaultValidationErrors => (Component => {
|
|||
<Component
|
||||
validationErrors={defaults(
|
||||
{},
|
||||
defaultValidationErrors,
|
||||
props.validationErrors
|
||||
props.validationErrors,
|
||||
defaultValidationErrors
|
||||
)}
|
||||
{...omit(props, ['validationErrors'])}
|
||||
/>
|
||||
|
|
|
@ -94,21 +94,21 @@
|
|||
}
|
||||
|
||||
//4 columns
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.flex-row {
|
||||
width: $cols4;
|
||||
}
|
||||
}
|
||||
|
||||
//6 columns
|
||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
||||
@media #{$medium} {
|
||||
.flex-row {
|
||||
width: $cols6;
|
||||
}
|
||||
}
|
||||
|
||||
// 8 columns
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
.flex-row {
|
||||
width: $cols9;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ const connect = require('react-redux').connect;
|
|||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
|
||||
const sessionActions = require('../../redux/session.js');
|
||||
const navigationActions = require('../../redux/navigation.js');
|
||||
|
||||
const IframeModal = require('../modal/iframe/modal.jsx');
|
||||
const Registration = require('../registration/registration.jsx');
|
||||
|
@ -15,10 +15,7 @@ class Intro extends React.Component {
|
|||
super(props);
|
||||
bindAll(this, [
|
||||
'handleShowVideo',
|
||||
'handleCloseVideo',
|
||||
'handleJoinClick',
|
||||
'handleCloseRegistration',
|
||||
'handleCompleteRegistration'
|
||||
'handleCloseVideo'
|
||||
]);
|
||||
this.state = {
|
||||
videoOpen: false
|
||||
|
@ -30,17 +27,6 @@ class Intro extends React.Component {
|
|||
handleCloseVideo () {
|
||||
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 () {
|
||||
return (
|
||||
<div className="intro">
|
||||
|
@ -92,7 +78,7 @@ class Intro extends React.Component {
|
|||
<a
|
||||
className="sprite sprite-3"
|
||||
href="#"
|
||||
onClick={this.handleJoinClick}
|
||||
onClick={this.props.handleOpenRegistration}
|
||||
>
|
||||
<img
|
||||
alt="Gobo"
|
||||
|
@ -111,10 +97,7 @@ class Intro extends React.Component {
|
|||
<div className="text subtext">{this.props.messages['intro.itsFree']}</div>
|
||||
</a>
|
||||
<Registration
|
||||
isOpen={this.state.registrationOpen}
|
||||
key="registration"
|
||||
onRegistrationDone={this.handleCompleteRegistration}
|
||||
onRequestClose={this.handleCloseRegistration}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -160,7 +143,7 @@ class Intro extends React.Component {
|
|||
}
|
||||
|
||||
Intro.propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
handleOpenRegistration: PropTypes.func,
|
||||
messages: PropTypes.shape({
|
||||
'intro.aboutScratch': PropTypes.string,
|
||||
'intro.forEducators': PropTypes.string,
|
||||
|
@ -194,6 +177,17 @@ const mapStateToProps = state => ({
|
|||
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;
|
||||
|
|
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 React = require('react');
|
||||
|
||||
const log = require('../../lib/log.js');
|
||||
|
||||
const Form = require('../forms/form.jsx');
|
||||
const Input = require('../forms/input.jsx');
|
||||
const Button = require('../forms/button.jsx');
|
||||
|
@ -24,8 +22,7 @@ class Login extends React.Component {
|
|||
}
|
||||
handleSubmit (formData) {
|
||||
this.setState({waiting: true});
|
||||
this.props.onLogIn(formData, err => {
|
||||
if (err) log.error(err);
|
||||
this.props.onLogIn(formData, () => {
|
||||
this.setState({waiting: false});
|
||||
});
|
||||
}
|
||||
|
@ -48,9 +45,6 @@ class Login extends React.Component {
|
|||
key="usernameInput"
|
||||
maxLength="30"
|
||||
name="username"
|
||||
ref={input => {
|
||||
this.username = input;
|
||||
}}
|
||||
type="text"
|
||||
/>
|
||||
<label
|
||||
|
@ -63,9 +57,6 @@ class Login extends React.Component {
|
|||
required
|
||||
key="passwordInput"
|
||||
name="password"
|
||||
ref={input => {
|
||||
this.password = input;
|
||||
}}
|
||||
type="password"
|
||||
/>
|
||||
{this.state.waiting ? [
|
||||
|
@ -75,7 +66,10 @@ class Login extends React.Component {
|
|||
key="submitButton"
|
||||
type="submit"
|
||||
>
|
||||
<Spinner />
|
||||
<Spinner
|
||||
className="spinner"
|
||||
color="blue"
|
||||
/>
|
||||
</Button>
|
||||
] : [
|
||||
<Button
|
||||
|
|
|
@ -2,6 +2,26 @@
|
|||
|
||||
.login {
|
||||
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 {
|
||||
padding-top: 5px;
|
||||
|
@ -15,7 +35,7 @@
|
|||
.spinner {
|
||||
margin: 0 .8rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
|
@ -24,13 +44,19 @@
|
|||
|
||||
a {
|
||||
margin-top: 15px;
|
||||
color: $ui-white;
|
||||
|
||||
&:link,
|
||||
&:visited,
|
||||
&:active {
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.error {
|
||||
border: 1px solid $active-dark-gray;
|
||||
border-radius: 5px;
|
||||
|
|
|
@ -9,14 +9,14 @@
|
|||
|
||||
// column-count required for Firefox, IE and Edge
|
||||
//4 columns
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.masonry {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
//6 columns
|
||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
||||
@media #{$medium} {
|
||||
.masonry {
|
||||
column-count: 1;
|
||||
}
|
||||
|
@ -24,14 +24,14 @@
|
|||
|
||||
|
||||
//8 columns
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
.masonry {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 12 columns
|
||||
@media only screen and (min-width: $desktop) {
|
||||
@media #{$big} {
|
||||
.masonry {
|
||||
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 "../../../frameless";
|
||||
|
||||
.mod-addToStudio * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mod-addToStudio {
|
||||
margin: 100px auto;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
width: 36.25rem; /* 580px; */
|
||||
height: 388px; /* 24.25rem; */
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
|
||||
@media #{$small}, #{$small-height} {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.addToStudio-modal-header {
|
||||
box-shadow: inset 0 -1px 0 0 $ui-blue-dark;
|
||||
background-color: $ui-blue;
|
||||
padding-top: .75rem;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.addToStudio-content-label {
|
||||
text-align: center;
|
||||
color: $type-white;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.addToStudio-modal-content {
|
||||
margin: 0 auto;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
line-height: 1.5rem;
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.studio-list-outer-scrollbox {
|
||||
position: relative;
|
||||
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 {
|
||||
margin-right: .5rem;
|
||||
padding-right: .5rem;
|
||||
height: 16.9375rem;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
|
@ -90,34 +78,36 @@
|
|||
pointer-events: none; /* pass clicks through to buttons underneath */
|
||||
}
|
||||
|
||||
|
||||
.studio-selector-button {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: .21875rem .21875rem;
|
||||
transition: all .5s;
|
||||
margin: .21875rem;
|
||||
border-radius: .5rem;
|
||||
background-color: $ui-white;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 16.1875rem; /* 259px */
|
||||
width: 48%;
|
||||
height: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media #{$small} {
|
||||
min-width: 98%;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-selector-button-text {
|
||||
position: absolute;
|
||||
/* per spec, should be:
|
||||
margin: .375rem 2.18375rem .375rem .6875rem
|
||||
but in practice, our css seems to vertically align text to top, where
|
||||
invision spec aligned to middle.
|
||||
*/
|
||||
margin: .575rem 2.18375rem .175rem .6875rem;
|
||||
width: 13.3125rem;
|
||||
height: 1rem; /* diff from spec, in case we ever do valign to middle */
|
||||
line-height: 1.25rem;
|
||||
margin: auto 2.18375rem auto .6875rem;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: "Helvetica Neue";
|
||||
font-size: .875rem;
|
||||
font-weight: regular;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.studio-selector-button-selected {
|
||||
|
@ -140,7 +130,7 @@
|
|||
|
||||
.studio-status-icon {
|
||||
position: absolute;
|
||||
margin: .5rem .625rem .5rem 14.0625rem;
|
||||
right: .625rem;
|
||||
border-radius: .75rem;
|
||||
padding: .0625rem .075rem;
|
||||
width: 1.5rem;
|
||||
|
@ -161,30 +151,30 @@
|
|||
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;
|
||||
height: 1.4rem;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.studio-status-icon--img {
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
.studio-status-icon-with-animation {
|
||||
animation-name: bump;
|
||||
animation-duration: .25s;
|
||||
animation-timing-function: cubic-bezier(.3, -3, .6, 3);
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
.action-button-text .spinner-smooth {
|
||||
margin: .2125rem auto;
|
||||
width: 1.875rem;
|
||||
height: 1rem;
|
||||
@keyframes bump {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
|
||||
.studio-status-icon .spinner-smooth {
|
||||
position: unset; /* don't understand why neither relative nor absolute work */
|
||||
100% {
|
||||
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,24 +31,24 @@ const AddToStudioModalPresentation = ({
|
|||
includesProject={studio.includesProject}
|
||||
key={studio.id}
|
||||
title={studio.title}
|
||||
onToggleStudio={onToggleStudio}
|
||||
onClick={onToggleStudio}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
useStandardSizes
|
||||
className="mod-addToStudio"
|
||||
contentLabel={contentLabel}
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onRequestClose}
|
||||
>
|
||||
<div>
|
||||
<div className="addToStudio-modal-header">
|
||||
<div className="addToStudio-content-label">
|
||||
<div className="addToStudio-modal-header modal-header">
|
||||
<div className="addToStudio-content-label content-label">
|
||||
{contentLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div className="addToStudio-modal-content">
|
||||
<div className="addToStudio-modal-content modal-content">
|
||||
<div className="studio-list-outer-scrollbox">
|
||||
<div className="studio-list-inner-scrollbox">
|
||||
<div className="studio-list-container">
|
||||
|
@ -83,7 +83,7 @@ const AddToStudioModalPresentation = ({
|
|||
type="submit"
|
||||
>
|
||||
<div className="action-button-text">
|
||||
<Spinner mode="smooth" />
|
||||
<Spinner />
|
||||
<FormattedMessage id="addToStudio.finishing" />
|
||||
</div>
|
||||
</Button>
|
||||
|
@ -101,7 +101,6 @@ const AddToStudioModalPresentation = ({
|
|||
</FlexRow>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,29 +1,36 @@
|
|||
const truncateAtWordBoundary = require('../../../lib/truncate').truncateAtWordBoundary;
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const classNames = require('classnames');
|
||||
|
||||
const Spinner = require('../../spinner/spinner.jsx');
|
||||
const AnimateHOC = require('./animate-hoc.jsx');
|
||||
|
||||
require('./modal.scss');
|
||||
|
||||
const StudioButton = ({
|
||||
hasRequestOutstanding,
|
||||
id,
|
||||
includesProject,
|
||||
title,
|
||||
onToggleStudio
|
||||
onClick,
|
||||
wasClicked
|
||||
}) => {
|
||||
const checkmark = (
|
||||
<img
|
||||
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"
|
||||
/>
|
||||
);
|
||||
const plus = (
|
||||
<img
|
||||
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"
|
||||
/>
|
||||
);
|
||||
|
@ -35,8 +42,7 @@ const StudioButton = ({
|
|||
{'studio-selector-button-selected':
|
||||
includesProject && !hasRequestOutstanding}
|
||||
)}
|
||||
data-id={id}
|
||||
onClick={onToggleStudio}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -44,17 +50,18 @@ const StudioButton = ({
|
|||
{'studio-selector-button-text-selected': includesProject || hasRequestOutstanding},
|
||||
{'studio-selector-button-text-unselected': !includesProject && !hasRequestOutstanding}
|
||||
)}
|
||||
title={title}
|
||||
>
|
||||
{truncateAtWordBoundary(title, 25)}
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'studio-status-icon',
|
||||
{'studio-status-icon-unselected': !includesProject}
|
||||
{'studio-status-icon-unselected': !includesProject && !hasRequestOutstanding}
|
||||
)}
|
||||
>
|
||||
{(hasRequestOutstanding ?
|
||||
(<Spinner mode="smooth" />) :
|
||||
<Spinner /> :
|
||||
(includesProject ? checkmark : plus))}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -63,10 +70,10 @@ const StudioButton = ({
|
|||
|
||||
StudioButton.propTypes = {
|
||||
hasRequestOutstanding: PropTypes.bool,
|
||||
id: PropTypes.number,
|
||||
includesProject: PropTypes.bool,
|
||||
onToggleStudio: PropTypes.func,
|
||||
title: PropTypes.string
|
||||
onClick: PropTypes.func,
|
||||
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');
|
||||
|
||||
ReactModal.setAppElement(document.getElementById('view'));
|
||||
ReactModal.setAppElement(document.getElementById('app'));
|
||||
|
||||
/**
|
||||
* Container for pop up windows (See: registration window)
|
||||
|
@ -23,11 +23,19 @@ class Modal extends React.Component {
|
|||
return this.modal.portal.requestClose();
|
||||
}
|
||||
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 (
|
||||
<ReactModal
|
||||
appElement={document.getElementById('view')}
|
||||
appElement={document.getElementById('app')}
|
||||
{...bodyOpenClassNameProp}
|
||||
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),
|
||||
beforeClose: classNames('modal-content', this.props.className)
|
||||
}}
|
||||
|
@ -60,7 +68,8 @@ class Modal extends React.Component {
|
|||
Modal.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
overlayClassName: PropTypes.string
|
||||
overlayClassName: PropTypes.string,
|
||||
useStandardSizes: PropTypes.bool
|
||||
};
|
||||
|
||||
module.exports = Modal;
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
@import "../../../colors";
|
||||
@import "../../../frameless";
|
||||
|
||||
.overflow-hidden {
|
||||
/* to avoid double scroll bars this
|
||||
gets added to body while modal is open */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
margin: 3.75rem auto;
|
||||
|
@ -10,9 +16,27 @@
|
|||
padding: 0;
|
||||
width: 48.75rem;
|
||||
|
||||
.modal-content { /* content inside of content */
|
||||
display: flex;
|
||||
border-radius: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
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 {
|
||||
|
@ -43,30 +67,27 @@ $modal-close-size: 1rem;
|
|||
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. */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
margin: 1.125rem .8275rem .9375rem .8275rem;
|
||||
line-height: 1.5rem;
|
||||
justify-content: flex-end !important;
|
||||
align-items: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
@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 {
|
||||
|
@ -83,3 +104,62 @@ $modal-close-size: 1rem;
|
|||
.action-button-text {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.action-button.disabled {
|
||||
background-color: $active-dark-gray;
|
||||
}
|
||||
|
||||
.error-text
|
||||
{
|
||||
display: block;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
background-color: $ui-orange;
|
||||
padding: 1rem;
|
||||
min-height: 1rem;
|
||||
overflow: visible;
|
||||
color: $type-white;
|
||||
}
|
||||
|
||||
.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 PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const connect = require('react-redux').connect;
|
||||
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||
const injectIntl = require('react-intl').injectIntl;
|
||||
const intlShape = require('react-intl').intlShape;
|
||||
const Modal = require('../base/modal.jsx');
|
||||
const classNames = require('classnames');
|
||||
|
||||
const Form = require('../../forms/form.jsx');
|
||||
const Button = require('../../forms/button.jsx');
|
||||
|
@ -12,6 +14,7 @@ const Select = require('../../forms/select.jsx');
|
|||
const Spinner = require('../../spinner/spinner.jsx');
|
||||
const TextArea = require('../../forms/textarea.jsx');
|
||||
const FlexRow = require('../../flex-row/flex-row.jsx');
|
||||
const previewActions = require('../../../redux/preview.js');
|
||||
|
||||
require('../../forms/button.scss');
|
||||
require('./modal.scss');
|
||||
|
@ -68,12 +71,24 @@ class ReportModal extends React.Component {
|
|||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleReportCategorySelect'
|
||||
'handleCategorySelect',
|
||||
'handleValid',
|
||||
'handleInvalid'
|
||||
]);
|
||||
this.state = {reportCategory: this.props.report.category};
|
||||
this.state = {
|
||||
category: '',
|
||||
notes: '',
|
||||
valid: false
|
||||
};
|
||||
}
|
||||
handleReportCategorySelect (name, value) {
|
||||
this.setState({reportCategory: value});
|
||||
handleCategorySelect (name, value) {
|
||||
this.setState({category: value});
|
||||
}
|
||||
handleValid () {
|
||||
this.setState({valid: true});
|
||||
}
|
||||
handleInvalid () {
|
||||
this.setState({valid: false});
|
||||
}
|
||||
lookupPrompt (value) {
|
||||
const prompt = REPORT_OPTIONS.find(item => item.value === value).prompt;
|
||||
|
@ -82,33 +97,54 @@ class ReportModal extends React.Component {
|
|||
render () {
|
||||
const {
|
||||
intl,
|
||||
isConfirmed,
|
||||
isError,
|
||||
isOpen,
|
||||
isWaiting,
|
||||
onReport, // eslint-disable-line no-unused-vars
|
||||
report,
|
||||
onRequestClose,
|
||||
type,
|
||||
...modalProps
|
||||
} = this.props;
|
||||
const submitEnabled = this.state.valid && !isWaiting;
|
||||
const submitDisabledParam = submitEnabled ? {} : {disabled: 'disabled'};
|
||||
const contentLabel = intl.formatMessage({id: `report.${type}`});
|
||||
return (
|
||||
<Modal
|
||||
useStandardSizes
|
||||
className="mod-report"
|
||||
contentLabel={contentLabel}
|
||||
isOpen={report.open}
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onRequestClose}
|
||||
{...modalProps}
|
||||
>
|
||||
<div>
|
||||
<div className="report-modal-header">
|
||||
<div className="report-content-label">
|
||||
<div className="report-modal-header modal-header">
|
||||
<div className="report-content-label content-label">
|
||||
{contentLabel}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
className="report"
|
||||
onSubmit={onReport}
|
||||
onInvalid={this.handleInvalid}
|
||||
onValid={this.handleValid}
|
||||
onValidSubmit={onReport}
|
||||
>
|
||||
<div className="report-modal-content">
|
||||
<div className="report-modal-content modal-content">
|
||||
{isConfirmed ? (
|
||||
<div className="received">
|
||||
<div className="received-header">
|
||||
<FormattedMessage id="report.receivedHeader" />
|
||||
</div>
|
||||
<FormattedMessage id="report.receivedBody" />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="instructions">
|
||||
<FormattedMessage
|
||||
id={`report.${type}Instructions`}
|
||||
key={`report.${type}Instructions`}
|
||||
values={{
|
||||
CommunityGuidelinesLink: (
|
||||
<a href="/community_guidelines">
|
||||
|
@ -117,6 +153,7 @@ class ReportModal extends React.Component {
|
|||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
required
|
||||
elementWrapperClassName="report-modal-field"
|
||||
|
@ -124,10 +161,16 @@ class ReportModal extends React.Component {
|
|||
name="report_category"
|
||||
options={REPORT_OPTIONS.map(option => ({
|
||||
value: option.value,
|
||||
label: this.props.intl.formatMessage(option.label)
|
||||
label: this.props.intl.formatMessage(option.label),
|
||||
key: option.value
|
||||
}))}
|
||||
value={this.state.reportCategory}
|
||||
onChange={this.handleReportCategorySelect}
|
||||
validationErrors={{
|
||||
isDefaultRequiredValue: this.props.intl.formatMessage({
|
||||
id: 'report.reasonMissing'
|
||||
})
|
||||
}}
|
||||
value={this.state.category}
|
||||
onChange={this.handleCategorySelect}
|
||||
/>
|
||||
<TextArea
|
||||
required
|
||||
|
@ -135,8 +178,11 @@ class ReportModal extends React.Component {
|
|||
elementWrapperClassName="report-modal-field"
|
||||
label={null}
|
||||
name="notes"
|
||||
placeholder={this.lookupPrompt(this.state.reportCategory)}
|
||||
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'})
|
||||
}}
|
||||
|
@ -144,28 +190,52 @@ class ReportModal extends React.Component {
|
|||
maxLength: 500,
|
||||
minLength: 20
|
||||
}}
|
||||
value={report.notes}
|
||||
value={this.state.notes}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isError && (
|
||||
<div className="error-text">
|
||||
<FormattedMessage id="report.error" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FlexRow className="action-buttons">
|
||||
{report.waiting ? [
|
||||
<div className="action-buttons-overflow-fix">
|
||||
{isConfirmed ? (
|
||||
<Button
|
||||
className="submit-button"
|
||||
disabled="disabled"
|
||||
className="action-button submit-button"
|
||||
type="button"
|
||||
onClick={onRequestClose}
|
||||
>
|
||||
<div className="action-button-text">
|
||||
<FormattedMessage id="general.close" />
|
||||
</div>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={classNames(
|
||||
'action-button',
|
||||
'submit-button',
|
||||
{disabled: !submitEnabled}
|
||||
)}
|
||||
{...submitDisabledParam}
|
||||
key="submitButton"
|
||||
type="submit"
|
||||
>
|
||||
{isWaiting ? (
|
||||
<div className="action-button-text">
|
||||
<Spinner />
|
||||
</Button>
|
||||
] : [
|
||||
<Button
|
||||
className="submit-button"
|
||||
key="submitButton"
|
||||
type="submit"
|
||||
>
|
||||
<FormattedMessage id="report.sending" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="action-button-text">
|
||||
<FormattedMessage id="report.send" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
]}
|
||||
)}
|
||||
</div>
|
||||
</FlexRow>
|
||||
</Form>
|
||||
</div>
|
||||
|
@ -176,15 +246,26 @@ class ReportModal extends React.Component {
|
|||
|
||||
ReportModal.propTypes = {
|
||||
intl: intlShape,
|
||||
isConfirmed: PropTypes.bool,
|
||||
isError: PropTypes.bool,
|
||||
isOpen: PropTypes.bool,
|
||||
isWaiting: PropTypes.bool,
|
||||
onReport: PropTypes.func,
|
||||
onRequestClose: PropTypes.func,
|
||||
report: PropTypes.shape({
|
||||
category: PropTypes.string,
|
||||
notes: PropTypes.string,
|
||||
open: PropTypes.bool,
|
||||
waiting: PropTypes.bool
|
||||
}),
|
||||
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 "../../../frameless";
|
||||
|
||||
.mod-report * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mod-report {
|
||||
margin: 100px auto;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
width: 30rem;
|
||||
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%;
|
||||
|
||||
.instructions {
|
||||
line-height: 1.5rem;
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.received {
|
||||
margin: 0 auto;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
line-height: 1.65rem;
|
||||
|
||||
.received-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin-top: .9375rem;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
$arrow-border-width: 1rem;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(23.5rem, 0);
|
||||
margin-left: $arrow-border-width;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: $arrow-border-width;
|
||||
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
background-color: $ui-orange;
|
||||
padding: 1rem;
|
||||
max-width: 18.75rem;
|
||||
min-width: 12rem;
|
||||
min-height: 1rem;
|
||||
overflow: visible;
|
||||
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 {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: -$arrow-border-width / 2;
|
||||
top: -.5rem;
|
||||
left: calc(50% - calc(#{$arrow-border-width} / 2));
|
||||
|
||||
transform: rotate(45deg);
|
||||
transform: rotate(135deg);
|
||||
|
||||
border-bottom: 1px solid $active-gray;
|
||||
border-left: 1px solid $active-gray;
|
||||
|
@ -71,6 +70,10 @@
|
|||
height: $arrow-border-width;
|
||||
|
||||
content: "";
|
||||
|
||||
@media #{$medium-and-smaller} {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,3 +81,16 @@
|
|||
.report-modal-field {
|
||||
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;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.modal-content.mod-ttt {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
@ -76,7 +76,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
||||
@media #{$medium} {
|
||||
.modal-content.mod-ttt {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
@ -90,7 +90,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
.modal-content.mod-ttt {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.li-right-ul.mod-2016 {
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
@ -55,7 +55,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
height: 100px;
|
||||
|
||||
.ul.mod-2016 {
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.li-right-ul.mod-2018 {
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
@ -60,7 +60,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
height: 100px;
|
||||
|
||||
.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 messageCountActions = require('../../../redux/message-count.js');
|
||||
const navigationActions = require('../../../redux/navigation.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 Dropdown = require('../../dropdown/dropdown.jsx');
|
||||
const Form = require('../../forms/form.jsx');
|
||||
const Input = require('../../forms/input.jsx');
|
||||
const log = require('../../../lib/log.js');
|
||||
const Login = require('../../login/login.jsx');
|
||||
const Modal = require('../../modal/base/modal.jsx');
|
||||
const LoginDropdown = require('../../login/login-dropdown.jsx');
|
||||
const CanceledDeletionModal = require('../../login/canceled-deletion-modal.jsx');
|
||||
const NavigationBox = require('../base/navigation.jsx');
|
||||
const Registration = require('../../registration/registration.jsx');
|
||||
const AccountNav = require('./accountnav.jsx');
|
||||
|
||||
require('./navigation.scss');
|
||||
|
||||
|
@ -29,34 +27,16 @@ class Navigation extends React.Component {
|
|||
super(props);
|
||||
bindAll(this, [
|
||||
'getProfileUrl',
|
||||
'handleJoinClick',
|
||||
'handleLoginClick',
|
||||
'handleCloseLogin',
|
||||
'handleLogIn',
|
||||
'handleLogOut',
|
||||
'handleAccountNavClick',
|
||||
'handleCloseAccountNav',
|
||||
'showCanceledDeletion',
|
||||
'handleCloseCanceledDeletion',
|
||||
'handleCloseRegistration',
|
||||
'handleCompleteRegistration',
|
||||
'handleSearchSubmit'
|
||||
]);
|
||||
this.state = {
|
||||
accountNavOpen: false,
|
||||
canceledDeletionOpen: false,
|
||||
loginOpen: false,
|
||||
loginError: null,
|
||||
registrationOpen: false,
|
||||
messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
if (this.props.session.session.user) {
|
||||
if (this.props.user) {
|
||||
const intervalId = setInterval(() => {
|
||||
this.props.dispatch(
|
||||
messageCountActions.getCount(this.props.session.session.user.username)
|
||||
);
|
||||
this.props.getMessageCount(this.props.user.username);
|
||||
}, 120000); // check for new messages every 2 mins.
|
||||
this.setState({ // eslint-disable-line react/no-did-mount-set-state
|
||||
messageCountIntervalId: intervalId
|
||||
|
@ -64,16 +44,11 @@ class Navigation extends React.Component {
|
|||
}
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.session.session.user !== this.props.session.session.user) {
|
||||
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
||||
loginOpen: false,
|
||||
accountNavOpen: false
|
||||
});
|
||||
if (this.props.session.session.user) {
|
||||
if (prevProps.user !== this.props.user) {
|
||||
this.props.closeAccountMenus();
|
||||
if (this.props.user) {
|
||||
const intervalId = setInterval(() => {
|
||||
this.props.dispatch(
|
||||
messageCountActions.getCount(this.props.session.session.user.username)
|
||||
);
|
||||
this.props.getMessageCount(this.props.user.username);
|
||||
}, 120000); // check for new messages every 2 mins.
|
||||
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
||||
messageCountIntervalId: intervalId
|
||||
|
@ -81,7 +56,7 @@ class Navigation extends React.Component {
|
|||
} else {
|
||||
// clear message count check, and set to default id.
|
||||
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
|
||||
messageCountIntervalId: -1
|
||||
});
|
||||
|
@ -92,102 +67,25 @@ class Navigation extends React.Component {
|
|||
// clear message interval if it exists
|
||||
if (this.state.messageCountIntervalId !== -1) {
|
||||
clearInterval(this.state.messageCountIntervalId);
|
||||
this.props.dispatch(messageCountActions.setCount(0));
|
||||
this.props.setMessageCount(0);
|
||||
this.setState({
|
||||
messageCountIntervalId: -1
|
||||
});
|
||||
}
|
||||
}
|
||||
getProfileUrl () {
|
||||
if (!this.props.session.session.user) return;
|
||||
return `/users/${this.props.session.session.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();
|
||||
if (!this.props.user) return;
|
||||
return `/users/${this.props.user.username}/`;
|
||||
}
|
||||
handleSearchSubmit (formData) {
|
||||
window.location.href = `/search/projects?q=${encodeURIComponent(formData.q)}`;
|
||||
}
|
||||
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 (
|
||||
<NavigationBox
|
||||
className={classNames({
|
||||
'logged-in': this.props.session.session.user
|
||||
'logged-in': this.props.user
|
||||
})}
|
||||
>
|
||||
<ul>
|
||||
|
@ -235,7 +133,7 @@ class Navigation extends React.Component {
|
|||
</Form>
|
||||
</li>
|
||||
{this.props.session.status === sessionActions.Status.FETCHED ? (
|
||||
this.props.session.session.user ? [
|
||||
this.props.user ? [
|
||||
<li
|
||||
className="link right messages"
|
||||
key="messages"
|
||||
|
@ -268,66 +166,18 @@ class Navigation extends React.Component {
|
|||
className="link right account-nav"
|
||||
key="account-nav"
|
||||
>
|
||||
<a
|
||||
className={classNames({
|
||||
'user-info': true,
|
||||
'open': this.state.accountNavOpen
|
||||
})}
|
||||
href="#"
|
||||
onClick={this.handleAccountNavClick}
|
||||
>
|
||||
<Avatar
|
||||
alt=""
|
||||
src={this.props.session.session.user.thumbnailUrl}
|
||||
<AccountNav
|
||||
classroomId={this.props.user.classroomId}
|
||||
isEducator={this.props.permissions.educator}
|
||||
isOpen={this.props.accountNavOpen}
|
||||
isStudent={this.props.permissions.student}
|
||||
profileUrl={this.getProfileUrl()}
|
||||
thumbnailUrl={this.props.user.thumbnailUrl}
|
||||
username={this.props.user.username}
|
||||
onClick={this.props.handleToggleAccountNav}
|
||||
onClickLogout={this.props.handleLogOut}
|
||||
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
|
||||
|
@ -336,16 +186,13 @@ class Navigation extends React.Component {
|
|||
>
|
||||
<a
|
||||
href="#"
|
||||
onClick={this.handleJoinClick}
|
||||
onClick={this.props.handleOpenRegistration}
|
||||
>
|
||||
<FormattedMessage id="general.joinScratch" />
|
||||
</a>
|
||||
</li>,
|
||||
<Registration
|
||||
isOpen={this.state.registrationOpen}
|
||||
key="registration"
|
||||
onRegistrationDone={this.handleCompleteRegistration}
|
||||
onRequestClose={this.handleCloseRegistration}
|
||||
/>,
|
||||
<li
|
||||
className="link right login-item"
|
||||
|
@ -355,53 +202,31 @@ class Navigation extends React.Component {
|
|||
className="ignore-react-onclickoutside"
|
||||
href="#"
|
||||
key="login-link"
|
||||
onClick={this.handleLoginClick}
|
||||
onClick={this.props.handleToggleLoginOpen}
|
||||
>
|
||||
<FormattedMessage id="general.signIn" />
|
||||
</a>
|
||||
<Dropdown
|
||||
className="login-dropdown with-arrow"
|
||||
isOpen={this.state.loginOpen}
|
||||
<LoginDropdown
|
||||
key="login-dropdown"
|
||||
onRequestClose={this.handleCloseLogin}
|
||||
>
|
||||
<Login
|
||||
error={this.state.loginError}
|
||||
onLogIn={this.handleLogIn}
|
||||
/>
|
||||
</Dropdown>
|
||||
</li>
|
||||
]) : []}
|
||||
</ul>
|
||||
<Modal
|
||||
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>
|
||||
<CanceledDeletionModal />
|
||||
</NavigationBox>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
permissions: PropTypes.shape({
|
||||
admin: PropTypes.bool,
|
||||
|
@ -412,16 +237,15 @@ Navigation.propTypes = {
|
|||
}),
|
||||
searchTerm: PropTypes.string,
|
||||
session: PropTypes.shape({
|
||||
session: PropTypes.shape({
|
||||
status: 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
|
||||
})
|
||||
}),
|
||||
status: PropTypes.string
|
||||
}),
|
||||
unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
||||
};
|
||||
|
||||
Navigation.defaultProps = {
|
||||
|
@ -431,12 +255,48 @@ Navigation.defaultProps = {
|
|||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountNavOpen: state.navigation && state.navigation.accountNavOpen,
|
||||
session: state.session,
|
||||
permissions: state.permissions,
|
||||
searchTerm: state.navigation.searchTerm,
|
||||
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);
|
||||
|
|
|
@ -163,78 +163,10 @@
|
|||
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
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
#navigation .inner {
|
||||
width: $cols4;
|
||||
|
||||
|
@ -242,20 +174,6 @@
|
|||
&.login-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.account-nav {
|
||||
margin-left: 0;
|
||||
|
||||
> a {
|
||||
.avatar {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.create,
|
||||
|
@ -272,7 +190,7 @@
|
|||
|
||||
|
||||
//6 columns
|
||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
||||
@media #{$medium} {
|
||||
#navigation .inner {
|
||||
width: $cols6;
|
||||
|
||||
|
@ -280,20 +198,6 @@
|
|||
&.login-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.account-nav {
|
||||
margin-left: 0;
|
||||
|
||||
> a {
|
||||
.avatar {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discuss,
|
||||
|
@ -308,13 +212,12 @@
|
|||
}
|
||||
|
||||
//8 columns
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
#navigation .inner {
|
||||
width: $cols8;
|
||||
|
||||
> ul > li {
|
||||
&.login-item,
|
||||
&.account-nav {
|
||||
&.login-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,19 +6,19 @@
|
|||
font-size: 4.5rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
||||
@media #{$medium} {
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
h1 {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@
|
|||
font-size: 4rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
h1,
|
||||
.title-banner-h1.mod-2017 {
|
||||
font-size: 2.5rem;
|
||||
|
@ -96,7 +96,7 @@
|
|||
width: 125px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
img {
|
||||
transform: translate(0, 5px);
|
||||
width: 85px;
|
||||
|
@ -108,7 +108,7 @@ section {
|
|||
padding: 64px 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
#view {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
const bindAll = require('lodash.bindall');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const connect = require('react-redux').connect;
|
||||
|
||||
const IframeModal = require('../modal/iframe/modal.jsx');
|
||||
const navigationActions = require('../../redux/navigation.js');
|
||||
|
||||
require('./registration.scss');
|
||||
|
||||
|
@ -26,7 +28,7 @@ class Registration extends React.Component {
|
|||
handleMessage (e) {
|
||||
if (e.origin !== window.location.origin) 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') {
|
||||
this.registrationIframe.contentWindow.location.reload();
|
||||
}
|
||||
|
@ -47,16 +49,32 @@ class Registration extends React.Component {
|
|||
}}
|
||||
isOpen={this.props.isOpen}
|
||||
src="/accounts/standalone-registration/"
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
onRequestClose={this.props.handleCloseRegistration}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Registration.propTypes = {
|
||||
isOpen: PropTypes.bool,
|
||||
onRegistrationDone: PropTypes.func,
|
||||
onRequestClose: PropTypes.func
|
||||
handleCloseRegistration: PropTypes.func,
|
||||
handleCompleteRegistration: 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;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.modal-content.mod-registration {
|
||||
width: 100%;
|
||||
overflow: scroll;
|
||||
|
|
|
@ -444,18 +444,20 @@ class DemographicsStep extends React.Component {
|
|||
handleChooseGender (name, gender) {
|
||||
this.setState({otherDisabled: gender !== 'other'});
|
||||
}
|
||||
handleValidSubmit (formData, reset, invalidate) {
|
||||
handleValidSubmit (formData) {
|
||||
return this.props.onNextStep(formData);
|
||||
}
|
||||
isValidBirthdate (year, month) {
|
||||
const birthdate = new Date(
|
||||
formData.user.birth.year,
|
||||
formData.user.birth.month - 1,
|
||||
year,
|
||||
month - 1,
|
||||
1
|
||||
);
|
||||
if (((Date.now() - birthdate) / (24 * 3600 * 1000 * 365.25)) < this.props.birthOffset) {
|
||||
return invalidate({
|
||||
'user.birth.year': this.props.intl.formatMessage({id: 'teacherRegistration.validationAge'})
|
||||
});
|
||||
return (((Date.now() - birthdate) / (24 * 3600 * 1000 * 365.25)) >= this.props.birthOffset);
|
||||
}
|
||||
return this.props.onNextStep(formData);
|
||||
birthDateValidator (values) {
|
||||
const isValid = this.isValidBirthdate(values['user.birth.year'], values['user.birth.month']);
|
||||
return isValid ? true : this.props.intl.formatMessage({id: 'teacherRegistration.validationAge'});
|
||||
}
|
||||
render () {
|
||||
const countryOptions = getCountryOptions(this.props.intl, DEFAULT_COUNTRY);
|
||||
|
@ -485,6 +487,9 @@ class DemographicsStep extends React.Component {
|
|||
}
|
||||
name="user.birth.month"
|
||||
options={this.getMonthOptions()}
|
||||
validations={{
|
||||
birthDateVal: values => this.birthDateValidator(values)
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
required
|
||||
|
|
|
@ -155,7 +155,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.registration-step {
|
||||
&.demographics-step {
|
||||
.radio {
|
||||
|
@ -174,7 +174,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.registration-step {
|
||||
.form {
|
||||
text-align: left;
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
.slide {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ a.social-messages-profile-link {
|
|||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.social-message {
|
||||
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 {
|
||||
text-align: left;
|
||||
}
|
||||
|
|
|
@ -1,29 +1,28 @@
|
|||
const range = require('lodash.range');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classNames = require('classnames');
|
||||
|
||||
require('./spinner.scss');
|
||||
|
||||
// Adapted from http://tobiasahlin.com/spinkit/
|
||||
const Spinner = ({
|
||||
mode
|
||||
}) => {
|
||||
const spinnerClassName = (mode === 'smooth' ? 'spinner-smooth' : 'spinner');
|
||||
const spinnerDivCount = (mode === 'smooth' ? 24 : 12);
|
||||
return (
|
||||
<div className={spinnerClassName}>
|
||||
{range(1, spinnerDivCount + 1).map(id => (
|
||||
<div
|
||||
className={`circle${id} circle`}
|
||||
key={`circle${id}`}
|
||||
className,
|
||||
color
|
||||
}) => (
|
||||
<img
|
||||
alt="loading animation"
|
||||
className={classNames('studio-status-icon-spinner', className)}
|
||||
src={`/svgs/modal/spinner-${color}.svg`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
Spinner.defaultProps = {
|
||||
color: 'white'
|
||||
};
|
||||
|
||||
Spinner.propTypes = {
|
||||
mode: PropTypes.string
|
||||
className: PropTypes.string,
|
||||
color: PropTypes.oneOf(['white', 'blue', 'transparent-gray'])
|
||||
};
|
||||
|
||||
module.exports = Spinner;
|
||||
|
|
|
@ -1,118 +1,44 @@
|
|||
@import "../../colors";
|
||||
|
||||
.spinner {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&:before {
|
||||
display: block;
|
||||
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;
|
||||
}
|
||||
}
|
||||
.studio-status-icon-spinner {
|
||||
/* This class can be used on an icon that should spin.
|
||||
It first plays the intro animation, then spins forever. */
|
||||
animation-name: intro, spin;
|
||||
animation-duration: .25s, .5s;
|
||||
animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear;
|
||||
animation-delay: 0s, .25s;
|
||||
animation-iteration-count: 1, infinite;
|
||||
animation-direction: normal;
|
||||
width: 1.4rem; /* standard is 1.4 rem but can be overwritten by parent */
|
||||
height: 1.4rem;
|
||||
-webkit-animation-name: intro, spin;
|
||||
-webkit-animation-duration: .25s, .5s;
|
||||
-webkit-animation-iteration-count: 1, infinite;
|
||||
-webkit-animation-delay: 0s, .25s;
|
||||
-webkit-animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
@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 intro {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@keyframes circleFadeDelay {
|
||||
0%,
|
||||
39%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*********************/
|
||||
/* type === "smooth" */
|
||||
/*********************/
|
||||
|
||||
.spinner-smooth {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&:before {
|
||||
display: block;
|
||||
animation: circleFadeDelaySmooth 1.8s infinite ease-in-out both;
|
||||
margin: 0 auto;
|
||||
border-radius: 100%;
|
||||
background-color: $ui-white;
|
||||
width: 30%;
|
||||
height: 20%;
|
||||
content: "";
|
||||
|
||||
.white & {
|
||||
background-color: darken($ui-blue, 8%);
|
||||
}
|
||||
}
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
-webkit-transform: rotate(0);
|
||||
}
|
||||
|
||||
@for $i from 1 through 24 {
|
||||
$rotation: 15deg * ($i - 1);
|
||||
$delay: -1.9s + $i * .075;
|
||||
|
||||
.circle#{$i} {
|
||||
transform: rotate($rotation);
|
||||
|
||||
&:before {
|
||||
animation-delay: $delay;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@keyframes circleFadeDelaySmooth {
|
||||
0%,
|
||||
35% {
|
||||
opacity: 0;
|
||||
},
|
||||
40% {
|
||||
opacity: 1;
|
||||
100% {
|
||||
transform: rotate(359deg);
|
||||
-webkit-transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ const Thumbnail = props => {
|
|||
<a
|
||||
href={props.href}
|
||||
key="titleElement"
|
||||
title={props.title}
|
||||
>
|
||||
{props.title}
|
||||
</a>
|
||||
|
|
|
@ -16,14 +16,13 @@
|
|||
|
||||
.thumbnail {
|
||||
margin: 7px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 1px $active-gray;
|
||||
background-color: $ui-white;
|
||||
padding-bottom: 4px;
|
||||
width: $thumbnail-width;
|
||||
|
||||
.thumbnail-image {
|
||||
margin: 8px auto;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 1px $active-gray;
|
||||
background-color: $ui-white;
|
||||
width: $thumbnail-inner-width;
|
||||
}
|
||||
|
||||
|
@ -45,10 +44,19 @@
|
|||
.thumbnail-title {
|
||||
float: left;
|
||||
max-width: 164px;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
|
||||
.thumbnail-creator a {
|
||||
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 {
|
||||
display: block;
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ const Raven = require('raven-js');
|
|||
};
|
||||
|
||||
window._locale = updateLocale();
|
||||
document.documentElement.lang = window._locale;
|
||||
})();
|
||||
|
||||
/**
|
||||
|
|
|
@ -106,6 +106,8 @@
|
|||
"navigation.signOut": "Sign out",
|
||||
|
||||
"extensionHeader.requirements": "Requirements",
|
||||
"extensionInstallation.addExtension": "In the editor, click on the \"Add Extensions\" button on the lower left.",
|
||||
|
||||
|
||||
"oschooser.choose": "Choose your OS:",
|
||||
|
||||
|
@ -172,6 +174,7 @@
|
|||
"registration.welcomeStepTitle": "Hurray! Welcome to Scratch!",
|
||||
|
||||
"thumbnail.by": "by",
|
||||
"report.error": "Something went wrong when trying to send your message. Please try again.",
|
||||
"report.project": "Report Project",
|
||||
"report.projectInstructions": "From the dropdown below, please select the reason why you feel this project is disrespectful or inappropriate or otherwise breaks the {CommunityGuidelinesLink}.",
|
||||
"report.CommunityGuidelinesLinkText": "Scratch Community Guidelines",
|
||||
|
@ -181,8 +184,11 @@
|
|||
"report.reasonScary": "Too Violent or Scary",
|
||||
"report.reasonLanguage": "Inappropriate Language",
|
||||
"report.reasonMusic": "Inappropriate Music",
|
||||
"report.reasonMissing": "Please select a reason",
|
||||
"report.reasonImage": "Inappropriate Images",
|
||||
"report.reasonPersonal": "Sharing Personal Contact Information",
|
||||
"report.receivedHeader": "We have received your report!",
|
||||
"report.receivedBody": "The Scratch Team will review the project based on the Scratch community guidelines.",
|
||||
"report.promptPlaceholder": "Select a reason why above.",
|
||||
"report.promptCopy": "Please provide a link to the original project",
|
||||
"report.promptUncredited": "Please provide links to the uncredited content",
|
||||
|
@ -194,5 +200,47 @@
|
|||
"report.promptImage": "Please say the name of the sprite or the backdrop with the inappropriate image",
|
||||
"report.tooLongError": "That's too long! Please find a way to shorten your text.",
|
||||
"report.tooShortError": "That's too short. Please describe in detail what's inappropriate or disrespectful about the project.",
|
||||
"report.send": "Send"
|
||||
"report.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"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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.
|
||||
|
@ -14,8 +14,8 @@ class Storage extends ScratchStorage {
|
|||
projectAsset => {
|
||||
const [projectId, revision] = projectAsset.assetId.split('.');
|
||||
return revision ?
|
||||
`${PROJECT_SERVER}/internalapi/project/${projectId}/get/${revision}` :
|
||||
`${PROJECT_SERVER}/internalapi/project/${projectId}/get/`;
|
||||
`${PROJECT_HOST}/internalapi/project/${projectId}/get/${revision}` :
|
||||
`${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 defaults = require('lodash.defaults');
|
||||
|
||||
const api = require('../lib/api');
|
||||
const log = require('../lib/log.js');
|
||||
const sessionActions = require('./session.js');
|
||||
|
||||
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) => {
|
||||
if (typeof state === 'undefined') {
|
||||
state = '';
|
||||
state = module.exports.getInitialState();
|
||||
}
|
||||
switch (action.type) {
|
||||
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:
|
||||
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 => ({
|
||||
type: Types.SET_SEARCH_TERM,
|
||||
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 async = require('async');
|
||||
const merge = require('lodash.merge');
|
||||
|
||||
const api = require('../lib/api');
|
||||
const log = require('../lib/log');
|
||||
|
@ -19,6 +22,7 @@ module.exports.getInitialState = () => ({
|
|||
original: module.exports.Status.NOT_FETCHED,
|
||||
parent: module.exports.Status.NOT_FETCHED,
|
||||
remixes: module.exports.Status.NOT_FETCHED,
|
||||
report: module.exports.Status.NOT_FETCHED,
|
||||
projectStudios: module.exports.Status.NOT_FETCHED,
|
||||
curatedStudios: module.exports.Status.NOT_FETCHED,
|
||||
studioRequests: {}
|
||||
|
@ -26,6 +30,7 @@ module.exports.getInitialState = () => ({
|
|||
projectInfo: {},
|
||||
remixes: [],
|
||||
comments: [],
|
||||
replies: {},
|
||||
faved: false,
|
||||
loved: false,
|
||||
original: {},
|
||||
|
@ -79,7 +84,56 @@ module.exports.previewReducer = (state, action) => {
|
|||
});
|
||||
case 'SET_COMMENTS':
|
||||
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':
|
||||
return Object.assign({}, state, {
|
||||
|
@ -145,6 +199,16 @@ module.exports.setProjectStudios = 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 => ({
|
||||
type: 'SET_CURATED_STUDIOS',
|
||||
items: items
|
||||
|
@ -172,6 +236,55 @@ module.exports.setStudioFetchStatus = (studioId, 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 => {
|
||||
const opts = {
|
||||
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 => {
|
||||
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHING));
|
||||
if (faved) {
|
||||
api({
|
||||
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 => {
|
||||
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.FETCHING));
|
||||
if (loved) {
|
||||
api({
|
||||
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 => {
|
||||
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING));
|
||||
api({
|
||||
uri: `/projects/${id}`,
|
||||
authentication: token,
|
||||
|
@ -490,3 +657,84 @@ module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
|
|||
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 permissionsReducer = require('./permissions.js').permissionsReducer;
|
||||
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`.
|
||||
|
@ -18,8 +19,9 @@ const sessionReducer = require('./session.js').sessionReducer;
|
|||
module.exports = opts => {
|
||||
opts = opts || {};
|
||||
return combineReducers(defaults(opts, {
|
||||
session: sessionReducer,
|
||||
messageCount: messageCountReducer,
|
||||
navigation: navigationReducer,
|
||||
permissions: permissionsReducer,
|
||||
messageCount: messageCountReducer
|
||||
session: sessionReducer
|
||||
}));
|
||||
};
|
||||
|
|
|
@ -77,13 +77,13 @@ module.exports.getActivity = (username, token) => (dispatch => {
|
|||
api({
|
||||
uri: `/users/${username}/following/users/activity?limit=5`,
|
||||
authentication: token
|
||||
}, (err, body) => {
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined') {
|
||||
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError('No session content'));
|
||||
return;
|
||||
|
@ -100,13 +100,13 @@ module.exports.getFeaturedGlobal = () => (dispatch => {
|
|||
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.FETCHING));
|
||||
api({
|
||||
uri: '/proxy/featured'
|
||||
}, (err, body) => {
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined') {
|
||||
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError('No session content'));
|
||||
return;
|
||||
|
@ -126,13 +126,13 @@ module.exports.getSharedByFollowing = (username, token) => (dispatch => {
|
|||
api({
|
||||
uri: `/users/${username}/following/users/projects`,
|
||||
authentication: token
|
||||
}, (err, body) => {
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined') {
|
||||
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError('No session content'));
|
||||
return;
|
||||
|
@ -152,13 +152,13 @@ module.exports.getInStudiosFollowing = (username, token) => (dispatch => {
|
|||
api({
|
||||
uri: `/users/${username}/following/studios/projects`,
|
||||
authentication: token
|
||||
}, (err, body) => {
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined') {
|
||||
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError('No session content'));
|
||||
return;
|
||||
|
@ -178,13 +178,13 @@ module.exports.getLovedByFollowing = (username, token) => (dispatch => {
|
|||
api({
|
||||
uri: `/users/${username}/following/users/loves`,
|
||||
authentication: token
|
||||
}, (err, body) => {
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined') {
|
||||
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError('No session content'));
|
||||
return;
|
||||
|
|
|
@ -308,6 +308,13 @@
|
|||
"view": "wedo2/wedo2",
|
||||
"title": "LEGO WeDo 2.0"
|
||||
},
|
||||
{
|
||||
"name": "wedo-legacy",
|
||||
"pattern": "^/wedo-legacy/?$",
|
||||
"routeAlias": "/wedo-legacy/?$",
|
||||
"view": "wedo2-legacy/wedo2",
|
||||
"title": "LEGO WeDo"
|
||||
},
|
||||
{
|
||||
"name": "ev3",
|
||||
"pattern": "^/ev3/?$",
|
||||
|
|
|
@ -35,8 +35,10 @@ const Components = () => (
|
|||
<Box title="Carousel component in a box!">
|
||||
<Carousel />
|
||||
</Box>
|
||||
<h1>This is a Spinner</h1>
|
||||
<Spinner />
|
||||
<h1>This is a blue Spinner</h1>
|
||||
<Spinner
|
||||
color="blue"
|
||||
/>
|
||||
<h1>Colors</h1>
|
||||
<div className="colors">
|
||||
<span className="ui-blue">$ui-blue</span>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
@import "../../../../frameless";
|
||||
|
||||
#view {
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@
|
|||
}
|
||||
|
||||
//8 columns
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.details {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
img {
|
||||
width: 50%;
|
||||
}
|
||||
|
@ -37,7 +37,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.uneven {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -80,7 +80,7 @@
|
|||
margin: 15px 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.flex-row {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -156,7 +156,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.flex-row {
|
||||
table {
|
||||
width: 100%;
|
||||
|
@ -164,7 +164,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $mobile) and (max-width: $desktop - 1) {
|
||||
@media #{$medium-and-intermediate} {
|
||||
.flex-row {
|
||||
table {
|
||||
width: $cols6;
|
||||
|
@ -172,7 +172,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.flex-row {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
h3 {
|
||||
display: none;
|
||||
margin-top: 0;
|
||||
|
@ -60,7 +60,7 @@
|
|||
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
@ -85,7 +85,7 @@
|
|||
max-width: 125px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
margin: .5rem;
|
||||
width: 125px;
|
||||
}
|
||||
|
@ -93,7 +93,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
.index {
|
||||
.flex-row {
|
||||
align-items: center;
|
||||
|
|
|
@ -19,13 +19,13 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
img {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
img {
|
||||
width: 70%;
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
|||
.lodging {
|
||||
text-align: left;
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.uneven {
|
||||
.short {
|
||||
display: none;
|
||||
|
@ -50,7 +50,7 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.flex-row {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
@ -69,13 +69,13 @@
|
|||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
ul {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
div {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -105,7 +105,7 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.sub-nav {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
@ -124,7 +124,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
.inner {
|
||||
h2 {
|
||||
&.breaking-title {
|
||||
|
|
|
@ -79,7 +79,7 @@ td {
|
|||
color: $type-white;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.index.mod-2017 {
|
||||
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 {
|
||||
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 {
|
||||
text-align: left;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
@import "../../../../frameless";
|
||||
|
||||
#view {
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@
|
|||
}
|
||||
|
||||
//8 columns
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.details {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
img {
|
||||
width: 50%;
|
||||
}
|
||||
|
@ -37,7 +37,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.uneven {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -85,7 +85,7 @@
|
|||
margin: 15px 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.flex-row {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -163,7 +163,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.flex-row {
|
||||
table {
|
||||
width: 100%;
|
||||
|
@ -171,7 +171,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $mobile) and (max-width: $desktop - 1) {
|
||||
@media #{$medium-and-intermediate} {
|
||||
.flex-row {
|
||||
table {
|
||||
width: $cols6;
|
||||
|
@ -179,7 +179,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.flex-row {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
h3 {
|
||||
display: none;
|
||||
margin-top: 0;
|
||||
|
@ -72,7 +72,7 @@
|
|||
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
@ -97,7 +97,7 @@
|
|||
max-width: 125px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
margin: .5rem;
|
||||
width: 125px;
|
||||
}
|
||||
|
@ -105,7 +105,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
.index {
|
||||
.flex-row {
|
||||
align-items: center;
|
||||
|
|
|
@ -19,13 +19,13 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
img {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
img {
|
||||
width: 70%;
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
|||
.lodging {
|
||||
text-align: left;
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.uneven {
|
||||
.short {
|
||||
display: none;
|
||||
|
@ -50,7 +50,7 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.flex-row {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
@ -69,13 +69,13 @@
|
|||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
ul {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
div {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -105,7 +105,7 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.sub-nav {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
@ -124,7 +124,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1) {
|
||||
@media #{$medium-and-smaller} {
|
||||
.inner {
|
||||
h2 {
|
||||
&.breaking-title {
|
||||
|
|
|
@ -86,6 +86,14 @@ const Credits = () => (
|
|||
<span className="name">DD Liu</span>
|
||||
</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>
|
||||
<img
|
||||
alt="Shruti Avatar"
|
||||
|
|
|
@ -170,7 +170,7 @@ $developer-spot: $ui-aqua;
|
|||
}
|
||||
|
||||
//4 columns
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
#view {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -196,7 +196,7 @@ $developer-spot: $ui-aqua;
|
|||
}
|
||||
|
||||
//6 columns
|
||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
||||
@media #{$medium} {
|
||||
#view {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -216,7 +216,7 @@ $developer-spot: $ui-aqua;
|
|||
}
|
||||
|
||||
//8 columns
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
#view {
|
||||
text-align: left;
|
||||
}
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
color: $ui-white;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.inner {
|
||||
.installation-column {
|
||||
max-width: 100%;
|
||||
|
@ -119,7 +119,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
.three-col-row {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
|
@ -31,19 +31,25 @@ class EV3 extends ExtensionLanding {
|
|||
render () {
|
||||
return (
|
||||
<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">
|
||||
<h2><img src="/images/ev3/ev3.svg" />LEGO MINDSTORMS EV3</h2>
|
||||
<h1><img
|
||||
alt=""
|
||||
src="/images/ev3/ev3.svg"
|
||||
/>LEGO MINDSTORMS EV3</h1>
|
||||
<FormattedMessage
|
||||
id="ev3.headerText"
|
||||
values={{
|
||||
ev3Link: (
|
||||
<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"
|
||||
target="_blank"
|
||||
>
|
||||
LEGO MINDSTORMS EV3
|
||||
LEGO MINDSTORMS Education EV3
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
|
@ -51,11 +57,17 @@ class EV3 extends ExtensionLanding {
|
|||
</FlexRow>
|
||||
<ExtensionRequirements>
|
||||
<span>
|
||||
<img src="/svgs/extensions/windows.svg" />
|
||||
<img
|
||||
alt=""
|
||||
src="/svgs/extensions/windows.svg"
|
||||
/>
|
||||
Windows 10+
|
||||
</span>
|
||||
<span>
|
||||
<img src="/svgs/extensions/mac.svg" />
|
||||
<img
|
||||
alt=""
|
||||
src="/svgs/extensions/mac.svg"
|
||||
/>
|
||||
macOS 10.13+
|
||||
</span>
|
||||
<span>
|
||||
|
@ -63,7 +75,10 @@ class EV3 extends ExtensionLanding {
|
|||
Bluetooth
|
||||
</span>
|
||||
<span>
|
||||
<img src="/svgs/extensions/scratch-link.svg" />
|
||||
<img
|
||||
alt=""
|
||||
src="/svgs/extensions/scratch-link.svg"
|
||||
/>
|
||||
Scratch Link
|
||||
</span>
|
||||
</ExtensionRequirements>
|
||||
|
@ -82,13 +97,20 @@ class EV3 extends ExtensionLanding {
|
|||
<Steps>
|
||||
<Step number={1}>
|
||||
<div className="step-image">
|
||||
<img src="/images/ev3/ev3-connect-1.png" />
|
||||
<img
|
||||
alt=""
|
||||
src="/images/ev3/ev3-connect-1.png"
|
||||
/>
|
||||
</div>
|
||||
<p><FormattedMessage id="ev3.turnOnEV3" /></p>
|
||||
</Step>
|
||||
<Step number={2}>
|
||||
<div className="step-image">
|
||||
<img src="/images/ev3/ev3-connect-2.png" />
|
||||
<img
|
||||
alt=""
|
||||
className="screenshot"
|
||||
src="/images/ev3/ev3-connect-2.png"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
|
@ -109,7 +131,11 @@ class EV3 extends ExtensionLanding {
|
|||
</Step>
|
||||
<Step number={3}>
|
||||
<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>
|
||||
<p><FormattedMessage id="ev3.addExtension" /></p>
|
||||
</Step>
|
||||
|
@ -119,19 +145,30 @@ class EV3 extends ExtensionLanding {
|
|||
<Steps>
|
||||
<Step>
|
||||
<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>
|
||||
<p><FormattedMessage id="ev3.acceptConnection" /></p>
|
||||
</Step>
|
||||
<Step>
|
||||
<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>
|
||||
<p><FormattedMessage id="ev3.acceptPasscode" /></p>
|
||||
</Step>
|
||||
<Step>
|
||||
<div className="step-image">
|
||||
<img
|
||||
alt={this.props.intl.formatMessage({id: `ev3.imgAlt${
|
||||
this.state.OS === OS_ENUM.WINDOWS ?
|
||||
'WaitForWindows' :
|
||||
'EnterPasscodeMac'
|
||||
}`})}
|
||||
className="screenshot"
|
||||
src={`/images/ev3/${
|
||||
this.state.OS === OS_ENUM.WINDOWS ?
|
||||
|
@ -170,7 +207,10 @@ class EV3 extends ExtensionLanding {
|
|||
/>
|
||||
</span>
|
||||
<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>
|
||||
</Step>
|
||||
<Step
|
||||
|
@ -188,7 +228,10 @@ class EV3 extends ExtensionLanding {
|
|||
/>
|
||||
</span>
|
||||
<div className="step-image">
|
||||
<img src="/images/ev3/motor-turn-block.png" />
|
||||
<img
|
||||
alt=""
|
||||
src="/images/ev3/motor-turn-block.png"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
@ -196,20 +239,23 @@ class EV3 extends ExtensionLanding {
|
|||
<h3><FormattedMessage id="ev3.starterProjects" /></h3>
|
||||
<Steps>
|
||||
<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'})}
|
||||
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltWaveHello'})}
|
||||
imageSrc="/images/ev3/starter-wave-hello.png"
|
||||
title={this.props.intl.formatMessage({id: 'ev3.waveHelloTitle'})}
|
||||
/>
|
||||
<ProjectCard
|
||||
cardUrl="https://downloads.scratch.mit.edu/ev3/ev3-distance-instrument.sb3"
|
||||
cardUrl="https://beta.scratch.mit.edu/#239076020"
|
||||
description={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentDescription'})}
|
||||
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltDistanceInstrument'})}
|
||||
imageSrc="/images/ev3/starter-distance-instrument.png"
|
||||
title={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentTitle'})}
|
||||
/>
|
||||
<ProjectCard
|
||||
cardUrl="https://downloads.scratch.mit.edu/ev3/ev3-space-tacos.sb3"
|
||||
cardUrl="https://beta.scratch.mit.edu/#239076044"
|
||||
description={this.props.intl.formatMessage({id: 'ev3.spaceTacosDescription'})}
|
||||
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltSpaceTacos'})}
|
||||
imageSrc="/images/ev3/starter-flying-game.png"
|
||||
title={this.props.intl.formatMessage({id: 'ev3.spaceTacosTitle'})}
|
||||
/>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.ev3 {
|
||||
.extension-header {
|
||||
background-color: $ui-aqua;
|
||||
background-color: $ui-orange;
|
||||
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.otherComputerConnectedText": "Only one computer can be connected to an EV3 at a time. If you have another computer connected to your EV3, disconnect the EV3 or close Scratch on that computer and try again.",
|
||||
"ev3.updateFirmwareTitle": "Try updating your EV3 firmware",
|
||||
"ev3.updateFirmwareText": "We recommend updating to EV3 firmware version 1.10E or above. See {firmwareUpdateLink}. We recommend following the instructions for \"Manual Firmware Update\".",
|
||||
"ev3.firmwareUpdateText": "firmware update instructions from LEGO"
|
||||
"ev3.updateFirmwareText": "We recommend updating to EV3 firmware version 1.10E or above. See {firmwareUpdateLink}.",
|
||||
"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
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.outer {
|
||||
.tabs {
|
||||
width: $cols4;
|
||||
|
@ -139,7 +139,7 @@ $base-bg: $ui-white;
|
|||
}
|
||||
|
||||
//6 columns
|
||||
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
|
||||
@media #{$medium} {
|
||||
.outer {
|
||||
.tabs {
|
||||
width: $cols6;
|
||||
|
@ -158,7 +158,7 @@ $base-bg: $ui-white;
|
|||
}
|
||||
|
||||
// 8 columns
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
.outer {
|
||||
.tabs {
|
||||
width: $cols8;
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $tablet - 1){
|
||||
@media #{$medium-and-smaller}{
|
||||
.guidelines-footer {
|
||||
img {
|
||||
display: none;
|
||||
|
|
|
@ -35,14 +35,6 @@ const Jobs = () => (
|
|||
MIT Media Lab, Cambridge, MA
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://scratch.mit.edu/jobs/moderator">
|
||||
Community Moderator
|
||||
</a>
|
||||
<span>
|
||||
Remote
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -129,7 +129,7 @@
|
|||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $mobile - 1) {
|
||||
@media #{$small} {
|
||||
.flex-row.admin-message-header,
|
||||
.flex-row.mod-comment-message {
|
||||
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.mod-comment-message {
|
||||
flex-direction: row;
|
||||
|
@ -159,7 +159,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
@media #{$intermediate} {
|
||||
.comment-text {
|
||||
max-width: 23.75rem;
|
||||
}
|
||||
|
|
|
@ -28,5 +28,11 @@
|
|||
"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.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 () {
|
||||
return (
|
||||
<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">
|
||||
<h2><img src="/images/microbit/microbit.svg" />micro:bit</h2>
|
||||
<h1><img
|
||||
alt=""
|
||||
src="/images/microbit/microbit.svg"
|
||||
/>micro:bit</h1>
|
||||
<FormattedMessage
|
||||
id="microbit.headerText"
|
||||
values={{
|
||||
|
@ -51,19 +57,31 @@ class MicroBit extends ExtensionLanding {
|
|||
</FlexRow>
|
||||
<ExtensionRequirements>
|
||||
<span>
|
||||
<img src="/svgs/extensions/windows.svg" />
|
||||
<img
|
||||
alt=""
|
||||
src="/svgs/extensions/windows.svg"
|
||||
/>
|
||||
Windows 10+
|
||||
</span>
|
||||
<span>
|
||||
<img src="/svgs/extensions/mac.svg" />
|
||||
<img
|
||||
alt=""
|
||||
src="/svgs/extensions/mac.svg"
|
||||
/>
|
||||
macOS 10.13+
|
||||
</span>
|
||||
<span>
|
||||
<img src="/svgs/extensions/bluetooth.svg" />
|
||||
<img
|
||||
alt=""
|
||||
src="/svgs/extensions/bluetooth.svg"
|
||||
/>
|
||||
Bluetooth 4.0
|
||||
</span>
|
||||
<span>
|
||||
<img src="/svgs/extensions/scratch-link.svg" />
|
||||
<img
|
||||
alt=""
|
||||
src="/svgs/extensions/scratch-link.svg"
|
||||
/>
|
||||
Scratch Link
|
||||
</span>
|
||||
</ExtensionRequirements>
|
||||
|
@ -82,7 +100,10 @@ class MicroBit extends ExtensionLanding {
|
|||
<Steps>
|
||||
<Step number={1}>
|
||||
<div className="step-image">
|
||||
<img src="/images/microbit/mbit-usb.png" />
|
||||
<img
|
||||
alt=""
|
||||
src="/images/microbit/mbit-usb.png"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
<FormattedMessage id="microbit.connectUSB" />
|
||||
|
@ -90,7 +111,10 @@ class MicroBit extends ExtensionLanding {
|
|||
</Step>
|
||||
<Step number={2}>
|
||||
<div className="step-image">
|
||||
<img src="/images/microbit/mbit-hex-download.png" />
|
||||
<img
|
||||
alt=""
|
||||
src="/images/microbit/mbit-hex-download.png"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
download
|
||||
|
@ -103,6 +127,7 @@ class MicroBit extends ExtensionLanding {
|
|||
<Step number={3}>
|
||||
<div className="step-image">
|
||||
<img
|
||||
alt={this.props.intl.formatMessage({id: 'microbit.imgAltDragDropHex'})}
|
||||
src={`/images/microbit/${
|
||||
this.state.OS === OS_ENUM.WINDOWS ? 'win' : 'mac'
|
||||
}-copy-hex.png`}
|
||||
|
@ -120,13 +145,20 @@ class MicroBit extends ExtensionLanding {
|
|||
<Steps>
|
||||
<Step number={1}>
|
||||
<div className="step-image">
|
||||
<img src="/images/microbit/mbit-connect-1.png" />
|
||||
<img
|
||||
alt=""
|
||||
src="/images/microbit/mbit-connect-1.png"
|
||||
/>
|
||||
</div>
|
||||
<p><FormattedMessage id="microbit.powerMicrobit" /></p>
|
||||
</Step>
|
||||
<Step number={2}>
|
||||
<div className="step-image">
|
||||
<img src="/images/microbit/mbit-connect-2.png" />
|
||||
<img
|
||||
alt=""
|
||||
className="screenshot"
|
||||
src="/images/microbit/mbit-connect-2.png"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
|
@ -147,7 +179,11 @@ class MicroBit extends ExtensionLanding {
|
|||
</Step>
|
||||
<Step number={3}>
|
||||
<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>
|
||||
<p><FormattedMessage id="microbit.addExtension" /></p>
|
||||
</Step>
|
||||
|
@ -175,7 +211,10 @@ class MicroBit extends ExtensionLanding {
|
|||
/>
|
||||
</span>
|
||||
<div className="step-image">
|
||||
<img src="/images/microbit/display-hello-block.png" />
|
||||
<img
|
||||
alt=""
|
||||
src="/images/microbit/display-hello-block.png"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
<Step
|
||||
|
@ -193,7 +232,10 @@ class MicroBit extends ExtensionLanding {
|
|||
/>
|
||||
</span>
|
||||
<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>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
@ -201,20 +243,23 @@ class MicroBit extends ExtensionLanding {
|
|||
<h3><FormattedMessage id="microbit.starterProjects" /></h3>
|
||||
<Steps>
|
||||
<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'})}
|
||||
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltHeartBeat'})}
|
||||
imageSrc="/images/microbit/starter-heart.png"
|
||||
title={this.props.intl.formatMessage({id: 'microbit.heartBeat'})}
|
||||
/>
|
||||
<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'})}
|
||||
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltTiltGuitar'})}
|
||||
imageSrc="/images/microbit/starter-guitar.png"
|
||||
title={this.props.intl.formatMessage({id: 'microbit.tiltGuitar'})}
|
||||
/>
|
||||
<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'})}
|
||||
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltOceanAdventure'})}
|
||||
imageSrc="/images/microbit/starter-fish.png"
|
||||
title={this.props.intl.formatMessage({id: 'microbit.oceanAdventure'})}
|
||||
/>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
.microbit {
|
||||
.extension-header {
|
||||
background-color: $ui-purple;
|
||||
background-color: $ui-mint-green;
|
||||
background-image: url("/images/microbit/mbit-pattern.svg");
|
||||
|
||||
.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;
|
||||
justify-content: center;
|
||||
flex-flow: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.extension-status {
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
{
|
||||
"addToStudio.title": "Add to Studio",
|
||||
"addToStudio.finishing": "Finishing up...",
|
||||
"preview.titleMaxLength": "Title is too long",
|
||||
"preview.musicExtensionChip": "Music",
|
||||
"preview.penExtensionChip": "Pen",
|
||||
"preview.speechExtensionChip": "Google Speech",
|
||||
"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"
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
const FormattedDate = require('react-intl').FormattedDate;
|
||||
const injectIntl = require('react-intl').injectIntl;
|
||||
const PropTypes = require('prop-types');
|
||||
const intlShape = require('react-intl').intlShape;
|
||||
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||
|
||||
const MediaQuery = require('react-responsive').default;
|
||||
const React = require('react');
|
||||
const Formsy = require('formsy-react').default;
|
||||
const classNames = require('classnames');
|
||||
const approx = require('approximate-number');
|
||||
|
||||
const GUI = require('scratch-gui').default;
|
||||
const IntlGUI = injectIntl(GUI);
|
||||
|
@ -13,24 +15,45 @@ const decorateText = require('../../lib/decorate-text.jsx');
|
|||
const FlexRow = require('../../components/flex-row/flex-row.jsx');
|
||||
const Button = require('../../components/forms/button.jsx');
|
||||
const Avatar = require('../../components/avatar/avatar.jsx');
|
||||
const CappedNumber = require('../../components/cappednumber/cappednumber.jsx');
|
||||
const ShareBanner = require('./share-banner.jsx');
|
||||
const RemixCredit = require('./remix-credit.jsx');
|
||||
const RemixList = require('./remix-list.jsx');
|
||||
const Stats = require('./stats.jsx');
|
||||
const StudioList = require('./studio-list.jsx');
|
||||
const Subactions = require('./subactions.jsx');
|
||||
const InplaceInput = require('../../components/forms/inplace-input.jsx');
|
||||
const AddToStudioModal = require('../../components/modal/addtostudio/container.jsx');
|
||||
const ReportModal = require('../../components/modal/report/modal.jsx');
|
||||
const TopLevelComment = require('./comment/top-level-comment.jsx');
|
||||
const ComposeComment = require('./comment/compose-comment.jsx');
|
||||
const ExtensionChip = require('./extension-chip.jsx');
|
||||
|
||||
const projectShape = require('./projectshape.jsx').projectShape;
|
||||
require('./preview.scss');
|
||||
|
||||
const frameless = require('../../lib/frameless');
|
||||
|
||||
// disable enter key submission on formsy input fields; otherwise formsy thinks
|
||||
// we meant to trigger the "See inside" button. Instead, treat these keypresses
|
||||
// as a blur, which will trigger a save.
|
||||
const onKeyPress = e => {
|
||||
if (e.target.type === 'text' && e.which === 13 /* Enter */) {
|
||||
e.preventDefault();
|
||||
e.target.blur();
|
||||
}
|
||||
};
|
||||
|
||||
const PreviewPresentation = ({
|
||||
assetHost,
|
||||
backpackOptions,
|
||||
canAddToStudio,
|
||||
canDeleteComments,
|
||||
canReport,
|
||||
canRestoreComments,
|
||||
comments,
|
||||
editable,
|
||||
extensions,
|
||||
faved,
|
||||
favoriteCount,
|
||||
intl,
|
||||
isFullScreen,
|
||||
isLoggedIn,
|
||||
isShared,
|
||||
|
@ -38,34 +61,45 @@ const PreviewPresentation = ({
|
|||
loveCount,
|
||||
originalInfo,
|
||||
parentInfo,
|
||||
projectHost,
|
||||
projectId,
|
||||
projectInfo,
|
||||
remixes,
|
||||
report,
|
||||
reportOpen,
|
||||
replies,
|
||||
addToStudioOpen,
|
||||
projectStudios,
|
||||
studios,
|
||||
userOwnsProject,
|
||||
onAddComment,
|
||||
onDeleteComment,
|
||||
onFavoriteClicked,
|
||||
onLoadMore,
|
||||
onLoveClicked,
|
||||
onReportClicked,
|
||||
onReportClose,
|
||||
onReportComment,
|
||||
onReportSubmit,
|
||||
onRestoreComment,
|
||||
onAddToStudioClicked,
|
||||
onAddToStudioClosed,
|
||||
onToggleStudio,
|
||||
onToggleComments,
|
||||
onSeeInside,
|
||||
onShare,
|
||||
onUpdate
|
||||
}) => {
|
||||
const shareDate = ((projectInfo.history && projectInfo.history.shared)) ? projectInfo.history.shared : '';
|
||||
const loadedCommentCount = comments.length + Object.keys(replies).reduce((acc, id) => acc + replies[id].length, 0);
|
||||
return (
|
||||
<div className="preview">
|
||||
<ShareBanner shared={isShared} />
|
||||
|
||||
{!isShared && (
|
||||
<ShareBanner onShare={onShare} />
|
||||
)}
|
||||
{ projectInfo && projectInfo.author && projectInfo.author.id && (
|
||||
<Formsy onKeyPress={onKeyPress}>
|
||||
<div className="inner">
|
||||
<Formsy>
|
||||
<FlexRow className="preview-row">
|
||||
<FlexRow className="preview-row force-row">
|
||||
<FlexRow className="project-header">
|
||||
<a href={`/users/${projectInfo.author.username}`}>
|
||||
<Avatar
|
||||
|
@ -75,16 +109,14 @@ const PreviewPresentation = ({
|
|||
</a>
|
||||
<div className="title">
|
||||
{editable ?
|
||||
|
||||
<InplaceInput
|
||||
className="project-title"
|
||||
handleUpdate={onUpdate}
|
||||
name="title"
|
||||
validationErrors={{
|
||||
maxLength: 'Sorry title is too long'
|
||||
// maxLength: props.intl.formatMessage({
|
||||
// id: 'project.titleMaxLength'
|
||||
// })
|
||||
maxLength: intl.formatMessage({
|
||||
id: 'preview.titleMaxLength'
|
||||
})
|
||||
}}
|
||||
validations={{
|
||||
maxLength: 100
|
||||
|
@ -92,7 +124,10 @@ const PreviewPresentation = ({
|
|||
value={projectInfo.title}
|
||||
/> :
|
||||
<React.Fragment>
|
||||
<div className="project-title">{projectInfo.title}</div>
|
||||
<div
|
||||
className="project-title no-edit"
|
||||
title={projectInfo.title}
|
||||
>{projectInfo.title}</div>
|
||||
{'by '}
|
||||
<a href={`/users/${projectInfo.author.username}`}>
|
||||
{projectInfo.author.username}
|
||||
|
@ -101,6 +136,7 @@ const PreviewPresentation = ({
|
|||
}
|
||||
</div>
|
||||
</FlexRow>
|
||||
<MediaQuery minWidth={frameless.mobile}>
|
||||
<div className="project-buttons">
|
||||
{/* TODO: Hide Remix button for now until implemented */}
|
||||
{(!userOwnsProject && false) &&
|
||||
|
@ -115,22 +151,68 @@ const PreviewPresentation = ({
|
|||
See Inside
|
||||
</Button>
|
||||
</div>
|
||||
</MediaQuery>
|
||||
</FlexRow>
|
||||
<FlexRow className="preview-row">
|
||||
<div className="guiPlayer">
|
||||
<IntlGUI
|
||||
isPlayerOnly
|
||||
assetHost={assetHost}
|
||||
backpackOptions={backpackOptions}
|
||||
basePath="/"
|
||||
className="guiPlayer"
|
||||
isFullScreen={isFullScreen}
|
||||
previewInfoVisible="false"
|
||||
projectHost={projectHost}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
<MediaQuery maxWidth={frameless.tablet - 1}>
|
||||
<FlexRow className="preview-row force-center">
|
||||
<Stats
|
||||
faved={faved}
|
||||
favoriteCount={favoriteCount}
|
||||
loveCount={loveCount}
|
||||
loved={loved}
|
||||
projectInfo={projectInfo}
|
||||
onFavoriteClicked={onFavoriteClicked}
|
||||
onLoveClicked={onLoveClicked}
|
||||
/>
|
||||
<Subactions
|
||||
addToStudioOpen={addToStudioOpen}
|
||||
canReport={canReport}
|
||||
projectInfo={projectInfo}
|
||||
reportOpen={reportOpen}
|
||||
shareDate={shareDate}
|
||||
studios={studios}
|
||||
onAddToStudioClicked={onAddToStudioClicked}
|
||||
onAddToStudioClosed={onAddToStudioClosed}
|
||||
onReportClicked={onReportClicked}
|
||||
onReportClose={onReportClose}
|
||||
onReportSubmit={onReportSubmit}
|
||||
onToggleStudio={onToggleStudio}
|
||||
/>
|
||||
</FlexRow>
|
||||
</MediaQuery>
|
||||
<FlexRow className="project-notes">
|
||||
<RemixCredit projectInfo={parentInfo} />
|
||||
<RemixCredit projectInfo={originalInfo} />
|
||||
{/* eslint-disable max-len */}
|
||||
<MediaQuery maxWidth={frameless.tablet - 1}>
|
||||
<FlexRow className="preview-row">
|
||||
<FlexRow className="extension-list">
|
||||
{extensions && extensions.map(extension => (
|
||||
<ExtensionChip
|
||||
extensionL10n={extension.l10nId}
|
||||
extensionName={extension.name}
|
||||
hasStatus={extension.hasStatus}
|
||||
iconURI={extension.icon && `/svgs/project/${extension.icon}`}
|
||||
key={extension.name || extension.l10nId}
|
||||
/>
|
||||
))}
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
</MediaQuery>
|
||||
<FlexRow className="description-block">
|
||||
<div className="project-textlabel">
|
||||
Instructions
|
||||
|
@ -197,94 +279,35 @@ const PreviewPresentation = ({
|
|||
{/* eslint-enable max-len */}
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
<MediaQuery minWidth={frameless.tablet}>
|
||||
<FlexRow className="preview-row">
|
||||
<FlexRow className="stats">
|
||||
<div
|
||||
className={classNames('project-loves', {loved: loved})}
|
||||
key="loves"
|
||||
onClick={onLoveClicked}
|
||||
>
|
||||
{approx(loveCount, {decimal: false})}
|
||||
</div>
|
||||
<div
|
||||
className={classNames('project-favorites', {favorited: faved})}
|
||||
key="favorites"
|
||||
onClick={onFavoriteClicked}
|
||||
>
|
||||
{approx(favoriteCount, {decimal: false})}
|
||||
</div>
|
||||
<div
|
||||
className="project-remixes"
|
||||
key="remixes"
|
||||
>
|
||||
{approx(projectInfo.stats.remixes, {decimal: false})}
|
||||
</div>
|
||||
<div
|
||||
className="project-views"
|
||||
key="views"
|
||||
>
|
||||
<CappedNumber value={projectInfo.stats.views} />
|
||||
</div>
|
||||
</FlexRow>
|
||||
<FlexRow className="subactions">
|
||||
<div className="share-date">
|
||||
<div className="copyleft">©</div>
|
||||
{' '}
|
||||
{/* eslint-disable react/jsx-sort-props */}
|
||||
{shareDate === null ?
|
||||
'Unshared' :
|
||||
<FormattedDate
|
||||
value={Date.parse(shareDate)}
|
||||
day="2-digit"
|
||||
month="short"
|
||||
year="numeric"
|
||||
<Stats
|
||||
faved={faved}
|
||||
favoriteCount={favoriteCount}
|
||||
loveCount={loveCount}
|
||||
loved={loved}
|
||||
projectInfo={projectInfo}
|
||||
onFavoriteClicked={onFavoriteClicked}
|
||||
onLoveClicked={onLoveClicked}
|
||||
/>
|
||||
}
|
||||
{/* eslint-enable react/jsx-sort-props */}
|
||||
</div>
|
||||
<FlexRow className="action-buttons">
|
||||
{(isLoggedIn && userOwnsProject) &&
|
||||
<React.Fragment>
|
||||
<Button
|
||||
className="action-button studio-button"
|
||||
key="add-to-studio-button"
|
||||
onClick={onAddToStudioClicked}
|
||||
>
|
||||
Add to Studio
|
||||
</Button>,
|
||||
<AddToStudioModal
|
||||
isOpen={addToStudioOpen}
|
||||
key="add-to-studio-modal"
|
||||
<Subactions
|
||||
addToStudioOpen={addToStudioOpen}
|
||||
canAddToStudio={canAddToStudio}
|
||||
canReport={canReport}
|
||||
projectInfo={projectInfo}
|
||||
reportOpen={reportOpen}
|
||||
shareDate={shareDate}
|
||||
studios={studios}
|
||||
onRequestClose={onAddToStudioClosed}
|
||||
onAddToStudioClicked={onAddToStudioClicked}
|
||||
onAddToStudioClosed={onAddToStudioClosed}
|
||||
onReportClicked={onReportClicked}
|
||||
onReportClose={onReportClose}
|
||||
onReportSubmit={onReportSubmit}
|
||||
onToggleStudio={onToggleStudio}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
<Button className="action-button copy-link-button">
|
||||
Copy Link
|
||||
</Button>
|
||||
{(isLoggedIn && !userOwnsProject) &&
|
||||
<React.Fragment>
|
||||
<Button
|
||||
className="action-button report-button"
|
||||
key="report-button"
|
||||
onClick={onReportClicked}
|
||||
>
|
||||
Report
|
||||
</Button>,
|
||||
<ReportModal
|
||||
key="report-modal"
|
||||
report={report}
|
||||
type="project"
|
||||
onReport={onReportSubmit}
|
||||
onRequestClose={onReportClose}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={frameless.tablet}>
|
||||
<FlexRow className="preview-row">
|
||||
<FlexRow className="extension-list">
|
||||
{extensions && extensions.map(extension => (
|
||||
|
@ -298,17 +321,87 @@ const PreviewPresentation = ({
|
|||
))}
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
</MediaQuery>
|
||||
</div>
|
||||
<div className="project-lower-container">
|
||||
<div className="inner">
|
||||
<FlexRow className="preview-row">
|
||||
<div className="comments-container">
|
||||
<div className="project-title" />
|
||||
<FlexRow className="comments-header">
|
||||
<h4><FormattedMessage id="preview.comments.header" /></h4>
|
||||
{userOwnsProject ? (
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
checked={!projectInfo.comments_allowed}
|
||||
className="comments-allowed-input"
|
||||
type="checkbox"
|
||||
onChange={onToggleComments}
|
||||
/>
|
||||
<FormattedMessage id="preview.comments.turnOff" />
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
</FlexRow>
|
||||
|
||||
<FlexRow className="comments-root-reply">
|
||||
{projectInfo.comments_allowed ? (
|
||||
isLoggedIn ? (
|
||||
<ComposeComment
|
||||
projectId={projectId}
|
||||
onAddComment={onAddComment}
|
||||
/>
|
||||
) : (
|
||||
/* TODO add box for signing in to leave a comment */
|
||||
null
|
||||
)
|
||||
) : (
|
||||
<div className="comments-turned-off">
|
||||
<FormattedMessage id="preview.comments.turnedOff" />
|
||||
</div>
|
||||
)}
|
||||
</FlexRow>
|
||||
|
||||
<FlexRow className="comments-list">
|
||||
{comments.map(comment => (
|
||||
<TopLevelComment
|
||||
author={comment.author}
|
||||
canDelete={canDeleteComments}
|
||||
canReply={isLoggedIn && projectInfo.comments_allowed}
|
||||
canReport={isLoggedIn}
|
||||
canRestore={canRestoreComments}
|
||||
content={comment.content}
|
||||
datetimeCreated={comment.datetime_created}
|
||||
id={comment.id}
|
||||
key={comment.id}
|
||||
parentId={comment.parent_id}
|
||||
projectId={projectId}
|
||||
replies={replies && replies[comment.id] ? replies[comment.id] : []}
|
||||
visibility={comment.visibility}
|
||||
onAddComment={onAddComment}
|
||||
onDelete={onDeleteComment}
|
||||
onReport={onReportComment}
|
||||
onRestore={onRestoreComment}
|
||||
/>
|
||||
))}
|
||||
{loadedCommentCount < projectInfo.stats.comments &&
|
||||
<Button
|
||||
className="button load-more-button"
|
||||
onClick={onLoadMore}
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
}
|
||||
</FlexRow>
|
||||
</div>
|
||||
<FlexRow className="column">
|
||||
<RemixList remixes={remixes} />
|
||||
<StudioList studios={projectStudios} />
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
</Formsy>
|
||||
</div>
|
||||
</div>
|
||||
</Formsy>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -316,37 +409,52 @@ const PreviewPresentation = ({
|
|||
|
||||
PreviewPresentation.propTypes = {
|
||||
addToStudioOpen: PropTypes.bool,
|
||||
assetHost: PropTypes.string,
|
||||
backpackOptions: PropTypes.shape({
|
||||
host: PropTypes.string,
|
||||
visible: PropTypes.bool
|
||||
}),
|
||||
canAddToStudio: PropTypes.bool,
|
||||
canDeleteComments: PropTypes.bool,
|
||||
canReport: PropTypes.bool,
|
||||
canRestoreComments: PropTypes.bool,
|
||||
comments: PropTypes.arrayOf(PropTypes.object),
|
||||
editable: PropTypes.bool,
|
||||
extensions: PropTypes.arrayOf(PropTypes.object),
|
||||
faved: PropTypes.bool,
|
||||
favoriteCount: PropTypes.number,
|
||||
intl: intlShape,
|
||||
isFullScreen: PropTypes.bool,
|
||||
isLoggedIn: PropTypes.bool,
|
||||
isShared: PropTypes.bool,
|
||||
loveCount: PropTypes.number,
|
||||
loved: PropTypes.bool,
|
||||
onAddComment: PropTypes.func,
|
||||
onAddToStudioClicked: PropTypes.func,
|
||||
onAddToStudioClosed: PropTypes.func,
|
||||
onDeleteComment: PropTypes.func,
|
||||
onFavoriteClicked: PropTypes.func,
|
||||
onLoadMore: PropTypes.func,
|
||||
onLoveClicked: PropTypes.func,
|
||||
onReportClicked: PropTypes.func.isRequired,
|
||||
onReportClose: PropTypes.func.isRequired,
|
||||
onReportComment: PropTypes.func.isRequired,
|
||||
onReportSubmit: PropTypes.func.isRequired,
|
||||
onRestoreComment: PropTypes.func,
|
||||
onSeeInside: PropTypes.func,
|
||||
onShare: PropTypes.func,
|
||||
onToggleComments: PropTypes.func,
|
||||
onToggleStudio: PropTypes.func,
|
||||
onUpdate: PropTypes.func,
|
||||
originalInfo: projectShape,
|
||||
parentInfo: projectShape,
|
||||
projectHost: PropTypes.string,
|
||||
projectId: PropTypes.string,
|
||||
projectInfo: projectShape,
|
||||
projectStudios: PropTypes.arrayOf(PropTypes.object),
|
||||
remixes: PropTypes.arrayOf(PropTypes.object),
|
||||
report: PropTypes.shape({
|
||||
category: PropTypes.string,
|
||||
notes: PropTypes.string,
|
||||
open: PropTypes.bool,
|
||||
waiting: PropTypes.bool
|
||||
}),
|
||||
replies: PropTypes.objectOf(PropTypes.array),
|
||||
reportOpen: PropTypes.bool,
|
||||
studios: PropTypes.arrayOf(PropTypes.object),
|
||||
userOwnsProject: PropTypes.bool
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue