diff --git a/package-lock.json b/package-lock.json index 5a6627281..59402a6ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,9 +101,9 @@ "optional": true }, "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -503,9 +503,9 @@ } }, "electron-to-chromium": { - "version": "1.3.726", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.726.tgz", - "integrity": "sha512-dw7WmrSu/JwtACiBzth8cuKf62NKL1xVJuNvyOg0jvruN/n4NLtGYoTzciQquCPNaS2eR+BT5GrxHbslfc/w1w==", + "version": "1.3.727", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz", + "integrity": "sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg==", "dev": true }, "semver": { @@ -1368,27 +1368,10 @@ } } }, - "@formatjs/ecma402-abstract": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.0.tgz", - "integrity": "sha512-0IQF4oDZdO8ruyrNJZuRle3F/YiGgRwTNrZyMI1N1X8GERZusOrXU9Stw+j/lyyfDWaJK44b+Qnri/qfLPCtGQ==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", - "dev": true - } - } - }, "@formatjs/intl-getcanonicallocales": { - "version": "1.5.9", - "resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.5.9.tgz", - "integrity": "sha512-bO0J3IaamFM3rU7noXo1bWSmPA8xuAL8NPk+k5Dy08ehiu/STT3sN+6DGLEvRCpb465CpjUWGCNknDFIcdu9hA==", + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.5.10.tgz", + "integrity": "sha512-tFqGxZ9HkAzphupybyCKdWHzL1ge/sY8TtzEK57Hs3RCxrv/y+VxIPrE+Izw2oCFowQBz76cyi0zT6PjHuWArA==", "dev": true, "requires": { "cldr-core": "38", @@ -1404,21 +1387,21 @@ } }, "@formatjs/intl-locale": { - "version": "2.4.23", - "resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.23.tgz", - "integrity": "sha512-5G0SjOsVxmX79dPaYk6KWxtdQ18UNK+E2JtAXvGxP8rSMqbJ/7cpDg95CU+YBXVKn6pRWefwqBsbjT5l+kK3yQ==", + "version": "2.4.24", + "resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.24.tgz", + "integrity": "sha512-+JOwvBRFS/GFuJlWiWbfAzBng0A+ANoGV1LRseXK+4uzp4Sn35GD8M/dfgU1lp2R2dTWpYie2yyoHe4k4aHF6w==", "dev": true, "requires": { - "@formatjs/ecma402-abstract": "1.7.0", - "@formatjs/intl-getcanonicallocales": "1.5.9", + "@formatjs/ecma402-abstract": "1.7.1", + "@formatjs/intl-getcanonicallocales": "1.5.10", "cldr-core": "38", "tslib": "^2.1.0" }, "dependencies": { "@formatjs/ecma402-abstract": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.0.tgz", - "integrity": "sha512-0IQF4oDZdO8ruyrNJZuRle3F/YiGgRwTNrZyMI1N1X8GERZusOrXU9Stw+j/lyyfDWaJK44b+Qnri/qfLPCtGQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz", + "integrity": "sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg==", "dev": true, "requires": { "tslib": "^2.1.0" @@ -1433,15 +1416,24 @@ } }, "@formatjs/intl-pluralrules": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.17.tgz", - "integrity": "sha512-SOX7lRrM1DQXZSzqoCztT+Gc6lnnoqGAVrX/YivQ7J8miPfxN8vgZhZN0MiEGPSLV0HJdO+AA+Xu0hUNJPlpnQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.18.tgz", + "integrity": "sha512-qRFITPsNoeXfsiGc97pp8mVgqcC7aQNuXsiJjY9LpXVTkYNfjUP4ZpbYXflM4xoWCXMJNz3ilsrQhZWXy9td5g==", "dev": true, "requires": { - "@formatjs/ecma402-abstract": "1.7.0", + "@formatjs/ecma402-abstract": "1.7.1", "tslib": "^2.1.0" }, "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz", + "integrity": "sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, "tslib": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", @@ -1451,15 +1443,24 @@ } }, "@formatjs/intl-relativetimeformat": { - "version": "8.1.7", - "resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-8.1.7.tgz", - "integrity": "sha512-S8OoqOwiSqU/T0umkjB2lZcyWS4Dh8sWd4m8V+MsSnraeijNho/wSXQhJAuHx0b2Xvnlddt7a5RwyUnLADUepA==", + "version": "8.1.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-8.1.8.tgz", + "integrity": "sha512-MIVrsgG7hvYrnes6TxJLflXhhTuxIaWCIdf6p5Iv6HguTtDJqqAFOCNRCqUnYQeYcNbgIQBgLb0Kh7djS0GU+w==", "dev": true, "requires": { - "@formatjs/ecma402-abstract": "1.7.0", + "@formatjs/ecma402-abstract": "1.7.1", "tslib": "^2.1.0" }, "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz", + "integrity": "sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, "tslib": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", @@ -10518,9 +10519,9 @@ } }, "handlebars": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", - "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", "dev": true, "requires": { "minimist": "^1.2.5", @@ -10543,9 +10544,9 @@ "dev": true }, "uglify-js": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.2.tgz", - "integrity": "sha512-G440NU6fewtnQftSgqRV1r2A5ChKbU1gqFCJ7I8S7MPpY/eZZfLGefaY6gUZYiWebMaO+txgiQ1ZyLDuNWJulg==", + "version": "3.13.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.5.tgz", + "integrity": "sha512-xtB8yEqIkn7zmOyS2zUNBsYCBRhDkvlNxMMY2smuJ/qA8NCHeQvKCF3i9Z4k8FJH4+PJvZRtMrPynfZ75+CSZw==", "dev": true, "optional": true } @@ -10814,9 +10815,9 @@ } }, "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "hsluv": { @@ -18918,9 +18919,9 @@ }, "dependencies": { "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -20855,9 +20856,9 @@ } }, "scratch-blocks": { - "version": "0.1.0-prerelease.20210505033858", - "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210505033858.tgz", - "integrity": "sha512-qsHsxVsXvB7cZnoLirkl+HRjj+X7VcJh9Rh7UX8JoMEJsfUfTRJfo3wSt7zv4iKx6EK/QIStQNs9AACSPVaA7Q==", + "version": "0.1.0-prerelease.20210512032919", + "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210512032919.tgz", + "integrity": "sha512-h8XAMbgGGZOHnNM4GwAzTsUY/f0ZKfwOdqt/Uh5/puFPC52uuGHkJp+IrTCMrDt4LkTq7uM/aAPyWMR/z2xwIQ==", "dev": true, "requires": { "exports-loader": "0.6.3", @@ -20865,9 +20866,9 @@ } }, "scratch-gui": { - "version": "0.1.0-prerelease.20210505040706", - "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210505040706.tgz", - "integrity": "sha512-KyHm41UAlIKomysMzXqKMkibs6WsibC2jCX/AbsYR47UNtFJLhjvUyJKv05EbGo9GAr/b1uRglTRtiXA8SWsvw==", + "version": "0.1.0-prerelease.20210512144423", + "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210512144423.tgz", + "integrity": "sha512-EjQmlthZD6WfIkb5XqOEvIdxkzeSWoEHk1gHRF/gvB/cfK5EXzf9ZBM/Q8Qkq9kd/kyddi8D/gNINDHRh4cfdQ==", "dev": true, "requires": { "arraybuffer-loader": "^1.0.6", @@ -20918,14 +20919,14 @@ "redux": "3.7.2", "redux-throttle": "0.1.1", "scratch-audio": "0.1.0-prerelease.20200528195344", - "scratch-blocks": "0.1.0-prerelease.20210505033858", - "scratch-l10n": "3.11.20210504031549", + "scratch-blocks": "0.1.0-prerelease.20210512032919", + "scratch-l10n": "3.11.20210512031514", "scratch-paint": "0.2.0-prerelease.20210407203313", "scratch-render": "0.1.0-prerelease.20210325231800", "scratch-render-fonts": "1.0.0-prerelease.20210401210003", "scratch-storage": "1.3.4", - "scratch-svg-renderer": "0.2.0-prerelease.20210408171934", - "scratch-vm": "0.2.0-prerelease.20210412181234", + "scratch-svg-renderer": "0.2.0-prerelease.20210511195415", + "scratch-vm": "0.2.0-prerelease.20210510162256", "startaudiocontext": "1.2.1", "style-loader": "^0.23.0", "text-encoding": "0.7.0", @@ -21310,19 +21311,6 @@ "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", "dev": true }, - "scratch-l10n": { - "version": "3.11.20210504031549", - "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210504031549.tgz", - "integrity": "sha512-R6CfX0p8LegrsqKm5s+8cSlvzg8Ijma4zGOet22yVtJbtSEcmFmi3LbwV5cKKPjoDWobZMYUhrazmlBLswKPQw==", - "dev": true, - "requires": { - "@babel/cli": "^7.1.2", - "@babel/core": "^7.1.2", - "babel-plugin-react-intl": "^3.0.1", - "react-intl": "^2.8.0", - "transifex": "1.6.6" - } - }, "scratch-storage": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-1.3.4.tgz", @@ -21400,9 +21388,9 @@ } }, "scratch-l10n": { - "version": "3.11.20210505031459", - "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210505031459.tgz", - "integrity": "sha512-1geGseNJ95UIyKaOWYIkdE0V2mSUxcrGR/wUE86727FqwYdo19nJyaXvzcv2mWWGdvDu3eV7ei3raD1ZXAniSA==", + "version": "3.11.20210512031514", + "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210512031514.tgz", + "integrity": "sha512-STUFHVKPyCMrfeKV9gLK5rf6SMtS8JB1+nV2Jf/4geSb3mAKfWLHOgRxIjRbHsGRhv2sZ8Z+wE6/zILpnnlNZQ==", "dev": true, "requires": { "@babel/cli": "^7.1.2", @@ -21636,9 +21624,9 @@ } }, "scratch-svg-renderer": { - "version": "0.2.0-prerelease.20210408171934", - "resolved": "https://registry.npmjs.org/scratch-svg-renderer/-/scratch-svg-renderer-0.2.0-prerelease.20210408171934.tgz", - "integrity": "sha512-kc59fyZlJ58ooW86VQo9oqXNzpR48RH7vObehekVyPq4FMRENwtv9gCZ5XitLPNsLEheFCJdfRVPkVsMAjhPYQ==", + "version": "0.2.0-prerelease.20210511195415", + "resolved": "https://registry.npmjs.org/scratch-svg-renderer/-/scratch-svg-renderer-0.2.0-prerelease.20210511195415.tgz", + "integrity": "sha512-zeT93lfMeJNWhj8cLfNeDWTZT/fDS2Fnz6btCJpvE5AAyel+8VE1Y9hBb1OJ+ap8vjA1O31TnDApIylRmA/g5w==", "dev": true, "requires": { "base64-js": "1.2.1", @@ -21678,9 +21666,9 @@ "dev": true }, "scratch-vm": { - "version": "0.2.0-prerelease.20210412181234", - "resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20210412181234.tgz", - "integrity": "sha512-xhLd4GJ9O9kgPzHj4LJhOALYbR/9FJBszrnsjKoiU2ErQD3/iZwWYqsnFjHKVvqv8wo27SAmF4crjDQyEIGY+A==", + "version": "0.2.0-prerelease.20210510162256", + "resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20210510162256.tgz", + "integrity": "sha512-14KHdLRoEcpHRsLPkiBNnMDNAtoGYr0ZkAUdHatwvqTfzS1eN22qytqdkJ8Mx3LIxP52K/IMOVq9rFVUZ4df3w==", "dev": true, "requires": { "@vernier/godirect": "1.5.0", @@ -25851,9 +25839,9 @@ "dev": true }, "ua-parser-js": { - "version": "0.7.20", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz", - "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==", + "version": "0.7.28", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz", + "integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index 1b954f597..75ed64f00 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "redux-mock-store": "^1.2.3", "redux-thunk": "2.0.1", "sass-loader": "6.0.6", - "scratch-gui": "0.1.0-prerelease.20210505040706", + "scratch-gui": "0.1.0-prerelease.20210512144423", "scratch-l10n": "latest", "selenium-webdriver": "3.6.0", "slick-carousel": "1.6.0", diff --git a/src/_colors.scss b/src/_colors.scss index 5401a6541..495c84469 100644 --- a/src/_colors.scss +++ b/src/_colors.scss @@ -26,6 +26,7 @@ $background-color: hsla(0, 0, 99, 1); //#FDFDFD /* 3.0 colors */ /* Using www naming convention for now, should be consistent with gui */ $ui-aqua: hsla(163, 85, 40, 1); // #0FBD8C Extension Primary +$ui-aqua-dark: hsla(163, 85, 30, 1); // #0B8E69 Extension Aqua 3 $ui-purple: hsla(260, 100, 70, 1); // #9966FF Looks Primary $ui-purple-dark: hsla(260, 60, 60, 1); // #774DCB Looks Secondary $ui-magenta: hsla(300, 53%, 60%, 1); /* #CF63CF Sounds Primary */ diff --git a/src/components/forms/button.jsx b/src/components/forms/button.jsx index b5b97e39a..7ac93878e 100644 --- a/src/components/forms/button.jsx +++ b/src/components/forms/button.jsx @@ -6,21 +6,38 @@ const React = require('react'); require('./button.scss'); const Button = props => { - const classes = classNames('button', props.className); + const classes = classNames('button', props.className, {'forms-close-button': props.isCloseType}); return ( ); }; Button.propTypes = { children: PropTypes.node, - className: PropTypes.string + className: PropTypes.string, + isCloseType: PropTypes.bool +}; + +Button.defaultProps = { + className: '', + isCloseType: false }; module.exports = Button; diff --git a/src/components/forms/button.scss b/src/components/forms/button.scss index 3a88a61f4..bc5ae82c6 100644 --- a/src/components/forms/button.scss +++ b/src/components/forms/button.scss @@ -53,3 +53,22 @@ $pass-bg: $ui-aqua; margin-right: -.25rem; } } + +.forms-close-button { + padding: 0; + + position: absolute; + top: 0.5rem; + right: 0.5rem; + border-radius: 1rem; + background-color: $active-gray; + cursor: pointer; + width: 2rem; + height: 2rem; + text-align: center; + line-height: 2rem; +} + +.forms-close-button img { + padding-top: 0.5rem; +} diff --git a/src/l10n.json b/src/l10n.json index 48859e7b4..35ebb79e6 100644 --- a/src/l10n.json +++ b/src/l10n.json @@ -248,6 +248,7 @@ "thumbnail.by": "by", "report.error": "Something went wrong when trying to send your message. Please try again.", "report.project": "Report Project", + "report.studio": "Report Studio", "report.projectInstructions": "When you send a report, it lets the Scratch Team know about projects that break the {CommunityGuidelinesLink}. Does something in this project break the {CommunityGuidelinesLink}? If you think it does, please tell us more.", "report.CommunityGuidelinesLinkText": "Scratch Community Guidelines", "report.reasonPlaceHolder": "Select a reason", diff --git a/src/redux/infinite-list.js b/src/redux/infinite-list.js index b09e10b12..61a2ab1a4 100644 --- a/src/redux/infinite-list.js +++ b/src/redux/infinite-list.js @@ -16,14 +16,6 @@ * the next state. */ -/** - * @typedef {function} InfiniteListFetcher - * A function to call that returns more data for the InfiniteList - * loadMore action. It must resolve to {items: [], moreToLoad} or - * reject with the error {statusCode}. - * @returns {Promise<{items:[], moreToLoad:boolean}>} - */ - /** * A redux module to create a list of items where more items can be loaded * using an API. Additionally, there are actions for prepending items @@ -35,17 +27,16 @@ */ const InfiniteList = key => { - const initialState = { + const getInitialState = () => ({ items: [], - offset: 0, error: null, loading: true, moreToLoad: false - }; + }); const reducer = (state, action) => { if (typeof state === 'undefined') { - state = initialState; + state = getInitialState(); } switch (action.type) { @@ -76,10 +67,11 @@ const InfiniteList = key => { ...state, items: state.items.filter((_, i) => i !== action.index) }; - case `${key}_PREPEND`: + case `${key}_CREATE`: return { ...state, - items: [action.item].concat(state.items) + items: action.atEnd ? state.items.concat([action.item]) : + [action.item].concat(state.items) }; case `${key}_ERROR`: return { @@ -88,33 +80,21 @@ const InfiniteList = key => { loading: false, moreToLoad: false }; + case `${key}_CLEAR`: + return getInitialState(); default: return state; } }; const actions = { - create: item => ({type: `${key}_PREPEND`, item}), + create: (item, atEnd = false) => ({type: `${key}_CREATE`, item, atEnd}), remove: index => ({type: `${key}_REMOVE`, index}), replace: (index, item) => ({type: `${key}_REPLACE`, index, item}), error: error => ({type: `${key}_ERROR`, error}), loading: () => ({type: `${key}_LOADING`}), append: (items, moreToLoad) => ({type: `${key}_APPEND`, items, moreToLoad}), - - /** - * Load more action returns a thunk. It takes a function to call to get more items. - * It will call the LOADING action before calling the fetcher, and call - * APPEND with the results or call ERROR. - * @param {InfiniteListFetcher} fetcher - function that returns a promise - * which must resolve to {items: [], moreToLoad}. - * @returns {function} a thunk that sequences the load and dispatches - */ - loadMore: fetcher => (dispatch => { - dispatch(actions.loading()); - return fetcher() - .then(({items, moreToLoad}) => dispatch(actions.append(items, moreToLoad))) - .catch(error => dispatch(actions.error(error))); - }) + clear: () => ({type: `${key}_CLEAR`}) }; const selector = state => state[key]; diff --git a/src/redux/session.js b/src/redux/session.js index 0ee0edd82..a9886ebfe 100644 --- a/src/redux/session.js +++ b/src/redux/session.js @@ -12,10 +12,11 @@ const Types = keyMirror({ SET_STATUS: null }); -const banWhitelistPaths = [ - '/accounts/banned-response/', - '/community_guidelines/', - '/community_guidelines' +const banGoodListPaths = [ + '/accounts/banned-response', + '/community_guidelines', + '/privacy_policy', + '/terms_of_use' ]; module.exports.Status = keyMirror({ @@ -67,7 +68,7 @@ const handleSessionResponse = (dispatch, body) => { if ( body.user && body.user.banned && - banWhitelistPaths.indexOf(window.location.pathname) === -1 + banGoodListPaths.every(goodPath => window.location.pathname.indexOf(goodPath) === -1) ) { window.location = '/accounts/banned-response/'; return; diff --git a/src/redux/studio-mutations.js b/src/redux/studio-mutations.js index 0ecce31bc..aa729fe7a 100644 --- a/src/redux/studio-mutations.js +++ b/src/redux/studio-mutations.js @@ -24,6 +24,8 @@ const Errors = keyMirror({ UNHANDLED: null }); +const MAX_IMAGE_BYTES = 524288; + const getInitialState = () => ({ mutationErrors: {}, // { [field]: , ... } isMutating: {} // { [field]: , ... } @@ -171,10 +173,14 @@ const mutateFollowingStudio = shouldFollow => ((dispatch, getState) => { }); const mutateStudioImage = input => ((dispatch, getState) => { + if (!input.files || !input.files[0]) return; const state = getState(); const studioId = selectStudioId(state); const currentImage = selectStudioImage(state); dispatch(startMutation('image')); + if (input.files[0].size && input.files[0].size > MAX_IMAGE_BYTES) { + return dispatch(completeMutation('image', currentImage, Errors.THUMBNAIL_TOO_LARGE)); + } const formData = new FormData(); formData.append('file', input.files[0]); api({ diff --git a/src/redux/studio.js b/src/redux/studio.js index bfe677399..29cfb39b1 100644 --- a/src/redux/studio.js +++ b/src/redux/studio.js @@ -91,6 +91,7 @@ const selectStudioDescription = state => state.studio.description; const selectStudioImage = state => state.studio.image; const selectStudioOpenToAll = state => state.studio.openToAll; const selectStudioCommentsAllowed = state => state.studio.commentsAllowed; +const selectStudioLoadFailed = state => state.studio.infoStatus === Status.ERROR; const selectIsFetchingInfo = state => state.studio.infoStatus === Status.FETCHING; const selectIsFollowing = state => state.studio.following; const selectIsFetchingRoles = state => state.studio.rolesStatus === Status.FETCHING; @@ -157,6 +158,7 @@ module.exports = { selectStudioImage, selectStudioOpenToAll, selectStudioCommentsAllowed, + selectStudioLoadFailed, selectIsFetchingInfo, selectIsFetchingRoles, selectIsFollowing diff --git a/src/views/developers/developers.jsx b/src/views/developers/developers.jsx index 858fde3cb..862deefeb 100644 --- a/src/views/developers/developers.jsx +++ b/src/views/developers/developers.jsx @@ -363,9 +363,9 @@ const Developers = () => ( githubLink: ( GitHub ), - emailLink: ( - - help@scratch.mit.edu + contactUsLink: ( + + ) }} diff --git a/src/views/developers/l10n.json b/src/views/developers/l10n.json index fd8c0604c..8b7d0c707 100644 --- a/src/views/developers/l10n.json +++ b/src/views/developers/l10n.json @@ -1,7 +1,7 @@ { "developers.hereLinkText": "here", "developers.title": "Scratch for Developers", - "developers.introLinkText": "Scratch Team at MIT", + "developers.introLinkText": "Scratch Team", "developers.intro": "On this page, you’ll find information about open source projects created and maintained by the {introLink}, as well as our thoughts on best practices for designing learning experiences for children.", "developers.projectsTitle": "Projects", "developers.principlesTitle": "Principles", @@ -56,5 +56,5 @@ "developers.faqDifferencesTitle": "What’s the difference between Blockly and Scratch Blocks?", "developers.faqDifferencesBody": "Scratch Blocks builds upon the Blockly code base, and is specifically designed with our principles in mind to support creative learning experiences.", "developers.faqCollabTitle": "I’d like to collaborate. How do I get in touch?", - "developers.faqCollabBody": "You can reach us over on {githubLink} or you can send an email to {emailLink}. We look forward to hearing from you!" + "developers.faqCollabBody": "You can reach us over on {githubLink} or on our {contactUsLink} page. We look forward to hearing from you!" } diff --git a/src/views/join/join.jsx b/src/views/join/join.jsx index d51659939..2a6cda393 100644 --- a/src/views/join/join.jsx +++ b/src/views/join/join.jsx @@ -2,8 +2,6 @@ const React = require('react'); const render = require('../../lib/render.jsx'); const Scratch3Registration = require('../../components/registration/scratch3-registration.jsx'); const ErrorBoundary = require('../../components/errorboundary/errorboundary.jsx'); -// Require this even though we don't use it because, without it, webpack runs out of memory... -const Page = require('../../components/page/www/page.jsx'); // eslint-disable-line no-unused-vars const initSentry = require('../../lib/sentry.js'); initSentry(); diff --git a/src/views/splash/donate/donate-banner.jsx b/src/views/splash/donate/donate-banner.jsx new file mode 100644 index 000000000..236387f30 --- /dev/null +++ b/src/views/splash/donate/donate-banner.jsx @@ -0,0 +1,56 @@ +const FormattedMessage = require('react-intl').FormattedMessage; +const injectIntl = require('react-intl').injectIntl; +const PropTypes = require('prop-types'); +const React = require('react'); + +const TitleBanner = require('../../../components/title-banner/title-banner.jsx'); +const Button = require('../../../components/forms/button.jsx'); + +require('./donate-banner.scss'); + +const navigateToDonatePage = () => { + window.location = 'https://secure.donationpay.org/scratchfoundation'; +}; + +const DonateTopBanner = ({ + onRequestClose +}) => ( + +
+ +
+

+ +

+ +
+
+ +
+); + +DonateTopBanner.propTypes = { + onRequestClose: PropTypes.func +}; + +module.exports = injectIntl(DonateTopBanner); diff --git a/src/views/splash/donate/donate-banner.scss b/src/views/splash/donate/donate-banner.scss new file mode 100644 index 000000000..522baf875 --- /dev/null +++ b/src/views/splash/donate/donate-banner.scss @@ -0,0 +1,77 @@ +@import "../../../colors"; +@import "../../../frameless"; + +$tile-height: 244px; + +.donate-banner { + display: flex; + position: fixed; + z-index: 8; + background-color: $ui-aqua-dark; + padding: 0; + overflow: hidden; + align-items: center; + justify-content: center; + + .donate-container { + display: flex; + margin: 0.375rem auto; + align-items: center; + + .donate-icon { + margin: 0.6875rem; + width: 1.75rem; + height: 1.75rem; + } + + .donate-central-items { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + } + + .donate-text { + text-align: left; + color: $ui-white; + font-size: 1rem; + font-weight: bold; + margin-right: 1rem; + max-width: 70vw; + } + + .donate-button { + margin: 0 7rem 0 .5rem; + border-radius: 1.25rem; + background-color: $ui-white; + color: $ui-aqua-dark; + padding: 0 1.75rem; + height: 2.25rem; + text-decoration: none; + line-height: .875rem; + font-size: 1rem; + font-weight: bold; + } + } + + .donate-close-button { + right: 1rem; + top: auto; + } +} + +@media only screen and (max-width: $mobileIntermediate) { + .donate-banner .donate-container .donate-central-items { + flex-wrap: wrap; + } + .donate-banner .donate-container .donate-icon { + padding-bottom: 2rem; + } + .donate-banner .donate-container .donate-button { + margin-left: 0; + margin-bottom: 1rem + } + .donate-banner .donate-close-button { + top: 1rem; + } +} diff --git a/src/views/splash/l10n.json b/src/views/splash/l10n.json index fd4db0e38..3927ecb24 100644 --- a/src/views/splash/l10n.json +++ b/src/views/splash/l10n.json @@ -29,6 +29,8 @@ "intro.watchVideo": "Watch Video", "news.scratchNews": "Scratch News", + "donatebanner.askSupport": "Scratch is the world's largest free coding community for kids. Your support makes a difference.", + "teacherbanner.greeting": "Hi", "teacherbanner.subgreeting": "Teacher Account", "teacherbanner.classesButton": "My Classes", diff --git a/src/views/splash/presentation.jsx b/src/views/splash/presentation.jsx index ed28fdd9f..b65dde823 100644 --- a/src/views/splash/presentation.jsx +++ b/src/views/splash/presentation.jsx @@ -32,12 +32,10 @@ const LoveProjectMessage = require('./activity-rows/love-project.jsx'); const RemixProjectMessage = require('./activity-rows/remix-project.jsx'); const ShareProjectMessage = require('./activity-rows/share-project.jsx'); -// Hour of Code Banner Components -const TopBanner = require('./hoc/top-banner.jsx'); -const MiddleBanner = require('./hoc/middle-banner.jsx'); - -const HOC_START_TIME = 1605484800000; // 2020-11-16 00:00:00 -const HOC_END_TIME = 1608681600000; // 2020-12-23 00:00:00 +// Banner Components +const DonateBanner = require('./donate/donate-banner.jsx'); +const HOCTopBanner = require('./hoc/top-banner.jsx'); +const HOCMiddleBanner = require('./hoc/middle-banner.jsx'); require('./splash.scss'); @@ -352,6 +350,7 @@ class SplashPresentation extends React.Component { // eslint-disable-line react/ const formatHTMLMessage = this.props.intl.formatHTMLMessage; const formatMessage = this.props.intl.formatMessage; + const messages = { 'general.viewAll': formatMessage({id: 'general.viewAll'}), 'news.scratchNews': formatMessage({id: 'news.scratchNews'}), @@ -412,21 +411,28 @@ class SplashPresentation extends React.Component { // eslint-disable-line react/ /> ] : []} { - this.props.sessionStatus === sessionActions.Status.FETCHED && - Object.keys(this.props.user).length === 0 && (// Only show top banner if user is not logged in - (Date.now() >= HOC_START_TIME && Date.now() < HOC_END_TIME) ? ( - - - - ) : ( - - ) + this.props.shouldShowHOCTopBanner && ( + + + + ) + } + { + this.props.shouldShowDonateBanner && ( + + ) + } + { + this.props.shouldShowIntro && ( + ) }
{ - this.props.sessionStatus === sessionActions.Status.FETCHED && - Object.keys(this.props.user).length !== 0 && // Only show if user is logged in - Date.now() >= HOC_START_TIME && // Show middle banner on and after Dec 3 - Date.now() < HOC_END_TIME && // Hide middle banner after Dec 14 - false && // we did not use this middle banner in last HoC - - - + this.props.shouldShowHOCMiddleBanner && ( + + + + ) }
= HOC_START_TIME && + Date.now() < HOC_END_TIME + ); + } + shouldShowHOCMiddleBanner () { + return false; // we did not use this middle banner in last HoC + } + shouldShowIntro () { + return ( + this.props.sessionStatus === sessionActions.Status.FETCHED && // done fetching session + Object.keys(this.props.user).length === 0 && // no user session found + this.shouldShowHOCTopBanner() !== true + ); + } + shouldShowDonateBanner () { + return ( + this.state.dismissedDonateBanner === false && + this.props.sessionStatus === sessionActions.Status.FETCHED && // done fetching session + Object.keys(this.props.user).length === 0 && // no user session found + Date.now() >= SCRATCH_WEEK_START_TIME && + Date.now() < SCRATCH_WEEK_END_TIME && + this.shouldShowHOCTopBanner() !== true + ); + } render () { const showEmailConfirmation = this.shouldShowEmailConfirmation() || false; + const showDonateBanner = this.shouldShowDonateBanner() || false; + const showHOCTopBanner = this.shouldShowHOCTopBanner() || false; + const showHOCMiddleBanner = this.shouldShowHOCMiddleBanner() || false; + const showIntro = this.shouldShowIntro() || false; const showWelcome = this.shouldShowWelcome(); const homepageRefreshStatus = this.getHomepageRefreshStatus(); @@ -163,9 +205,14 @@ class Splash extends React.Component { refreshCacheStatus={homepageRefreshStatus} sessionStatus={this.props.sessionStatus} sharedByFollowing={this.props.shared} + shouldShowDonateBanner={showDonateBanner} shouldShowEmailConfirmation={showEmailConfirmation} + shouldShowHOCTopBanner={showHOCTopBanner} + shouldShowIntro={showIntro} + shouldShowHOCMiddleBanner={showHOCMiddleBanner} shouldShowWelcome={showWelcome} user={this.props.user} + onCloseDonateBanner={this.handleCloseDonateBanner} onCloseAdminPanel={this.handleCloseAdminPanel} onDismiss={this.handleDismiss} onHideEmailConfirmationModal={this.handleHideEmailConfirmationModal} diff --git a/src/views/studio/l10n.json b/src/views/studio/l10n.json new file mode 100644 index 000000000..c35e84f35 --- /dev/null +++ b/src/views/studio/l10n.json @@ -0,0 +1,48 @@ +{ + "studio.tabNavProjects": "Projects", + "studio.tabNavCurators": "Curators", + "studio.tabNavComments": "Comments", + "studio.tabNavActivity": "Activity", + + "studio.title": "Title", + "studio.description": "Description", + "studio.thumbnail": "Thumbnail", + + "studio.projectsHeader": "Projects", + "studio.addProjectsHeader": "Add Projects", + "studio.addProject": "Add", + + "studio.projectsEmptyCanAdd1": "Your studio is looking a little empty.", + "studio.projectsEmptyCanAdd2": "Add your first project!", + "studio.projectsEmpty1": "This studio has no projects yet.", + "studio.projectsEmpty2": "Suggest projects you want to add in the comments!", + "studio.browseProjects": "Browse Projects", + + "studio.creatorRole": "Studio Creator", + + "studio.managersHeader": "Managers", + + "studio.unfollowStudio": "Unfollow Studio", + "studio.followStudio": "Follow Studio", + + "studio.curatorsHeader": "Curators", + "studio.inviteCuratorsHeader": "Invite Curators", + "studio.inviteCurator": "Invite", + "studio.curatorAcceptInvite": "Accept Invite", + "studio.curatorsEmptyCanAdd1": "You don’t have curators right now.", + "studio.curatorsEmptyCanAdd2": "Add some curators to collaborate with!", + "studio.curatorsEmpty1": "This studio has no curators right now.", + + "studio.commentsHeader": "Comments", + + "studio.sharedFilter": "Shared", + "studio.favoritedFilter": "Favorited", + "studio.recentFilter": "Recent", + + "studio.activityAddProjectToStudio": "{profileLink} added the project {projectLink}", + "studio.activityRemoveProjectStudio": "{profileLink} removed the project {projectLink}", + "studio.activityUpdateStudio": "{profileLink} made edits to the title, thumbnail, or description", + "studio.activityBecomeCurator": "{newCuratorProfileLink} accepted an invitation from {inviterProfileLink} to curate this studio", + "studio.activityRemoveCurator": "{removerProfileLink} removed the curator {removedProfileLink}", + "studio.activityBecomeOwner": "{promotedProfileLink} was promoted to manager by {promotorProfileLink}" +} diff --git a/src/views/studio/lib/fetchers.js b/src/views/studio/lib/fetchers.js deleted file mode 100644 index 0b11e2e08..000000000 --- a/src/views/studio/lib/fetchers.js +++ /dev/null @@ -1,9 +0,0 @@ -// TODO move this to studio-activity-actions, include pagination -const activityFetcher = studioId => - fetch(`${process.env.API_HOST}/studios/${studioId}/activity`) - .then(response => response.json()) - .then(data => ({items: data, moreToLoad: false})); // No pagination on the activity feed - -export { - activityFetcher -}; diff --git a/src/views/studio/lib/redux-modules.js b/src/views/studio/lib/redux-modules.js index 05fb888d6..6cadf65d4 100644 --- a/src/views/studio/lib/redux-modules.js +++ b/src/views/studio/lib/redux-modules.js @@ -5,6 +5,8 @@ const curators = InfiniteList('curators'); const managers = InfiniteList('managers'); const activity = InfiniteList('activity'); +const userProjects = InfiniteList('user-projects'); + export { - projects, curators, managers, activity + projects, curators, managers, activity, userProjects }; diff --git a/src/views/studio/lib/studio-activity-actions.js b/src/views/studio/lib/studio-activity-actions.js new file mode 100644 index 000000000..4a694cfa3 --- /dev/null +++ b/src/views/studio/lib/studio-activity-actions.js @@ -0,0 +1,43 @@ +import keyMirror from 'keymirror'; + +import api from '../../../lib/api'; +import {activity} from './redux-modules'; +import {selectStudioId} from '../../../redux/studio'; + +const Errors = keyMirror({ + NETWORK: null, + SERVER: null, + PERMISSION: null +}); + +const normalizeError = (err, body, res) => { + if (err) return Errors.NETWORK; + if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION; + if (res.statusCode !== 200) return Errors.SERVER; + return null; +}; + +const loadActivity = () => ((dispatch, getState) => { + const state = getState(); + const studioId = selectStudioId(state); + const items = activity.selector(state).items; + const params = {limit: 20}; + if (items.length > 0) { + // dateLimit is the newest notification you want to get back, which is + // the date of the oldest one we've already loaded + params.dateLimit = items[items.length - 1].datetime_created; + } + api({ + uri: `/studios/${studioId}/activity/`, + params + }, (err, body, res) => { + const error = normalizeError(err, body, res); + if (error) return dispatch(activity.actions.error(error)); + const ids = items.map(item => item.id); + // Deduplication is needed because pagination based on date can contain duplicates + const deduped = body.filter(item => ids.indexOf(item.id) === -1); + dispatch(activity.actions.append(deduped, body.length === params.limit)); + }); +}); + +export {loadActivity}; diff --git a/src/views/studio/lib/studio-member-actions.js b/src/views/studio/lib/studio-member-actions.js index 2e8749b7b..894cb5354 100644 --- a/src/views/studio/lib/studio-member-actions.js +++ b/src/views/studio/lib/studio-member-actions.js @@ -8,13 +8,24 @@ import {selectStudioId, setRoles} from '../../../redux/studio'; const Errors = keyMirror({ NETWORK: null, SERVER: null, - PERMISSION: null + PERMISSION: null, + DUPLICATE: null, + UNKNOWN_USERNAME: null, + RATE_LIMIT: null }); const normalizeError = (err, body, res) => { if (err) return Errors.NETWORK; if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION; + if (res.statusCode === 404) return Errors.UNKNOWN_USERNAME; + if (res.statusCode === 429) return Errors.RATE_LIMIT; if (res.statusCode !== 200) return Errors.SERVER; + if (body && body.status === 'error') { + if (body.message.indexOf('already a curator') !== -1) { + return Errors.DUPLICATE; + } + return Errors.UNHANDLED; + } return null; }; @@ -132,7 +143,7 @@ const promoteCurator = username => ((dispatch, getState) => new Promise((resolve const index = curatorList.findIndex(v => v.username === username); const curatorItem = curatorList[index]; if (index !== -1) dispatch(curators.actions.remove(index)); - dispatch(managers.actions.create(curatorItem)); + dispatch(managers.actions.create(curatorItem, true)); return resolve(); }); })); @@ -156,7 +167,7 @@ const acceptInvitation = () => ((dispatch, getState) => new Promise((resolve, re if (userError) return reject(userError); // Note: this assumes that the user items from the curator endpoint // are the same structure as the single user data returned from /users/:username - dispatch(curators.actions.create(userBody)); + dispatch(curators.actions.create(userBody, true)); dispatch(setRoles({invited: false, curator: true})); return resolve(); }); diff --git a/src/views/studio/lib/studio-project-actions.js b/src/views/studio/lib/studio-project-actions.js index 0de1015df..a1b1c9c27 100644 --- a/src/views/studio/lib/studio-project-actions.js +++ b/src/views/studio/lib/studio-project-actions.js @@ -9,12 +9,16 @@ import {projects} from './redux-modules'; const Errors = keyMirror({ NETWORK: null, SERVER: null, - PERMISSION: null + PERMISSION: null, + UNKNOWN_PROJECT: null, + RATE_LIMIT: null }); const normalizeError = (err, body, res) => { if (err) return Errors.NETWORK; if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION; + if (res.statusCode === 404) return Errors.UNKNOWN_PROJECT; + if (res.statusCode === 429) return Errors.RATE_LIMIT; if (res.statusCode !== 200) return Errors.SERVER; return null; }; @@ -46,7 +50,7 @@ const loadProjects = () => ((dispatch, getState) => { */ const generateProjectListItem = (postBody, infoBody) => ({ // Fields from the POST to add the project to the studio - id: postBody.projectId, + id: parseInt(postBody.projectId, 10), actor_id: postBody.actorId, // Fields from followup GET for more project info title: infoBody.title, diff --git a/src/views/studio/lib/user-projects-actions.js b/src/views/studio/lib/user-projects-actions.js new file mode 100644 index 000000000..b932ef206 --- /dev/null +++ b/src/views/studio/lib/user-projects-actions.js @@ -0,0 +1,59 @@ +import keyMirror from 'keymirror'; +import api from '../../../lib/api'; +import {selectUsername} from '../../../redux/session'; +import {userProjects, projects} from './redux-modules'; + +const Errors = keyMirror({ + NETWORK: null, + SERVER: null, + PERMISSION: null +}); + +const Filters = keyMirror({ + SHARED: null, + FAVORITED: null, + RECENT: null +}); + +const Uris = { + [Filters.SHARED]: username => `/users/${username}/projects`, + [Filters.FAVORITED]: username => `/users/${username}/favorites`, + [Filters.RECENT]: username => `/users/${username}/recent` +}; + +const normalizeError = (err, body, res) => { + if (err) return Errors.NETWORK; + if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION; + if (res.statusCode !== 200) return Errors.SERVER; + return null; +}; + +const loadUserProjects = type => ((dispatch, getState) => { + const state = getState(); + const username = selectUsername(state); + const projectCount = userProjects.selector(state).items.length; + const projectsPerPage = 20; + dispatch(userProjects.actions.loading()); + api({ + uri: Uris[type](username), + params: {limit: projectsPerPage, offset: projectCount} + }, (err, body, res) => { + const error = normalizeError(err, body, res); + if (error) return dispatch(userProjects.actions.error(error)); + const moreToLoad = body.length === projectsPerPage; + const studioProjectIds = projects.selector(getState()).items.map(item => item.id); + const loadedProjects = body.map(item => Object.assign(item, { + inStudio: studioProjectIds.indexOf(item.id) !== -1 + })); + dispatch(userProjects.actions.append(loadedProjects, moreToLoad)); + }); +}); + +// Re-export clear so that the consumer can manage filter changes +const clearUserProjects = userProjects.actions.clear; + +export { + Filters, + loadUserProjects, + clearUserProjects +}; diff --git a/src/views/studio/modals/user-projects-modal.jsx b/src/views/studio/modals/user-projects-modal.jsx new file mode 100644 index 000000000..cf6c57051 --- /dev/null +++ b/src/views/studio/modals/user-projects-modal.jsx @@ -0,0 +1,121 @@ +/* eslint-disable react/jsx-no-bind */ +import React, {useEffect, useState} from 'react'; +import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; +import classNames from 'classnames'; +import {FormattedMessage} from 'react-intl'; + +import {addProject, removeProject} from '../lib/studio-project-actions'; +import {userProjects} from '../lib/redux-modules'; +import {Filters, loadUserProjects, clearUserProjects} from '../lib/user-projects-actions'; + +import Modal from '../../../components/modal/base/modal.jsx'; +import ModalTitle from '../../../components/modal/base/modal-title.jsx'; +import ModalInnerContent from '../../../components/modal/base/modal-inner-content.jsx'; +import SubNavigation from '../../../components/subnavigation/subnavigation.jsx'; +import UserProjectsTile from './user-projects-tile.jsx'; + +import './user-projects-modal.scss'; + +const UserProjectsModal = ({ + items, error, loading, moreToLoad, onLoadMore, onClear, + onAdd, onRemove, onRequestClose +}) => { + const [filter, setFilter] = useState(Filters.SHARED); + + useEffect(() => { + onClear(); + onLoadMore(filter); + }, [filter]); + + return ( + + + +
  • setFilter(Filters.SHARED)} + > + +
  • +
  • setFilter(Filters.FAVORITED)} + > + +
  • +
  • setFilter(Filters.RECENT)} + > + +
  • +
    + + {error &&
    Error loading {filter}: {error}
    } +
    + {items.map(project => ( + + ))} +
    + {loading ? Loading... : ( + moreToLoad ? + : + No more to load + )} +
    +
    +
    +
    + ); +}; + +UserProjectsModal.propTypes = { + items: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.id, + image: PropTypes.string, + title: PropTypes.string, + inStudio: PropTypes.bool + })), + loading: PropTypes.bool, + error: PropTypes.object, // eslint-disable-line react/forbid-prop-types + moreToLoad: PropTypes.bool, + onLoadMore: PropTypes.func, + onClear: PropTypes.func, + onAdd: PropTypes.func, + onRemove: PropTypes.func, + onRequestClose: PropTypes.func +}; + +const mapStateToProps = state => ({ + ...userProjects.selector(state) +}); + +const mapDispatchToProps = ({ + onLoadMore: loadUserProjects, + onClear: clearUserProjects, + onAdd: addProject, + onRemove: removeProject +}); + +export default connect(mapStateToProps, mapDispatchToProps)(UserProjectsModal); diff --git a/src/views/studio/modals/user-projects-modal.scss b/src/views/studio/modals/user-projects-modal.scss new file mode 100644 index 000000000..66aaeab4d --- /dev/null +++ b/src/views/studio/modals/user-projects-modal.scss @@ -0,0 +1,92 @@ +@import "../../../colors"; +@import "../../../frameless"; + +.user-projects-modal { + .user-projects-modal-title { + box-shadow: inset 0 -1px 0 0 $ui-blue-dark; + background-color: $ui-blue; + border-top-left-radius: 12px; + border-top-right-radius: 12px; + padding-top: .75rem; + width: 100%; + height: 3rem; + } + .user-projects-modal-nav { + padding: 6px 12px; + li { + cursor: pointer; + background: rgba(0, 0, 0, 0.15); + &.active { background: $ui-blue; } + } + } + .user-projects-modal-content { + padding: 0 30px 30px; + background: #E9F1FC; + max-height: 80vh; + overflow-y: auto; + overscroll-behavior: contain; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + } +} + + +.studio-tile-dynamic-remove, +.studio-tile-dynamic-add { + position: absolute; + top: 10px; + right: 10px; + width: 32px; + height: 32px; + background: rgba(0, 0, 0, 0.25); + border: 3px solid rgba(0, 0, 0, 0.1); + background-clip: padding-box; + color: white; + border-radius: 100%; + font-weight: bold; + margin: 0; + padding: 0; + line-height: 32px; + text-align: center; +} +.studio-tile-dynamic-remove { + background: #0FBD8C; + background-clip: padding-box; + border: 3px solid rgba(15, 189, 140, 0.2); +} + +.user-projects-modal-grid { + margin-top: 12px; + display: grid; + + grid-template-columns: repeat(3, minmax(0,1fr)); + @media #{$medium} { /* Keep 3 columns to narrower width since it is in a modal */ + & { grid-template-columns: repeat(2, minmax(0,1fr)); } + } + @media #{$small} { + & { grid-template-columns: repeat(1, minmax(0,1fr)); } + } + column-gap: 14px; + row-gap: 14px; + + .studio-projects-load-more { + grid-column: 1 / -1; + } + + .studio-project-bottom { + padding: 8px 10px 8px 10px; + } + .studio-project-avatar { + width: 32px; + height: 32px; + } + .studio-project-info { + margin: 0; + } + .studio-project-title { + font-size: 12px; + } + .studio-project-username { + font-size: 12px; + } +} diff --git a/src/views/studio/modals/user-projects-tile.jsx b/src/views/studio/modals/user-projects-tile.jsx new file mode 100644 index 000000000..762899b0f --- /dev/null +++ b/src/views/studio/modals/user-projects-tile.jsx @@ -0,0 +1,58 @@ +/* eslint-disable react/jsx-no-bind */ +import React, {useState} from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => { + const [submitting, setSubmitting] = useState(false); + const [added, setAdded] = useState(inStudio); + const [error, setError] = useState(null); + const toggle = () => { + setSubmitting(true); + setError(null); + (added ? onRemove(id) : onAdd(id)) + .then(() => { + setAdded(!added); + setSubmitting(false); + }) + .catch(e => { + setError(e); + setSubmitting(false); + }); + }; + return ( +
    e.key === 'Enter' && toggle()} + > + +
    +
    {title}
    +
    + {added ? '✔' : '+'} +
    + {error &&
    {error}
    } +
    +
    + ); +}; + +UserProjectsTile.propTypes = { + id: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + image: PropTypes.string.isRequired, + inStudio: PropTypes.bool.isRequired, + onAdd: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired +}; + +export default UserProjectsTile; diff --git a/src/views/studio/studio-activity.jsx b/src/views/studio/studio-activity.jsx index 955272341..f84d59074 100644 --- a/src/views/studio/studio-activity.jsx +++ b/src/views/studio/studio-activity.jsx @@ -1,38 +1,206 @@ import React, {useEffect} from 'react'; import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; import {connect} from 'react-redux'; -import {useParams} from 'react-router'; import {activity} from './lib/redux-modules'; -import {activityFetcher} from './lib/fetchers'; +import {loadActivity} from './lib/studio-activity-actions'; import Debug from './debug.jsx'; +import classNames from 'classnames'; -const StudioActivity = ({items, loading, error, onInitialLoad}) => { - const {studioId} = useParams(); - // Fetch the data if none has been loaded yet. This would run only once, - // since studioId doesnt change, but the component is potentially mounted - // multiple times because of tab routing, so need to check for empty items. +import SocialMessage from '../../components/social-message/social-message.jsx'; + +import './studio.scss'; + +const getComponentForItem = item => { + switch (item.type) { + case 'addprojecttostudio': + return ( + + + {item.actor_username} + + ), + projectLink: ( + + {item.project_title} + + ) + }} + /> + + ); + case 'removeprojectstudio': + return ( + + + {item.actor_username} + + ), + projectLink: ( + + {item.project_title} + + ) + }} + /> + + ); + case 'updatestudio': + return ( + + + {item.actor_username} + + ) + }} + /> + + ); + case 'becomecurator': + return ( + + + {item.actor_username} + + ), + inviterProfileLink: ( + + {item.username} + + ) + }} + /> + + ); + case 'removecuratorstudio': + return ( + + + {item.username} + + ), + removerProfileLink: ( + + {item.actor_username} + + ) + }} + /> + + ); + case 'becomeownerstudio': + return ( + + + {item.recipient_username} + + ), + promotorProfileLink: ( + + {item.actor_username} + + ) + }} + /> + + ); + } +}; + +const StudioActivity = ({items, loading, error, moreToLoad, onLoadMore}) => { useEffect(() => { - if (studioId && items.length === 0) onInitialLoad(studioId); - }, [studioId]); // items.length intentionally left out + if (items.length === 0) onLoadMore(); + }, []); return ( -
    +

    Activity

    {loading &&
    Loading...
    } {error && } -
    - {items.map((item, index) => - () +
      + {items.map(item => + getComponentForItem(item) )} +
    +
    + {moreToLoad && + + }
    ); @@ -42,13 +210,13 @@ StudioActivity.propTypes = { items: PropTypes.array, // eslint-disable-line react/forbid-prop-types loading: PropTypes.bool, error: PropTypes.object, // eslint-disable-line react/forbid-prop-types - onInitialLoad: PropTypes.func + moreToLoad: PropTypes.bool, + onLoadMore: PropTypes.func }; export default connect( state => activity.selector(state), - dispatch => ({ - onInitialLoad: studioId => dispatch( - activity.actions.loadMore(activityFetcher.bind(null, studioId, 0))) - }) + { + onLoadMore: loadActivity + } )(StudioActivity); diff --git a/src/views/studio/studio-comments.jsx b/src/views/studio/studio-comments.jsx index 2cd1495dd..7e0b020e7 100644 --- a/src/views/studio/studio-comments.jsx +++ b/src/views/studio/studio-comments.jsx @@ -56,7 +56,7 @@ const StudioComments = ({ return (
    -

    Comments

    +

    {canEditCommentsAllowed && }
    {shouldShowCommentComposer && commentsAllowed && diff --git a/src/views/studio/studio-curator-invite.jsx b/src/views/studio/studio-curator-invite.jsx index d3568555b..7fe1a129a 100644 --- a/src/views/studio/studio-curator-invite.jsx +++ b/src/views/studio/studio-curator-invite.jsx @@ -3,6 +3,7 @@ import React, {useState} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import classNames from 'classnames'; +import {FormattedMessage} from 'react-intl'; import {acceptInvitation} from './lib/studio-member-actions'; @@ -26,7 +27,7 @@ const StudioCuratorInvite = ({onSubmit}) => { setSubmitting(false); }); }} - >Accept invite + > {error &&
    {error}
    }
    ); diff --git a/src/views/studio/studio-curator-inviter.jsx b/src/views/studio/studio-curator-inviter.jsx index 6c2541de0..fce1ce3df 100644 --- a/src/views/studio/studio-curator-inviter.jsx +++ b/src/views/studio/studio-curator-inviter.jsx @@ -3,39 +3,44 @@ import React, {useState} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import classNames from 'classnames'; +import {FormattedMessage} from 'react-intl'; import {inviteCurator} from './lib/studio-member-actions'; +import FlexRow from '../../components/flex-row/flex-row.jsx'; const StudioCuratorInviter = ({onSubmit}) => { const [value, setValue] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); - + const submit = () => { + setSubmitting(true); + setError(null); + onSubmit(value) + .then(() => setValue('')) + .catch(e => setError(e)) + .then(() => setSubmitting(false)); + }; return (
    -

    ✦ Invite Curators

    - setValue(e.target.value)} - /> - - {error &&
    {error}
    } +

    + + e.key === 'Enter' && submit()} + onChange={e => setValue(e.target.value)} + /> + + {error &&
    {error}
    } +
    ); }; diff --git a/src/views/studio/studio-curators.jsx b/src/views/studio/studio-curators.jsx index cddb42851..c2608b021 100644 --- a/src/views/studio/studio-curators.jsx +++ b/src/views/studio/studio-curators.jsx @@ -1,6 +1,8 @@ import React, {useEffect} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; +import {FormattedMessage} from 'react-intl'; +import classNames from 'classnames'; import {curators} from './lib/redux-modules'; import Debug from './debug.jsx'; @@ -18,7 +20,7 @@ const StudioCurators = ({ }, []); return (
    -

    Curators

    +

    {canInviteCurators && } {showCuratorInvite && } {error && }
    - {items.map(item => - () + {items.length === 0 && !loading ? ( +
    + + {canInviteCurators ? ( +
    +
    +
    +
    + ) : ( +
    +
    +
    + )} +
    + ) : ( + + {items.map(item => + () + )} + {moreToLoad && +
    + +
    + } +
    )} -
    - {loading ? Loading... : ( - moreToLoad ? - : - No more to load - )} -
    ); }; diff --git a/src/views/studio/studio-follow.jsx b/src/views/studio/studio-follow.jsx index b20feac96..5e3a51b9a 100644 --- a/src/views/studio/studio-follow.jsx +++ b/src/views/studio/studio-follow.jsx @@ -2,6 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; +import {FormattedMessage} from 'react-intl'; + import {selectIsFollowing} from '../../redux/studio'; import {selectCanFollowStudio} from '../../redux/studio-permissions'; import { @@ -28,7 +30,9 @@ const StudioFollow = ({ onClick={() => handleFollow(!isFollowing)} > {isMutating ? '...' : ( - isFollowing ? 'Unfollow Studio' : 'Follow Studio' + isFollowing ? + : + )} {followingError &&
    Error mutating following: {followingError}
    } diff --git a/src/views/studio/studio-managers.jsx b/src/views/studio/studio-managers.jsx index ffb750a97..064e173b5 100644 --- a/src/views/studio/studio-managers.jsx +++ b/src/views/studio/studio-managers.jsx @@ -1,6 +1,8 @@ import React, {useEffect} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; +import {FormattedMessage} from 'react-intl'; +import classNames from 'classnames'; import {managers} from './lib/redux-modules'; import {loadManagers} from './lib/studio-member-actions'; @@ -15,7 +17,7 @@ const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => { return (
    -

    Managers

    +

    {error && { image={item.profile.images['90x90']} />) )} -
    - {loading ? Loading... : ( - moreToLoad ? - : - No more to load - )} -
    + {moreToLoad && +
    + +
    + }
    ); diff --git a/src/views/studio/studio-member-tile.jsx b/src/views/studio/studio-member-tile.jsx index a6736b973..208316dfc 100644 --- a/src/views/studio/studio-member-tile.jsx +++ b/src/views/studio/studio-member-tile.jsx @@ -3,6 +3,7 @@ import React, {useState} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import classNames from 'classnames'; +import {FormattedMessage} from 'react-intl'; import { selectCanRemoveCurators, selectCanRemoveManager, selectCanPromoteCurators @@ -33,7 +34,7 @@ const StudioMemberTile = ({ href={userUrl} className="studio-member-name" >{username} - {isCreator &&
    Studio Creator
    } + {isCreator &&
    }
    {canRemove && - {error &&
    {error}
    } +

    + + e.key === 'Enter' && submit()} + onChange={e => setValue(e.target.value)} + /> + + {error &&
    {error}
    } +
    + + {modalOpen && setModalOpen(false)} />} +
    ); }; diff --git a/src/views/studio/studio-projects.jsx b/src/views/studio/studio-projects.jsx index db9c62e2a..08801c133 100644 --- a/src/views/studio/studio-projects.jsx +++ b/src/views/studio/studio-projects.jsx @@ -2,6 +2,7 @@ import React, {useEffect} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import StudioOpenToAll from './studio-open-to-all.jsx'; +import {FormattedMessage} from 'react-intl'; import {projects} from './lib/redux-modules'; import {selectCanAddProjects, selectCanEditOpenToAll} from '../../redux/studio-permissions'; @@ -9,6 +10,7 @@ import Debug from './debug.jsx'; import StudioProjectAdder from './studio-project-adder.jsx'; import StudioProjectTile from './studio-project-tile.jsx'; import {loadProjects} from './lib/studio-project-actions.js'; +import classNames from 'classnames'; const StudioProjects = ({ canAddProjects, canEditOpenToAll, items, error, loading, moreToLoad, onLoadMore @@ -19,7 +21,7 @@ const StudioProjects = ({ return (
    -

    Projects

    +

    {canEditOpenToAll && } {canAddProjects && } {error && }
    - {items.map(item => - () + {items.length === 0 && !loading ? ( +
    + {canAddProjects ? ( + + +
    +
    +
    +
    +
    + ) : ( + + +
    +
    +
    +
    +
    + )} +
    + ) : ( + + {items.map(item => + () + )} + {moreToLoad && +
    + +
    + } +
    )} -
    - {loading ? Loading... : ( - moreToLoad ? - : - No more to load - )} -
    ); diff --git a/src/views/studio/studio-report.jsx b/src/views/studio/studio-report.jsx index 984fa5d0b..bf8c91416 100644 --- a/src/views/studio/studio-report.jsx +++ b/src/views/studio/studio-report.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; +import {FormattedMessage} from 'react-intl'; import { Fields, @@ -24,15 +25,15 @@ const StudioReport = ({

    Reporting

    {canReport && ( - + )} {isOpen && (
    -
    Report Studio Modal
    +
    {previouslyReported ? (
    Submitted the report!
    - +
    ) : ( @@ -40,9 +41,9 @@ const StudioReport = ({ value={field} onChange={e => handleSetField(e.target.value)} > - - - + + + {error && (
    @@ -54,9 +55,9 @@ const StudioReport = ({ disabled={isSubmitting} onClick={handleSubmit} > - Submit + - + )}
    diff --git a/src/views/studio/studio-tab-nav.jsx b/src/views/studio/studio-tab-nav.jsx index c6a490b3e..a6ea0fdd6 100644 --- a/src/views/studio/studio-tab-nav.jsx +++ b/src/views/studio/studio-tab-nav.jsx @@ -1,10 +1,11 @@ import React from 'react'; import {useRouteMatch, NavLink} from 'react-router-dom'; import SubNavigation from '../../components/subnavigation/subnavigation.jsx'; +import {FormattedMessage} from 'react-intl'; const StudioTabNav = () => { - const match = useRouteMatch(); - + const {params: {studioPath, studioId}} = useRouteMatch(); + const base = `/${studioPath}/${studioId}`; return ( { > -
  • Projects
  • +
  • -
  • Curators
  • +
  • -
  • Comments
  • +
  • -
  • Activity
  • +
  • ); diff --git a/src/views/studio/studio.jsx b/src/views/studio/studio.jsx index 001fc3655..2d4bd65c4 100644 --- a/src/views/studio/studio.jsx +++ b/src/views/studio/studio.jsx @@ -6,9 +6,14 @@ import { Redirect, useRouteMatch } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; + import Page from '../../components/page/www/page.jsx'; import render from '../../lib/render.jsx'; +import NotAvailable from '../../components/not-available/not-available.jsx'; + import StudioTabNav from './studio-tab-nav.jsx'; import StudioProjects from './studio-projects.jsx'; @@ -22,59 +27,72 @@ import { projects, curators, managers, - activity + activity, + userProjects } from './lib/redux-modules'; -const {getInitialState, studioReducer} = require('../../redux/studio'); +const {getInitialState, studioReducer, selectStudioLoadFailed} = require('../../redux/studio'); const {studioReportReducer} = require('../../redux/studio-report'); const {commentsReducer} = require('../../redux/comments'); const {studioMutationsReducer} = require('../../redux/studio-mutations'); import './studio.scss'; -const StudioShell = () => { +const StudioShell = ({studioLoadFailed}) => { const match = useRouteMatch(); return ( -
    -
    - -
    -
    - -
    - - - - - - - - - - - - - {/* We can force /projects back to / this way */} - - - - - - + studioLoadFailed ? + : +
    +
    + +
    +
    + +
    + + + + + + + + + + + + + {/* We can force /projects back to / this way */} + + + + + + +
    -
    ); }; +StudioShell.propTypes = { + studioLoadFailed: PropTypes.bool +}; + +const ConnectedStudioShell = connect( + state => ({ + studioLoadFailed: selectStudioLoadFailed(state) + }), +)(StudioShell); + render( {/* Use variable studioPath to support /studio-playground/ or future route */} - + @@ -85,6 +103,7 @@ render( [curators.key]: curators.reducer, [managers.key]: managers.reducer, [activity.key]: activity.reducer, + [userProjects.key]: userProjects.reducer, comments: commentsReducer, studio: studioReducer, studioMutations: studioMutationsReducer, diff --git a/src/views/studio/studio.scss b/src/views/studio/studio.scss index 1b12c50f8..a2a885da8 100644 --- a/src/views/studio/studio.scss +++ b/src/views/studio/studio.scss @@ -5,7 +5,7 @@ $radius: 8px; .studio-page { background-color: #E9F1FC; - + #view { /* Reset some defaults on width and margin */ background-color: transparent; @@ -35,7 +35,7 @@ $radius: 8px; .studio-info { justify-self: center; width: 300px; - height: fit-content; + height: max-content; display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; @@ -63,7 +63,7 @@ $radius: 8px; .studio-tab-nav { border-bottom: 1px solid $active-dark-gray; padding-bottom: 8px; - li { background: $active-gray; } + li { background: rgba(0, 0, 0, 0.15); } .active > li { background: $ui-blue; } } @@ -72,12 +72,12 @@ $radius: 8px; margin-top: 20px; display: grid; - grid-template-columns: minmax(0, 1fr); - @media #{$medium} { + grid-template-columns: repeat(3, minmax(0,1fr)); + @media #{$medium-and-intermediate} { & { grid-template-columns: repeat(2, minmax(0,1fr)); } } - @media #{$big} { - & { grid-template-columns: repeat(3, minmax(0,1fr)); } + @media #{$small} { + & { grid-template-columns: repeat(1, minmax(0,1fr)); } } column-gap: 30px; row-gap: 20px; @@ -91,6 +91,9 @@ $radius: 8px; background: white; border-radius: 8px; border: 1px solid $ui-border; + position: relative; + margin: 0; + padding: 0; .studio-project-image { max-width: 100%; @@ -123,6 +126,7 @@ $radius: 8px; font-weight: 700; font-size: 14px; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; } .studio-project-username { @@ -130,6 +134,7 @@ $radius: 8px; font-weight: 700; font-size: 12px; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; } .studio-project-remove { @@ -143,13 +148,12 @@ $radius: 8px; .studio-members-grid { margin-top: 20px; display: grid; - - grid-template-columns: minmax(0, 1fr); - @media #{$medium} { + grid-template-columns: repeat(3, minmax(0,1fr)); + @media #{$medium-and-intermediate} { & { grid-template-columns: repeat(2, minmax(0,1fr)); } } - @media #{$big} { - & { grid-template-columns: repeat(3, minmax(0,1fr)); } + @media #{$small} { + & { grid-template-columns: repeat(1, minmax(0,1fr)); } } column-gap: 30px; row-gap: 20px; @@ -187,6 +191,7 @@ $radius: 8px; font-weight: 700; font-size: 14px; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; } .studio-member-role { @@ -194,6 +199,7 @@ $radius: 8px; font-weight: 400; font-size: 12px; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; } .studio-member-remove, .studio-member-promote { @@ -209,15 +215,19 @@ $radius: 8px; .studio-adder-section { margin-top: 20px; - display: flex; - flex-wrap: wrap; h3 { color: #4C97FF; } + .flex-row { + margin: 0 -6px; + & > * { + margin: 0 6px; + } + } + input { - flex-basis: 80%; flex-grow: 1; display: inline-block; margin: .5em 0; @@ -228,11 +238,48 @@ $radius: 8px; } button { - flex-grow: 1; + flex-grow: 0; } - input + button { - margin-inline-start: 12px; + .studio-adder-vertical-divider { + border: 1px solid $ui-border; + align-self: stretch; + } +} + +.studio-activity { + .studio-messages-list { + border: 1px solid $ui-border; + border-bottom-width: 0; + border-radius: 5px; + background-color: $ui-white; + padding-left: 0px; + } + .studio-activity-icon { + opacity: 1; + } + /* Ensure that line breaks occur only the message text, not in the flex layout or date */ + .social-message-date { + white-space: nowrap; + } + .flex-row.mod-social-message { + flex-wrap: nowrap; + } +} + +.studio-empty { + grid-column: 1 / -1; /* take up all columns */ + text-align: center; + + .studio-empty-img { + margin-top: 45px; + margin-bottom: 25px; + } + + .studio-empty-msg { + font-size: 20px; + line-height: 30px; + font-style: italic; } } @@ -264,3 +311,7 @@ $radius: 8px; cursor: wait !important; opacity: .5; } + +.mod-clickable { + cursor: pointer; +} diff --git a/src/views/teachers/faq/faq.jsx b/src/views/teachers/faq/faq.jsx index fcab5e6cb..c0ab66f09 100644 --- a/src/views/teachers/faq/faq.jsx +++ b/src/views/teachers/faq/faq.jsx @@ -50,25 +50,6 @@ const TeacherFaq = props => (
    -
    -
    - - help@scratch.mit.edu - - ) - }} - /> -
    -
      -
    • -
    • -
    • -
    -
    diff --git a/src/views/teachers/faq/l10n.json b/src/views/teachers/faq/l10n.json index 39f26ead2..3aee57b4b 100644 --- a/src/views/teachers/faq/l10n.json +++ b/src/views/teachers/faq/l10n.json @@ -10,11 +10,6 @@ "teacherfaq.teacherSignUpBody": "To request a Teacher Account, please go to the teacher account request form.", "teacherfaq.classMultipleTeachersTitle": "Can a class have multiple teachers?", "teacherfaq.classMultipleTeachersBody": "A class can only have one teacher account associated with it.", - "teacherfaq.convertToTeacherTitle": "I already have a Scratch account, can you make it a Teacher account?", - "teacherfaq.convertToTeacherList": "Please send an email to {helpEmail} and be sure to include the following information:", - "teacherfaq.convertToTeacherUsername": "Your Scratch username (Please double check that you know your username by logging into it before contacting us!)", - "teacherfaq.convertToTeacherEmail": "The email address associated with your Scratch account", - "teacherfaq.convertToTeacherBirth": "The birth month and birth year associated with your Scratch account", "teacherfaq.teacherPersonalTitle": "Why do you need to know my personal information during registration?", "teacherfaq.teacherPersonalBody": "We use this information to verify the account creator is an educator. We will not share this information with anyone else, and it will not be shared publicly on the site.", "teacherfaq.teacherGoogleTitle": "Does Scratch connect with Google Classroom, Clever or any other classroom management service?", diff --git a/static/images/studios/curators-empty.png b/static/images/studios/curators-empty.png new file mode 100644 index 000000000..76589073a Binary files /dev/null and b/static/images/studios/curators-empty.png differ diff --git a/static/images/studios/projects-empty-can-add.png b/static/images/studios/projects-empty-can-add.png new file mode 100644 index 000000000..937e2e5e5 Binary files /dev/null and b/static/images/studios/projects-empty-can-add.png differ diff --git a/static/images/studios/projects-empty.png b/static/images/studios/projects-empty.png new file mode 100644 index 000000000..d23d9df7b Binary files /dev/null and b/static/images/studios/projects-empty.png differ diff --git a/static/svgs/studio/activity-curator.svg b/static/svgs/studio/activity-curator.svg new file mode 100644 index 000000000..291a22e3a --- /dev/null +++ b/static/svgs/studio/activity-curator.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/svgs/studio/activity-edit.svg b/static/svgs/studio/activity-edit.svg new file mode 100644 index 000000000..74d899763 --- /dev/null +++ b/static/svgs/studio/activity-edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/svgs/studio/activity-project.svg b/static/svgs/studio/activity-project.svg new file mode 100644 index 000000000..464a7f6bc --- /dev/null +++ b/static/svgs/studio/activity-project.svg @@ -0,0 +1,4 @@ + + + + diff --git a/test/unit/redux/infinite-list.test.js b/test/unit/redux/infinite-list.test.js index 9e9995a3d..03287e06f 100644 --- a/test/unit/redux/infinite-list.test.js +++ b/test/unit/redux/infinite-list.test.js @@ -1,4 +1,3 @@ -/* global Promise */ import InfiniteList from '../../../src/redux/infinite-list'; const module = InfiniteList('test-key'); @@ -93,15 +92,31 @@ describe('Infinite List redux module', () => { }); describe('CREATE', () => { - let action; - beforeEach(() => { - action = module.actions.create(7); - }); - test('prepends the given item', () => { + test('prepends the given item by default', () => { + const action = module.actions.create(7); initialState.items = [8, 9, 10, 11]; const newState = module.reducer(initialState, action); expect(newState.items).toEqual([7, 8, 9, 10, 11]); }); + test('appends the given item if given `atEnd` arg', () => { + const action = module.actions.create(7, true); + initialState.items = [8, 9, 10, 11]; + const newState = module.reducer(initialState, action); + expect(newState.items).toEqual([8, 9, 10, 11, 7]); + }); + }); + + describe('CLEAR', () => { + test('resets everything back to the initial state', () => { + const state = { + error: new Error(), + items: [1, 2, 3], + loading: 'something not initial', + moreToLoad: 'something not initial' + }; + const newState = module.reducer(state, module.actions.clear()); + expect(newState).toEqual(initialState); + }); }); describe('ERROR', () => { @@ -134,40 +149,12 @@ describe('Infinite List redux module', () => { expect(typeof module.actions[key]).toBe('function'); } }); - - describe('loadMore', () => { - test('returns a thunk function, rather than a standard action object', () => { - expect(typeof module.actions.loadMore()).toBe('function'); - }); - test('calls loading and the fetcher', () => { - let dispatch = jest.fn(); - let fetcher = jest.fn(() => new Promise(() => { })); // that never resolves - module.actions.loadMore(fetcher)(dispatch); - expect(dispatch).toHaveBeenCalledWith(module.actions.loading()); - expect(fetcher).toHaveBeenCalled(); - }); - test('calls append with resolved result from fetcher', async () => { - let dispatch = jest.fn(); - let fetcher = jest.fn(() => Promise.resolve({items: ['a', 'b'], moreToLoad: false})); - await module.actions.loadMore(fetcher)(dispatch); - expect(dispatch.mock.calls[1][0]) // the second call to dispatch, after LOADING - .toEqual(module.actions.append(['a', 'b'], false)); - }); - test('calls error with rejecting promise from fetcher', async () => { - let error = new Error(); - let dispatch = jest.fn(); - let fetcher = jest.fn(() => Promise.reject(error)); - await module.actions.loadMore(fetcher)(dispatch); - expect(dispatch.mock.calls[1][0]) // the second call to dispatch, after LOADING - .toEqual(module.actions.error(error)); - }); - }); }); describe('selector', () => { test('will return the slice of state defined by the key', () => { const state = { - [module.key]: module.reducer(undefined, {}) // eslint-disable-line no-undefined + [module.key]: initialState }; expect(module.selector(state)).toBe(initialState); });