mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 09:35:56 -05:00
Merge pull request #5387 from LLK/release/2021-05-12
[Master] Release 2021-05-12
This commit is contained in:
commit
d6e2a43b92
51 changed files with 1375 additions and 431 deletions
166
package-lock.json
generated
166
package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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
|
||||
className={classes}
|
||||
{...omit(props, ['className', 'children'])}
|
||||
{...omit(props, ['className', 'children', 'isCloseType'])}
|
||||
>
|
||||
{props.children}
|
||||
{
|
||||
props.isCloseType ? (
|
||||
<img
|
||||
alt="close-icon"
|
||||
className="modal-content-close-img"
|
||||
draggable="false"
|
||||
src="/svgs/modal/close-x.svg"
|
||||
/>
|
||||
) : [
|
||||
props.children
|
||||
]
|
||||
}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
Button.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string
|
||||
className: PropTypes.string,
|
||||
isCloseType: PropTypes.bool
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
className: '',
|
||||
isCloseType: false
|
||||
};
|
||||
|
||||
module.exports = Button;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -24,6 +24,8 @@ const Errors = keyMirror({
|
|||
UNHANDLED: null
|
||||
});
|
||||
|
||||
const MAX_IMAGE_BYTES = 524288;
|
||||
|
||||
const getInitialState = () => ({
|
||||
mutationErrors: {}, // { [field]: <error>, ... }
|
||||
isMutating: {} // { [field]: <boolean>, ... }
|
||||
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -363,9 +363,9 @@ const Developers = () => (
|
|||
githubLink: (
|
||||
<a href="https://github.com/LLK/">GitHub</a>
|
||||
),
|
||||
emailLink: (
|
||||
<a href="mailto:help@scratch.mit.edu">
|
||||
help@scratch.mit.edu
|
||||
contactUsLink: (
|
||||
<a href="/contact-us">
|
||||
<FormattedMessage id="general.contactUs" />
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
|
|
|
@ -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!"
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
56
src/views/splash/donate/donate-banner.jsx
Normal file
56
src/views/splash/donate/donate-banner.jsx
Normal file
|
@ -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
|
||||
}) => (
|
||||
<TitleBanner className="donate-banner">
|
||||
<div className="donate-container">
|
||||
<img
|
||||
className="donate-icon"
|
||||
src="/images/ideas/try-it-icon.svg"
|
||||
/>
|
||||
<div className="donate-central-items">
|
||||
<p className="donate-text">
|
||||
<FormattedMessage id="donatebanner.askSupport" />
|
||||
</p>
|
||||
<Button
|
||||
className="donate-button"
|
||||
key="add-to-studio-button"
|
||||
onClick={navigateToDonatePage}
|
||||
>
|
||||
<FormattedMessage id="general.donate" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
isCloseType
|
||||
className="donate-close-button"
|
||||
key="closeButton"
|
||||
name="closeButton"
|
||||
type="button"
|
||||
onClick={onRequestClose}
|
||||
>
|
||||
<div className="action-button-text">
|
||||
<FormattedMessage id="general.close" />
|
||||
</div>
|
||||
</Button>
|
||||
</TitleBanner>
|
||||
);
|
||||
|
||||
DonateTopBanner.propTypes = {
|
||||
onRequestClose: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = injectIntl(DonateTopBanner);
|
77
src/views/splash/donate/donate-banner.scss
Normal file
77
src/views/splash/donate/donate-banner.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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) ? (
|
||||
<MediaQuery
|
||||
key="frameless-tablet"
|
||||
minWidth={frameless.tabletPortrait}
|
||||
>
|
||||
<TopBanner />
|
||||
</MediaQuery>
|
||||
) : (
|
||||
<Intro
|
||||
key="intro"
|
||||
messages={messages}
|
||||
/>
|
||||
)
|
||||
this.props.shouldShowHOCTopBanner && (
|
||||
<MediaQuery
|
||||
key="frameless-tablet"
|
||||
minWidth={frameless.tabletPortrait}
|
||||
>
|
||||
<HOCTopBanner />
|
||||
</MediaQuery>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.props.shouldShowDonateBanner && (
|
||||
<DonateBanner
|
||||
onRequestClose={this.props.onCloseDonateBanner}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.props.shouldShowIntro && (
|
||||
<Intro
|
||||
key="intro"
|
||||
messages={messages}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div
|
||||
|
@ -464,17 +470,14 @@ class SplashPresentation extends React.Component { // eslint-disable-line react/
|
|||
{featured.shift()}
|
||||
</div>
|
||||
{
|
||||
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
|
||||
<MediaQuery
|
||||
key="frameless-desktop"
|
||||
minWidth={frameless.tabletPortrait}
|
||||
>
|
||||
<MiddleBanner />
|
||||
</MediaQuery>
|
||||
this.props.shouldShowHOCMiddleBanner && (
|
||||
<MediaQuery
|
||||
key="frameless-desktop"
|
||||
minWidth={frameless.tabletPortrait}
|
||||
>
|
||||
<HOCMiddleBanner />
|
||||
</MediaQuery>
|
||||
)
|
||||
}
|
||||
|
||||
<div
|
||||
|
@ -573,6 +576,7 @@ SplashPresentation.propTypes = {
|
|||
lovedByFollowing: PropTypes.arrayOf(PropTypes.object),
|
||||
news: PropTypes.arrayOf(PropTypes.object),
|
||||
onCloseAdminPanel: PropTypes.func.isRequired,
|
||||
onCloseDonateBanner: PropTypes.func.isRequired,
|
||||
onDismiss: PropTypes.func.isRequired,
|
||||
onHideEmailConfirmationModal: PropTypes.func.isRequired,
|
||||
onOpenAdminPanel: PropTypes.func.isRequired,
|
||||
|
@ -581,7 +585,11 @@ SplashPresentation.propTypes = {
|
|||
refreshCacheStatus: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
sessionStatus: PropTypes.string.isRequired,
|
||||
sharedByFollowing: PropTypes.arrayOf(PropTypes.object),
|
||||
shouldShowDonateBanner: PropTypes.bool.isRequired,
|
||||
shouldShowEmailConfirmation: PropTypes.bool.isRequired,
|
||||
shouldShowHOCTopBanner: PropTypes.bool.isRequired,
|
||||
shouldShowIntro: PropTypes.bool.isRequired,
|
||||
shouldShowHOCMiddleBanner: PropTypes.bool.isRequired,
|
||||
shouldShowWelcome: PropTypes.bool.isRequired,
|
||||
user: PropTypes.object.isRequired // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
|
|
@ -12,6 +12,11 @@ const splashActions = require('../../redux/splash.js');
|
|||
const Page = require('../../components/page/www/page.jsx');
|
||||
const SplashPresentation = require('./presentation.jsx');
|
||||
|
||||
const SCRATCH_WEEK_START_TIME = 1621224000000; // 2021-05-17 00:00:00
|
||||
const SCRATCH_WEEK_END_TIME = 1621828800000; // 2021-05-24 00:00:00
|
||||
const HOC_START_TIME = 1605484800000; // 2020-11-16 00:00:00
|
||||
const HOC_END_TIME = 1608681600000; // 2020-12-23 00:00:00
|
||||
|
||||
class Splash extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
@ -22,6 +27,7 @@ class Splash extends React.Component {
|
|||
'handleShowEmailConfirmationModal',
|
||||
'handleHideEmailConfirmationModal',
|
||||
'handleCloseAdminPanel',
|
||||
'handleCloseDonateBanner',
|
||||
'handleOpenAdminPanel',
|
||||
'handleDismiss',
|
||||
'shouldShowWelcome',
|
||||
|
@ -29,8 +35,9 @@ class Splash extends React.Component {
|
|||
]);
|
||||
this.state = {
|
||||
adminPanelOpen: false,
|
||||
dismissedDonateBanner: false,
|
||||
news: [], // gets news posts from the scratch Tumblr
|
||||
emailConfirmationModalOpen: false, // flag that determines whether to show banner to request email conf.
|
||||
emailConfirmationModalOpen: false,
|
||||
refreshCacheStatus: 'notrequested'
|
||||
};
|
||||
}
|
||||
|
@ -114,6 +121,9 @@ class Splash extends React.Component {
|
|||
handleOpenAdminPanel () {
|
||||
this.setState({adminPanelOpen: true});
|
||||
}
|
||||
handleCloseDonateBanner () {
|
||||
this.setState({dismissedDonateBanner: true});
|
||||
}
|
||||
handleShowEmailConfirmationModal () {
|
||||
this.setState({emailConfirmationModalOpen: true});
|
||||
}
|
||||
|
@ -144,8 +154,40 @@ class Splash extends React.Component {
|
|||
this.props.flags.confirm_email_banner
|
||||
);
|
||||
}
|
||||
shouldShowHOCTopBanner () {
|
||||
return (
|
||||
this.props.sessionStatus === sessionActions.Status.FETCHED && // done fetching session
|
||||
Object.keys(this.props.user).length === 0 && // no user session found
|
||||
Date.now() >= 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}
|
||||
|
|
48
src/views/studio/l10n.json
Normal file
48
src/views/studio/l10n.json
Normal file
|
@ -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}"
|
||||
}
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
||||
|
|
43
src/views/studio/lib/studio-activity-actions.js
Normal file
43
src/views/studio/lib/studio-activity-actions.js
Normal file
|
@ -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};
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
59
src/views/studio/lib/user-projects-actions.js
Normal file
59
src/views/studio/lib/user-projects-actions.js
Normal file
|
@ -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
|
||||
};
|
121
src/views/studio/modals/user-projects-modal.jsx
Normal file
121
src/views/studio/modals/user-projects-modal.jsx
Normal file
|
@ -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 (
|
||||
<Modal
|
||||
isOpen
|
||||
className="user-projects-modal"
|
||||
onRequestClose={onRequestClose}
|
||||
>
|
||||
<ModalTitle
|
||||
className="user-projects-modal-title modal-header"
|
||||
title="Add to Studio"
|
||||
/>
|
||||
<SubNavigation
|
||||
align="left"
|
||||
className="user-projects-modal-nav"
|
||||
>
|
||||
<li
|
||||
className={classNames({active: filter === Filters.SHARED})}
|
||||
onClick={() => setFilter(Filters.SHARED)}
|
||||
>
|
||||
<FormattedMessage id="studio.sharedFilter" />
|
||||
</li>
|
||||
<li
|
||||
className={classNames({active: filter === Filters.FAVORITED})}
|
||||
onClick={() => setFilter(Filters.FAVORITED)}
|
||||
>
|
||||
<FormattedMessage id="studio.favoritedFilter" />
|
||||
</li>
|
||||
<li
|
||||
className={classNames({active: filter === Filters.RECENT})}
|
||||
onClick={() => setFilter(Filters.RECENT)}
|
||||
>
|
||||
<FormattedMessage id="studio.recentFilter" />
|
||||
</li>
|
||||
</SubNavigation>
|
||||
<ModalInnerContent className="user-projects-modal-content">
|
||||
{error && <div>Error loading {filter}: {error}</div>}
|
||||
<div className="user-projects-modal-grid">
|
||||
{items.map(project => (
|
||||
<UserProjectsTile
|
||||
key={project.id}
|
||||
id={project.id}
|
||||
title={project.title}
|
||||
image={project.image}
|
||||
inStudio={project.inStudio}
|
||||
onAdd={onAdd}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
<div className="studio-projects-load-more">
|
||||
{loading ? <small>Loading...</small> : (
|
||||
moreToLoad ?
|
||||
<button onClick={() => onLoadMore(filter)}>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</button> :
|
||||
<small>No more to load</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ModalInnerContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
92
src/views/studio/modals/user-projects-modal.scss
Normal file
92
src/views/studio/modals/user-projects-modal.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
58
src/views/studio/modals/user-projects-tile.jsx
Normal file
58
src/views/studio/modals/user-projects-tile.jsx
Normal file
|
@ -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 (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
className={classNames('studio-project-tile', {
|
||||
'mod-clickable': true,
|
||||
'mod-mutating': submitting
|
||||
})}
|
||||
onClick={toggle}
|
||||
onKeyDown={e => e.key === 'Enter' && toggle()}
|
||||
>
|
||||
<img
|
||||
className="studio-project-image"
|
||||
src={image}
|
||||
/>
|
||||
<div className="studio-project-bottom">
|
||||
<div className="studio-project-title">{title}</div>
|
||||
<div className={`studio-tile-dynamic-${added ? 'remove' : 'add'}`}>
|
||||
{added ? '✔' : '+'}
|
||||
</div>
|
||||
{error && <div>{error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
|
@ -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 (
|
||||
<SocialMessage
|
||||
datetime={item.datetime_created}
|
||||
iconSrc="/svgs/studio/activity-project.svg"
|
||||
iconAlt="project activity icon"
|
||||
imgClassName="studio-activity-icon"
|
||||
key={item.id}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="studio.activityAddProjectToStudio"
|
||||
values={{
|
||||
profileLink: (
|
||||
<a href={`/users/${item.actor_username}`}>
|
||||
{item.actor_username}
|
||||
</a>
|
||||
),
|
||||
projectLink: (
|
||||
<a href={`/projects/${item.project_id}`}>
|
||||
{item.project_title}
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SocialMessage>
|
||||
);
|
||||
case 'removeprojectstudio':
|
||||
return (
|
||||
<SocialMessage
|
||||
datetime={item.datetime_created}
|
||||
iconSrc="/svgs/studio/activity-project.svg"
|
||||
iconAlt="project activity icon"
|
||||
imgClassName="studio-activity-icon"
|
||||
key={item.id}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="studio.activityRemoveProjectStudio"
|
||||
values={{
|
||||
profileLink: (
|
||||
<a href={`/users/${item.actor_username}`}>
|
||||
{item.actor_username}
|
||||
</a>
|
||||
),
|
||||
projectLink: (
|
||||
<a href={`/projects/${item.project_id}`}>
|
||||
{item.project_title}
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SocialMessage>
|
||||
);
|
||||
case 'updatestudio':
|
||||
return (
|
||||
<SocialMessage
|
||||
datetime={item.datetime_created}
|
||||
iconSrc="/svgs/studio/activity-edit.svg"
|
||||
iconAlt="edit activity icon"
|
||||
imgClassName="studio-activity-icon"
|
||||
key={item.id}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="studio.activityUpdateStudio"
|
||||
values={{
|
||||
profileLink: (
|
||||
<a href={`/users/${item.actor_username}`}>
|
||||
{item.actor_username}
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SocialMessage>
|
||||
);
|
||||
case 'becomecurator':
|
||||
return (
|
||||
<SocialMessage
|
||||
datetime={item.datetime_created}
|
||||
iconSrc="/svgs/studio/activity-curator.svg"
|
||||
iconAlt="curator activity icon"
|
||||
imgClassName="studio-activity-icon"
|
||||
key={item.id}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="studio.activityBecomeCurator"
|
||||
values={{
|
||||
// Beware, DB seems to think actor is new curator and username is inviter
|
||||
newCuratorProfileLink: (
|
||||
<a href={`/users/${item.actor_username}`}>
|
||||
{item.actor_username}
|
||||
</a>
|
||||
),
|
||||
inviterProfileLink: (
|
||||
<a href={`/users/${item.username}`}>
|
||||
{item.username}
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SocialMessage>
|
||||
);
|
||||
case 'removecuratorstudio':
|
||||
return (
|
||||
<SocialMessage
|
||||
datetime={item.datetime_created}
|
||||
iconSrc="/svgs/studio/activity-curator.svg"
|
||||
iconAlt="curator activity icon"
|
||||
imgClassName="studio-activity-icon"
|
||||
key={item.id}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="studio.activityRemoveCurator"
|
||||
values={{
|
||||
removedProfileLink: (
|
||||
<a href={`/users/${item.username}`}>
|
||||
{item.username}
|
||||
</a>
|
||||
),
|
||||
removerProfileLink: (
|
||||
<a href={`/users/${item.actor_username}`}>
|
||||
{item.actor_username}
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SocialMessage>
|
||||
);
|
||||
case 'becomeownerstudio':
|
||||
return (
|
||||
<SocialMessage
|
||||
datetime={item.datetime_created}
|
||||
iconSrc="/svgs/studio/activity-curator.svg"
|
||||
iconAlt="curator activity icon"
|
||||
imgClassName="studio-activity-icon"
|
||||
key={item.id}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="studio.activityBecomeOwner"
|
||||
values={{
|
||||
promotedProfileLink: (
|
||||
<a href={`/users/${item.recipient_username}`}>
|
||||
{item.recipient_username}
|
||||
</a>
|
||||
),
|
||||
promotorProfileLink: (
|
||||
<a href={`/users/${item.actor_username}`}>
|
||||
{item.actor_username}
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SocialMessage>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<div className="studio-activity">
|
||||
<h2>Activity</h2>
|
||||
{loading && <div>Loading...</div>}
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
data={error}
|
||||
/>}
|
||||
<div>
|
||||
{items.map((item, index) =>
|
||||
(<Debug
|
||||
label="Activity Item"
|
||||
data={item}
|
||||
key={index}
|
||||
/>)
|
||||
<ul
|
||||
className="studio-messages-list"
|
||||
>
|
||||
{items.map(item =>
|
||||
getComponentForItem(item)
|
||||
)}
|
||||
</ul>
|
||||
<div>
|
||||
{moreToLoad &&
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': loading
|
||||
})}
|
||||
onClick={onLoadMore}
|
||||
>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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);
|
||||
|
|
|
@ -56,7 +56,7 @@ const StudioComments = ({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h2>Comments</h2>
|
||||
<h2><FormattedMessage id="studio.commentsHeader" /></h2>
|
||||
{canEditCommentsAllowed && <StudioCommentsAllowed />}
|
||||
<div>
|
||||
{shouldShowCommentComposer && commentsAllowed &&
|
||||
|
|
|
@ -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</button>
|
||||
><FormattedMessage id="studio.curatorAcceptInvite" /></button>
|
||||
{error && <div>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
<div className="studio-adder-section">
|
||||
<h3>✦ Invite Curators</h3>
|
||||
<input
|
||||
disabled={submitting}
|
||||
type="text"
|
||||
placeholder="<username>"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': submitting
|
||||
})}
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onSubmit(value)
|
||||
.then(() => setValue(''))
|
||||
.catch(e => setError(e))
|
||||
.then(() => setSubmitting(false));
|
||||
}}
|
||||
>Invite</button>
|
||||
{error && <div>{error}</div>}
|
||||
<h3><FormattedMessage id="studio.inviteCuratorsHeader" /></h3>
|
||||
<FlexRow>
|
||||
<input
|
||||
disabled={submitting}
|
||||
type="text"
|
||||
placeholder="<username>"
|
||||
value={value}
|
||||
onKeyDown={e => e.key === 'Enter' && submit()}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': submitting
|
||||
})}
|
||||
disabled={submitting}
|
||||
onClick={submit}
|
||||
><FormattedMessage id="studio.inviteCurator" /></button>
|
||||
{error && <div>{error}</div>}
|
||||
</FlexRow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 (<div className="studio-members">
|
||||
<h2>Curators</h2>
|
||||
<h2><FormattedMessage id="studio.curatorsHeader" /></h2>
|
||||
{canInviteCurators && <CuratorInviter />}
|
||||
{showCuratorInvite && <CuratorInvite />}
|
||||
{error && <Debug
|
||||
|
@ -26,22 +28,48 @@ const StudioCurators = ({
|
|||
data={error}
|
||||
/>}
|
||||
<div className="studio-members-grid">
|
||||
{items.map(item =>
|
||||
(<CuratorTile
|
||||
key={item.username}
|
||||
username={item.username}
|
||||
image={item.profile.images['90x90']}
|
||||
/>)
|
||||
{items.length === 0 && !loading ? (
|
||||
<div className="studio-empty">
|
||||
<img
|
||||
width="179"
|
||||
height="111"
|
||||
className="studio-empty-img"
|
||||
src="/images/studios/curators-empty.png"
|
||||
/>
|
||||
{canInviteCurators ? (
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.curatorsEmptyCanAdd1" /></div>
|
||||
<div><FormattedMessage id="studio.curatorsEmptyCanAdd2" /></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.curatorsEmpty1" /></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{items.map(item =>
|
||||
(<CuratorTile
|
||||
key={item.username}
|
||||
username={item.username}
|
||||
image={item.profile.images['90x90']}
|
||||
/>)
|
||||
)}
|
||||
{moreToLoad &&
|
||||
<div className="studio-members-load-more">
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': loading
|
||||
})}
|
||||
onClick={onLoadMore}
|
||||
>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div className="studio-members-load-more">
|
||||
{loading ? <small>Loading...</small> : (
|
||||
moreToLoad ?
|
||||
<button onClick={onLoadMore}>
|
||||
Load more
|
||||
</button> :
|
||||
<small>No more to load</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
};
|
||||
|
|
|
@ -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 ?
|
||||
<FormattedMessage id="studio.unfollowStudio" /> :
|
||||
<FormattedMessage id="studio.followStudio" />
|
||||
)}
|
||||
</button>
|
||||
{followingError && <div>Error mutating following: {followingError}</div>}
|
||||
|
|
|
@ -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 (
|
||||
<div className="studio-members">
|
||||
<h2>Managers</h2>
|
||||
<h2><FormattedMessage id="studio.managersHeader" /></h2>
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
data={error}
|
||||
|
@ -29,15 +31,18 @@ const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
|
|||
image={item.profile.images['90x90']}
|
||||
/>)
|
||||
)}
|
||||
<div className="studio-members-load-more">
|
||||
{loading ? <small>Loading...</small> : (
|
||||
moreToLoad ?
|
||||
<button onClick={onLoadMore}>
|
||||
Load more
|
||||
</button> :
|
||||
<small>No more to load</small>
|
||||
)}
|
||||
</div>
|
||||
{moreToLoad &&
|
||||
<div className="studio-members-load-more">
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': loading
|
||||
})}
|
||||
onClick={onLoadMore}
|
||||
>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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}</a>
|
||||
{isCreator && <div className="studio-member-role">Studio Creator</div>}
|
||||
{isCreator && <div className="studio-member-role"><FormattedMessage id="studio.creatorRole" /></div>}
|
||||
</div>
|
||||
{canRemove &&
|
||||
<button
|
||||
|
|
|
@ -3,39 +3,54 @@ 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 {addProject} from './lib/studio-project-actions';
|
||||
import UserProjectsModal from './modals/user-projects-modal.jsx';
|
||||
import FlexRow from '../../components/flex-row/flex-row.jsx';
|
||||
|
||||
const StudioProjectAdder = ({onSubmit}) => {
|
||||
const [value, setValue] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const submit = () => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onSubmit(value)
|
||||
.then(() => setValue(''))
|
||||
.catch(e => setError(e))
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
return (
|
||||
<div className="studio-adder-section">
|
||||
<h3>✦ Add Projects</h3>
|
||||
<input
|
||||
disabled={submitting}
|
||||
type="text"
|
||||
placeholder="<project id>"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': submitting
|
||||
})}
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onSubmit(value)
|
||||
.then(() => setValue(''))
|
||||
.catch(e => setError(e))
|
||||
.then(() => setSubmitting(false));
|
||||
}}
|
||||
>Add</button>
|
||||
{error && <div>{error}</div>}
|
||||
<h3><FormattedMessage id="studio.addProjectsHeader" /></h3>
|
||||
<FlexRow>
|
||||
<input
|
||||
disabled={submitting}
|
||||
type="text"
|
||||
placeholder="<project id>"
|
||||
value={value}
|
||||
onKeyDown={e => e.key === 'Enter' && submit()}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': submitting
|
||||
})}
|
||||
disabled={submitting}
|
||||
onClick={submit}
|
||||
><FormattedMessage id="studio.addProject" /></button>
|
||||
{error && <div>{error}</div>}
|
||||
<div className="studio-adder-vertical-divider" />
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
<FormattedMessage id="studio.browseProjects" />
|
||||
</button>
|
||||
{modalOpen && <UserProjectsModal onRequestClose={() => setModalOpen(false)} />}
|
||||
</FlexRow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<div className="studio-projects">
|
||||
<h2>Projects</h2>
|
||||
<h2><FormattedMessage id="studio.projectsHeader" /></h2>
|
||||
{canEditOpenToAll && <StudioOpenToAll />}
|
||||
{canAddProjects && <StudioProjectAdder />}
|
||||
{error && <Debug
|
||||
|
@ -27,27 +29,64 @@ const StudioProjects = ({
|
|||
data={error}
|
||||
/>}
|
||||
<div className="studio-projects-grid">
|
||||
{items.map(item =>
|
||||
(<StudioProjectTile
|
||||
fetching={loading}
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
title={item.title}
|
||||
image={item.image}
|
||||
avatar={item.avatar['90x90']}
|
||||
username={item.username}
|
||||
addedBy={item.actor_id}
|
||||
/>)
|
||||
{items.length === 0 && !loading ? (
|
||||
<div className="studio-empty">
|
||||
{canAddProjects ? (
|
||||
<React.Fragment>
|
||||
<img
|
||||
width="388"
|
||||
height="265"
|
||||
className="studio-empty-img"
|
||||
src="/images/studios/projects-empty-can-add.png"
|
||||
/>
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.projectsEmptyCanAdd1" /></div>
|
||||
<div><FormattedMessage id="studio.projectsEmptyCanAdd2" /></div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<img
|
||||
width="186"
|
||||
height="138"
|
||||
className="studio-empty-img"
|
||||
src="/images/studios/projects-empty.png"
|
||||
/>
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.projectsEmpty1" /></div>
|
||||
<div><FormattedMessage id="studio.projectsEmpty2" /></div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{items.map(item =>
|
||||
(<StudioProjectTile
|
||||
fetching={loading}
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
title={item.title}
|
||||
image={item.image}
|
||||
avatar={item.avatar['90x90']}
|
||||
username={item.username}
|
||||
addedBy={item.actor_id}
|
||||
/>)
|
||||
)}
|
||||
{moreToLoad &&
|
||||
<div className="studio-projects-load-more">
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': loading
|
||||
})}
|
||||
onClick={onLoadMore}
|
||||
>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div className="studio-projects-load-more">
|
||||
{loading ? <small>Loading...</small> : (
|
||||
moreToLoad ?
|
||||
<button onClick={onLoadMore}>
|
||||
Load more
|
||||
</button> :
|
||||
<small>No more to load</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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 = ({
|
|||
<div>
|
||||
<h3>Reporting</h3>
|
||||
{canReport && (
|
||||
<button onClick={handleOpen}>Report</button>
|
||||
<button onClick={handleOpen}><FormattedMessage id="general.report" /></button>
|
||||
)}
|
||||
{isOpen && (
|
||||
<div style={{padding: '1rem', margin: '1rem', border: '1px solid green'}}>
|
||||
<div>Report Studio Modal</div>
|
||||
<div><FormattedMessage id="report.studio" /></div>
|
||||
{previouslyReported ? (
|
||||
<React.Fragment>
|
||||
<div>Submitted the report!</div>
|
||||
<button onClick={handleClose}>Close</button>
|
||||
<button onClick={handleClose}><FormattedMessage id="general.close" /></button>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
|
@ -40,9 +41,9 @@ const StudioReport = ({
|
|||
value={field}
|
||||
onChange={e => handleSetField(e.target.value)}
|
||||
>
|
||||
<option value={Fields.TITLE}>Title</option>
|
||||
<option value={Fields.DESCRIPTION}>Description</option>
|
||||
<option value={Fields.THUMBNAIL}>Thumbnail</option>
|
||||
<option value={Fields.TITLE}><FormattedMessage id="studio.title" /></option>
|
||||
<option value={Fields.DESCRIPTION}><FormattedMessage id="studio.description" /></option>
|
||||
<option value={Fields.THUMBNAIL}><FormattedMessage id="studio.thumbnail" /></option>
|
||||
</select>
|
||||
{error && (
|
||||
<div>
|
||||
|
@ -54,9 +55,9 @@ const StudioReport = ({
|
|||
disabled={isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Submit
|
||||
<FormattedMessage id="report.send" />
|
||||
</button>
|
||||
<button onClick={handleClose}>Cancel</button>
|
||||
<button onClick={handleClose}><FormattedMessage id="general.cancel" /></button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<SubNavigation
|
||||
align="left"
|
||||
|
@ -12,28 +13,28 @@ const StudioTabNav = () => {
|
|||
>
|
||||
<NavLink
|
||||
activeClassName="active"
|
||||
to={`${match.url}`}
|
||||
to={base}
|
||||
exact
|
||||
>
|
||||
<li>Projects</li>
|
||||
<li><FormattedMessage id="studio.tabNavProjects" /></li>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
activeClassName="active"
|
||||
to={`${match.url}/curators`}
|
||||
to={`${base}/comments`}
|
||||
>
|
||||
<li>Curators</li>
|
||||
<li><FormattedMessage id="studio.tabNavComments" /></li>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
activeClassName="active"
|
||||
to={`${match.url}/comments`}
|
||||
to={`${base}/curators`}
|
||||
>
|
||||
<li> Comments</li>
|
||||
<li><FormattedMessage id="studio.tabNavCurators" /></li>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
activeClassName="active"
|
||||
to={`${match.url}/activity`}
|
||||
to={`${base}/activity`}
|
||||
>
|
||||
<li>Activity</li>
|
||||
<li><FormattedMessage id="studio.tabNavActivity" /></li>
|
||||
</NavLink>
|
||||
</SubNavigation>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
<div className="studio-shell">
|
||||
<div className="studio-info">
|
||||
<StudioInfo />
|
||||
</div>
|
||||
<div className="studio-tabs">
|
||||
<StudioTabNav />
|
||||
<div>
|
||||
<Switch>
|
||||
<Route path={`${match.path}/curators`}>
|
||||
<StudioManagers />
|
||||
<StudioCurators />
|
||||
</Route>
|
||||
<Route path={`${match.path}/comments`}>
|
||||
<StudioComments />
|
||||
</Route>
|
||||
<Route path={`${match.path}/activity`}>
|
||||
<StudioActivity />
|
||||
</Route>
|
||||
<Route path={`${match.path}/projects`}>
|
||||
{/* We can force /projects back to / this way */}
|
||||
<Redirect to={match.url} />
|
||||
</Route>
|
||||
<Route path={match.path}>
|
||||
<StudioProjects />
|
||||
</Route>
|
||||
</Switch>
|
||||
studioLoadFailed ?
|
||||
<NotAvailable /> :
|
||||
<div className="studio-shell">
|
||||
<div className="studio-info">
|
||||
<StudioInfo />
|
||||
</div>
|
||||
<div className="studio-tabs">
|
||||
<StudioTabNav />
|
||||
<div>
|
||||
<Switch>
|
||||
<Route path={`${match.path}/curators`}>
|
||||
<StudioManagers />
|
||||
<StudioCurators />
|
||||
</Route>
|
||||
<Route path={`${match.path}/comments`}>
|
||||
<StudioComments />
|
||||
</Route>
|
||||
<Route path={`${match.path}/activity`}>
|
||||
<StudioActivity />
|
||||
</Route>
|
||||
<Route path={`${match.path}/projects`}>
|
||||
{/* We can force /projects back to / this way */}
|
||||
<Redirect to={match.url} />
|
||||
</Route>
|
||||
<Route path={match.path}>
|
||||
<StudioProjects />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StudioShell.propTypes = {
|
||||
studioLoadFailed: PropTypes.bool
|
||||
};
|
||||
|
||||
const ConnectedStudioShell = connect(
|
||||
state => ({
|
||||
studioLoadFailed: selectStudioLoadFailed(state)
|
||||
}),
|
||||
)(StudioShell);
|
||||
|
||||
render(
|
||||
<Page className="studio-page">
|
||||
<Router>
|
||||
<Switch>
|
||||
{/* Use variable studioPath to support /studio-playground/ or future route */}
|
||||
<Route path="/:studioPath/:studioId">
|
||||
<StudioShell />
|
||||
<ConnectedStudioShell />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -50,25 +50,6 @@ const TeacherFaq = props => (
|
|||
<dd><FormattedHTMLMessage id="teacherfaq.teacherSignUpBody" /></dd>
|
||||
<dt><FormattedMessage id="teacherfaq.classMultipleTeachersTitle" /></dt>
|
||||
<dd><FormattedMessage id="teacherfaq.classMultipleTeachersBody" /></dd>
|
||||
<dt><FormattedMessage id="teacherfaq.convertToTeacherTitle" /></dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="teacherfaq.convertToTeacherList"
|
||||
values={{
|
||||
helpEmail: (
|
||||
<a href="mailto:help@scratch.mit.edu">
|
||||
help@scratch.mit.edu
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</dd>
|
||||
<ul>
|
||||
<li><FormattedMessage id="teacherfaq.convertToTeacherUsername" /></li>
|
||||
<li><FormattedMessage id="teacherfaq.convertToTeacherEmail" /></li>
|
||||
<li><FormattedMessage id="teacherfaq.convertToTeacherBirth" /></li>
|
||||
</ul>
|
||||
|
||||
<dt><FormattedMessage id="teacherfaq.teacherPersonalTitle" /></dt>
|
||||
<dd><FormattedMessage id="teacherfaq.teacherPersonalBody" /></dd>
|
||||
<dt><FormattedMessage id="teacherfaq.teacherGoogleTitle" /></dt>
|
||||
|
|
|
@ -10,11 +10,6 @@
|
|||
"teacherfaq.teacherSignUpBody": "To request a Teacher Account, please go to the teacher account <a href=\"/educators/register\">request form</a>.",
|
||||
"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?",
|
||||
|
|
BIN
static/images/studios/curators-empty.png
Normal file
BIN
static/images/studios/curators-empty.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
static/images/studios/projects-empty-can-add.png
Normal file
BIN
static/images/studios/projects-empty-can-add.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
BIN
static/images/studios/projects-empty.png
Normal file
BIN
static/images/studios/projects-empty.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.7 KiB |
4
static/svgs/studio/activity-curator.svg
Normal file
4
static/svgs/studio/activity-curator.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.2531 7.47107C5.2531 8.16594 5.51512 8.80522 5.9491 9.28437C4.50749 10.1253 3.54431 11.8131 3.54431 13.7407C3.54431 14.5562 4.19249 15.1128 5.00253 15.4536C5.82565 15.7998 6.90529 15.9673 7.97081 15.9673C9.03633 15.9673 10.116 15.7998 10.9391 15.4536C11.7491 15.1128 12.3973 14.5562 12.3973 13.7407V13.7342C12.3973 13.6496 12.3973 13.5472 12.3834 13.4418C12.3334 12.4083 12.0039 11.4543 11.4801 10.6824C11.0859 10.1059 10.5795 9.62615 9.99447 9.28522C10.4318 8.80663 10.6887 8.1706 10.6887 7.47107C10.6887 5.96646 9.47307 4.75 7.97416 4.75C6.46912 4.75 5.2531 5.96602 5.2531 7.47107Z" fill="#4C97FF"/>
|
||||
<path d="M15.0868 5.71516C14.0615 5.71516 13.2332 6.54343 13.2332 7.56872C13.2332 8.00598 13.385 8.40622 13.6373 8.72277C13.1654 9.02039 12.7758 9.45561 12.5108 9.97982C12.4698 10.0609 12.476 10.1578 12.527 10.233C13.0003 10.9304 13.3012 11.7992 13.3451 12.7461C13.3501 12.8539 13.4236 12.9463 13.5274 12.9753C14.3677 13.2105 15.4588 13.2317 16.3459 13.0414C16.7892 12.9463 17.2012 12.7943 17.5086 12.5735C17.8194 12.3501 18.0443 12.0384 18.0443 11.637C18.0443 10.393 17.4408 9.29929 16.5294 8.72307C16.7819 8.40648 16.9338 8.00612 16.9338 7.56872C16.9338 6.5441 16.1062 5.71516 15.0868 5.71516Z" fill="#4C97FF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
3
static/svgs/studio/activity-edit.svg
Normal file
3
static/svgs/studio/activity-edit.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0592 15.9577C9.96446 16.0524 9.85758 16.1132 9.72976 16.1709L7.56659 17.0793C7.48799 16.8113 7.29336 16.4707 6.93361 16.1109C6.58592 15.7633 6.25738 15.5807 5.99914 15.4901L6.89672 13.3378C6.94337 13.199 7.0271 13.0934 7.1208 12.9997C7.14262 12.9998 7.16333 12.9791 7.17418 12.9682L7.20476 12.9376C7.35075 12.7917 7.55585 12.7028 7.79617 12.6709C8.40705 12.5953 9.11521 12.8852 9.64935 13.4193C10.1835 13.9535 10.4734 14.6616 10.3857 15.2605C10.3505 15.458 10.2854 15.6613 10.1833 15.8116C10.1529 15.864 10.1114 15.9055 10.069 15.9479L10.0592 15.9577ZM13.6054 6.53701C13.9427 6.19967 14.4603 6.12739 14.9902 6.28635C15.2973 6.37845 15.4255 6.75916 15.2154 6.96926L10.177 12.0077C10.0566 12.128 9.86135 12.1478 9.70333 12.047C9.54003 11.9449 9.37127 11.857 9.20119 11.7836C8.92376 11.664 8.82736 11.315 9.02168 11.1207L13.6054 6.53701ZM16.5317 9.46328L11.9489 14.046C11.7586 14.2364 11.4084 14.1545 11.2923 13.8845C11.2173 13.705 11.1239 13.5307 11.0163 13.362C10.9099 13.1964 10.9364 12.99 11.0616 12.8648L16.0882 7.83815C16.2964 7.63003 16.677 7.75614 16.7711 8.06339C16.9356 8.59871 16.871 9.12397 16.5317 9.46328ZM18.5347 4.534C17.2459 3.24525 15.3869 3.01376 14.3838 4.01691L6.29247 12.1082C6.09421 12.3065 5.91898 12.5278 5.81284 12.7984L4.10642 16.8959C3.89183 17.4132 4.05796 18.0587 4.53397 18.5347C5.00998 19.0107 5.65549 19.1769 6.1728 18.9623L10.2703 17.2558C10.5398 17.1486 10.7611 16.9734 10.9594 16.7751L19.0507 8.68382C20.0538 7.68067 19.8234 5.82274 18.5347 4.534Z" fill="#4C97FF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
4
static/svgs/studio/activity-project.svg
Normal file
4
static/svgs/studio/activity-project.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.79431 5C6.79431 4.44772 7.24203 4 7.79431 4H17.7943C18.3466 4 18.7943 4.44772 18.7943 5V12C18.7943 12.5523 18.3466 13 17.7943 13H7.79431C7.24203 13 6.79431 12.5523 6.79431 12V5Z" fill="#4C97FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.79431 7H4.79431C4.24203 7 3.79431 7.44772 3.79431 8V14C3.79431 15.1046 4.68974 16 5.79431 16H14.7943C15.3466 16 15.7943 15.5523 15.7943 15V14H7.79431C6.68974 14 5.79431 13.1046 5.79431 12V7ZM6.79431 7H14.7943C15.3466 7 15.7943 7.44772 15.7943 8V13H7.79431C7.24203 13 6.79431 12.5523 6.79431 12V7Z" fill="#4C97FF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 678 B |
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue