diff --git a/.travis.yml b/.travis.yml index 6766f4c43..c9fb8efb4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ 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} @@ -28,6 +28,11 @@ env: - BACKPACK_HOST_VAR=BACKPACK_HOST_$TRAVIS_BRANCH - BACKPACK_HOST=${!BACKPACK_HOST_VAR} - BACKPACK_HOST=${BACKPACK_HOST:-$BACKPACK_HOST_STAGING} + - CLOUDDATA_HOST_master=clouddata.scratch.mit.edu + - CLOUDDATA_HOST_STAGING=varserver2.scratch.ly + - CLOUDDATA_HOST_VAR=CLOUDDATA_HOST_$TRAVIS_BRANCH + - CLOUDDATA_HOST=${!CLOUDDATA_HOST_VAR} + - CLOUDDATA_HOST=${CLOUDDATA_HOST:-$CLOUDDATA_HOST_STAGING} - ROOT_URL_master=https://scratch.mit.edu - ROOT_URL_STAGING=https://scratch.ly - ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH diff --git a/.tx/config b/.tx/config index 2fedccb09..b61791f04 100644 --- a/.tx/config +++ b/.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/.json @@ -156,3 +156,19 @@ source_file = src/views/microbit/l10n.json source_lang = en type = KEYVALUEJSON +[scratch-website.3faq-l10njson] +file_filter = localizations/preview-faq/.json +source_file = src/views/preview-faq/l10n.json +source_lang = en +type = KEYVALUEJSON + +[scratch-website.search-l10njson] +file_filter = localizations/search/.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 diff --git a/README.md b/README.md index 25e52a316..57ab0aa11 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,9 @@ To stop the process that is making the site available to your web browser (creat | 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 | +| `ASSET_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 | +| `PROJECT_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 | diff --git a/package.json b/package.json index 5817bc436..3f52c53fc 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "redux-thunk": "2.0.1", "sass-lint": "1.5.1", "sass-loader": "6.0.6", - "scratch-gui": "develop", + "scratch-gui": "latest", "scratchr2_translations": "git://github.com/LLK/scratchr2_translations.git#master", "slick-carousel": "1.6.0", "source-map-support": "0.3.2", diff --git a/src/_colors.scss b/src/_colors.scss index 5ef54a7ce..33c5560c3 100644 --- a/src/_colors.scss +++ b/src/_colors.scss @@ -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 diff --git a/src/_frameless.scss b/src/_frameless.scss index 854e109c1..0881a3ec6 100644 --- a/src/_frameless.scss +++ b/src/_frameless.scss @@ -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; diff --git a/src/components/box/box.scss b/src/components/box/box.scss index 4b9392771..bc981dd94 100644 --- a/src/components/box/box.scss +++ b/src/components/box/box.scss @@ -7,9 +7,9 @@ $base-bg: $ui-white; display: inline-block; border: 1px solid $ui-border; 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 { diff --git a/src/components/card/card.scss b/src/components/card/card.scss index b2b70c22c..d23f5f2e3 100644 --- a/src/components/card/card.scss +++ b/src/components/card/card.scss @@ -65,7 +65,7 @@ margin: 0 0 -3rem -4rem; } - .row { + .row { margin-bottom: 1.2rem; &.has-error { @@ -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; diff --git a/src/components/dropdown/dropdown.scss b/src/components/dropdown/dropdown.scss index 8db2148a7..02b691f83 100644 --- a/src/components/dropdown/dropdown.scss +++ b/src/components/dropdown/dropdown.scss @@ -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; @@ -88,8 +89,4 @@ content: ""; } } - - @media only screen and (max-width: $tablet - 1) { - min-width: 160px; - } } diff --git a/src/components/flex-row/flex-row.scss b/src/components/flex-row/flex-row.scss index 4b0f28069..eff8370e3 100644 --- a/src/components/flex-row/flex-row.scss +++ b/src/components/flex-row/flex-row.scss @@ -25,7 +25,7 @@ } } - @media only screen and (max-width: $tablet - 1) { + @media #{$medium-and-smaller} { flex-direction: column; &.uneven { diff --git a/src/components/footer/conference/footer.scss b/src/components/footer/conference/footer.scss index 3694c2d87..3eafcc4cb 100644 --- a/src/components/footer/conference/footer.scss +++ b/src/components/footer/conference/footer.scss @@ -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; diff --git a/src/components/forms/inplace-input.scss b/src/components/forms/inplace-input.scss index 33bc254b3..65b3094e1 100644 --- a/src/components/forms/inplace-input.scss +++ b/src/components/forms/inplace-input.scss @@ -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; diff --git a/src/components/forms/select.scss b/src/components/forms/select.scss index 04433fdcc..4159086b7 100644 --- a/src/components/forms/select.scss +++ b/src/components/forms/select.scss @@ -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; diff --git a/src/components/grid/grid.scss b/src/components/grid/grid.scss index 253210619..3cd926af2 100644 --- a/src/components/grid/grid.scss +++ b/src/components/grid/grid.scss @@ -7,7 +7,7 @@ $thumbnail-width: 220px; $thumbnail-inner-width: 204px; - + $project-height: 208px; $gallery-height: 164px; @@ -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; } diff --git a/src/components/masonrygrid/masonrygrid.scss b/src/components/masonrygrid/masonrygrid.scss index 867f33350..98b75c95f 100644 --- a/src/components/masonrygrid/masonrygrid.scss +++ b/src/components/masonrygrid/masonrygrid.scss @@ -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; } diff --git a/src/components/modal/addtostudio/container.jsx b/src/components/modal/addtostudio/container.jsx index 582f194eb..a03f09074 100644 --- a/src/components/modal/addtostudio/container.jsx +++ b/src/components/modal/addtostudio/container.jsx @@ -8,7 +8,8 @@ class AddToStudioModal extends React.Component { super(props); bindAll(this, [ 'handleRequestClose', - 'handleSubmit' + 'handleSubmit', + 'handleToggleStudio' ]); this.state = { @@ -48,6 +49,14 @@ class AddToStudioModal extends React.Component { }); } + handleToggleStudio (id) { + const studioId = parseInt(id, 10); + if (isNaN(studioId)) { // sanity check in case event had no integer data-id + return; + } + this.props.onToggleStudio(this.props.studios.find(studio => studio.id === studioId)); + } + render () { return ( ); } diff --git a/src/components/modal/addtostudio/modal.scss b/src/components/modal/addtostudio/modal.scss index caa532bb4..92f42b0b8 100644 --- a/src/components/modal/addtostudio/modal.scss +++ b/src/components/modal/addtostudio/modal.scss @@ -1,52 +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%; - 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; @@ -93,35 +82,32 @@ display: flex; position: relative; transition: all .5s; - margin: .21875rem .21875rem; + 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: 1.25rem; /* diff from spec, in case we ever do valign to middle; changed to match line-height because else with overflow hidden it cuts off some letters */ + margin: auto 2.18375rem auto .6875rem; + min-width: 0; overflow: hidden; text-overflow: ellipsis; - line-height: 1.25rem; white-space: nowrap; font-family: "Helvetica Neue"; font-size: .875rem; font-weight: regular; + flex-shrink: 1; } .studio-selector-button-selected { @@ -144,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; diff --git a/src/components/modal/addtostudio/presentation.jsx b/src/components/modal/addtostudio/presentation.jsx index 168a26b7c..7152ef8cc 100644 --- a/src/components/modal/addtostudio/presentation.jsx +++ b/src/components/modal/addtostudio/presentation.jsx @@ -37,70 +37,69 @@ const AddToStudioModalPresentation = ({ return ( -
-
-
- {contentLabel} +
+
+ {contentLabel} +
+
+
+
+
+
+ {studioButtons} +
+
-
-
-
-
- {studioButtons} + + +
+ +
- - - - + + {waitingToClose ? [ - {waitingToClose ? [ - - ] : [ - - ]} - - -
+ ] : [ + + ]} + +
); diff --git a/src/components/modal/base/modal.jsx b/src/components/modal/base/modal.jsx index c06c8e5ee..156d435ce 100644 --- a/src/components/modal/base/modal.jsx +++ b/src/components/modal/base/modal.jsx @@ -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 ( ( + +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+ + + +
+
+
+
+); + + +DeleteModal.propTypes = { + intl: intlShape, + onDelete: PropTypes.func, + onReport: PropTypes.func, + onRequestClose: PropTypes.func +}; + +module.exports = injectIntl(DeleteModal); diff --git a/src/components/modal/comments/modal.scss b/src/components/modal/comments/modal.scss new file mode 100644 index 000000000..40a526fe3 --- /dev/null +++ b/src/components/modal/comments/modal.scss @@ -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; + } +} diff --git a/src/components/modal/comments/report-comment.jsx b/src/components/modal/comments/report-comment.jsx new file mode 100644 index 000000000..613e6dffd --- /dev/null +++ b/src/components/modal/comments/report-comment.jsx @@ -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 +}) => ( + +
+
+
+ +
+
+ +
+
+
+ {isConfirmed ? ( + + ) : ( + + )} +
+
+
+ +
+ + {isConfirmed ? null : ( + + )} +
+
+
+
+); + + +ReportModal.propTypes = { + intl: intlShape, + isConfirmed: PropTypes.bool, + isOwnSpace: PropTypes.bool, + onReport: PropTypes.func, + onRequestClose: PropTypes.func, + type: PropTypes.string +}; + +module.exports = injectIntl(ReportModal); diff --git a/src/components/modal/report/modal.jsx b/src/components/modal/report/modal.jsx index e376398f5..99e564468 100644 --- a/src/components/modal/report/modal.jsx +++ b/src/components/modal/report/modal.jsx @@ -111,6 +111,7 @@ class ReportModal extends React.Component { const contentLabel = intl.formatMessage({id: `report.${type}`}); return (
-
-
+
+
{contentLabel}
@@ -130,7 +131,7 @@ class ReportModal extends React.Component { onValid={this.handleValid} onValidSubmit={onReport} > -
+
{isConfirmed ? (
diff --git a/src/components/modal/report/modal.scss b/src/components/modal/report/modal.scss index 375ac6227..b6db6da85 100644 --- a/src/components/modal/report/modal.scss +++ b/src/components/modal/report/modal.scss @@ -1,40 +1,15 @@ @import "../../../colors"; @import "../../../frameless"; -.mod-report * { - box-sizing: border-box; -} - -.mod-report { - margin: 100px auto; - outline: none; - padding: 0; - width: 36.25rem; /* 580px; */ - user-select: none; -} .report-modal-header { - border-radius: 1rem 1rem 0 0; box-shadow: inset 0 -1px 0 0 $ui-coral-dark; background-color: $ui-coral; - padding-top: .75rem; - width: 100%; - height: 3rem; - box-sizing: border-box; -} - -.report-content-label { - text-align: center; - color: $type-white; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 1rem; - font-weight: bold; } .report-modal-content { margin: 1rem auto; width: 80%; - font-size: .875rem; .instructions { line-height: 1.5rem; @@ -58,28 +33,33 @@ .validation-message { $arrow-border-width: 1rem; display: block; - position: absolute; - top: 0; - left: 100%; /* position to the right of parent */ - margin-left: $arrow-border-width; + position: relative; + z-index: 1; + margin-top: $arrow-border-width; + border: 1px solid $active-gray; border-radius: 5px; background-color: $ui-orange; padding: 1rem; min-width: 12rem; - max-width: 18.75rem; min-height: 1rem; overflow: visible; color: $type-white; + @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; @@ -90,6 +70,10 @@ height: $arrow-border-width; content: ""; + + @media #{$medium-and-smaller} { + display: none; + } } } } @@ -100,10 +84,13 @@ .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; } diff --git a/src/components/modal/ttt/modal.scss b/src/components/modal/ttt/modal.scss index 1604eca7b..b152e9025 100644 --- a/src/components/modal/ttt/modal.scss +++ b/src/components/modal/ttt/modal.scss @@ -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; } diff --git a/src/components/navigation/conference/2016/navigation.scss b/src/components/navigation/conference/2016/navigation.scss index bc1536c16..13c74c539 100644 --- a/src/components/navigation/conference/2016/navigation.scss +++ b/src/components/navigation/conference/2016/navigation.scss @@ -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 { diff --git a/src/components/navigation/conference/2018/navigation.scss b/src/components/navigation/conference/2018/navigation.scss index 7651d846f..6e36aaab6 100644 --- a/src/components/navigation/conference/2018/navigation.scss +++ b/src/components/navigation/conference/2018/navigation.scss @@ -11,7 +11,7 @@ align-items: center; list-style-type: none; } - + .li-left-ul.mod-2018 { padding-left: 0; } @@ -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 { diff --git a/src/components/navigation/www/accountnav.scss b/src/components/navigation/www/accountnav.scss index ebee28122..e4bc29710 100644 --- a/src/components/navigation/www/accountnav.scss +++ b/src/components/navigation/www/accountnav.scss @@ -56,7 +56,7 @@ } //4 columns -@media only screen and (max-width: $mobile - 1) { +@media #{$small} { .account-nav { margin-left: 0; @@ -74,7 +74,7 @@ //6 columns -@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) { +@media #{$medium} { .account-nav { margin-left: 0; @@ -91,7 +91,7 @@ } //8 columns -@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) { +@media #{$intermediate} { .account-nav { margin-left: 0; } diff --git a/src/components/navigation/www/navigation.scss b/src/components/navigation/www/navigation.scss index 62429789a..b07300010 100644 --- a/src/components/navigation/www/navigation.scss +++ b/src/components/navigation/www/navigation.scss @@ -166,7 +166,7 @@ } //4 columns -@media only screen and (max-width: $mobile - 1) { +@media #{$small} { #navigation .inner { width: $cols4; @@ -190,7 +190,7 @@ //6 columns -@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) { +@media #{$medium} { #navigation .inner { width: $cols6; @@ -212,7 +212,7 @@ } //8 columns -@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) { +@media #{$intermediate} { #navigation .inner { width: $cols8; diff --git a/src/components/page/conference/page.scss b/src/components/page/conference/page.scss index 5f0f2fbf2..e164d2eb2 100644 --- a/src/components/page/conference/page.scss +++ b/src/components/page/conference/page.scss @@ -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%; diff --git a/src/components/registration/registration.scss b/src/components/registration/registration.scss index acd5c2d8c..3b33ba0a1 100644 --- a/src/components/registration/registration.scss +++ b/src/components/registration/registration.scss @@ -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; diff --git a/src/components/registration/steps.jsx b/src/components/registration/steps.jsx index a1d752f99..f9c0b3b4c 100644 --- a/src/components/registration/steps.jsx +++ b/src/components/registration/steps.jsx @@ -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 this.props.onNextStep(formData); + return (((Date.now() - birthdate) / (24 * 3600 * 1000 * 365.25)) >= this.props.birthOffset); + } + 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) + }} /> + + +
+ ) : null} + + + {projectInfo.comments_allowed ? ( + isLoggedIn ? ( + + ) : ( + /* TODO add box for signing in to leave a comment */ + null + ) + ) : ( +
+ +
+ )} +
+ {comments.map(comment => ( ))} - {comments.length < projectInfo.stats.comments && - + {moreCommentsToLoad && + }
@@ -381,7 +424,7 @@ const PreviewPresentation = ({
- + )}
); @@ -394,6 +437,12 @@ PreviewPresentation.propTypes = { host: PropTypes.string, visible: PropTypes.bool }), + canAddToStudio: PropTypes.bool, + canDeleteComments: PropTypes.bool, + canReport: PropTypes.bool, + canRestoreComments: PropTypes.bool, + canShare: PropTypes.bool, + cloudHost: PropTypes.string, comments: PropTypes.arrayOf(PropTypes.object), editable: PropTypes.bool, extensions: PropTypes.arrayOf(PropTypes.object), @@ -405,15 +454,22 @@ PreviewPresentation.propTypes = { isShared: PropTypes.bool, loveCount: PropTypes.number, loved: PropTypes.bool, + moreCommentsToLoad: 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, @@ -425,7 +481,7 @@ PreviewPresentation.propTypes = { remixes: PropTypes.arrayOf(PropTypes.object), replies: PropTypes.objectOf(PropTypes.array), reportOpen: PropTypes.bool, - studios: PropTypes.arrayOf(PropTypes.object), + singleCommentId: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), userOwnsProject: PropTypes.bool }; diff --git a/src/views/preview/preview.jsx b/src/views/preview/preview.jsx index 03f4fe48d..2b057fe05 100644 --- a/src/views/preview/preview.jsx +++ b/src/views/preview/preview.jsx @@ -12,6 +12,7 @@ const render = require('../../lib/render.jsx'); const storage = require('../../lib/storage.js').default; const log = require('../../lib/log'); const EXTENSION_INFO = require('../../lib/extensions.js').default; +const jar = require('../../lib/jar.js'); const PreviewPresentation = require('./presentation.jsx'); const projectShape = require('./projectshape.jsx').projectShape; @@ -23,6 +24,8 @@ const sessionActions = require('../../redux/session.js'); const navigationActions = require('../../redux/navigation.js'); const previewActions = require('../../redux/preview.js'); +const frameless = require('../../lib/frameless'); + const GUI = require('scratch-gui'); const IntlGUI = injectIntl(GUI.default); @@ -31,6 +34,9 @@ class Preview extends React.Component { super(props); bindAll(this, [ 'addEventListeners', + 'fetchCommunityData', + 'handleAddComment', + 'handleDeleteComment', 'handleToggleStudio', 'handleFavoriteToggle', 'handleLoadMore', @@ -38,63 +44,70 @@ class Preview extends React.Component { 'handlePopState', 'handleReportClick', 'handleReportClose', + 'handleReportComment', 'handleReportSubmit', + 'handleRestoreComment', 'handleAddToStudioClick', 'handleAddToStudioClose', 'handleSeeInside', + 'handleShare', + 'handleUpdateProjectId', 'handleUpdateProjectTitle', 'handleUpdate', + 'handleToggleComments', 'initCounts', 'pushHistory', - 'renderLogin' + 'renderLogin', + 'setScreenFromOrientation' ]); const pathname = window.location.pathname.toLowerCase(); const parts = pathname.split('/').filter(Boolean); // parts[0]: 'preview' // parts[1]: either :id or 'editor' // parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen' + + // Get single-comment id from url hash, using the #comments-{id} scheme from scratch2 + const commentHashPrefix = '#comments-'; + const singleCommentId = window.location.hash.indexOf(commentHashPrefix) !== -1 && + parseInt(window.location.hash.replace(commentHashPrefix, ''), 10); + this.state = { extensions: [], favoriteCount: 0, loveCount: 0, - projectId: parts[1] === 'editor' ? 0 : parts[1], + projectId: parts[1] === 'editor' ? '0' : parts[1], + singleCommentId: singleCommentId, addToStudioOpen: false, reportOpen: false }; - this.getExtensions(this.state.projectId); this.addEventListeners(); + /* In the beginning, if user is on mobile and landscape, go to fullscreen */ + this.setScreenFromOrientation(); } - componentDidUpdate (prevProps) { - if (this.props.sessionStatus !== prevProps.sessionStatus && - this.props.sessionStatus === sessionActions.Status.FETCHED && - this.state.projectId) { - if (this.props.user) { - const username = this.props.user.username; - const token = this.props.user.token; - this.props.getTopLevelComments(this.state.projectId, this.props.comments.length); - this.props.getProjectInfo(this.state.projectId, token); - this.props.getRemixes(this.state.projectId, token); - this.props.getProjectStudios(this.state.projectId, token); - this.props.getCuratedStudios(username); - this.props.getFavedStatus(this.state.projectId, username, token); - this.props.getLovedStatus(this.state.projectId, username, token); - } else { - this.props.getTopLevelComments(this.state.projectId, this.props.comments.length); - this.props.getProjectInfo(this.state.projectId); - this.props.getRemixes(this.state.projectId); - this.props.getProjectStudios(this.state.projectId); - } + componentDidUpdate (prevProps, prevState) { + if (this.state.projectId > 0 && + ((this.props.sessionStatus !== prevProps.sessionStatus && + this.props.sessionStatus === sessionActions.Status.FETCHED) || + (this.state.projectId !== prevState.projectId))) { + this.fetchCommunityData(); + this.getExtensions(this.state.projectId); + } + if (this.state.projectId === '0' && this.state.projectId !== prevState.projectId) { + this.props.resetProject(); } if (this.props.projectInfo.id !== prevProps.projectInfo.id) { - this.getExtensions(this.state.projectId); - this.initCounts(this.props.projectInfo.stats.favorites, this.props.projectInfo.stats.loves); - if (this.props.projectInfo.remix.parent !== null) { - this.props.getParentInfo(this.props.projectInfo.remix.parent); - } - if (this.props.projectInfo.remix.root !== null && - this.props.projectInfo.remix.root !== this.props.projectInfo.remix.parent - ) { - this.props.getOriginalInfo(this.props.projectInfo.remix.root); + if (typeof this.props.projectInfo.id === 'undefined') { + this.initCounts(0, 0); + } else { + this.initCounts(this.props.projectInfo.stats.favorites, this.props.projectInfo.stats.loves); + if (this.props.projectInfo.remix.parent !== null) { + this.props.getParentInfo(this.props.projectInfo.remix.parent); + } + if (this.props.projectInfo.remix.root !== null && + this.props.projectInfo.remix.root !== this.props.projectInfo.remix.parent + ) { + this.props.getOriginalInfo(this.props.projectInfo.remix.root); + } } } if (this.props.playerMode !== prevProps.playerMode || this.props.fullScreen !== prevProps.fullScreen) { @@ -106,41 +119,113 @@ class Preview extends React.Component { } addEventListeners () { window.addEventListener('popstate', this.handlePopState); + window.addEventListener('orientationchange', this.setScreenFromOrientation); } removeEventListeners () { window.removeEventListener('popstate', this.handlePopState); + window.removeEventListener('orientationchange', this.setScreenFromOrientation); + } + fetchCommunityData () { + if (this.props.userPresent) { + const username = this.props.user.username; + const token = this.props.user.token; + if (this.state.singleCommentId) { + this.props.getCommentById(this.state.projectId, this.state.singleCommentId, + this.props.isAdmin, token); + } else { + this.props.getTopLevelComments(this.state.projectId, this.props.comments.length, + this.props.isAdmin, token); + } + this.props.getProjectInfo(this.state.projectId, token); + this.props.getRemixes(this.state.projectId, token); + this.props.getProjectStudios(this.state.projectId, token); + this.props.getCuratedStudios(username); + this.props.getFavedStatus(this.state.projectId, username, token); + this.props.getLovedStatus(this.state.projectId, username, token); + } else { + if (this.state.singleCommentId) { + this.props.getCommentById(this.state.projectId, this.state.singleCommentId); + } else { + this.props.getTopLevelComments(this.state.projectId, this.props.comments.length); + } + this.props.getProjectInfo(this.state.projectId); + this.props.getRemixes(this.state.projectId); + this.props.getProjectStudios(this.state.projectId); + } + } + setScreenFromOrientation () { + /* + * If the user is on a mobile device, switching to + * landscape format should make the fullscreen mode active + */ + const isMobileDevice = screen.height <= frameless.mobile || screen.width <= frameless.mobile; + const isAModalOpen = this.state.addToStudioOpen || this.state.reportOpen; + if (this.props.playerMode && isMobileDevice && !isAModalOpen) { + const isLandscape = screen.height < screen.width; + if (isLandscape) { + this.props.setFullScreen(true); + } else { + this.props.setFullScreen(false); + } + } } getExtensions (projectId) { - storage - .load(storage.AssetType.Project, projectId, storage.DataFormat.JSON) - .then(projectAsset => { // NOTE: this is turning up null, breaking the line below. - let input = projectAsset.data; - if (typeof input === 'object' && !(input instanceof ArrayBuffer) && - !ArrayBuffer.isView(input)) { // taken from scratch-vm - // If the input is an object and not any ArrayBuffer - // or an ArrayBuffer view (this includes all typed arrays and DataViews) - // turn the object into a JSON string, because we suspect - // this is a project.json as an object - // validate expects a string or buffer as input - // TODO not sure if we need to check that it also isn't a data view - input = JSON.stringify(input); - } - parser(projectAsset.data, false, (err, projectData) => { - if (err) { - log.error(`Unhandled project parsing error: ${err}`); - return; + if (projectId > 0) { + storage + .load(storage.AssetType.Project, projectId, storage.DataFormat.JSON) + .then(projectAsset => { // NOTE: this is turning up null, breaking the line below. + let input = projectAsset.data; + if (typeof input === 'object' && !(input instanceof ArrayBuffer) && + !ArrayBuffer.isView(input)) { // taken from scratch-vm + // If the input is an object and not any ArrayBuffer + // or an ArrayBuffer view (this includes all typed arrays and DataViews) + // turn the object into a JSON string, because we suspect + // this is a project.json as an object + // validate expects a string or buffer as input + // TODO not sure if we need to check that it also isn't a data view + input = JSON.stringify(input); } - const extensionSet = new Set(); - if (projectData[0].extensions) { - projectData[0].extensions.forEach(extension => { - extensionSet.add(EXTENSION_INFO[extension]); + parser(projectAsset.data, false, (err, projectData) => { + if (err) { + log.error(`Unhandled project parsing error: ${err}`); + return; + } + const extensionSet = new Set(); + if (projectData[0].extensions) { + projectData[0].extensions.forEach(extension => { + extensionSet.add(EXTENSION_INFO[extension]); + }); + } + this.setState({ + extensions: Array.from(extensionSet) }); - } - this.setState({ - extensions: Array.from(extensionSet) }); }); + } else { // projectId is default or invalid; empty the extensions array + this.setState({ + extensions: [] }); + } + } + handleToggleComments () { + this.props.updateProject( + this.props.projectInfo.id, + {comments_allowed: !this.props.projectInfo.comments_allowed}, + this.props.user.username, + this.props.user.token + ); + } + handleAddComment (comment, topLevelCommentId) { + this.props.handleAddComment(comment, topLevelCommentId); + } + handleDeleteComment (id, topLevelCommentId) { + this.props.handleDeleteComment(this.state.projectId, id, topLevelCommentId, this.props.user.token); + } + handleReportComment (id, topLevelCommentId) { + this.props.handleReportComment(this.state.projectId, id, topLevelCommentId, this.props.user.token); + } + handleRestoreComment (id, topLevelCommentId) { + this.props.handleRestoreComment(this.state.projectId, id, topLevelCommentId, this.props.user.token); } handleReportClick () { this.setState({reportOpen: true}); @@ -155,7 +240,7 @@ class Preview extends React.Component { this.setState({addToStudioOpen: false}); } handleReportSubmit (formData) { - this.props.reportProject(this.state.projectId, formData); + this.props.reportProject(this.state.projectId, formData, this.props.user.token); } handlePopState () { const path = window.location.pathname.toLowerCase(); @@ -190,17 +275,12 @@ class Preview extends React.Component { ); } } - handleToggleStudio (id) { - const studioId = parseInt(id, 10); - if (isNaN(studioId)) { // sanity check in case event had no integer data-id - return; - } - const studio = this.props.studios.find(thisStudio => (thisStudio.id === studioId)); + handleToggleStudio (studio) { // only send add or leave request to server if we know current status if ((typeof studio !== 'undefined') && ('includesProject' in studio)) { this.props.toggleStudio( (studio.includesProject === false), - studioId, + studio.id, this.props.projectInfo.id, this.props.user.token ); @@ -224,7 +304,8 @@ class Preview extends React.Component { } } handleLoadMore () { - this.props.getTopLevelComments(this.state.projectId, this.props.comments.length); + this.props.getTopLevelComments(this.state.projectId, this.props.comments.length, + this.props.isAdmin, this.props.user && this.props.user.token); } handleLoveToggle () { this.props.setLovedStatus( @@ -247,7 +328,12 @@ class Preview extends React.Component { this.props.setPlayer(false); } handleShare () { - // This is just a placeholder, but enables the button in the editor + this.props.updateProject( + this.props.projectInfo.id, + {isPublished: true}, + this.props.user.username, + this.props.user.token + ); } handleUpdate (jsonData) { this.props.updateProject( @@ -262,6 +348,28 @@ class Preview extends React.Component { title: title }); } + handleSetLanguage (locale) { + jar.set('scratchlanguage', locale); + } + handleUpdateProjectId (projectId, callback) { + this.setState({projectId: projectId}, () => { + const parts = window.location.pathname.toLowerCase() + .split('/') + .filter(Boolean); + let newUrl; + if (projectId === '0') { + newUrl = `/${parts[0]}/editor`; + } else { + newUrl = `/${parts[0]}/${projectId}/editor`; + } + history.pushState( + {projectId: projectId}, + {projectId: projectId}, + newUrl + ); + if (callback) callback(); + }); + } initCounts (favorites, loves) { this.setState({ favoriteCount: favorites, @@ -293,6 +401,12 @@ class Preview extends React.Component { addToStudioOpen={this.state.addToStudioOpen} assetHost={this.props.assetHost} backpackOptions={this.props.backpackOptions} + canAddToStudio={this.props.canAddToStudio} + canDeleteComments={this.props.isAdmin || this.props.userOwnsProject} + canReport={this.props.canReport} + canRestoreComments={this.props.isAdmin} + canShare={this.props.canShare} + cloudHost={this.props.cloudHost} comments={this.props.comments} editable={this.props.isEditable} extensions={this.state.extensions} @@ -303,6 +417,7 @@ class Preview extends React.Component { isShared={this.props.isShared} loveCount={this.state.loveCount} loved={this.props.loved} + moreCommentsToLoad={this.props.moreCommentsToLoad} originalInfo={this.props.original} parentInfo={this.props.parent} projectHost={this.props.projectHost} @@ -312,37 +427,52 @@ class Preview extends React.Component { remixes={this.props.remixes} replies={this.props.replies} reportOpen={this.state.reportOpen} - studios={this.props.studios} + singleCommentId={this.state.singleCommentId} userOwnsProject={this.props.userOwnsProject} + onAddComment={this.handleAddComment} onAddToStudioClicked={this.handleAddToStudioClick} onAddToStudioClosed={this.handleAddToStudioClose} + onDeleteComment={this.handleDeleteComment} onFavoriteClicked={this.handleFavoriteToggle} onLoadMore={this.handleLoadMore} onLoveClicked={this.handleLoveToggle} onReportClicked={this.handleReportClick} onReportClose={this.handleReportClose} + onReportComment={this.handleReportComment} onReportSubmit={this.handleReportSubmit} + onRestoreComment={this.handleRestoreComment} onSeeInside={this.handleSeeInside} + onShare={this.handleShare} + onToggleComments={this.handleToggleComments} onToggleStudio={this.handleToggleStudio} onUpdate={this.handleUpdate} /> : @@ -359,9 +489,19 @@ Preview.propTypes = { host: PropTypes.string, visible: PropTypes.bool }), + canAddToStudio: PropTypes.bool, + canCreateCopy: PropTypes.bool, + canCreateNew: PropTypes.bool, + canRemix: PropTypes.bool, + canReport: PropTypes.bool, + canSave: PropTypes.bool, + canShare: PropTypes.bool, + cloudHost: PropTypes.string, comments: PropTypes.arrayOf(PropTypes.object), + enableCommunity: PropTypes.bool, faved: PropTypes.bool, fullScreen: PropTypes.bool, + getCommentById: PropTypes.func.isRequired, getCuratedStudios: PropTypes.func.isRequired, getFavedStatus: PropTypes.func.isRequired, getLovedStatus: PropTypes.func.isRequired, @@ -371,14 +511,20 @@ Preview.propTypes = { getProjectStudios: PropTypes.func.isRequired, getRemixes: PropTypes.func.isRequired, getTopLevelComments: PropTypes.func.isRequired, + handleAddComment: PropTypes.func, + handleDeleteComment: PropTypes.func, handleLogIn: PropTypes.func, handleLogOut: PropTypes.func, handleOpenRegistration: PropTypes.func, + handleReportComment: PropTypes.func, + handleRestoreComment: PropTypes.func, handleToggleLoginOpen: PropTypes.func, + isAdmin: PropTypes.bool, isEditable: PropTypes.bool, isLoggedIn: PropTypes.bool, isShared: PropTypes.bool, loved: PropTypes.bool, + moreCommentsToLoad: PropTypes.bool, original: projectShape, parent: projectShape, playerMode: PropTypes.bool, @@ -388,12 +534,12 @@ Preview.propTypes = { remixes: PropTypes.arrayOf(PropTypes.object), replies: PropTypes.objectOf(PropTypes.array), reportProject: PropTypes.func, + resetProject: PropTypes.func, sessionStatus: PropTypes.string, setFavedStatus: PropTypes.func.isRequired, setFullScreen: PropTypes.func.isRequired, setLovedStatus: PropTypes.func.isRequired, setPlayer: PropTypes.func.isRequired, - studios: PropTypes.arrayOf(PropTypes.object), toggleStudio: PropTypes.func.isRequired, updateProject: PropTypes.func.isRequired, user: PropTypes.shape({ @@ -406,7 +552,8 @@ Preview.propTypes = { email: PropTypes.string, classroomId: PropTypes.string }), - userOwnsProject: PropTypes.bool + userOwnsProject: PropTypes.bool, + userPresent: PropTypes.bool }; Preview.defaultProps = { @@ -415,56 +562,37 @@ Preview.defaultProps = { host: process.env.BACKPACK_HOST, visible: true }, + cloudHost: process.env.CLOUDDATA_HOST, projectHost: process.env.PROJECT_HOST, sessionStatus: sessionActions.Status.NOT_FETCHED, - user: {} -}; - -// Build consolidated curatedStudios object from all studio info. -// We add flags to indicate whether the project is currently in the studio, -// and the status of requests to join/leave studios. -const consolidateStudiosInfo = (curatedStudios, projectStudios, currentStudioIds, studioRequests) => { - const consolidatedStudios = []; - - projectStudios.forEach(projectStudio => { - const includesProject = (currentStudioIds.indexOf(projectStudio.id) !== -1); - const consolidatedStudio = - Object.assign({}, projectStudio, {includesProject: includesProject}); - consolidatedStudios.push(consolidatedStudio); - }); - - // copy the curated studios that project is not in - curatedStudios.forEach(curatedStudio => { - if (!projectStudios.some(projectStudio => (projectStudio.id === curatedStudio.id))) { - const includesProject = (currentStudioIds.indexOf(curatedStudio.id) !== -1); - const consolidatedStudio = - Object.assign({}, curatedStudio, {includesProject: includesProject}); - consolidatedStudios.push(consolidatedStudio); - } - }); - - // set studio state to hasRequestOutstanding==true if it's being fetched, - // false if it's not - consolidatedStudios.forEach(consolidatedStudio => { - const id = consolidatedStudio.id; - consolidatedStudio.hasRequestOutstanding = - ((id in studioRequests) && - (studioRequests[id] === previewActions.Status.FETCHING)); - }); - return consolidatedStudios; + user: {}, + userPresent: false }; const mapStateToProps = state => { - const projectInfoPresent = Object.keys(state.preview.projectInfo).length > 0; - const userPresent = state.session.session.user && + const projectInfoPresent = state.preview.projectInfo && + Object.keys(state.preview.projectInfo).length > 0 && state.preview.projectInfo.id > 0; + const userPresent = state.session.session.user !== null && + typeof state.session.session.user !== 'undefined' && Object.keys(state.session.session.user).length > 0; const isLoggedIn = state.session.status === sessionActions.Status.FETCHED && userPresent; + const isAdmin = isLoggedIn && state.session.session.permissions.admin; const authorPresent = projectInfoPresent && state.preview.projectInfo.author && Object.keys(state.preview.projectInfo.author).length > 0; + const userOwnsProject = isLoggedIn && authorPresent && + state.session.session.user.id === state.preview.projectInfo.author.id; return { + canAddToStudio: userOwnsProject, + canCreateCopy: userOwnsProject && projectInfoPresent, + canCreateNew: isLoggedIn, + canRemix: isLoggedIn && projectInfoPresent && !userOwnsProject, + canReport: isLoggedIn && !userOwnsProject, + canSave: isLoggedIn && userOwnsProject, + canShare: userOwnsProject && state.permissions.social, comments: state.preview.comments, + enableCommunity: projectInfoPresent, faved: state.preview.faved, fullScreen: state.scratchGui.mode.isFullScreen, // project is editable iff logged in user is the author of the project, or @@ -473,12 +601,11 @@ const mapStateToProps = state => { ((authorPresent && state.preview.projectInfo.author.username === state.session.session.user.username) || state.permissions.admin === true), isLoggedIn: isLoggedIn, + isAdmin: isAdmin, // if we don't have projectInfo, assume it's shared until we know otherwise - isShared: !projectInfoPresent || ( - state.preview.projectInfo.history && - state.preview.projectInfo.history.shared && - state.preview.projectInfo.history.shared.length > 0), + isShared: !projectInfoPresent || state.preview.projectInfo.is_published, loved: state.preview.loved, + moreCommentsToLoad: state.preview.moreCommentsToLoad, original: state.preview.original, parent: state.preview.parent, playerMode: state.scratchGui.mode.isPlayerOnly, @@ -487,16 +614,25 @@ const mapStateToProps = state => { remixes: state.preview.remixes, replies: state.preview.replies, sessionStatus: state.session.status, // check if used - studios: consolidateStudiosInfo(state.preview.curatedStudios, - state.preview.projectStudios, state.preview.currentStudioIds, - state.preview.status.studioRequests), user: state.session.session.user, - userOwnsProject: isLoggedIn && authorPresent && - state.session.session.user.id === state.preview.projectInfo.author.id + userOwnsProject: userOwnsProject, + userPresent: userPresent }; }; const mapDispatchToProps = dispatch => ({ + handleAddComment: (comment, topLevelCommentId) => { + dispatch(previewActions.addNewComment(comment, topLevelCommentId)); + }, + handleDeleteComment: (projectId, commentId, topLevelCommentId, token) => { + dispatch(previewActions.deleteComment(projectId, commentId, topLevelCommentId, token)); + }, + handleReportComment: (projectId, commentId, topLevelCommentId, token) => { + dispatch(previewActions.reportComment(projectId, commentId, topLevelCommentId, token)); + }, + handleRestoreComment: (projectId, commentId, topLevelCommentId, token) => { + dispatch(previewActions.restoreComment(projectId, commentId, topLevelCommentId, token)); + }, handleOpenRegistration: event => { event.preventDefault(); dispatch(navigationActions.setRegistrationOpen(true)); @@ -537,8 +673,11 @@ const mapDispatchToProps = dispatch => ({ dispatch(previewActions.leaveStudio(studioId, id, token)); } }, - getTopLevelComments: (id, offset) => { - dispatch(previewActions.getTopLevelComments(id, offset)); + getTopLevelComments: (id, offset, isAdmin, token) => { + dispatch(previewActions.getTopLevelComments(id, offset, isAdmin, token)); + }, + getCommentById: (projectId, commentId, isAdmin, token) => { + dispatch(previewActions.getCommentById(projectId, commentId, isAdmin, token)); }, getFavedStatus: (id, username, token) => { dispatch(previewActions.getFavedStatus(id, username, token)); @@ -552,8 +691,11 @@ const mapDispatchToProps = dispatch => ({ setLovedStatus: (loved, id, username, token) => { dispatch(previewActions.setLovedStatus(loved, id, username, token)); }, - reportProject: (id, formData) => { - dispatch(previewActions.reportProject(id, formData)); + reportProject: (id, formData, token) => { + dispatch(previewActions.reportProject(id, formData, token)); + }, + resetProject: () => { + dispatch(previewActions.resetProject()); }, setOriginalInfo: info => { dispatch(previewActions.setOriginalInfo(info)); @@ -600,6 +742,9 @@ render( preview: previewActions.previewReducer, ...GUI.guiReducers }, - {scratchGui: initGuiState(GUI.guiInitialState)}, + { + locales: GUI.initLocale(GUI.localesInitialState, window._locale), + scratchGui: initGuiState(GUI.guiInitialState) + }, GUI.guiMiddleware ); diff --git a/src/views/preview/preview.scss b/src/views/preview/preview.scss index cc2bb204a..e81b4c122 100644 --- a/src/views/preview/preview.scss +++ b/src/views/preview/preview.scss @@ -6,21 +6,15 @@ $player-width: 482px; $player-height: 406px; $stage-width: 480px; -/* screen sizes */ -$small: "screen and (max-width : #{$mobile}-1)"; -$medium: "screen and (min-width : #{$mobile}) and (max-width : #{$tablet}-1)"; -$big: "screen and (min-width : #{$tablet})"; -$medium-and-small: "screen and (max-width : #{$tablet}-1)"; - /* override view padding for share banner */ #view { padding: 0; + width: 100%; } .gui { position: absolute; top: 0; - z-index: 11; margin: 0; width: 100%; height: 100%; @@ -28,12 +22,24 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; .preview { + .inner { + margin: 0 auto; + + @media #{$medium-and-smaller} { + max-width: 90%; + } + + @media #{$intermediate} { + width: 90%; + min-width: 640px; + } + } + .project-title { font-size: 1.75rem; font-weight: 500; &.has-error { - .validation-message { right: 0; } @@ -59,6 +65,14 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; .inplace-input { height: calc(3rem - 4px); } + + @media #{$medium-and-smaller} { + flex-direction: row; + } + + @media #{$small} { + margin-right: 0; + } } img { @@ -83,13 +97,17 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; text-align: left; font-size: .8rem; flex-grow: 1; + + @media #{$medium-and-smaller} { + min-width: 100%; + } } .validation-message { + $arrow-border-width: 1rem; display: block; position: absolute; z-index: 1; - $arrow-border-width: 1rem; margin-top: $arrow-border-width; border: 1px solid $active-gray; border-radius: 5px; @@ -101,7 +119,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; color: $type-white; font-size: 1rem; - @media #{$medium-and-small} { + @media #{$medium-and-smaller} { margin-top: calc($arrow-border-width / 2); width: 100%; } @@ -124,7 +142,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; content: ""; - @media #{$medium-and-small} { + @media #{$medium-and-smaller} { display: none; } } @@ -153,7 +171,36 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; } .comments-container { - width: 65%; + padding-right: 1.5rem; + min-width: 65%; + max-width: 100%; + flex: 1; + + @media #{$medium-and-smaller} { + padding: 0; + width: 100%; + } + + .comment, .comment-top-row, .comment-bottom-row { + flex-direction: row; + } + + .comment-bubble { + text-align: left; + } + + .comments-turned-off { + border: 1px solid $ui-blue-25percent; + border-radius: .5rem; + background: $ui-blue-10percent; + padding: 1.5rem 0; + width: 100%; + text-align: center; + } + + .comments-allowed-input { + margin-right: 3px; + } } .remix-button, @@ -195,11 +242,27 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; justify-content: space-between; align-items: flex-start; flex-wrap: nowrap; + + &.force-row { + @media #{$medium-and-smaller} { + flex-direction: row; + } + } } .guiPlayer { display: inline-block; width: $player-width; + + @media #{$small} { + width: 100%; + } + } + + .force-center { + @media #{$medium-and-smaller} { + align-self: center; + } } .project-notes { @@ -209,26 +272,17 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; flex: 1; flex-flow: column; + @media #{$medium-and-smaller} { + margin-top: 1rem; + margin-left: 0; + width: 100%; + } + > .description-block:first-child { margin-top: 1rem; } } - .share-date { - margin-right: .75rem; - vertical-align: middle; - line-height: 2rem; - color: $type-gray; - font-size: .875rem; - } - - .subactions { - margin-left: 1.5rem; - justify-content: flex-end; - align-items: flex-start; - flex: 1; - } - .remix-credit { margin-bottom: 1rem; border: 1px solid $ui-blue-10percent; @@ -237,7 +291,11 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; padding: .5rem; width: calc(100% - 1rem); flex-wrap: nowrap; - align-items: flex-start; + align-items: center; + + @media #{$medium-and-smaller} { + flex-direction: row; + } } .credit-text { @@ -278,6 +336,12 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; margin-bottom: 0; } + .project-description-form { + display: flex; + width: 100%; + flex-grow: 1; + } + .project-description-edit { display: flex; margin-bottom: .75rem; @@ -312,146 +376,6 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; } } - .copyleft { - display: inline-block; - transform: scale(-1, 1); - margin: 0; - text-align: right; - } - - .stats { - line-height: 2rem; - justify-content: flex-start; - } - - .project-loves, - .project-favorites, - .project-remixes, - .project-views { - - display: inline; - padding-right: 2rem; - font-size: 1rem; - font-weight: bold; - - &:before { - display: inline-block; - margin-right: .5rem; - background-repeat: no-repeat; - background-position: center center; - background-size: contain; - width: 1.5rem; - height: 1.5rem; - vertical-align: -.25rem; - content: ""; - } - } - - .project-loves { - - cursor: pointer; - - &:before { - opacity: .5; - background-image: url("/svgs/project/love-gray.svg"); - } - } - - .project-loves.loved { - - &:before { - opacity: 1; - background-image: url("/svgs/project/love-red.svg"); - } - } - - .project-favorites { - - cursor: pointer; - - &:before { - opacity: .5; - background-image: url("/svgs/project/fav-gray.svg"); - } - } - - .project-favorites.favorited { - - &:before { - opacity: 1; - background-image: url("/svgs/project/fav-yellow.svg"); - } - } - - .project-remixes { - - &:before { - opacity: .5; - background-image: url("/svgs/project/remix-gray.svg"); - } - } - - .project-views { - - &:before { - opacity: .5; - background-image: url("/svgs/project/views-gray.svg"); - } - } - - .action-buttons { - display: flex; - margin-top: 0; - color: $type-white; - font-size: .8rem; - font-weight: 500; - justify-content: flex-end; - flex-wrap: wrap; - } - - .action-button { - margin: 0 0 0 .5rem; - border-radius: 19px; - background-color: $ui-blue; - padding: 0 .75rem; - height: 2rem; - text-decoration: none; - line-height: .875rem; - font-size: .75rem; - font-weight: normal; - - // &:hover { - // transition: background-color .25s ease; - // border-color: transparent; - // background-color: $active-gray; - // } - // - // &:active { - // border: 0 solid transparent; - // box-shadow: inset 0 0 5px $box-shadow-gray; - // background-color: $active-dark-gray; - // padding: calc(.75em + 1px) calc(1.5em + 1px); - // } - // - // &.report { - // border: 1px solid $ui-coral; - // background-color: $ui-coral; - // - // &:hover { - // transition: background-color .25s ease; - // border-color: transparent; - // background-color: $active-gray; - // } - // - // &:active { - // border: 0 solid transparent; - // box-shadow: inset 0 0 5px $box-shadow-gray; - // background-color: $active-dark-gray; - // padding: calc(.75em + 1px) calc(1.5em + 1px); - // } - // } - } - .comments-header { padding: 0 0 1rem 0; justify-content: space-between; @@ -461,42 +385,6 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; } } - .studio-button, - .copy-link-button, - .report-button { - &:before { - display: inline-block; - margin-right: .25rem; - background-repeat: no-repeat; - background-position: center center; - background-size: contain; - width: .875rem; - height: .875rem; - vertical-align: bottom; - content: ""; - } - } - - .studio-button { - &:before { - background-image: url("/svgs/project/studio-add-white.svg"); - } - } - - .copy-link-button { - &:before { - background-image: url("/svgs/project/copy-link-white.svg"); - } - } - - .report-button { - background-color: $ui-coral; - - &:before { - background-image: url("/svgs/project/report-white.svg"); - } - } - .project-lower-container { margin-top: 1rem; background-color: $ui-blue-10percent; @@ -517,7 +405,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; justify-content: flex-start; flex-direction: row; - @media #{$medium-and-small} { + @media #{$medium-and-smaller} { justify-content: center; } @@ -542,11 +430,21 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)"; .thumbnail-column { display: inline-block; width: 100%; + + /* TODO: the following can be transferred to + src/components/thumbnailcolumn/thumbnailcolumn.scss + after testing */ + @media #{$medium-and-smaller} { + flex-direction: row; + + .thumbnail { + display: inline-block; + } + } + } + + @media #{$medium-and-smaller} { + margin-top: 1rem; } } } - -.report-text textarea { - // override min-height from default settings (for teacher registration) - min-height: 8rem; -} diff --git a/src/views/preview/remix-credit.jsx b/src/views/preview/remix-credit.jsx index a3557d454..52a7744ef 100644 --- a/src/views/preview/remix-credit.jsx +++ b/src/views/preview/remix-credit.jsx @@ -1,4 +1,5 @@ const React = require('react'); +const FormattedMessage = require('react-intl').FormattedMessage; const FlexRow = require('../../components/flex-row/flex-row.jsx'); const Avatar = require('../../components/avatar/avatar.jsx'); const projectShape = require('./projectshape.jsx').projectShape; @@ -13,16 +14,24 @@ const RemixCredit = props => { src={`https://cdn2.scratch.mit.edu/get_image/user/${projectInfo.author.id}_48x48.png`} />
- Thanks to - {projectInfo.author.username} - for the original project - {projectInfo.title} - . + + {projectInfo.author.username} + + ), + projectLink: ( + + {projectInfo.title} + + ) + }} + />
); diff --git a/src/views/preview/remix-list.jsx b/src/views/preview/remix-list.jsx index f9fb4d7c3..485557afa 100644 --- a/src/views/preview/remix-list.jsx +++ b/src/views/preview/remix-list.jsx @@ -1,5 +1,6 @@ const React = require('react'); const PropTypes = require('prop-types'); +const FormattedMessage = require('react-intl').FormattedMessage; const FlexRow = require('../../components/flex-row/flex-row.jsx'); const ThumbnailColumn = require('../../components/thumbnailcolumn/thumbnailcolumn.jsx'); const projectShape = require('./projectshape.jsx').projectShape; @@ -10,11 +11,11 @@ const RemixList = props => { return (
- Remixes +
{remixes.length === 0 ? ( // TODO: style remix invitation - Invite user to remix + ) : ( { - if (props.shared) return null; - return ( -
-
- - - This project is not shared — so only you can see it. Click share to let everyone see it! - - - -
-
- ); -}; +const ShareBanner = ({onShare}) => ( +
+ + + + + + +
+); ShareBanner.propTypes = { - shared: PropTypes.bool.isRequired + onShare: PropTypes.func }; module.exports = ShareBanner; diff --git a/src/views/preview/share-banner.scss b/src/views/preview/share-banner.scss index 8c5140ccb..0524699f4 100644 --- a/src/views/preview/share-banner.scss +++ b/src/views/preview/share-banner.scss @@ -2,29 +2,34 @@ $navigation-height: 50px; -.shareBanner { +.share-banner-outer { background-color: $ui-orange-25percent; width: 100%; overflow: hidden; color: $ui-orange; } +.share-banner { + align-items: center; + justify-content: space-between; +} + .share-button { - margin-top: 0; background-color: $ui-orange; font-size: .875rem; font-weight: normal; - - &:before { - display: inline-block; - margin-right: .5rem; - background-image: url("/svgs/project/share-white.svg"); - background-repeat: no-repeat; - background-position: center center; - background-size: contain; - width: 1.25rem; - height: 1.25rem; - vertical-align: middle; - content: ""; - } + + // don't show an image in share button, for now. + // &:before { + // display: inline-block; + // margin-right: .5rem; + // background-image: url("/svgs/project/share-white.svg"); + // background-repeat: no-repeat; + // background-position: center center; + // background-size: contain; + // width: 1.25rem; + // height: 1.25rem; + // vertical-align: middle; + // content: ""; + // } } diff --git a/src/views/preview/stats.jsx b/src/views/preview/stats.jsx new file mode 100644 index 000000000..2fb08b22c --- /dev/null +++ b/src/views/preview/stats.jsx @@ -0,0 +1,53 @@ +const PropTypes = require('prop-types'); +const React = require('react'); +const FlexRow = require('../../components/flex-row/flex-row.jsx'); +const classNames = require('classnames'); +const approx = require('approximate-number'); + +const CappedNumber = require('../../components/cappednumber/cappednumber.jsx'); +const projectShape = require('./projectshape.jsx').projectShape; + +require('./stats.scss'); + +const Stats = props => ( + +
+ {approx(props.loveCount, {decimal: false})} +
+
+ {approx(props.favoriteCount, {decimal: false})} +
+
+ {approx(props.projectInfo.stats.remixes, {decimal: false})} +
+
+ +
+
+); + +Stats.propTypes = { + faved: PropTypes.bool, + favoriteCount: PropTypes.number, + loveCount: PropTypes.number, + loved: PropTypes.bool, + onFavoriteClicked: PropTypes.func, + onLoveClicked: PropTypes.func, + projectInfo: projectShape +}; + +module.exports = Stats; diff --git a/src/views/preview/stats.scss b/src/views/preview/stats.scss new file mode 100644 index 000000000..5b159aaaf --- /dev/null +++ b/src/views/preview/stats.scss @@ -0,0 +1,94 @@ +@import "../../frameless"; + +.stats { + line-height: 2rem; + justify-content: flex-start; + + @media #{$medium-and-smaller} { + margin: 0; + width: 100%; + justify-content: center; + flex-direction: row; + } + + & > div { + @media #{$medium-and-smaller} { + padding: 0 1rem; + } + } +} + +.project-loves, +.project-favorites, +.project-remixes, +.project-views { + + display: inline; + padding-right: 2rem; + font-size: 1rem; + font-weight: bold; + + &:before { + display: inline-block; + margin-right: .5rem; + background-repeat: no-repeat; + background-position: center center; + background-size: contain; + width: 1.5rem; + height: 1.5rem; + vertical-align: -.25rem; + content: ""; + } +} + +.project-loves { + + cursor: pointer; + + &:before { + opacity: .5; + background-image: url("/svgs/project/love-gray.svg"); + } +} + +.project-loves.loved { + + &:before { + opacity: 1; + background-image: url("/svgs/project/love-red.svg"); + } +} + +.project-favorites { + + cursor: pointer; + + &:before { + opacity: .5; + background-image: url("/svgs/project/fav-gray.svg"); + } +} + +.project-favorites.favorited { + + &:before { + opacity: 1; + background-image: url("/svgs/project/fav-yellow.svg"); + } +} + +.project-remixes { + + &:before { + opacity: .5; + background-image: url("/svgs/project/remix-gray.svg"); + } +} + +.project-views { + + &:before { + opacity: .5; + background-image: url("/svgs/project/views-gray.svg"); + } +} diff --git a/src/views/preview/studio-list.jsx b/src/views/preview/studio-list.jsx index dbb4f1417..33f3f4f55 100644 --- a/src/views/preview/studio-list.jsx +++ b/src/views/preview/studio-list.jsx @@ -1,5 +1,6 @@ const React = require('react'); const PropTypes = require('prop-types'); +const FormattedMessage = require('react-intl').FormattedMessage; const FlexRow = require('../../components/flex-row/flex-row.jsx'); const ThumbnailColumn = require('../../components/thumbnailcolumn/thumbnailcolumn.jsx'); const projectShape = require('./projectshape.jsx').projectShape; @@ -10,11 +11,11 @@ const StudioList = props => { return (
- Studios +
{studios.length === 0 ? ( // TODO: style remix invitation - Invite user to add to studio + ) : ( ( + +
+
©
+ {' '} + {/* eslint-disable react/jsx-sort-props */} + {props.shareDate === null ? + 'Unshared' : + + } + {/* eslint-enable react/jsx-sort-props */} +
+ + {props.canAddToStudio && + + + {props.addToStudioOpen && ( + + )} + + } + + {(props.canReport) && + + + {props.reportOpen && ( + + )} + + } + +
+); + +Subactions.propTypes = { + addToStudioOpen: PropTypes.bool, + canAddToStudio: PropTypes.bool, + canReport: PropTypes.bool, + onAddToStudioClicked: PropTypes.func, + onAddToStudioClosed: PropTypes.func, + onReportClicked: PropTypes.func.isRequired, + onReportClose: PropTypes.func.isRequired, + onReportSubmit: PropTypes.func.isRequired, + onToggleStudio: PropTypes.func, + reportOpen: PropTypes.bool, + shareDate: PropTypes.string +}; + +module.exports = Subactions; diff --git a/src/views/preview/subactions.scss b/src/views/preview/subactions.scss new file mode 100644 index 000000000..b778b6bb5 --- /dev/null +++ b/src/views/preview/subactions.scss @@ -0,0 +1,110 @@ +@import "../../colors"; +@import "../../frameless"; + +.subactions { + margin-left: 1.5rem; + justify-content: flex-end; + align-items: flex-start; + flex: 1; + + @media #{$medium-and-smaller} { + margin-top: 1rem; + width: 100%; + } + + .share-date { + margin-right: .75rem; + vertical-align: middle; + line-height: 2rem; + color: $type-gray; + font-size: .875rem; + + @media #{$small} { + margin-top: 1rem; + width: 100%; + order: 100; + } + + .copyleft { + display: inline-block; + transform: scale(-1, 1); + margin: 0; + text-align: right; + + @media #{$medium-and-smaller} { + padding: 0; + } + } + } + .action-buttons { + display: flex; + margin-top: 0; + color: $type-white; + font-size: .8rem; + font-weight: 500; + justify-content: flex-end; + flex-wrap: wrap; + + .action-button { + margin: 0 0 0 .5rem; + border-radius: 19px; + background-color: $ui-blue; + padding: 0 .75rem; + height: 2rem; + text-decoration: none; + line-height: .875rem; + font-size: .75rem; + font-weight: normal; + + &.studio-button, + &.copy-link-button, + &.report-button { + &:before { + display: inline-block; + margin-right: .25rem; + background-repeat: no-repeat; + background-position: center center; + background-size: contain; + width: .875rem; + height: .875rem; + vertical-align: bottom; + content: ""; + } + } + + &.studio-button { + &:before { + background-image: url("/svgs/project/studio-add-white.svg"); + } + } + + &.copy-link-button { + &:before { + background-image: url("/svgs/project/copy-link-white.svg"); + } + } + + &.report-button { + background-color: $ui-coral; + + &:before { + background-image: url("/svgs/project/report-white.svg"); + } + } + } + } +} + +.subactions, .subactions .action-buttons { + @media #{$medium-and-smaller} { + margin: 0; + justify-content: center; + flex-direction: row; + } + + & > div, .action-button { + @media #{$medium-and-smaller} { + padding: 0 1rem; + } + } +} diff --git a/src/views/search/search.jsx b/src/views/search/search.jsx index ab348cfcd..b0c5e8a99 100644 --- a/src/views/search/search.jsx +++ b/src/views/search/search.jsx @@ -78,7 +78,12 @@ class Search extends React.Component { while (term.indexOf('&') > -1) { term = term.substring(0, term.indexOf('&')); } - term = decodeURIComponent(term.split('+').join(' ')); + try { + term = decodeURIComponent(term); + } catch (e) { + // Error means that term was not URI encoded and decoding failed. + // We can silence this error because not all query strings are intended to be decoded. + } this.props.dispatch(navigationActions.setSearchTerm(term)); } componentDidUpdate (prevProps) { diff --git a/src/views/search/search.scss b/src/views/search/search.scss index 93e3c3fc0..07bf1f911 100644 --- a/src/views/search/search.scss +++ b/src/views/search/search.scss @@ -169,7 +169,7 @@ $base-bg: $ui-white; } //4 columns -@media only screen and (max-width: $mobile - 1) { +@media #{$small} { .outer { .search { width: $cols4; @@ -197,7 +197,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; @@ -216,7 +216,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; diff --git a/src/views/splash/beta/middle-banner.scss b/src/views/splash/beta/middle-banner.scss index 8578b51d1..ca0e759f4 100644 --- a/src/views/splash/beta/middle-banner.scss +++ b/src/views/splash/beta/middle-banner.scss @@ -65,7 +65,7 @@ } } -@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) { +@media #{$intermediate} { .beta-middle-banner { margin: 20px auto 40px auto; width: $cols8; diff --git a/src/views/splash/splash.jsx b/src/views/splash/splash.jsx index 9eac6b052..06655fd96 100644 --- a/src/views/splash/splash.jsx +++ b/src/views/splash/splash.jsx @@ -71,7 +71,10 @@ class Splash extends React.Component { getNews () { api({ uri: '/news?limit=3' - }, (err, body) => { + }, (err, body, resp) => { + if (resp.statusCode !== 200) { + return log.error(`Unexpected status code ${resp.statusCode} received from news request`); + } if (!body) return log.error('No response body'); if (!err) return this.setState({news: body}); }); diff --git a/src/views/splash/splash.scss b/src/views/splash/splash.scss index 4fd6f225a..cf6ab16e3 100644 --- a/src/views/splash/splash.scss +++ b/src/views/splash/splash.scss @@ -96,7 +96,7 @@ } //4 columns -@media only screen and (max-width: $mobile - 1) { +@media #{$small} { .splash { .splash-header { flex-wrap: wrap; @@ -119,7 +119,7 @@ } //6 columns -@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) { +@media #{$medium} { .splash { .splash-header { flex-wrap: wrap; @@ -143,7 +143,7 @@ } //6 columns -@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) { +@media #{$intermediate} { .splash { .splash-header { margin: 0 auto; diff --git a/src/views/teachers/landing/landing.scss b/src/views/teachers/landing/landing.scss index 8863ab74b..324f36c3a 100644 --- a/src/views/teachers/landing/landing.scss +++ b/src/views/teachers/landing/landing.scss @@ -247,7 +247,7 @@ $story-width: $cols3; } //4 columns -@media only screen and (max-width: $mobile - 1) { +@media #{$small} { $story-width: $cols4; .stories { @@ -311,7 +311,7 @@ $story-width: $cols3; } //6 columns -@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) { +@media #{$medium} { $story-width: $cols3; .stories { @@ -366,7 +366,7 @@ $story-width: $cols3; //8 columns -@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) { +@media #{$intermediate} { $story-width: $cols4; .stories { @@ -500,7 +500,7 @@ $story-width: $cols3; } // 12 columns -@media only screen and (min-width: $desktop) { +@media #{$big} { $story-width: $cols3; .stories { diff --git a/src/views/tips/tips.scss b/src/views/tips/tips.scss index f9b4c62aa..26adec97c 100644 --- a/src/views/tips/tips.scss +++ b/src/views/tips/tips.scss @@ -83,7 +83,7 @@ img.tips-icon { height: 1.75rem; } //4 columns -@media only screen and (max-width: $mobile - 1) { +@media #{$small} { .title-banner { &.masthead { @@ -121,7 +121,7 @@ img.tips-icon { } //6 columns -@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) { +@media #{$medium} { .title-banner { &.masthead { @@ -152,7 +152,7 @@ img.tips-icon { //8 columns -@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) { +@media #{$intermediate} { .title-banner { &.masthead { padding-bottom: 2rem; @@ -189,7 +189,7 @@ img.tips-icon { } // 12 columns -@media only screen and (min-width: $desktop) { +@media #{$big} { .title-banner { &.masthead { padding-bottom: 1.25rem; diff --git a/src/views/wedo2-legacy/wedo2.scss b/src/views/wedo2-legacy/wedo2.scss index c661e2e32..56c80034d 100644 --- a/src/views/wedo2-legacy/wedo2.scss +++ b/src/views/wedo2-legacy/wedo2.scss @@ -156,7 +156,7 @@ // Responsive Behavior //4 columns -@media only screen and (max-width: $mobile - 1) { +@media #{$small} { .wedo { .inner { margin: 0 auto; @@ -186,7 +186,7 @@ } //6 columns -@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) { +@media #{$medium} { .wedo { .project-list, .columns3 { @@ -210,7 +210,7 @@ } //8 columns -@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) { +@media #{$intermediate} { #view { text-align: center; } diff --git a/static/cdn/README.txt b/static/cdn/README.txt new file mode 100644 index 000000000..377200033 --- /dev/null +++ b/static/cdn/README.txt @@ -0,0 +1,12 @@ +# Maintenance and Trouble Status Pages + +The contents of these two files are loaded directly from our CDN provider. This allows us to present information about the backend systems that are unable to respond, either due to maintenance or some other backend systems trouble. + +#### maintenance.html + +A general message about work being performed on the systems + +#### trouble.html + +A message about trouble occurring that is currently being worked on. + diff --git a/static/cdn/maintenance.html b/static/cdn/maintenance.html new file mode 100644 index 000000000..b6c818a28 --- /dev/null +++ b/static/cdn/maintenance.html @@ -0,0 +1,208 @@ + + + + + Scratch is down for maintenance! + + + +
+
+
+
+ +
+

The Scratch Team is making some changes to the website.

+
+

+ Try reloading this page in a few minutes. + +

+

You can still Download Scratch and make projects.

+ +
+
+ + diff --git a/static/cdn/trouble.html b/static/cdn/trouble.html new file mode 100644 index 000000000..59f3241cc --- /dev/null +++ b/static/cdn/trouble.html @@ -0,0 +1,208 @@ + + + + + Scratch is having trouble! + + + +
+
+
+
+ +
+

The Scratch Team is working hard to fix something that broke.

+
+

+ Try reloading this page in a few minutes. + +

+

You can still Download Scratch and make projects.

+ +
+
+ + diff --git a/static/images/scratchlink/mac-toolbar.png b/static/images/scratchlink/mac-toolbar.png index 43fe68ad9..045131070 100644 Binary files a/static/images/scratchlink/mac-toolbar.png and b/static/images/scratchlink/mac-toolbar.png differ diff --git a/static/images/scratchlink/windows-toolbar.png b/static/images/scratchlink/windows-toolbar.png index b89dd2bda..2c9c2d8d7 100644 Binary files a/static/images/scratchlink/windows-toolbar.png and b/static/images/scratchlink/windows-toolbar.png differ diff --git a/static/svgs/extensions/scratch-link.svg b/static/svgs/extensions/scratch-link.svg index 2c11932f7..97e82f23c 100644 --- a/static/svgs/extensions/scratch-link.svg +++ b/static/svgs/extensions/scratch-link.svg @@ -1,13 +1,10 @@ - + scratch-link - Created with Sketch. - - - - + + - \ No newline at end of file + diff --git a/test/integration/package.json b/test/integration/package.json index 909e95e91..811acf4df 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -1,6 +1,6 @@ { "dependencies": { "selenium-webdriver": "3.6.0", - "chromedriver": "2.37.0" + "chromedriver": "2.43.1" } } diff --git a/test/integration/selenium-helpers.js b/test/integration/selenium-helpers.js index 5b6e6e392..42157b029 100644 --- a/test/integration/selenium-helpers.js +++ b/test/integration/selenium-helpers.js @@ -61,7 +61,7 @@ class SeleniumHelper { let driverConfig = { browserName: 'chrome', platform: 'macOS 10.13', - version: '67.0' + version: '70.0' }; var driver = new webdriver.Builder() .withCapabilities({ diff --git a/webpack.config.js b/webpack.config.js index ca7377562..f10a056d0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -167,6 +167,7 @@ module.exports = { 'process.env.API_HOST': '"' + (process.env.API_HOST || 'https://api.scratch.mit.edu') + '"', 'process.env.ASSET_HOST': '"' + (process.env.ASSET_HOST || 'https://assets.scratch.mit.edu') + '"', 'process.env.BACKPACK_HOST': '"' + (process.env.BACKPACK_HOST || 'https://backpack.scratch.mit.edu') + '"', + 'process.env.CLOUDDATA_HOST': '"' + (process.env.CLOUDDATA_HOST || 'clouddata.scratch.mit.edu') + '"', 'process.env.PROJECT_HOST': '"' + (process.env.PROJECT_HOST || 'https://projects.scratch.mit.edu') + '"', 'process.env.SCRATCH_ENV': '"' + (process.env.SCRATCH_ENV || 'development') + '"' }),