Merge branch 'studio-comments-off' of https://github.com/picklesrus/scratch-www into studio-comments-off

resolve conflicts
This commit is contained in:
picklesrus 2021-06-15 17:25:22 -04:00
commit 91f1c7ccf9
48 changed files with 866 additions and 360 deletions

109
package-lock.json generated
View file

@ -225,17 +225,17 @@
"dev": true
},
"@babel/core": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.5.tgz",
"integrity": "sha512-RN/AwP2DJmQTZSfiDaD+JQQ/J99KsIpOCfBE5pL+5jJSt7nI3nYGoAXZu+ffYSQ029NLs2DstZb+eR81uuARgg==",
"version": "7.14.6",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.6.tgz",
"integrity": "sha512-gJnOEWSqTk96qG5BoIrl5bVtc23DCycmIePPYnamY9RboYdI4nFy5vAQMSl81O5K/W0sLDWfGysnOECC+KUUCA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.14.5",
"@babel/generator": "^7.14.5",
"@babel/helper-compilation-targets": "^7.14.5",
"@babel/helper-module-transforms": "^7.14.5",
"@babel/helpers": "^7.14.5",
"@babel/parser": "^7.14.5",
"@babel/helpers": "^7.14.6",
"@babel/parser": "^7.14.6",
"@babel/template": "^7.14.5",
"@babel/traverse": "^7.14.5",
"@babel/types": "^7.14.5",
@ -308,9 +308,9 @@
}
},
"@babel/parser": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz",
"integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==",
"version": "7.14.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz",
"integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==",
"dev": true
},
"@babel/template": {
@ -695,9 +695,9 @@
}
},
"@babel/parser": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz",
"integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==",
"version": "7.14.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz",
"integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==",
"dev": true
},
"@babel/template": {
@ -914,9 +914,9 @@
}
},
"@babel/parser": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz",
"integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==",
"version": "7.14.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz",
"integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==",
"dev": true
},
"@babel/template": {
@ -1076,9 +1076,9 @@
"dev": true
},
"@babel/helpers": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.5.tgz",
"integrity": "sha512-xtcWOuN9VL6nApgVHtq3PPcQv5qFBJzoSZzJ/2c0QK/IP/gxVcoWSNQwFEGvmbQsuS9rhYqjILDGGXcTkA705Q==",
"version": "7.14.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.6.tgz",
"integrity": "sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA==",
"dev": true,
"requires": {
"@babel/template": "^7.14.5",
@ -1147,9 +1147,9 @@
}
},
"@babel/parser": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz",
"integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==",
"version": "7.14.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz",
"integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==",
"dev": true
},
"@babel/template": {
@ -17223,9 +17223,9 @@
"dev": true
},
"postcss": {
"version": "7.0.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz",
"integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==",
"version": "7.0.36",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz",
"integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@ -17883,9 +17883,9 @@
"dev": true
},
"postcss": {
"version": "7.0.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz",
"integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==",
"version": "7.0.36",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz",
"integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@ -20873,9 +20873,9 @@
}
},
"scratch-blocks": {
"version": "0.1.0-prerelease.20210609211941",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210609211941.tgz",
"integrity": "sha512-lVPmB4DMpM9RxEXcWMBPsm8qdT63Ef90A9HBZnUmh+eCdIc8bylJyNKLNfbiDHItVuEhA0KE/UBMMCy7fanEBA==",
"version": "0.1.0-prerelease.20210615035054",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210615035054.tgz",
"integrity": "sha512-/YGZN3QiMa41gtcyHUUUVTfhztoT7zEb0+N+FeBmtbkZfeESOtEugJ0y0ftttUY7WJRga35TTDp1UdEyo7sxAg==",
"dev": true,
"requires": {
"exports-loader": "0.6.3",
@ -20883,9 +20883,9 @@
}
},
"scratch-gui": {
"version": "0.1.0-prerelease.20210610000654",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210610000654.tgz",
"integrity": "sha512-0XqUnKAii9mJ9HGhfQE88bi8WBuRr5pG3JuuGLWL4KLI6qrMtDAcefp8GgU1PGjtRgdeAL7IDwLeaPAiQBR7BQ==",
"version": "0.1.0-prerelease.20210615041617",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210615041617.tgz",
"integrity": "sha512-HVmqbo9MLfV5tvc5owt7BT+fJKj5QRrz+c4S2vc8WyAsxMEerAYKfaNCHI89LmA5CRb42sgb0Z6YnwlsNVsaiw==",
"dev": true,
"requires": {
"arraybuffer-loader": "^1.0.6",
@ -20936,14 +20936,14 @@
"redux": "3.7.2",
"redux-throttle": "0.1.1",
"scratch-audio": "0.1.0-prerelease.20200528195344",
"scratch-blocks": "0.1.0-prerelease.20210609211941",
"scratch-l10n": "3.11.20210609031630",
"scratch-paint": "0.2.0-prerelease.20210407203313",
"scratch-blocks": "0.1.0-prerelease.20210615035054",
"scratch-l10n": "3.12.20210615031544",
"scratch-paint": "0.2.0-prerelease.20210615011117",
"scratch-render": "0.1.0-prerelease.20210325231800",
"scratch-render-fonts": "1.0.0-prerelease.20210401210003",
"scratch-storage": "1.3.5",
"scratch-svg-renderer": "0.2.0-prerelease.20210511195415",
"scratch-vm": "0.2.0-prerelease.20210601191643",
"scratch-vm": "0.2.0-prerelease.20210615010833",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
"text-encoding": "0.7.0",
@ -21199,9 +21199,9 @@
}
},
"postcss": {
"version": "7.0.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz",
"integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==",
"version": "7.0.36",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz",
"integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@ -21328,19 +21328,6 @@
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
"dev": true
},
"scratch-l10n": {
"version": "3.11.20210609031630",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210609031630.tgz",
"integrity": "sha512-gqBAjoWNYPm6KY5TlFIdkxnWQO3cjuwO89EQj9DeX16iS9B3n+l8E7NrO0uvZPSs2MTOjGXLkD0sWS8iyI18iA==",
"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.5",
"resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-1.3.5.tgz",
@ -21407,9 +21394,9 @@
}
},
"scratch-l10n": {
"version": "3.11.20210610031613",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210610031613.tgz",
"integrity": "sha512-1OKFX+E408wGP8KEImdtnHIT2QIMJd7ovqeT3i8CsOIH7+BCYw8lDewqRO5X7XjUWVQUEDi0x8jRmLm7IH/G8g==",
"version": "3.12.20210615031544",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.12.20210615031544.tgz",
"integrity": "sha512-8U2y0wu+xy29ayND5bY4odklo9D/5mVW1XQ+YrBx7rykUPHiJOzPUYHvTUQVXC0CI8khAtW98Lt3SI9m4MxBuw==",
"dev": true,
"requires": {
"@babel/cli": "^7.1.2",
@ -21420,9 +21407,9 @@
}
},
"scratch-paint": {
"version": "0.2.0-prerelease.20210407203313",
"resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-0.2.0-prerelease.20210407203313.tgz",
"integrity": "sha512-dAg+7Bh8X4PxukXnIXN1NVDELSCmPsTRh2a2taM1MGIl9zqZLTo3nxz95qZ2aC6tnVZYY/oJRjl9UAnm47Fe4g==",
"version": "0.2.0-prerelease.20210615011117",
"resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-0.2.0-prerelease.20210615011117.tgz",
"integrity": "sha512-rgJwmtKXhbfZLtsR5jurxcFLNCUkNiDHPFYJq2bjA1n0HYYfNoG/r68fe+WfBQ6jppsOqFCNmRaNDqoF1VgsTg==",
"dev": true,
"requires": {
"@scratch/paper": "0.11.20200728195508",
@ -21676,9 +21663,9 @@
"dev": true
},
"scratch-vm": {
"version": "0.2.0-prerelease.20210601191643",
"resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20210601191643.tgz",
"integrity": "sha512-SWXa176Ymo2EER+dEF5yJXGOaq7xekHcmggEJ2p+8vt3LZUlBpmUlL/U1FTY65wjaYLxQWMi7q+d+IpnO/vkEg==",
"version": "0.2.0-prerelease.20210615010833",
"resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20210615010833.tgz",
"integrity": "sha512-xft3AjqII4j/mbDNs89/CUyjB82i0r20QW/xiGRKmKwRUVSyccfHwjJ6XGcxseSUkvjRqNtl4PXcSjYVL2vN8A==",
"dev": true,
"requires": {
"@vernier/godirect": "1.5.0",

View file

@ -8,9 +8,9 @@
"test:lint": "eslint . --ext .js,.jsx,.json",
"test:lint:ci": "eslint . --ext .js,.jsx,.json --format junit -o ./test/results/lint-results.xml",
"test:integration": "npm run test:integration:jest && npm run test:smoke",
"test:integration:jest": "jest ./test/integration/*.test.js --reporters=default",
"test:integration:jest": "jest ./test/integration/*.test.js --reporters=default --runInBand",
"test:integration:remote": "npm run test:integration:jest:remote && npm run test:smoke:sauce",
"test:integration:jest:remote": "SMOKE_REMOTE=true jest ./test/integration/*.test.js --reporters=default",
"test:integration:jest:remote": "SMOKE_REMOTE=true jest ./test/integration/*.test.js --reporters=default --runInBand",
"test:smoke": "tap ./test/integration-legacy/smoke-testing/*.js --timeout=3600 --no-coverage -R classic",
"test:smoke:verbose": "tap ./test/integration-legacy/smoke-testing/*.js --timeout=3600 --no-coverage -R spec",
"test:smoke:sauce": "SMOKE_REMOTE=true tap ./test/integration-legacy/smoke-testing/*.js --timeout=60000 --no-coverage -R classic",
@ -126,8 +126,8 @@
"redux-mock-store": "^1.2.3",
"redux-thunk": "2.0.1",
"sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20210610000654",
"scratch-l10n": "3.11.20210610031613",
"scratch-gui": "0.1.0-prerelease.20210615041617",
"scratch-l10n": "3.12.20210615031544",
"selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0",
"style-loader": "0.12.3",

View file

@ -145,8 +145,8 @@ const ConferenceFooter = props => (
</div>
</FlexRow>
<LanguageChooser locale={props.intl.locale} />
<div className="conf2020-organized">
<FormattedMessage id="conference-2020.organizedBy" />
<div className="conf2021-organized">
<FormattedMessage id="conference-2021.organizedBy" />
</div>
</FooterBox>
);

View file

@ -6,9 +6,9 @@ require('./navigation.scss');
const Navigation = () => (
<NavigationBox>
<ul className="ul mod-2020">
<li className="li-left mod-logo mod-2020">
<ul className="li-left-ul mod-2020">
<ul className="ul mod-2021">
<li className="li-left mod-logo mod-2021">
<ul className="li-left-ul mod-2021">
<li>
<a
className="logo-a"

View file

@ -2,8 +2,8 @@
@import "../../../../frameless";
#navigation {
.ul.mod-2020,
.li-left-ul.mod-2020 {
.ul.mod-2021,
.li-left-ul.mod-2021 {
display: flex;
justify-content: space-between;
flex-flow: row nowrap;
@ -11,7 +11,7 @@
list-style-type: none;
}
.li-left-ul.mod-2020 {
.li-left-ul.mod-2021 {
padding-left: 0;
}
@ -57,7 +57,7 @@
@media #{$medium-and-smaller} {
height: 100px;
.ul.mod-2020 {
.ul.mod-2021 {
justify-content: center;
flex-flow: row wrap;
}

View file

@ -1,8 +1,8 @@
const PropTypes = require('prop-types');
const React = require('react');
const Navigation = require('../../../navigation/conference/2020/navigation.jsx');
const Footer = require('../../../footer/conference/2020/footer.jsx');
const Navigation = require('../../../navigation/conference/2021/navigation.jsx');
const Footer = require('../../../footer/conference/2021/footer.jsx');
require('../page.scss');

View file

@ -147,6 +147,8 @@ module.exports.selectStudioCommentsGloballyEnabled = state =>
module.exports.selectMuteStatus = state => get(state, ['session', 'session', 'permissions', 'mute_status'],
{muteExpiresAt: 0, offenses: [], showWarning: false});
module.exports.selectIsMuted = state => (module.exports.selectMuteStatus(state).muteExpiresAt || 0) * 1000 > Date.now();
module.exports.selectNewStudiosLaunched = state => get(state, ['session', 'session', 'flags', 'new_studios_launched'],
false);
module.exports.selectHasFetchedSession = state => state.session.status === module.exports.Status.FETCHED;

View file

@ -21,7 +21,8 @@ const Errors = keyMirror({
THUMBNAIL_INVALID: null,
TEXT_TOO_LONG: null,
REQUIRED_FIELD: null,
UNHANDLED: null
UNHANDLED: null,
USER_MUTED: null
});
const MAX_IMAGE_BYTES = 524288;
@ -89,9 +90,7 @@ const selectFollowingMutationError = state => state.studioMutations.mutationErro
const selectIsMutatingImage = state => state.studioMutations.isMutating.image;
const selectImageMutationError = state => state.studioMutations.mutationErrors.image;
const selectIsMutatingOpenToAll = state => state.studioMutations.isMutating.openToAll;
const selectOpenToAllMutationError = state => state.studioMutations.mutationErrors.openToAll;
const selectIsMutatingCommentsAllowed = state => state.studioMutations.isMutating.commentsAllowed;
const selectCommentsAllowedMutationError = state => state.studioMutations.mutationErrors.commentsAllowed;
// Thunks
/**
@ -104,6 +103,7 @@ const selectCommentsAllowedMutationError = state => state.studioMutations.mutati
*/
const normalizeError = (err, body, res) => {
if (err) return Errors.NETWORK;
if (res.statusCode === 403 && body.mute_status) return Errors.USER_MUTED;
if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION;
if (res.statusCode !== 200) return Errors.SERVER;
try {
@ -221,6 +221,8 @@ const mutateStudioCommentsAllowed = shouldAllow => ((dispatch, getState) => {
}, (err, body, res) => {
const error = normalizeError(err, body, res);
const wasAllowed = selectStudioCommentsAllowed(state);
// eslint-disable-next-line no-console
console.error(`Error mutating commentsAllowed: ${error}`);
dispatch(completeMutation('commentsAllowed', error ? wasAllowed : shouldAllow, error));
});
});
@ -237,6 +239,8 @@ const mutateStudioOpenToAll = shouldBeOpen => ((dispatch, getState) => {
}, (err, body, res) => {
const error = normalizeError(err, body, res);
const wasOpen = selectStudioOpenToAll(getState());
// eslint-disable-next-line no-console
console.error(`Error mutating openToAll: ${error}`);
dispatch(completeMutation('openToAll', error ? wasOpen : shouldBeOpen, error));
});
});
@ -264,7 +268,5 @@ module.exports = {
selectIsMutatingImage,
selectImageMutationError,
selectIsMutatingCommentsAllowed,
selectCommentsAllowedMutationError,
selectIsMutatingOpenToAll,
selectOpenToAllMutationError
selectIsMutatingOpenToAll
};

View file

@ -12,6 +12,9 @@ const Status = keyMirror({
ERROR: null
});
const STUDIO_MANAGER_LIMIT = 40;
const STUDIO_MANAGER_THRESHOLD = 30;
const getInitialState = () => ({
infoStatus: Status.FETCHING,
title: '',
@ -20,6 +23,7 @@ const getInitialState = () => ({
commentsAllowed: false,
image: '',
followers: 0,
managers: 0,
owner: null,
// BEWARE: classroomId is only loaded if the user is an educator
@ -98,6 +102,9 @@ const selectStudioLastUpdated = state => state.studio.updated;
const selectStudioLoadFailed = state => state.studio.infoStatus === Status.ERROR;
const selectStudioCommentCount = state => state.studio.commentCount;
const selectStudioFollowerCount = state => state.studio.followers;
const selectStudioManagerCount = state => state.studio.managers;
const selectStudioHasReachedManagerThreshold = state => state.studio.managers >= STUDIO_MANAGER_THRESHOLD;
const selectStudioHasReachedManagerLimit = state => state.studio.managers >= STUDIO_MANAGER_LIMIT;
const selectStudioProjectCount = state => state.studio.projectCount;
const selectIsFetchingInfo = state => state.studio.infoStatus === Status.FETCHING;
const selectIsFollowing = state => state.studio.following;
@ -121,6 +128,7 @@ const getInfo = () => ((dispatch, getState) => {
updated: new Date(body.history.modified),
commentCount: body.stats.comments,
followers: body.stats.followers,
managers: body.stats.managers,
projectCount: body.stats.projects,
owner: body.owner
}));
@ -169,6 +177,10 @@ module.exports = {
setInfo,
setRoles,
// Constants
STUDIO_MANAGER_LIMIT,
STUDIO_MANAGER_THRESHOLD,
// Selectors
selectStudioId,
selectStudioTitle,
@ -180,6 +192,9 @@ module.exports = {
selectStudioLoadFailed,
selectStudioCommentCount,
selectStudioFollowerCount,
selectStudioManagerCount,
selectStudioHasReachedManagerThreshold,
selectStudioHasReachedManagerLimit,
selectStudioProjectCount,
selectIsFetchingInfo,
selectIsFetchingRoles,

View file

@ -25,7 +25,7 @@
"name": "conference-index",
"pattern": "^/conference/?(\\?.*)?$",
"routeAlias": "/conference(?!/201[4-9])",
"redirect": "/conference/2020"
"redirect": "/conference/2021"
},
{
"name": "conference-index-2017",
@ -83,7 +83,13 @@
"name": "conference-index-2020",
"pattern": "^/conference/2020/?$",
"routeAlias": "/conference(?!/201[4-9])",
"view": "conference/2020/index/index",
"redirect": "/conference/2021"
},
{
"name": "conference-index-2021",
"pattern": "^/conference/2021/?$",
"routeAlias": "/conference(?!/201[4-9])",
"view": "conference/2021/index/index",
"title": "Scratch Conferences",
"viewportWidth": "device-width"
},

View file

@ -1,18 +0,0 @@
{
"conference-2020.title": "Scratch Around the World:",
"conference-2020.subtitle": "An Online Conference",
"conference-2020.dateDesc": "July 22, 2021",
"conference-2020.locationDetails": "Online",
"conference-2020.date": "When:",
"conference-2020.location": "Where:",
"conference-2020.desc1": "Join us for Scratch Around the World, an online conference for educators interested in creative learning with Scratch.",
"conference-2020.desc1a": "Although we are not able to meet in person this year, we are excited to find ways to connect and share with others in the global Scratch educator community.",
"conference-2020.desc3": "The conference will be free of charge.",
"conference-2020.register": "Go to registration page",
"conference-2020.stayDesc2": "For additional questions, contact the Scratch Conference Team at {emailLink}",
"conference-2020.organizedBy": "The Scratch Conference is organized by the Lifelong Kindergarten group at the MIT Media Lab in collaboration with the Scratch Foundation."
}

View file

@ -3,47 +3,47 @@ const FormattedMessage = require('react-intl').FormattedMessage;
const React = require('react');
const render = require('../../../../lib/render.jsx');
const Page = require('../../../../components/page/conference/2020/page.jsx');
const Page = require('../../../../components/page/conference/2021/page.jsx');
const TitleBanner = require('../../../../components/title-banner/title-banner.jsx');
require('../../../../components/forms/button.scss');
require('./index.scss');
const ConferenceSplash = () => (
<div className="index mod-2020">
<TitleBanner className="mod-conference mod-2020">
<div className="title-banner-image mod-2020" />
<h1 className="title-banner-h1 mod-2020">
<div className="index mod-2021">
<TitleBanner className="mod-conference mod-2021">
<div className="title-banner-image mod-2021" />
<h1 className="title-banner-h1 mod-2021">
<center>
<FormattedMessage id="conference-2020.title" />
<FormattedMessage id="conference-2021.title" />
<br />
<FormattedMessage id="conference-2020.subtitle" />
<FormattedMessage id="conference-2021.subtitle" />
</center>
</h1>
<h3 className="title-banner-h3 mod-2020">
<FormattedMessage id="conference-2020.dateDesc" />
<h3 className="title-banner-h3 mod-2021">
<FormattedMessage id="conference-2021.dateDesc" />
</h3>
</TitleBanner>
<div className="inner">
<section className="conf2020-panel mod-desc">
<p className="conf2020-panel-desc">
<FormattedMessage id="conference-2020.desc1" />{' '}
<strong><FormattedMessage id="conference-2020.desc1a" /></strong>
<section className="conf2021-panel mod-desc">
<p className="conf2021-panel-desc">
<FormattedMessage id="conference-2021.desc1" />{' '}
<strong><FormattedMessage id="conference-2021.desc1a" /></strong>
<br />
<br />
<FormattedMessage id="conference-2020.desc3" />
<FormattedMessage id="conference-2021.desc3" />
</p>
<table className="conf2020-panel-details">
<table className="conf2021-panel-details">
<tbody>
<tr className="conf2020-panel-row">
<td className="conf2020-panel-row-icon">
<tr className="conf2021-panel-row">
<td className="conf2021-panel-row-icon">
<img
alt="Calendar Icon"
className="conf2020-panel-row-icon-image"
className="conf2021-panel-row-icon-image"
src="/svgs/conference/index/calendar-icon-solid.svg"
/>
</td>
<td><FormattedMessage id="conference-2020.date" /></td>
<td><FormattedMessage id="conference-2021.date" /></td>
<td>
<FormattedDate
day="2-digit"
@ -53,31 +53,31 @@ const ConferenceSplash = () => (
/>
</td>
</tr>
<tr className="conf2020-panel-row">
<td className="conf2020-panel-row-icon">
<tr className="conf2021-panel-row">
<td className="conf2021-panel-row-icon">
<img
alt="Map Icon"
className="conf2020-panel-row-icon-image"
className="conf2021-panel-row-icon-image"
src="/svgs/conference/index/map-icon-solid.svg"
/>
</td>
<td><FormattedMessage id="conference-2020.location" /></td>
<td><FormattedMessage id="conference-2020.locationDetails" /></td>
<td><FormattedMessage id="conference-2021.location" /></td>
<td><FormattedMessage id="conference-2021.locationDetails" /></td>
</tr>
</tbody>
</table>
<a
className="button mod-2020-panel"
className="button mod-2021-panel"
href="http://scratch2021.eventbrite.com/"
>
<FormattedMessage id="conference-2020.register" />
<FormattedMessage id="conference-2021.register" />
</a>
</section>
<section className="conf2020-panel mod-stay">
<p className="conf2020-panel-desc">
<section className="conf2021-panel mod-stay">
<p className="conf2021-panel-desc">
<FormattedMessage
id="conference-2020.stayDesc2"
id="conference-2021.stayDesc2"
values={{
emailLink: <a href="mailto:conference@scratch.mit.edu">
conference@scratch.mit.edu
@ -86,7 +86,7 @@ const ConferenceSplash = () => (
/>
<br />
<br />
<FormattedMessage id="conference-2020.organizedBy" />
<FormattedMessage id="conference-2021.organizedBy" />
</p>
</section>
</div>

View file

@ -1,53 +1,53 @@
@import "../../../../colors";
@import "../../../../frameless";
.title-banner.mod-conference.mod-2020 {
.title-banner.mod-conference.mod-2021 {
padding-top: 0;
}
.title-banner-image.mod-2020 {
.title-banner-image.mod-2021 {
opacity: .75;
margin-bottom: 1.75rem;
background-image: url("/images/conference/index/2020/title-banner.jpg");
background-image: url("/images/conference/index/2021/title-banner.jpg");
background-position: center;
background-size: cover;
width: 100%;
height: 20rem;
}
.title-banner-h1.mod-2020 {
.title-banner-h1.mod-2021 {
line-height: 1.25em;
}
.conf2020-panel,
.title-banner-h3.mod-2020 {
.conf2021-panel,
.title-banner-h3.mod-2021 {
width: 48.75rem;
margin: auto;
}
.title-banner-h3.mod-2020 {
.title-banner-h3.mod-2021 {
margin: 2rem auto 0;
}
.title-banner-h3.mod-2020 {
.title-banner-h3.mod-2021 {
text-align: center;
color: $type-white;
}
.conf2020-panel {
.conf2021-panel {
border-bottom: 1px solid $ui-border;
}
.conf2020-panel.mod-last {
.conf2021-panel.mod-last {
border-bottom: 0;
}
.flex-row.conf2020-panel-title {
.flex-row.conf2021-panel-title {
justify-content: flex-start;
align-items: center;
}
.conf2020-panel-desc {
.conf2021-panel-desc {
margin: 2rem 0;
}
@ -60,14 +60,14 @@ td {
vertical-align: middle;
}
.conf2020-panel-row-icon-image {
.conf2021-panel-row-icon-image {
width: 1.5rem;
height: 1.5rem;
}
.mod-registration .conf2020-panel-desc {
.mod-registration .conf2021-panel-desc {
margin-bottom: 0;
}
.button.mod-2020-panel {
.button.mod-2021-panel {
display: block;
margin: 2rem auto 0;
background-color: $ui-orange;
@ -76,7 +76,7 @@ td {
text-align: center;
color: $type-white;
}
.conf2020-organized {
.conf2021-organized {
font-size: .875rem;
margin: 0 auto;
text-align: center;
@ -85,93 +85,93 @@ td {
}
@media only screen and (max-width: $mobile - 1) {
.index.mod-2020 {
.index.mod-2021 {
text-align: left;
}
.title-banner-image.mod-2020 {
.title-banner-image.mod-2021 {
height: 10rem;
}
.conf2020-panel,
.title-banner-h3.mod-2020 {
.conf2021-panel,
.title-banner-h3.mod-2021 {
width: initial;
}
.conf2020-panel {
.conf2021-panel {
margin: auto .5rem;
}
.title-banner-h3.mod-2020 {
.title-banner-h3.mod-2021 {
margin: 1rem .5rem .5rem;
font-size: 1.1rem;
}
.flex-row.conf2020-panel-title {
.flex-row.conf2021-panel-title {
flex-direction: row;
}
.conf2020-panel-title-text {
.conf2021-panel-title-text {
max-width: 14rem;
}
.conf2020-panel-row > td {
.conf2021-panel-row > td {
padding: .75rem .375rem .75rem 0;
}
}
@media only screen and (min-width: $mobile) and (max-width: $tabletPortrait - 1) {
.index.mod-2020 {
.index.mod-2021 {
text-align: left;
}
.title-banner-image.mod-2020 {
.title-banner-image.mod-2021 {
height: 10rem;
}
.conf2020-panel,
.title-banner-h3.mod-2020 {
.conf2021-panel,
.title-banner-h3.mod-2021 {
margin: auto .5rem ;
width: initial;
}
.title-banner-h3.mod-2020 {
.title-banner-h3.mod-2021 {
font-size: 1.1rem;
}
.flex-row.conf2020-panel-title {
.flex-row.conf2021-panel-title {
flex-direction: row;
}
.conf2020-panel-title-text {
.conf2021-panel-title-text {
max-width: 18.75rem;
}
.button.mod-2020-panel {
.button.mod-2021-panel {
width: 5.75rem;
}
}
@media only screen and (min-width: $tabletPortrait) and (max-width: $desktop - 1) {
.index.mod-2020 {
.index.mod-2021 {
text-align: left;
}
.title-banner-image.mod-2020 {
.title-banner-image.mod-2021 {
height: 15rem;
}
.conf2020-panel,
.title-banner-h3.mod-2020 {
.conf2021-panel,
.title-banner-h3.mod-2021 {
margin: auto;
width: 38.75rem;
}
.title-banner-h3.mod-2020 {
.title-banner-h3.mod-2021 {
font-size: 1.1rem;
}
.button.mod-2020-panel {
.button.mod-2021-panel {
width: 8.75rem;
}
}

View file

@ -0,0 +1,18 @@
{
"conference-2021.title": "Scratch Around the World:",
"conference-2021.subtitle": "An Online Conference",
"conference-2021.dateDesc": "July 22, 2021",
"conference-2021.locationDetails": "Online",
"conference-2021.date": "When:",
"conference-2021.location": "Where:",
"conference-2021.desc1": "Join us for Scratch Around the World, an online conference for educators interested in creative learning with Scratch.",
"conference-2021.desc1a": "Although we are not able to meet in person this year, we are excited to find ways to connect and share with others in the global Scratch educator community.",
"conference-2021.desc3": "The conference will be free of charge.",
"conference-2021.register": "Go to registration page",
"conference-2021.stayDesc2": "For additional questions, contact the Scratch Conference Team at {emailLink}",
"conference-2021.organizedBy": "The Scratch Conference is organized by the Lifelong Kindergarten group at the MIT Media Lab in collaboration with the Scratch Foundation."
}

View file

@ -46,5 +46,6 @@
"project.cloudVariables": "Cloud Variables",
"project.cloudDataLink": "See Data",
"project.usernameBlockAlert": "This project can detect who is using it, through the \"username\" block. To hide your identity, sign out before using the project.",
"project.inappropriateUpdate": "Hmm...the bad word detector thinks there is a problem with your text. Please change it and remember to be respectful."
"project.inappropriateUpdate": "Hmm...the bad word detector thinks there is a problem with your text. Please change it and remember to be respectful.",
"project.mutedAddToStudio": "You will be able to add to studios again {inDuration}."
}

View file

@ -160,7 +160,6 @@ $stage-width: 480px;
margin-top: $arrow-border-width;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
padding: 1rem;
max-width: 18.75rem;
min-height: 1rem;
@ -185,7 +184,6 @@ $stage-width: 480px;
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
width: $arrow-border-width;
height: $arrow-border-width;

View file

@ -8,90 +8,133 @@ const Button = require('../../components/forms/button.jsx');
const AddToStudioModal = require('./add-to-studio.jsx');
const SocialModal = require('../../components/modal/social/container.jsx');
const ReportModal = require('../../components/modal/report/modal.jsx');
const {connect} = require('react-redux');
const {selectShowProjectMuteError} = require('../../redux/studio-permissions.js');
const {useState} = require('react');
const projectShape = require('./projectshape.jsx').projectShape;
import {selectNewStudiosLaunched} from '../../redux/session.js';
import StudioMuteEditMessage from '../studio/studio-mute-edit-message.jsx';
require('./subactions.scss');
const Subactions = props => (
<FlexRow className="subactions">
<div className="share-date">
<div className="copyleft">&copy;</div>
{' '}
{/* eslint-disable react/jsx-sort-props */}
{props.shareDate ? (
<FormattedDate
value={Date.parse(props.shareDate)}
day="2-digit"
month="short"
year="numeric"
/>
) : 'Unshared'}
{/* eslint-enable react/jsx-sort-props */}
</div>
<FlexRow className="action-buttons">
{(props.canReport) &&
<React.Fragment>
<Button
className="action-button report-button"
key="report-button"
onClick={props.onReportClicked}
>
<FormattedMessage id="general.report" />
</Button>
{props.reportOpen && (
<ReportModal
isOpen
key="report-modal"
type="project"
onReport={props.onReportSubmit}
onRequestClose={props.onReportClose}
/>
)}
</React.Fragment>
}
{props.canAddToStudio &&
<React.Fragment>
<Button
className="action-button studio-button"
key="add-to-studio-button"
onClick={props.onAddToStudioClicked}
>
<FormattedMessage id="addToStudio.title" />
</Button>
{props.addToStudioOpen && (
<AddToStudioModal
isOpen
isAdmin={props.isAdmin}
key="add-to-studio-modal"
userOwnsProject={props.userOwnsProject}
onRequestClose={props.onAddToStudioClosed}
onToggleStudio={props.onToggleStudio}
/>
)}
</React.Fragment>
}
{/* only show copy link button, modal if project is shared */}
{props.isShared && props.projectInfo && props.projectInfo.id && (
<React.Fragment>
<Button
className="action-button copy-link-button"
onClick={props.onSocialClicked}
>
<FormattedMessage id="general.copyLink" />
</Button>
{props.socialOpen && (
<SocialModal
isOpen
key="social-modal"
projectId={props.projectInfo && props.projectInfo.id}
onRequestClose={props.onSocialClosed}
/>
)}
</React.Fragment>
)}
const Subactions = ({
addToStudioOpen,
canAddToStudio,
canReport,
isAdmin,
isShared,
onAddToStudioClicked,
onAddToStudioClosed,
onReportClicked,
onReportClose,
onReportSubmit,
onSocialClicked,
onSocialClosed,
onToggleStudio,
projectInfo,
reportOpen,
shareDate,
showAddToStudioMuteError,
socialOpen,
userOwnsProject
}) => {
const [showMuteMessage, setShowMuteMessage] = useState(false);
return (
<FlexRow className="subactions">
<div className="share-date">
<div className="copyleft">&copy;</div>
{' '}
{/* eslint-disable react/jsx-sort-props */}
{shareDate ? (
<FormattedDate
value={Date.parse(shareDate)}
day="2-digit"
month="short"
year="numeric"
/>
) : 'Unshared'}
{/* eslint-enable react/jsx-sort-props */}
</div>
<FlexRow className="action-buttons">
{(canReport) &&
<React.Fragment>
<Button
className="action-button report-button"
key="report-button"
onClick={onReportClicked}
>
<FormattedMessage id="general.report" />
</Button>
{reportOpen && (
<ReportModal
isOpen
key="report-modal"
type="project"
onReport={onReportSubmit}
onRequestClose={onReportClose}
/>
)}
</React.Fragment>
}
{canAddToStudio &&
<React.Fragment>
<div
style={{position: 'relative'}}
/* eslint-disable react/jsx-no-bind */
onMouseEnter={() => showAddToStudioMuteError && setShowMuteMessage(true)}
onMouseLeave={() => showAddToStudioMuteError && setShowMuteMessage(false)}
/* eslint-enable react/jsx-no-bind */
>
<Button
className="action-button studio-button"
disabled={showAddToStudioMuteError}
key="add-to-studio-button"
onClick={showMuteMessage ? null : onAddToStudioClicked}
>
<FormattedMessage id="addToStudio.title" />
</Button>
{showMuteMessage && <StudioMuteEditMessage
className="studio-button-error"
messageId="project.mutedAddToStudio"
/>}
</div>
{addToStudioOpen && (
<AddToStudioModal
isOpen
isAdmin={isAdmin}
key="add-to-studio-modal"
userOwnsProject={userOwnsProject}
onRequestClose={onAddToStudioClosed}
onToggleStudio={onToggleStudio}
/>
)}
</React.Fragment>
}
{/* only show copy link button, modal if project is shared */}
{isShared && projectInfo && projectInfo.id && (
<React.Fragment>
<Button
className="action-button copy-link-button"
onClick={onSocialClicked}
>
<FormattedMessage id="general.copyLink" />
</Button>
{socialOpen && (
<SocialModal
isOpen
key="social-modal"
projectId={projectInfo && projectInfo.id}
onRequestClose={onSocialClosed}
/>
)}
</React.Fragment>
)}
</FlexRow>
</FlexRow>
</FlexRow>
);
);
};
Subactions.propTypes = {
addToStudioOpen: PropTypes.bool,
@ -110,8 +153,13 @@ Subactions.propTypes = {
projectInfo: projectShape,
reportOpen: PropTypes.bool,
shareDate: PropTypes.string,
showAddToStudioMuteError: PropTypes.bool,
socialOpen: PropTypes.bool,
userOwnsProject: PropTypes.bool
};
module.exports = Subactions;
module.exports = connect(
state => ({
showAddToStudioMuteError: selectShowProjectMuteError(state) && selectNewStudiosLaunched(state)
})
)(Subactions);

View file

@ -109,3 +109,10 @@
}
}
}
.studio-button-error {
top: auto;
transform: none;
width: 100%;
margin-left: 0;
}

View file

@ -23,6 +23,11 @@
"studio.openToAll": "Anyone can add projects",
"studio.addProjects.noSharedYet": "You dont have shared projects that you can add to this studio yet.",
"studio.addProjects.noFavoritedYet": "You dont have favorite projects that you can add to this studio yet.",
"studio.addProjects.noRecentYet": "You dont have recently viewed projects that you can add to this studio yet.",
"studio.addProjects.noStudentsYet": "You dont have student projects that you can add to this studio yet.",
"studio.projectsEmptyCanAdd1": "Your studio is looking a little empty.",
"studio.projectsEmptyCanAdd2": "Add your first project!",
"studio.projectsEmpty1": "This studio has no projects yet.",
@ -65,9 +70,18 @@
"studio.curatorAddAndDeleteProjects": "add and delete projects",
"studio.curatorIfYouTrust": "If you trust this person and youre sure you want to give them extra permissions, click Promote.",
"studio.managerLimitReachedHeader": "This studio has reached the limit of {managerLimit} managers.",
"studio.managerLimitMessageCollaborative": "Its great to see that this studio is collaborative!",
"studio.managerLimitMessageRemoveManagers": "Before you can add another manager, you will need to remove an existing manager.",
"studio.managerCountInfo": "{numberOfManagers} of {managerLimit}",
"studio.managerThresholdInfo": "This studio has {numberOfManagers} managers. Studios can have a maximum of {managerLimit} managers.",
"studio.managerThresholdRemoveManagers": "Before you can add another manager, you will need to remove managers until there are fewer than {managerLimit}.",
"studio.remove": "Remove",
"studio.promote": "Promote",
"studio.cancel": "Cancel",
"studio.okay": "Okay",
"studio.commentsHeader": "Comments",
"studio.commentsNotAllowed": "Commenting for this studio has been turned off.",
@ -98,10 +112,11 @@
"studio.reportThanksForLettingUsKnow": "Thanks for letting us know!",
"studio.reportYourFeedback": "Your feedback will help us make Scratch better.",
"studios.mutedCurators": "You will be able to invite curators and add managers again {inDuration}.",
"studios.mutedProjects": "You will be able to add and remove projects again {inDuration}.",
"studios.mutedEdit": "You will be able to edit studios again {inDuration}.",
"studios.mutedPaused": "Your account has been paused from using studios until then.",
"studio.mutedCurators": "You will be able to invite curators and add managers again {inDuration}.",
"studio.mutedProjects": "You will be able to add and remove projects again {inDuration}.",
"studio.mutedEdit": "You will be able to edit studios again {inDuration}.",
"studio.mutedPaused": "Your account has been paused from using studios until then.",
"studio.mutedError": "Your account has been paused from using studios. Refresh for more information.",
"studio.alertProjectAdded": "\"{title}\" added to studio",
"studio.alertProjectAlreadyAdded": "That project is already in this studio",

View file

@ -3,19 +3,25 @@ import keyMirror from 'keymirror';
import api from '../../../lib/api';
import {curators, managers} from './redux-modules';
import {selectUsername} from '../../../redux/session';
import {selectStudioId, setRoles} from '../../../redux/studio';
import {selectStudioId, setRoles, setInfo} from '../../../redux/studio';
const Errors = keyMirror({
NETWORK: null,
SERVER: null,
PERMISSION: null,
DUPLICATE: null,
USER_MUTED: null,
UNKNOWN_USERNAME: null,
RATE_LIMIT: null
RATE_LIMIT: null,
MANAGER_LIMIT: null
});
const normalizeError = (err, body, res) => {
if (err) return Errors.NETWORK;
if (res.statusCode === 400 && body.message === 'too many owners') {
return Errors.MANAGER_LIMIT;
}
if (res.statusCode === 403 && body.mute_status) return Errors.USER_MUTED;
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;
@ -81,6 +87,7 @@ const removeManager = username => ((dispatch, getState) => new Promise((resolve,
if (selectUsername(state) === username) {
dispatch(setRoles({manager: false}));
}
dispatch(setInfo({managers: state.studio.managers - 1}));
return resolve();
});
}));
@ -143,6 +150,7 @@ const promoteCurator = username => ((dispatch, getState) => new Promise((resolve
const curatorItem = curatorList[index];
if (index !== -1) dispatch(curators.actions.remove(index));
dispatch(managers.actions.create(curatorItem, true));
dispatch(setInfo({managers: state.studio.managers + 1}));
return resolve();
});
}));

View file

@ -12,11 +12,13 @@ const Errors = keyMirror({
PERMISSION: null,
UNKNOWN_PROJECT: null,
RATE_LIMIT: null,
DUPLICATE: null
DUPLICATE: null,
USER_MUTED: null
});
const normalizeError = (err, body, res) => {
if (err) return Errors.NETWORK;
if (res.statusCode === 403 && body.mute_status) return Errors.USER_MUTED;
if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION;
if (res.statusCode === 404) return Errors.UNKNOWN_PROJECT;
if (res.statusCode === 409) return Errors.DUPLICATE;

View file

@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
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 './manager-limit-modal.scss';
import {STUDIO_MANAGER_LIMIT} from '../../../redux/studio.js';
const ManagerLimitModal = ({
handleClose
}) => (
<Modal
isOpen
className="manager-limit-modal"
onRequestClose={handleClose}
>
<ModalTitle
className="manager-limit-title"
/>
<div
className="manager-limit-content"
>
<img
src="/svgs/studio/manager-limit-illustration.svg"
className="manager-limit-image"
/>
<ModalInnerContent
className="manager-limit-inner"
>
<h2>
<FormattedMessage
id="studio.managerLimitReachedHeader"
values={{
managerLimit: STUDIO_MANAGER_LIMIT
}}
/>
</h2>
<p><FormattedMessage id="studio.managerLimitMessageCollaborative" /></p>
<p><FormattedMessage id="studio.managerLimitMessageRemoveManagers" /></p>
<div
className="manager-limit-button-row"
>
<button
className="button"
onClick={handleClose}
>
<FormattedMessage id="studio.okay" />
</button>
</div>
</ModalInnerContent>
</div>
</Modal>
);
ManagerLimitModal.propTypes = {
handleClose: PropTypes.func
};
export default ManagerLimitModal;

View file

@ -0,0 +1,44 @@
@import "../../../colors";
.manager-limit-modal {
width: 520px;
.manager-limit-title {
background: $ui-blue;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
padding-top: .75rem;
width: 100%;
height: 3rem;
cursor: pointer;
}
h2 {
line-height: 2rem;
font-size: 1.5rem;
margin-bottom: 1rem;
}
p {
font-size: 1rem;
}
.manager-limit-content {
display: flex;
align-items: flex-start;
}
.manager-limit-image {
margin-top: 2rem;
}
.manager-limit-inner {
padding: 2.5rem 1.5rem 1.5rem 2.5rem;
}
.manager-limit-button-row {
display: flex;
justify-content: flex-end;
}
}

View file

@ -5,6 +5,7 @@ import {FormattedMessage} from 'react-intl';
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 Alert from '../../../components/alert/alert.jsx';
import './promote-modal.scss';
@ -43,24 +44,27 @@ const PromoteModal = ({
<li><FormattedMessage id="studio.curatorAddAndDeleteProjects" /></li>
</ul>
<span><FormattedMessage id="studio.curatorIfYouTrust" /></span>
<div
className="promote-button-row"
>
<button
className="button cancel-button"
onClick={handleClose}
>
<FormattedMessage id="studio.cancel" />
</button>
<button
className="button"
onClick={handlePromote}
>
<FormattedMessage id="studio.promote" />
</button>
</div>
</ModalInnerContent>
</div>
<div className="promote-alert-and-button-row">
<Alert className="studio-alert promote-alert" />
<div
className="promote-button-row"
>
<button
className="button cancel-button"
onClick={handleClose}
>
<FormattedMessage id="studio.cancel" />
</button>
<button
className="button"
onClick={handlePromote}
>
<FormattedMessage id="studio.promote" />
</button>
</div>
</div>
</Modal>
);

View file

@ -33,9 +33,24 @@
padding: 2rem;
}
.promote-alert-and-button-row {
padding: 0 1.5rem 1.5rem 1.5rem;
}
// Override alert-wrapper positioning for this modal
.alert-wrapper {
position: unset;
}
.promote-alert {
width: 100%;
padding: 0 1rem;
}
.promote-button-row {
display: flex;
justify-content: flex-end;
padding-top: 1.5rem;
}
.button {

View file

@ -75,9 +75,12 @@
}
.studio-report-tile-image {
border-radius: 0.5rem;
max-width: 130px;
max-height: 100px;
width: 150px;
height: 98px;
border-radius: 4px;
background: white;
box-sizing: border-box;
border: 2px solid rgba(0, 0, 0, 0.15);
}
.studio-report-selected {

View file

@ -77,33 +77,61 @@ const UserProjectsModal = ({
<AlertProvider>
{error && <div>Error loading {filter}: {error}</div>}
<Alert className="studio-alert" />
<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}
{items.length > 0 ? (
<React.Fragment>
<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>
{moreToLoad &&
<div className="studio-projects-load-more">
<button
className={classNames('button', {
'mod-mutating': loading
})}
onClick={() => onLoadMore(filter)}
>
<FormattedMessage id="general.loadMore" />
</button>
</div>
}
</React.Fragment>
) :
<div className="studio-projects-empty">
<img
src="/svgs/studio/add-to-studio-empty.svg"
/>
))}
</div>
{moreToLoad &&
<div className="studio-projects-load-more">
<button
className={classNames('button', {
'mod-mutating': loading
})}
onClick={() => onLoadMore(filter)}
>
<FormattedMessage id="general.loadMore" />
</button>
<div className="studio-projects-empty-text">
{filter === Filters.SHARED &&
<FormattedMessage id="studio.addProjects.noSharedYet" />}
{filter === Filters.FAVORITED &&
<FormattedMessage id="studio.addProjects.noFavoritedYet" />}
{filter === Filters.RECENT &&
<FormattedMessage id="studio.addProjects.noRecentYet" />}
{filter === Filters.STUDENTS &&
<FormattedMessage id="studio.addProjects.noStudentsYet" />}
</div>
</div>
}
</AlertProvider>
</ModalInnerContent>
<div className="studio-projects-done-row">
<button
className="button"
onClick={onRequestClose}
>
<FormattedMessage id="general.done" />
</button>
</div>
</Modal>
);
};

View file

@ -22,19 +22,39 @@
.user-projects-modal-content {
padding: 0 30px 30px;
background: #E9F1FC;
max-height: calc(100vh - 200px);
max-height: calc(100vh - 270px);
min-height: 300px;
overflow-y: auto;
overscroll-behavior: contain;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
@media #{$intermediate-and-smaller} {
& { max-height: calc(100vh - 105px); }
& { max-height: calc(100vh - 175px); }
}
}
.studio-projects-load-more {
display: contents;
}
.studio-projects-done-row {
display: flex;
justify-content: flex-end;
padding: 6px 12px;
}
.studio-projects-empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 4rem;
}
.studio-projects-empty-text {
color: hsla(215, 100, 65, .75);
width: 325px;
text-align: center;
margin-top: 1rem;
}
}
.studio-tile-added {

View file

@ -7,13 +7,13 @@ import classNames from 'classnames';
import {selectStudioCommentsAllowed, selectIsFetchingInfo} from '../../redux/studio';
import {
mutateStudioCommentsAllowed, selectIsMutatingCommentsAllowed, selectCommentsAllowedMutationError
mutateStudioCommentsAllowed, selectIsMutatingCommentsAllowed
} from '../../redux/studio-mutations';
import ToggleSlider from '../../components/forms/toggle-slider.jsx';
const StudioCommentsAllowed = ({
commentsAllowedError, isFetching, isMutating, commentsAllowed, handleUpdate
isFetching, isMutating, commentsAllowed, handleUpdate
}) => (
<div>
{isFetching ? (
@ -33,14 +33,12 @@ const StudioCommentsAllowed = ({
})}
onChange={e => handleUpdate(e.target.checked)}
/>
{commentsAllowedError && <div>Error mutating commentsAllowed: {commentsAllowedError}</div>}
</div>
)}
</div>
);
StudioCommentsAllowed.propTypes = {
commentsAllowedError: PropTypes.string,
isFetching: PropTypes.bool,
isMutating: PropTypes.bool,
commentsAllowed: PropTypes.bool,
@ -51,8 +49,7 @@ export default connect(
state => ({
commentsAllowed: selectStudioCommentsAllowed(state),
isFetching: selectIsFetchingInfo(state),
isMutating: selectIsMutatingCommentsAllowed(state),
commentsAllowedError: selectCommentsAllowedMutationError(state)
isMutating: selectIsMutatingCommentsAllowed(state)
}),
{
handleUpdate: mutateStudioCommentsAllowed

View file

@ -64,7 +64,7 @@ const StudioCuratorInvite = ({showCuratorInvite, onSubmit}) => {
};
StudioCuratorInvite.propTypes = {
showCuratorInvite: PropTypes.func,
showCuratorInvite: PropTypes.bool,
onSubmit: PropTypes.func
};

View file

@ -16,6 +16,7 @@ const errorToMessageId = error => {
case Errors.PERMISSION: return 'studio.curatorErrors.generic';
case Errors.DUPLICATE: return 'studio.curatorErrors.alreadyCurator';
case Errors.UNKNOWN_USERNAME: return 'studio.curatorErrors.unknownUsername';
case Errors.USER_MUTED: return 'studio.mutedError';
case Errors.RATE_LIMIT: return 'studio.curatorErrors.tooFast';
default: return 'studio.curatorErrors.generic';
}

View file

@ -21,6 +21,7 @@ const errorToMessageId = error => {
case Errors.INAPPROPRIATE: return 'studio.updateErrors.inappropriate';
case Errors.TEXT_TOO_LONG: return 'studio.updateErrors.textTooLong';
case Errors.REQUIRED_FIELD: return 'studio.updateErrors.requiredField';
case Errors.USER_MUTED: return 'studio.mutedError';
default: return 'studio.updateErrors.generic';
}
};

View file

@ -21,6 +21,7 @@ const errorToMessageId = error => {
switch (error) {
case Errors.THUMBNAIL_INVALID: return 'studio.updateErrors.thumbnailInvalid';
case Errors.THUMBNAIL_TOO_LARGE: return 'studio.updateErrors.thumbnailTooLarge';
case Errors.USER_MUTED: return 'studio.mutedError';
default: return 'studio.updateErrors.generic';
}
};

View file

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from '../../components/forms/button.jsx';
const StudioInfoBox = ({showInfoBox, onClose, ...props}) => {
if (!showInfoBox) return null;
return (
<div className="studio-invitation studio-info-box"> {/* TODO move more styling into studio-info-box? */}
{props.children}
<Button
className="studio-info-close-button"
isCloseType
onClick={onClose}
/>
</div>
);
};
StudioInfoBox.propTypes = {
showInfoBox: PropTypes.bool,
onClose: PropTypes.func,
children: PropTypes.node
};
export default StudioInfoBox;

View file

@ -1,28 +1,89 @@
import React, {useEffect} from 'react';
import React, {useEffect, useState} 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 {
STUDIO_MANAGER_LIMIT,
selectStudioManagerCount,
selectStudioHasReachedManagerLimit,
selectStudioHasReachedManagerThreshold
} from '../../redux/studio.js';
import {loadManagers} from './lib/studio-member-actions';
import Debug from './debug.jsx';
import {ManagerTile} from './studio-member-tile.jsx';
import StudioInfoBox from './studio-info-box.jsx';
import AlertProvider from '../../components/alert/alert-provider.jsx';
import Alert from '../../components/alert/alert.jsx';
import {
selectCanRemoveManager, selectCanPromoteCurators
} from '../../redux/studio-permissions';
const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
const StudioManagers = ({
canPromoteCurators,
canRemoveManagers,
managerCount,
hasReachedManagerLimit,
hasReachedManagerThreshold,
items,
error,
loading,
moreToLoad,
onLoadMore
}) => {
useEffect(() => {
if (items.length === 0) onLoadMore();
}, []);
const [infoBoxDismissed, setInfoBoxDismissed] = useState(false);
const showManagerLimitInfoBox =
!infoBoxDismissed && canPromoteCurators &&
canRemoveManagers && hasReachedManagerLimit;
return (
<AlertProvider>
<div className="studio-members">
<Alert className="studio-alert" />
<div className="studio-header-container">
<StudioInfoBox
showInfoBox={showManagerLimitInfoBox}
// eslint-disable-next-line react/jsx-no-bind
onClose={() => setInfoBoxDismissed(true)}
>
<div className="manager-threshold-message">
<span className="manager-threshold-info">
<FormattedMessage
id="studio.managerThresholdInfo"
values={{
numberOfManagers: managerCount,
managerLimit: STUDIO_MANAGER_LIMIT
}}
/>
</span>
<FormattedMessage
id="studio.managerThresholdRemoveManagers"
values={{
managerLimit: STUDIO_MANAGER_LIMIT
}}
/>
</div>
</StudioInfoBox>
<div className="studio-header-container studio-managers-header">
<h2><FormattedMessage id="studio.managersHeader" /></h2>
{canPromoteCurators && canRemoveManagers && hasReachedManagerThreshold &&
<div className="studio-manager-count">
<FormattedMessage
id="studio.managerCountInfo"
values={{
numberOfManagers: managerCount,
managerLimit: STUDIO_MANAGER_LIMIT
}}
/>
</div>
}
</div>
{error && <Debug
label="Error"
@ -56,6 +117,11 @@ const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
};
StudioManagers.propTypes = {
canPromoteCurators: PropTypes.bool,
canRemoveManagers: PropTypes.bool,
managerCount: PropTypes.number,
hasReachedManagerLimit: PropTypes.bool,
hasReachedManagerThreshold: PropTypes.bool,
items: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.id,
username: PropTypes.string,
@ -72,7 +138,14 @@ StudioManagers.propTypes = {
};
export default connect(
state => managers.selector(state),
state => ({
canPromoteCurators: selectCanPromoteCurators(state),
canRemoveManagers: selectCanRemoveManager(state),
managerCount: selectStudioManagerCount(state),
hasReachedManagerLimit: selectStudioHasReachedManagerLimit(state),
hasReachedManagerThreshold: selectStudioHasReachedManagerThreshold(state),
...managers.selector(state)
}),
{
onLoadMore: loadManagers
}

View file

@ -6,15 +6,19 @@ import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import PromoteModal from './modals/promote-modal.jsx';
import ManagerLimitModal from './modals/manager-limit-modal.jsx';
import {
selectCanRemoveCurator, selectCanRemoveManager, selectCanPromoteCurators
} from '../../redux/studio-permissions';
import {
Errors,
promoteCurator,
removeCurator,
removeManager
} from './lib/studio-member-actions';
import {selectStudioHasReachedManagerLimit} from '../../redux/studio';
import {useAlertContext} from '../../components/alert/alert-context';
import OverflowMenu from '../../components/overflow-menu/overflow-menu.jsx';
@ -22,11 +26,12 @@ import removeIcon from './icons/remove-icon.svg';
import promoteIcon from './icons/curator-icon.svg';
const StudioMemberTile = ({
canRemove, canPromote, onRemove, onPromote, isCreator, // mapState props
canRemove, canPromote, onRemove, onPromote, isCreator, hasReachedManagerLimit, // mapState props
username, image // own props
}) => {
const [submitting, setSubmitting] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [managerLimitReached, setManagerLimitReached] = useState(false);
const {errorAlert, successAlert} = useAlertContext();
const userUrl = `/users/${username}`;
return (
@ -80,25 +85,35 @@ const StudioMemberTile = ({
</OverflowMenu>
}
{modalOpen &&
<PromoteModal
handleClose={() => setModalOpen(false)}
handlePromote={() => {
onPromote(username)
.then(() => {
successAlert({
id: 'studio.alertManagerPromote',
values: {name: username}
((hasReachedManagerLimit || managerLimitReached) ?
<ManagerLimitModal
handleClose={() => setModalOpen(false)}
/> :
<PromoteModal
handleClose={() => setModalOpen(false)}
handlePromote={() => {
onPromote(username)
.then(() => {
successAlert({
id: 'studio.alertManagerPromote',
values: {name: username}
});
})
.catch(error => {
if (error === Errors.MANAGER_LIMIT) {
setManagerLimitReached(true);
setModalOpen(true);
} else {
errorAlert({
id: 'studio.alertManagerPromoteError',
values: {name: username}
});
}
});
})
.catch(() => {
errorAlert({
id: 'studio.alertManagerPromoteError',
values: {name: username}
});
});
}}
username={username}
/>
}}
username={username}
/>
)
}
</div>
);
@ -111,7 +126,8 @@ StudioMemberTile.propTypes = {
onPromote: PropTypes.func,
username: PropTypes.string,
image: PropTypes.string,
isCreator: PropTypes.bool
isCreator: PropTypes.bool,
hasReachedManagerLimit: PropTypes.bool
};
const ManagerTile = connect(
@ -128,7 +144,8 @@ const ManagerTile = connect(
const CuratorTile = connect(
(state, ownProps) => ({
canRemove: selectCanRemoveCurator(state, ownProps.username),
canPromote: selectCanPromoteCurators(state)
canPromote: selectCanPromoteCurators(state),
hasReachedManagerLimit: selectStudioHasReachedManagerLimit(state)
}),
{
onRemove: removeCurator,

View file

@ -9,12 +9,15 @@ import {selectMuteStatus} from '../../redux/session';
import {formatRelativeTime} from '../../lib/format-time.js';
const StudioMuteEditMessage = ({
className,
messageId,
muteExpiresAtMs
}) => (
<ValidationMessage
className={className}
mode="info"
message={<FormattedMessage
id="studios.mutedEdit"
id={messageId}
values={{
inDuration: formatRelativeTime(muteExpiresAtMs, window._locale)
}}
@ -24,9 +27,15 @@ const StudioMuteEditMessage = ({
StudioMuteEditMessage.propTypes = {
className: PropTypes.string,
messageId: PropTypes.string,
muteExpiresAtMs: PropTypes.number
};
StudioMuteEditMessage.defaultProps = {
messageId: 'studio.mutedEdit'
};
export default connect(
state => ({
muteExpiresAtMs: (selectMuteStatus(state).muteExpiresAt * 1000 || 0)

View file

@ -7,13 +7,13 @@ import classNames from 'classnames';
import {selectStudioOpenToAll, selectIsFetchingInfo} from '../../redux/studio';
import {
mutateStudioOpenToAll, selectIsMutatingOpenToAll, selectOpenToAllMutationError
mutateStudioOpenToAll, selectIsMutatingOpenToAll
} from '../../redux/studio-mutations';
import ToggleSlider from '../../components/forms/toggle-slider.jsx';
const StudioOpenToAll = ({
openToAllError, isFetching, isMutating, openToAll, handleUpdate
isFetching, isMutating, openToAll, handleUpdate
}) => (
<div>
{isFetching ? (
@ -29,14 +29,12 @@ const StudioOpenToAll = ({
})}
onChange={e => handleUpdate(e.target.checked)}
/>
{openToAllError && <div>Error mutating openToAll: {openToAllError}</div>}
</div>
)}
</div>
);
StudioOpenToAll.propTypes = {
openToAllError: PropTypes.string,
isFetching: PropTypes.bool,
isMutating: PropTypes.bool,
openToAll: PropTypes.bool,
@ -47,8 +45,7 @@ export default connect(
state => ({
openToAll: selectStudioOpenToAll(state),
isFetching: selectIsFetchingInfo(state),
isMutating: selectIsMutatingOpenToAll(state),
openToAllError: selectOpenToAllMutationError(state)
isMutating: selectIsMutatingOpenToAll(state)
}),
{
handleUpdate: mutateStudioOpenToAll

View file

@ -18,6 +18,7 @@ const errorToMessageId = error => {
case Errors.DUPLICATE: return 'studio.projectErrors.duplicate';
case Errors.RATE_LIMIT: return 'studio.projectErrors.tooFast';
case Errors.UNKNOWN_PROJECT: return 'studio.projectErrors.checkUrl';
case Errors.USER_MUTED: return 'studio.mutedError';
default: return 'studio.projectErrors.generic';
}
};

View file

@ -38,13 +38,13 @@ const StudioProjects = ({
<p>
<div>
<FormattedMessage
id="studios.mutedProjects"
id="studio.mutedProjects"
values={{
inDuration: formatRelativeTime(muteExpiresAtMs, window._locale)
}}
/>
</div>
<div><FormattedMessage id="studios.mutedPaused" /></div>
<div><FormattedMessage id="studio.mutedPaused" /></div>
</p>
</CommentingStatus>
}

View file

@ -49,6 +49,7 @@ const StudioTabNav = ({isFetchingInfo, commentCount, projectCount}) => {
>
<NavLink
activeClassName="active"
className="nav_link"
to={base}
exact
>
@ -67,6 +68,7 @@ const StudioTabNav = ({isFetchingInfo, commentCount, projectCount}) => {
</NavLink>
<NavLink
activeClassName="active"
className="nav_link"
to={`${base}/comments`}
>
<li><img
@ -84,6 +86,7 @@ const StudioTabNav = ({isFetchingInfo, commentCount, projectCount}) => {
</NavLink>
<NavLink
activeClassName="active"
className="nav_link"
to={`${base}/curators`}
>
<li><img
@ -92,6 +95,7 @@ const StudioTabNav = ({isFetchingInfo, commentCount, projectCount}) => {
</NavLink>
<NavLink
activeClassName="active"
className="nav_link"
to={`${base}/activity`}
>
<li><img

View file

@ -17,6 +17,7 @@ const errorToMessageId = error => {
case Errors.INAPPROPRIATE: return 'studio.updateErrors.inappropriate';
case Errors.TEXT_TOO_LONG: return 'studio.updateErrors.textTooLong';
case Errors.REQUIRED_FIELD: return 'studio.updateErrors.requiredField';
case Errors.USER_MUTED: return 'studio.mutedError';
default: return 'studio.updateErrors.generic';
}
};

View file

@ -48,7 +48,6 @@ import {selectShowCuratorMuteError} from '../../redux/studio-permissions.js';
const StudioShell = ({showCuratorMuteError, muteExpiresAtMs, studioLoadFailed}) => {
const match = useRouteMatch();
return (
studioLoadFailed ?
<NotAvailable /> :
@ -68,13 +67,13 @@ const StudioShell = ({showCuratorMuteError, muteExpiresAtMs, studioLoadFailed})
<p>
<div>
<FormattedMessage
id="studios.mutedCurators"
id="studio.mutedCurators"
values={{
inDuration: formatRelativeTime(muteExpiresAtMs, window._locale)
}}
/>
</div>
<div><FormattedMessage id="studios.mutedPaused" /></div>
<div><FormattedMessage id="studio.mutedPaused" /></div>
</p>
</CommentingStatus>
}

View file

@ -113,6 +113,9 @@ $radius: 8px;
.studio-title {
font-size: 28px;
font-weight: 700;
word-wrap: break-word;
max-height: 8rem;
overflow-y: auto;
}
.studio-description {
@ -147,9 +150,11 @@ $radius: 8px;
.studio-image {
width: 300px;
height: 225px;
object-fit: cover;
height: 195px;
border-radius: 8px;
background: white;
box-sizing: border-box;
border: 2px solid rgba(0, 0, 0, 0.15);
}
.studio-follow-button {
@ -167,26 +172,65 @@ $radius: 8px;
li {
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.15);
background: white;
border: 1px solid rgba(0, 0, 0, 0.15);
color: #575e75;
padding: 0.5em 0.75em 0.5em 0.5em;
&:active {
padding: calc(0.5em + 1px) calc(0.75em + 1px) calc(0.5em + 1px) calc(0.5em + 1px);
padding: calc(0.5em) calc(0.75em) calc(0.5em) calc(0.5em);
}
img {
margin-right: 0.5em;
width: 1.5em;
filter: invert(0.55);
}
.tab-count {
font-weight: normal;
}
}
.active > li { background: $ui-blue; }
.active > li {
background: $ui-blue;
color: white;
img {
filter: invert(0);
}
}
a.nav_link:hover > li {
background: $ui-blue-25percent;
border: 1px solid $ui-blue-10percent;
}
}
.studio-projects, .studio-members {
position: relative;
}
.studio-manager-count {
margin-left: 1.5rem;
width: 4.875rem;
height: 2rem;
border: 1px solid $ui-blue;
border-radius: 1rem;
font-weight: bold;
color: $ui-blue;
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
}
.manager-threshold-message {
display: flex;
flex-direction: column;
.manager-threshold-info {
font-weight: bold;
}
}
.studio-projects-grid {
margin-top: 20px;
display: grid;
@ -425,6 +469,10 @@ $radius: 8px;
width: 100%;
}
.studio-managers-header {
justify-content: flex-start;
}
.studio-compose-container {
padding-top: 8px;
}
@ -463,10 +511,6 @@ $radius: 8px;
box-sizing: border-box;
min-height: 85px; /* So the box doesn't change height after being accepted */
display: flex;
justify-content: space-between;
align-items: center;
@media #{$intermediate-and-smaller} {
flex-direction: column;
.studio-invitation-msg {
@ -477,6 +521,10 @@ $radius: 8px;
}
.studio-info-box {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 4px;
background: $ui-blue-10percent;
border: 1px solid $ui-blue-25percent;
@ -489,6 +537,10 @@ $radius: 8px;
background: #FFF0DF;
border: 1px solid $ui-dark-orange;
}
.studio-info-close-button {
position: unset;
}
}
.studio-thumb-edit-button {

View file

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View file

@ -0,0 +1,46 @@
<svg width="108" height="100" viewBox="0 0 108 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<rect x="4" y="4" width="61.0169" height="53.6164" rx="8" fill="#E5F0FF"/>
<rect x="6" y="6" width="57.0169" height="49.6164" rx="6" stroke="#4D97FF" stroke-opacity="0.4" stroke-width="4"/>
</g>
<path d="M4 12C4 7.58172 7.58172 4 12 4H57.0169C61.4352 4 65.0169 7.58172 65.0169 12V38.5912H4V12Z" fill="#4D97FF" fill-opacity="0.4"/>
<g filter="url(#filter1_d)">
<rect x="23.4912" y="23.0249" width="61.0169" height="53.6164" rx="8" fill="#E5F0FF"/>
<rect x="25.4912" y="25.0249" width="57.0169" height="49.6164" rx="6" stroke="#4D97FF" stroke-opacity="0.4" stroke-width="4"/>
</g>
<path d="M23.4912 31.0249C23.4912 26.6066 27.0729 23.0249 31.4912 23.0249H76.5082C80.9264 23.0249 84.5082 26.6066 84.5082 31.0249V57.6161H23.4912V31.0249Z" fill="#4D97FF" fill-opacity="0.4"/>
<g filter="url(#filter2_d)">
<rect x="42.9834" y="42.0505" width="61.0169" height="53.6164" rx="8" fill="#E5F0FF"/>
<rect x="44.9834" y="44.0505" width="57.0169" height="49.6164" rx="6" stroke="#4D97FF" stroke-opacity="0.4" stroke-width="4"/>
</g>
<path d="M42.9834 50.0505C42.9834 45.6323 46.5651 42.0505 50.9834 42.0505H96.0003C100.419 42.0505 104 45.6323 104 50.0505V76.6417H42.9834V50.0505Z" fill="#4D97FF" fill-opacity="0.4"/>
<defs>
<filter id="filter0_d" x="0" y="0" width="69.0169" height="61.6164" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feMorphology radius="4" operator="dilate" in="SourceAlpha" result="effect1_dropShadow"/>
<feOffset/>
<feColorMatrix type="matrix" values="0 0 0 0 0.92549 0 0 0 0 0.956863 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter1_d" x="19.4912" y="19.0249" width="69.0169" height="61.6164" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feMorphology radius="4" operator="dilate" in="SourceAlpha" result="effect1_dropShadow"/>
<feOffset/>
<feColorMatrix type="matrix" values="0 0 0 0 0.92549 0 0 0 0 0.956863 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter2_d" x="38.9834" y="38.0505" width="69.0169" height="61.6164" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feMorphology radius="4" operator="dilate" in="SourceAlpha" result="effect1_dropShadow"/>
<feOffset/>
<feColorMatrix type="matrix" values="0 0 0 0 0.92549 0 0 0 0 0.956863 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,13 @@
<svg width="147" height="177" viewBox="0 0 147 177" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="147" height="177">
<rect width="147" height="177" fill="#C4C4C4"/>
</mask>
<g mask="url(#mask0)">
<path opacity="0.3" fill-rule="evenodd" clip-rule="evenodd" d="M33.9398 7.55682C-35.3557 20.2186 -47.9335 60.1599 -31.6109 105.026C-13.5055 154.792 26.3575 162 56.0398 162C112.261 162 147.912 115.074 144.813 75.0489C140.463 18.8722 97.1725 -8.51898 33.9398 7.55682Z" fill="#4C97FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M50.3484 85.0694C47.5164 82.1959 45.7738 78.2819 45.7738 73.9683C45.7738 65.1492 53.0579 58 62.0434 58C71.0288 58 78.3129 65.1492 78.3129 73.9683C78.3129 78.188 76.6453 82.0255 73.9215 84.8804C78.2202 87.4007 81.5395 91.7172 83.3202 98.3465C86.5106 110.224 86.6615 119 62.1493 119C35.4939 119 36.8136 112.094 39.1314 100.445C40.534 93.3955 44.8485 88.1292 50.3484 85.0694Z" fill="#4C97FF"/>
<path d="M88.5631 103.728C90.6639 104.007 93.3097 104.149 96.625 104.149C112.679 104.149 113.209 99.3602 111.12 91.9985C109.895 87.6813 107.34 84.537 104.136 82.6747C106.136 80.7799 107.379 78.1256 107.379 75.1875C107.379 69.4306 102.607 64.7637 96.7188 64.7637C90.831 64.7637 86.0581 69.4306 86.0581 75.1875C86.0581 78.1305 87.3055 80.7887 89.3115 82.6842C86.8981 84.0728 84.8312 86.1225 83.4243 88.6337C85.0253 91.094 86.2901 93.984 87.1832 97.3088C87.7501 99.4192 88.2693 101.593 88.5631 103.728Z" fill="#4C97FF"/>
<path d="M39.0426 90.2273C37.599 86.7062 35.2335 84.0812 32.3783 82.4219C34.4866 80.4247 35.7971 77.6269 35.7971 74.53C35.7971 68.4619 30.7662 63.5427 24.5602 63.5427C18.3542 63.5427 13.3232 68.4619 13.3232 74.53C13.3232 77.6321 14.6381 80.434 16.7526 82.4319C12.8602 84.6715 9.82293 88.5425 8.82444 93.2917L8.75077 93.6414C7.19305 101.03 6.34382 105.057 24.4614 105.057C28.7439 105.057 31.9784 104.734 34.3854 104.125C34.6067 102.687 34.8955 101.236 35.175 99.8319L35.2083 99.6644C35.9121 96.1269 37.2449 92.9667 39.0426 90.2273Z" fill="#4C97FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M97.1271 28.5975L97.4373 33.4696L93.592 36.5021C92.281 37.534 92.721 39.6134 94.3354 40.022L99.0767 41.2173L100.764 45.7967C101.339 47.3557 103.458 47.5748 104.35 46.1676L106.968 42.0329L111.858 41.8327C113.524 41.7635 114.392 39.8194 113.331 38.5415L110.208 34.7909L111.542 30.0879C111.996 28.4847 110.416 27.064 108.866 27.6834L104.316 29.498L100.253 26.7926C98.8684 25.8726 97.0229 26.9382 97.1271 28.5975Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M136.101 12.2555L133.655 12.9905L131.662 11.3929C130.983 10.848 129.975 11.3182 129.955 12.1881L129.9 14.7425L127.763 16.1439C127.036 16.621 127.172 17.7255 127.993 18.0124L130.405 18.855L131.077 21.3198C131.306 22.1593 132.398 22.371 132.925 21.6794L134.472 19.6457L137.023 19.7675C137.893 19.809 138.433 18.8362 137.936 18.1212L136.481 16.0206L137.385 13.6326C137.693 12.8188 136.934 12.0059 136.101 12.2555Z" fill="#4C97FF" fill-opacity="0.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -139,7 +139,7 @@ describe('www-integration footer links', () => {
await clickText('Scratch Conference');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/conference\/2020\/?$/);
expect(pathname).toMatch(/^\/conference\/2021\/?$/);
});
});