Merge branch 'develop' into force-account-rename

This commit is contained in:
Tom Lum 2023-07-11 12:49:16 -04:00 committed by GitHub
commit 132280477b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
208 changed files with 3520 additions and 27889 deletions
.circleci
README.mdpackage-lock.jsonpackage.json
src
_colors.scss
components
init.jsl10n.json
lib
main.scss
redux
routes.jsontemplate-config.jstemplate.ejs
views

View file

@ -128,6 +128,7 @@ workflows:
branches:
only:
- develop
- beta
- /^hotfix\/.*/
- /^release\/.*/
- integration-tests:
@ -141,6 +142,7 @@ workflows:
branches:
only:
- develop
- beta
- /^hotfix\/.*/
- /^release\/.*/
- build-and-deploy-production:
@ -190,5 +192,6 @@ workflows:
ignore:
- develop
- master
- beta
- /^hotfix\/.*/
- /^release\/.*/

View file

@ -165,9 +165,9 @@ the beginning of the command, before `npm start`:
| `ASSET_HOST` | `https://assets.scratch.mit.edu` | Hostname for asset requests |
| `BACKPACK_HOST` | `https://backpack.scratch.mit.edu` | Hostname for backpack requests |
| `PROJECT_HOST` | `https://projects.scratch.mit.edu` | Hostname for project requests |
| `SENTRY_DSN` | `''` | DSN for Sentry |
| `FALLBACK` | `''` | Pass-through location for old site |
| `GA_TRACKER` | `''` | Where to log Google Analytics data |
| `GTM_ID` | `''` | Google Tag Manager ID |
| `GTM_ENV_AUTH` | `''` | Google Tag Manager env and auth info |
| `NODE_ENV` | `null` | If not `production`, app acts like development |
| `PORT` | `8333` | Port for devserver (http://localhost:XXXX) |

27244
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -35,16 +35,15 @@
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/llk/scratch-www.git"
"url": "git+ssh://git@github.com/scratchfoundation/scratch-www.git"
},
"author": "Massachusetts Institute of Technology",
"license": "BSD-3-Clause",
"bugs": {
"url": "https://github.com/llk/scratch-www/issues"
"url": "https://github.com/scratchfoundation/scratch-www/issues"
},
"homepage": "https://github.com/llk/scratch-www#readme",
"homepage": "https://github.com/scratchfoundation/scratch-www#readme",
"dependencies": {
"@sentry/browser": "4.4.2",
"bunyan": "1.8.15",
"clipboard-copy": "2.0.1",
"express": "4.16.1",
@ -57,12 +56,14 @@
"react-twitter-embed": "^3.0.3",
"react-use": "^17.3.1",
"scratch-parser": "5.1.1",
"scratch-storage": "2.0.2"
"scratch-storage": "2.2.1"
},
"devDependencies": {
"@formatjs/intl-locale": "2.4.34",
"@formatjs/intl-pluralrules": "4.1.0",
"@formatjs/intl-relativetimeformat": "8.1.8",
"@formatjs/intl-datetimeformat": "6.4.3",
"@formatjs/intl-locale": "3.0.11",
"@formatjs/intl-numberformat": "8.3.3",
"@formatjs/intl-pluralrules": "5.1.8",
"@formatjs/intl-relativetimeformat": "11.1.8",
"async": "3.2.2",
"autoprefixer": "10.4.2",
"babel-cli": "6.26.0",
@ -119,7 +120,7 @@
"query-string": "5.1.1",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-intl": "2.9.0",
"react-intl": "5.25.1",
"react-modal": "3.11.1",
"react-onclickoutside": "6.7.1",
"react-plotly.js": "2.4.0",
@ -128,14 +129,15 @@
"react-slick": "0.16.0",
"react-string-replace": "0.4.1",
"react-telephone-input": "4.3.4",
"react-test-renderer": "16.14.0",
"redux": "3.5.2",
"redux-mock-store": "1.5.4",
"redux-thunk": "2.0.1",
"regenerator-runtime": "0.13.9",
"sass": "1.49.7",
"sass-loader": "10.2.1",
"scratch-gui": "1.3.9",
"scratch-l10n": "3.15.20230125032128",
"scratch-gui": "2.0.55",
"scratch-l10n": "3.15.20230711032212",
"selenium-webdriver": "4.1.0",
"slick-carousel": "1.6.0",
"style-loader": "0.12.3",

View file

@ -13,6 +13,7 @@ $ui-orange-90percent: hsla(38, 100%, 55%, .9);
$ui-dark-orange: hsla(30, 100%, 55%, 1); // ##FF8C1A Variables Primary
$ui-red: hsla(20, 100%, 55%, 1); /* #FF661A */
$ui-red-dark: hsla(20, 100%, 40%, 1); /* #CC4400 */
$ui-red-25percent: hsla(20, 100%, 55%, .25);
$ui-green-35percent: rgba(126, 225, 195, .35);
@ -27,11 +28,15 @@ $motion-blue-3: hsla(215, 60%, 50%, 1);//#3373CC
/* UI Secondary Colors */
/* 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-aqua: hsla(144, 45%, 36%, 1);
$ui-aqua-dark: darken($ui-aqua, 10%);
$ui-purple: hsla(260, 100%, 70%, 1); // #9966FF Looks Primary
$ui-purple-dark: hsla(260, 60%, 60%, 1); // #774DCB Looks Secondary
$ui-purple-dark: hsla(260, 60%, 60%, 1); // #855CD6 Looks Secondary
$ui-purple-darker: hsla(260, 46%, 54%, 1);
$ui-purple-10percent: hsla(260, 60%, 60%, .1);
$ui-purple-25percent: hsla(260, 60%, 60%, .25);
$ui-magenta: hsla(300, 53%, 60%, 1); /* #CF63CF Sounds Primary */
$ui-magenta-dark: hsla(300, 48%, 50%, 1); /* #BD42BD Sounds Tertiary */
$ui-yellow: hsla(45, 100%, 50%, 1); // #FFBF00 Events Primary
$ui-coral: hsla(350, 100%, 70%, 1); // #FF6680 More Blocks Primary
@ -59,11 +64,12 @@ $transparent-light-blue: rgba(229, 240, 254, 0);
/* Typography Colors */
$header-gray: hsla(225, 15%, 40%, 1); //#575E75
$type-gray: hsla(225, 15%, 40%, 1); //#575E75
$type-dark-gray: hsla(0, 0%, 23%, 1);
$type-gray-75percent: hsla(225, 15%, 40%, .75);
$type-gray-60percent: hsla(225, 15%, 40%, .6);
$type-white: hsla(0, 100%, 100%, 1); //#FFF
$link-blue: $ui-blue;
$link-purple: $ui-purple-dark;
/* Down Deep */
$dd-darkblue: hsla(195, 72.4%, 17.1%, 1);

View file

@ -35,7 +35,7 @@
&.has-error {
.input {
border: 1px solid $ui-orange;
border: 1px solid $ui-red-dark;
}
}

View file

@ -1,5 +1,4 @@
const classNames = require('classnames');
const FormattedRelative = require('react-intl').FormattedRelative;
const PropTypes = require('prop-types');
const React = require('react');
@ -13,21 +12,12 @@ const CommentText = props => (
className="mod-comment"
text={props.comment}
/>
{typeof props.datetimeCreated === 'undefined' ? [] : [
<p
className="comment-text-timestamp"
key="comment-text-timestamp"
>
<FormattedRelative value={new Date(props.datetimeCreated)} />
</p>
]}
</div>
);
CommentText.propTypes = {
className: PropTypes.string,
comment: PropTypes.string.isRequired,
datetimeCreated: PropTypes.string
comment: PropTypes.string.isRequired
};
module.exports = CommentText;

View file

@ -35,9 +35,3 @@
margin: 0;
overflow: hidden;
}
.comment-text-timestamp {
margin: 1rem 0 0;
color: $ui-dark-gray;
font-size: .8rem;
}

View file

@ -37,6 +37,6 @@ $navigation-height: 50px;
}
&.warning {
background-color: $ui-orange;
background-color: $ui-red-dark;
}
}

View file

@ -7,7 +7,7 @@
right: 0;
border: 1px solid $active-gray;
border-radius: 0 0 5px 5px;
background-color: $ui-blue;
background-color: $ui-purple-dark;
padding: 10px;
min-width: 9rem;
max-width: 16.25rem;
@ -16,9 +16,10 @@
font-size: .8125rem;
font-weight: normal;
&.staging {
background-color: $ui-orange;
}
// Temporary removal of staging styling for testing purposes
// &.staging {
// background-color: $ui-orange;
// }
&.open {
display: block;
@ -82,7 +83,7 @@
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-blue;
background-color: $ui-purple-dark;
width: $arrow-border-width;
height: $arrow-border-width;

View file

@ -1,6 +1,5 @@
const PropTypes = require('prop-types');
const React = require('react');
const Sentry = require('@sentry/browser');
const CrashMessageComponent = require('../crashmessage/crashmessage.jsx');
import log from '../../lib/log.js';
@ -9,28 +8,37 @@ class ErrorBoundary extends React.Component {
constructor (props) {
super(props);
this.state = {
hasError: false,
errorId: null
error: null,
errorInfo: null
};
}
/**
* Handle an error caught by this ErrorBoundary component.
* @param {Error} error - the error that was caught.
* @param {React.ErrorInfo} errorInfo - the React error info associated with the error.
*/
componentDidCatch (error, errorInfo) {
// Display fallback UI
Sentry.withScope(scope => {
scope.setTag('project', 'scratch-www');
if (this.props.componentName) {
scope.setTag('component', this.props.componentName);
}
Object.keys(errorInfo).forEach(key => {
scope.setExtra(key, errorInfo[key]);
error = error || {
stack: 'Unknown stack',
message: 'Unknown error'
};
errorInfo = errorInfo || {
componentStack: 'Unknown component stack'
};
// only remember the first error: later errors might just be side effects of that first one
if (!this.state.error) {
// store error & errorInfo for debugging
this.setState({
error,
errorInfo
});
Sentry.captureException(error);
});
this.setState({
hasError: true,
errorId: Sentry.lastEventId()
});
log.error(`Unhandled Error: ${error}, info: ${errorInfo}`);
}
// report every error in the console
const componentInfo = this.props.componentName ? ` in ${this.props.componentName}` : '';
log.error(`Unhandled Error${componentInfo}: ${error.stack}\nComponent stack: ${errorInfo.componentStack}`);
}
handleBack () {
@ -38,10 +46,9 @@ class ErrorBoundary extends React.Component {
}
render () {
if (this.state.hasError) {
if (this.state.error) {
return (
<CrashMessageComponent
eventId={this.state.errorId}
onBack={this.handleBack}
/>
);

View file

@ -39,7 +39,7 @@
.headline-icon {
height: 40px;
width: 40px;
margin: auto;
margin: auto 0;
}
.download {

View file

@ -1,15 +1,14 @@
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const FormattedMessage = require('react-intl').FormattedMessage;
const React = require('react');
const FlexRow = require('../../../flex-row/flex-row.jsx');
const FooterBox = require('../../container/footer.jsx');
const LanguageChooser = require('../../../languagechooser/languagechooser.jsx');
const {getLocale} = require('../../../../lib/locales.js');
require('../footer.scss');
const ConferenceFooter = props => (
const ConferenceFooter = () => (
<FooterBox>
<FlexRow className="scratch-links">
<div className="family">
@ -86,12 +85,8 @@ const ConferenceFooter = props => (
</div>
</div>
</FlexRow>
<LanguageChooser locale={props.intl.locale} />
<LanguageChooser locale={getLocale()} />
</FooterBox>
);
ConferenceFooter.propTypes = {
intl: intlShape
};
module.exports = injectIntl(ConferenceFooter);
module.exports = ConferenceFooter;

View file

@ -1,15 +1,14 @@
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const React = require('react');
const FlexRow = require('../../../flex-row/flex-row.jsx');
const FooterBox = require('../../container/footer.jsx');
const LanguageChooser = require('../../../languagechooser/languagechooser.jsx');
const {getLocale} = require('../../../../lib/locales.js');
require('../footer.scss');
const ConferenceFooter = props => (
const ConferenceFooter = () => (
<FooterBox>
<div className="collaborators">
<h4>Sponsors</h4>
@ -213,12 +212,8 @@ const ConferenceFooter = props => (
</div>
</div>
</FlexRow>
<LanguageChooser locale={props.intl.locale} />
<LanguageChooser locale={getLocale()} />
</FooterBox>
);
ConferenceFooter.propTypes = {
intl: intlShape
};
module.exports = injectIntl(ConferenceFooter);
module.exports = ConferenceFooter;

View file

@ -1,15 +1,14 @@
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const FormattedMessage = require('react-intl').FormattedMessage;
const React = require('react');
const FlexRow = require('../../../flex-row/flex-row.jsx');
const FooterBox = require('../../container/footer.jsx');
const LanguageChooser = require('../../../languagechooser/languagechooser.jsx');
const {getLocale} = require('../../../../lib/locales');
require('../footer.scss');
const ConferenceFooter = props => (
const ConferenceFooter = () => (
<FooterBox>
<FlexRow className="scratch-links">
<div className="family">
@ -107,12 +106,8 @@ const ConferenceFooter = props => (
</div>
</div>
</FlexRow>
<LanguageChooser locale={props.intl.locale} />
<LanguageChooser locale={getLocale()} />
</FooterBox>
);
ConferenceFooter.propTypes = {
intl: intlShape
};
module.exports = injectIntl(ConferenceFooter);
module.exports = ConferenceFooter;

View file

@ -1,13 +1,11 @@
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const React = require('react');
const PropTypes = require('prop-types');
const FlexRow = require('../../../flex-row/flex-row.jsx');
const FooterBox = require('../../container/footer.jsx');
const LanguageChooser = require('../../../languagechooser/languagechooser.jsx');
const {getLocale} = require('../../../../lib/locales.js');
require('../footer.scss');
@ -146,7 +144,7 @@ const ConferenceFooter = props => (
</div>
</div>
</FlexRow>
<LanguageChooser locale={props.intl.locale} />
<LanguageChooser locale={getLocale()} />
<div className="organized-by-message">
<FormattedMessage id={props.organizedByMsgId} />
</div>
@ -154,8 +152,7 @@ const ConferenceFooter = props => (
);
ConferenceFooter.propTypes = {
intl: intlShape,
organizedByMsgId: PropTypes.string
};
module.exports = injectIntl(ConferenceFooter);
module.exports = ConferenceFooter;

View file

@ -1,6 +1,5 @@
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const MediaQuery = require('react-responsive').default;
const connect = require('react-redux').connect;
const PropTypes = require('prop-types');
@ -10,6 +9,8 @@ const FooterBox = require('../container/footer.jsx');
const LanguageChooser = require('../../languagechooser/languagechooser.jsx');
const frameless = require('../../../lib/frameless');
const intlShape = require('../../../lib/intl-shape');
const {getLocale} = require('../../../lib/locales.js');
const getScratchWikiLink = require('../../../lib/scratch-wiki');
require('./footer.scss');
@ -25,7 +26,7 @@ const Footer = props => (
</a>
</dd>
<dd>
<a href="https://www.scratchfoundation.org/opportunities">
<a href="https://www.scratchfoundation.org/careers">
<FormattedMessage id="general.jobs" />
</a>
</dd>
@ -91,7 +92,7 @@ const Footer = props => (
</a>
</dd>
<dd>
<a href="https://www.scratchfoundation.org/opportunities">
<a href="https://www.scratchfoundation.org/careers">
<FormattedMessage id="general.jobs" />
</a>
</dd>
@ -168,6 +169,11 @@ const Footer = props => (
<FormattedMessage id="general.privacyPolicy" />
</a>
</dd>
<dd>
<a href="/cookies">
<FormattedMessage id="general.cookies" />
</a>
</dd>
<dd>
<a href="/DMCA">
<FormattedMessage id="general.dmca" />
@ -213,12 +219,12 @@ const Footer = props => (
</dl>
</div>
</MediaQuery>
<LanguageChooser locale={props.intl.locale} />
<LanguageChooser locale={getLocale()} />
</FooterBox>
);
Footer.propTypes = {
intl: intlShape.isRequired,
intl: intlShape.isRequired, // eslint-disable-line react/no-unused-prop-types
scratchWikiLink: PropTypes.string
};

View file

@ -12,17 +12,17 @@
font-size: .875rem;
&:focus {
box-shadow: 0 0 0 .25rem $ui-blue-25percent;
box-shadow: 0 0 0 .25rem $ui-purple-25percent;
outline: none;
border: 1px solid $ui-blue;
border: 1px solid $ui-purple-dark;
transition: all .5s ease, font-size 0s;
}
&.fail {
border: 1px solid $ui-orange;
border: 1px solid $ui-red-dark;
&:focus {
box-shadow: 0 0 0 .25rem $ui-orange-25percent;
box-shadow: 0 0 0 .25rem $ui-red-25percent;
outline: none;
}
}

View file

@ -2,7 +2,7 @@
.select {
.fail {
border: 1px solid $ui-orange;
border: 1px solid $ui-purple-dark;
&:focus {
box-shadow: 0 0 0 .25rem $ui-orange-25percent;

View file

@ -9,7 +9,7 @@ $pass-bg: $ui-aqua;
margin: .5em 0;
border: 0;
border-radius: .5rem;
background-color: $ui-blue;
background-color: $ui-purple-dark;
cursor: pointer;
padding: 1em 1.25em;
color: $type-white;
@ -24,7 +24,7 @@ $pass-bg: $ui-aqua;
/* DATA BUTTON STATES */
&.white {
background-color: $base-bg;
color: $ui-blue;
color: $ui-purple-dark;
}
&.pass {

View file

@ -13,12 +13,12 @@
&:focus {
transition: all .5s ease;
outline: none;
border: 2px solid $ui-blue;
box-shadow: 0 0 0 4px $ui-blue-25percent;
border: 2px solid $ui-purple-dark;
box-shadow: 0 0 0 4px $ui-purple-25percent;
}
&.fail {
border: 1px solid $ui-orange;
border: 1px solid $ui-red-dark;
}
&.pass {
@ -51,12 +51,12 @@
&:focus {
transition: all .2s ease;
outline: none;
border: 2px solid $ui-blue;
box-shadow: 0 0 0 4px $ui-blue-25percent;
border: 2px solid $ui-purple-dark;
box-shadow: 0 0 0 4px $ui-purple-25percent;
}
&.fail {
border: 1px solid $ui-orange;
border: 1px solid $ui-red-dark;
}
&::placeholder {

View file

@ -23,11 +23,11 @@ $base-bg: $ui-light-gray;
&:focus {
transition: all .5s ease;
outline: none;
border: 1px solid $ui-blue;
border: 1px solid $ui-purple-dark;
}
&.fail {
border: 1px solid $ui-orange;
border: 1px solid $ui-red-dark;
}
&.pass {

View file

@ -8,10 +8,10 @@ const ReactPhoneInput = require('react-telephone-input/lib/withStyles').default;
const Row = require('formsy-react-components').Row;
const Help = require('formsy-react-components/release/components/help').default;
const ErrorMessages = require('formsy-react-components/release/components/error-messages').default;
const intl = require('react-intl');
const defaultValidationHOC = require('./validations.jsx').defaultValidationHOC;
const inputHOC = require('./input-hoc.jsx');
const intl = require('../../lib/intl.jsx');
const validationHOCFactory = require('./validations.jsx').validationHOCFactory;
require('./row.scss');

View file

@ -15,6 +15,7 @@
&:focus {
outline: none;
border-color: $ui-purple-dark;
}
}
}

View file

@ -33,7 +33,7 @@
&:focus {
outline: none;
border: 1px solid $ui-blue;
border: 1px solid $ui-purple-dark;
}
&:-moz-focusring {

View file

@ -16,11 +16,11 @@
&:focus {
transition: all 1s ease;
outline: none;
border: 1px solid $ui-blue;
border: 1px solid $ui-purple-dark;
}
&.fail {
border: 1px solid $ui-orange;
border: 1px solid $ui-red-dark;
}
&::placeholder {

View file

@ -11,7 +11,7 @@
margin-left: $arrow-border-width;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
background-color: $ui-red-dark;
padding: 1rem;
max-width: 18.75rem;
min-height: 1rem;
@ -32,7 +32,7 @@
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
background-color: $ui-red-dark;
width: $arrow-border-width;
height: $arrow-border-width;
@ -73,18 +73,18 @@
}
.validation-error {
background-color: $ui-orange;
background-color: $ui-red-dark;
&:before {
background-color: $ui-orange;
background-color: $ui-red-dark;
}
}
.validation-info {
background-color: $ui-blue;
background-color: $ui-blue-dark;
box-shadow: 0 0 4px 2px rgba(0, 0, 0, .15);
&:before {
background-color: $ui-blue;
background-color: $ui-blue-dark;
}
}

View file

@ -1,5 +1,5 @@
const defaults = require('lodash.defaultsdeep');
const intl = require('../../lib/intl.jsx');
const intl = require('react-intl');
const omit = require('lodash.omit');
const PropTypes = require('prop-types');
const React = require('react');

View file

@ -1,13 +1,14 @@
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const bindAll = require('lodash.bindall');
const connect = require('react-redux').connect;
const PropTypes = require('prop-types');
const React = require('react');
const intlShape = require('../../lib/intl-shape');
const Button = require('../forms/button.jsx');
const Spinner = require('../spinner/spinner.jsx');
require('./helpwidget.scss');
// map Scratch locale to supported Freshdesk locale

View file

@ -28,7 +28,7 @@
box-shadow: 0 0 4px 2px rgba(0, 0, 0, .15);
padding: .75rem;
overflow: visible;
background-color: $ui-blue;
background-color: $ui-blue-dark;
color: $type-white;
line-height: 1.25rem;
text-align: left;
@ -48,7 +48,7 @@
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-blue;
background-color: $ui-blue-dark;
width: $arrow-border-width;
height: $arrow-border-width;

View file

@ -8,7 +8,7 @@
.title-banner {
&.masthead {
background-color: $ui-blue-dark;
background-color: $ui-purple-dark;
padding-bottom: .5rem;
h1 {

View file

@ -12,7 +12,7 @@
.intro-container {
min-height: 24rem;
justify-content: space-between;
background-color: $ui-blue;
background-color: $ui-purple-dark;
background-size: 624px 325px;
background-repeat: no-repeat;
background-position: right;
@ -80,7 +80,7 @@
.intro-button {
border-radius: 4px;
background-color: $ui-white;
color: $ui-blue;
color: $ui-purple-dark;
padding: .625rem .75rem;
font-size: 1rem;
margin-right: .75rem;

View file

@ -3,9 +3,10 @@ const classNames = require('classnames');
const React = require('react');
const PropTypes = require('prop-types');
import {Formik} from 'formik';
const {injectIntl, intlShape} = require('react-intl');
const {injectIntl} = require('react-intl');
const FormattedMessage = require('react-intl').FormattedMessage;
const intlShape = require('../../lib/intl-shape');
const FormikSelect = require('../../components/formik-forms/formik-select.jsx');
const JoinFlowStep = require('./join-flow-step.jsx');
const InfoButton = require('../info-button/info-button.jsx');

View file

@ -3,9 +3,10 @@ const classNames = require('classnames');
const React = require('react');
const PropTypes = require('prop-types');
import {Formik} from 'formik';
const {injectIntl, intlShape} = require('react-intl');
const {injectIntl} = require('react-intl');
const countryData = require('../../lib/country-data');
const intlShape = require('../../lib/intl-shape');
const FormikSelect = require('../../components/formik-forms/formik-select.jsx');
const JoinFlowStep = require('./join-flow-step.jsx');
const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx');

View file

@ -3,14 +3,16 @@ const classNames = require('classnames');
const React = require('react');
const PropTypes = require('prop-types');
import {Formik} from 'formik';
const {injectIntl, intlShape} = require('react-intl');
const {injectIntl} = require('react-intl');
const FormattedMessage = require('react-intl').FormattedMessage;
const intlShape = require('../../lib/intl-shape');
const validate = require('../../lib/validate');
const JoinFlowStep = require('./join-flow-step.jsx');
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
const InfoButton = require('../info-button/info-button.jsx');
const Captcha = require('../../components/captcha/captcha.jsx');
require('./join-flow-steps.scss');
class EmailStep extends React.Component {

View file

@ -3,9 +3,10 @@ const classNames = require('classnames');
const React = require('react');
const PropTypes = require('prop-types');
import {Formik} from 'formik';
const {injectIntl, intlShape} = require('react-intl');
const {injectIntl} = require('react-intl');
const FormattedMessage = require('react-intl').FormattedMessage;
const intlShape = require('../../lib/intl-shape');
const FormikRadioButton = require('../../components/formik-forms/formik-radio-button.jsx');
const JoinFlowStep = require('./join-flow-step.jsx');
const InfoButton = require('../info-button/info-button.jsx');

View file

@ -9,7 +9,7 @@
margin-bottom: .5rem;
&:focus {
box-shadow: 0 0 0 .25rem $ui-blue-25percent;
box-shadow: 0 0 0 .25rem $ui-purple-25percent;
}
}

View file

@ -3,10 +3,10 @@ const connect = require('react-redux').connect;
const defaults = require('lodash.defaultsdeep');
const PropTypes = require('prop-types');
const React = require('react');
const injectIntl = require('react-intl').injectIntl;
const api = require('../../lib/api');
const injectIntl = require('../../lib/intl.jsx').injectIntl;
const intlShape = require('../../lib/intl.jsx').intlShape;
const intlShape = require('../../lib/intl-shape');
const sessionActions = require('../../redux/session.js');
const validate = require('../../lib/validate');
@ -221,15 +221,11 @@ class JoinFlow extends React.Component {
resetState () {
this.setState(this.initialState);
}
sendAnalytics (path) {
const gaID = window.GA_ID;
if (!window.ga) {
return;
}
window.ga('send', {
hitType: 'pageview',
page: path,
tid: gaID
sendAnalytics (joinFlowStep) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'join_flow',
joinFlowStep
});
}

View file

@ -3,7 +3,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const injectIntl = require('react-intl').injectIntl;
const intl = require('../../lib/intl.jsx');
const intlShape = require('../../lib/intl-shape');
const Spinner = require('../../components/spinner/spinner.jsx');
const ModalTitle = require('../modal/base/modal-title.jsx');
@ -29,7 +29,7 @@ const NextStepButton = props => (
NextStepButton.propTypes = {
content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
intl: intl.intlShape,
intl: intlShape,
waiting: PropTypes.bool
};

View file

@ -8,11 +8,11 @@
border-bottom-left-radius: 1rem;
border-bottom-right-radius: 1rem;
height: 5.1875rem;
background-color: $ui-orange;
background-color: $ui-purple-dark;
&:hover {
transition: background-color .25s ease;
background-color: $ui-orange-90percent;
background-color: $ui-purple-darker;
}
/* match the small window setting for modal as a whole */

View file

@ -2,8 +2,9 @@ const bindAll = require('lodash.bindall');
const React = require('react');
const PropTypes = require('prop-types');
const FormattedMessage = require('react-intl').FormattedMessage;
const {injectIntl, intlShape} = require('react-intl');
const {injectIntl} = require('react-intl');
const intlShape = require('../../lib/intl-shape');
const JoinFlowStep = require('./join-flow-step.jsx');
require('./join-flow-steps.scss');

View file

@ -3,8 +3,9 @@ const classNames = require('classnames');
const React = require('react');
const PropTypes = require('prop-types');
import {Formik} from 'formik';
const {injectIntl, intlShape} = require('react-intl');
const {injectIntl} = require('react-intl');
const intlShape = require('../../lib/intl-shape');
const validate = require('../../lib/validate');
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx');

View file

@ -3,8 +3,9 @@ const React = require('react');
const PropTypes = require('prop-types');
import {Formik} from 'formik';
const FormattedMessage = require('react-intl').FormattedMessage;
const {injectIntl, intlShape} = require('react-intl');
const {injectIntl} = require('react-intl');
const intlShape = require('../../lib/intl-shape');
const JoinFlowStep = require('./join-flow-step.jsx');
require('./join-flow-steps.scss');

View file

@ -1,10 +1,10 @@
const bindAll = require('lodash.bindall');
const classNames = require('classnames');
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const PropTypes = require('prop-types');
const React = require('react');
const intlShape = require('../../lib/intl-shape');
const jar = require('../../lib/jar.js');
const languages = require('scratch-l10n').default;
const Form = require('../forms/form.jsx');

View file

@ -3,8 +3,8 @@ const connect = require('react-redux').connect;
const FormattedMessage = require('react-intl').FormattedMessage;
const PropTypes = require('prop-types');
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const intlShape = require('../../lib/intl-shape');
const navigationActions = require('../../redux/navigation.js');
const Modal = require('../modal/base/modal.jsx');

View file

@ -160,15 +160,15 @@ are not obscured by gradient overlay */
}
.studio-status-icon-unselected {
background-color: $ui-blue;
background-color: $ui-purple-dark;
}
.submit-button {
background-color: $ui-blue;
background-color: $ui-purple-dark;
}
.submit-button-waiting {
background-color: $ui-blue;
background-color: $ui-purple-dark;
}
.studio-status-icon-plus-img,

View file

@ -2,9 +2,9 @@ const PropTypes = require('prop-types');
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx');
const intlShape = require('../../../lib/intl-shape');
const Form = require('../../forms/form.jsx');
const Button = require('../../forms/button.jsx');
const Spinner = require('../../spinner/spinner.jsx');

View file

@ -2,9 +2,9 @@ const PropTypes = require('prop-types');
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx');
const intlShape = require('../../../lib/intl-shape');
const Button = require('../../forms/button.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx');

View file

@ -17,8 +17,8 @@ $medium-and-small: "screen and (max-width : #{$mobileIntermediate}-1)";
.report-modal-header {
border-radius: 1rem 1rem 0 0;
box-shadow: inset 0 -1px 0 0 $ui-coral-dark;
background-color: $ui-coral;
box-shadow: inset 0 -1px 0 0 $ui-red-dark;
background-color: $ui-red-dark;
padding-top: .75rem;
width: 100%;
height: 3rem;

View file

@ -2,9 +2,9 @@ const PropTypes = require('prop-types');
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx');
const intlShape = require('../../../lib/intl-shape');
const Button = require('../../forms/button.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx');

View file

@ -3,7 +3,6 @@ const PropTypes = require('prop-types');
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx');
const ModalInnerContent = require('../base/modal-inner-content.jsx');
const Button = require('../../forms/button.jsx');
@ -12,9 +11,11 @@ const FlexRow = require('../../flex-row/flex-row.jsx');
const MuteStep = require('./mute-step.jsx');
const FeedbackForm = require('./feedback-form.jsx');
const classNames = require('classnames');
require('./modal.scss');
const api = require('../../../lib/api');
const intlShape = require('../../../lib/intl-shape');
require('./modal.scss');
const steps = {
COMMENT_ISSUE: 0,

View file

@ -4,9 +4,9 @@ const React = require('react');
const connect = require('react-redux').connect;
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx');
const intlShape = require('../../../lib/intl-shape');
const ModalTitle = require('../base/modal-title.jsx');
const ModalInnerContent = require('../base/modal-inner-content.jsx');
const Select = require('../../forms/select.jsx');

View file

@ -110,8 +110,8 @@
&:focus {
transition: all .2s ease;
outline: none;
border: 2px solid $ui-blue;
box-shadow: 0 0 0 4px $ui-blue-25percent;
border: 2px solid $ui-purple-dark;
box-shadow: 0 0 0 4px $ui-purple-25percent;
}
&.social-textarea {

View file

@ -1,9 +1,9 @@
const PropTypes = require('prop-types');
const React = require('react');
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const classNames = require('classnames');
const intlShape = require('../../../lib/intl-shape');
const Modal = require('../base/modal.jsx');
const ModalTitle = require('../base/modal-title.jsx');
const ModalInnerContent = require('../base/modal-inner-content.jsx');

View file

@ -20,7 +20,8 @@ const TTTModal = props => (
'cardsUrl',
'guideUrl',
'thumbImage',
'modalImage'
'modalImage',
'modalImageDescription'
]
)}
>
@ -29,7 +30,7 @@ const TTTModal = props => (
<a href={props.tutorialUrl}>
<div className="ttt-img-container">
<img
alt=""
alt={props.modalImageDescription}
className="mod-ttt-img"
src={props.modalImage}
/>
@ -91,6 +92,7 @@ TTTModal.propTypes = {
description: PropTypes.string.isRequired,
guideUrl: PropTypes.string.isRequired,
modalImage: PropTypes.string.isRequired,
modalImageDescription: PropTypes.string,
thumbImage: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
tutorialUrl: PropTypes.string.isRequired

View file

@ -9,11 +9,12 @@
border-bottom: 1px solid $active-gray;
box-shadow: 0 0 3px $box-shadow-gray;
background-color: $ui-blue;
background-color: $ui-purple-dark;
&.staging {
background-color: $ui-orange;
}
// Temporary removal of staging styling for testing purposes
// &.staging {
// background-color: $ui-orange;
// }
width: 100%;

View file

@ -3,10 +3,10 @@ const classNames = require('classnames');
const connect = require('react-redux').connect;
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const PropTypes = require('prop-types');
const React = require('react');
const intlShape = require('../../../lib/intl-shape');
const messageCountActions = require('../../../redux/message-count.js');
const navigationActions = require('../../../redux/navigation.js');
const sessionActions = require('../../../redux/session.js');

View file

@ -2,17 +2,18 @@
@import "../../../frameless";
#navigation {
&.staging {
.messages {
.message-count {
display: none;
// Temporary removal of staging styling for testing purposes
// &.staging {
// .messages {
// .message-count {
// display: none;
&.show {
background-color: $ui-blue;
}
}
}
}
// &.show {
// background-color: $ui-blue;
// }
// }
// }
// }
.logo {
margin-right: 10px;

View file

@ -35,7 +35,7 @@
h4 {
display: block;
color: $link-blue;
color: $link-purple;
font-size: .85rem;
}

View file

@ -34,7 +34,7 @@
}
&.active {
background-color: $ui-blue;
background-color: $ui-purple-dark;
}
}
}

View file

@ -6,6 +6,7 @@ const Navigation = require('../../navigation/www/navigation.jsx');
const Footer = require('../../footer/www/footer.jsx');
const DonorRecognition = require('./donor-recognition.jsx');
const ErrorBoundary = require('../../errorboundary/errorboundary.jsx');
const PrivacyBanner = require('../../privacy-banner/privacy-banner.jsx');
const today = new Date();
const semi = today.getDate() === 1 && today.getMonth() === 3;
@ -25,6 +26,7 @@ const Page = ({
>
<Navigation />
</nav>
<PrivacyBanner />
<main id="view">
{children}
</main>

View file

@ -0,0 +1,107 @@
const bindAll = require('lodash.bindall');
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const connect = require('react-redux').connect;
const PropTypes = require('prop-types');
const React = require('react');
const TitleBanner = require('../title-banner/title-banner.jsx');
const Button = require('../forms/button.jsx');
const jar = require('../../lib/jar.js');
require('./privacy-banner.scss');
const PRIVACY_UPDATE_START_TIME = 1684987200000; // May 25 2023 0000 ET
const PRIVACY_UPDATE_END_TIME = 1686887999000; // Jun 15 2023 1159 ET
class PrivacyBanner extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'shouldShowBanner',
'handleCloseBanner'
]);
}
shouldShowBanner () {
const seen = jar.get('scratchpolicyseen');
return (
Date.now() >= PRIVACY_UPDATE_START_TIME &&
Date.now() < PRIVACY_UPDATE_END_TIME &&
typeof seen === 'undefined' &&
typeof this.props.user !== 'undefined'
);
}
handleCloseBanner () {
const opts = {
expires: new Date(new Date().setDate(new Date().getDate() + 21)) // expires after 3 weeks
};
this.setState({dismissedPrivacyBanner: true});
jar.set('scratchpolicyseen', true, opts);
}
render () {
const showBanner = this.shouldShowBanner();
const privacyPolicyLink = chunks => <a href="/privacy_policy">{chunks}</a>;
if (showBanner) {
return (
<aside className="privacy-aside">
<TitleBanner className="privacy-banner">
<div className="privacy-banner-container">
<img
aria-hidden="true"
alt=""
className="lightbulb-icon"
src="/images/ideas/bulb-icon.svg"
/>
<div className="privacy-banner-centered">
<p className="privacy-banner-text">
<FormattedMessage
id="privacyBanner.update"
values={{
a: privacyPolicyLink
}}
/>
</p>
</div>
<Button
isCloseType
className="privacy-close-button"
key="closeButton"
name="closeButton"
type="button"
onClick={this.handleCloseBanner}
>
<div className="action-button-text">
<FormattedMessage id="general.close" />
</div>
</Button>
</div>
</TitleBanner>
</aside>
);
}
// if we're not showing the banner, return null to not render anything
return null;
}
}
const mapStateToProps = state => ({
user: state.session && state.session.session && state.session.session.user
});
PrivacyBanner.propTypes = {
// onRequestClose: PropTypes.func
user: PropTypes.shape({
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
thumbnailUrl: PropTypes.string,
username: PropTypes.string
})
};
const ConnectedPrivacyBanner = connect(
mapStateToProps
)(PrivacyBanner);
module.exports = injectIntl(ConnectedPrivacyBanner);

View file

@ -0,0 +1,75 @@
@import "../../colors";
@import "../../frameless";
.privacy-aside {
position: sticky;
}
.privacy-banner {
display: flex;
z-index: 8;
background-color: $ui-purple-darker;
padding: 0;
overflow: hidden;
align-items: center;
justify-content: center;
margin-bottom: -50px;
margin-top: 50px;
.privacy-banner-container {
display: flex;
margin: 0.375rem auto;
align-items: center;
.lightbulb-icon {
margin: 0.6875rem;
width: 1.75rem;
height: 1.75rem;
}
.privacy-banner-centered {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
.privacy-banner-text {
text-align: left;
color: $ui-white;
font-size: 1rem;
font-weight: bold;
margin-right: 1rem;
max-width: 70vw;
}
}
.privacy-close-button {
right: 1rem;
top: auto;
}
a {
color: $ui-white;
text-decoration: underline;
}
}
@media only screen and (max-width: $mobileIntermediate) {
.privacy-banner .privacy-banner-container .privacy-banner-centered {
flex-wrap: wrap;
}
.privacy-banner .privacy-banner-container .lightbulb-icon {
padding-bottom: 2rem;
}
.privacy-banner .privacy-banner-container {
margin-left: 0;
margin-bottom: 1rem
}
.privacy-banner .donate-close-button {
top: 1rem;
}
}

View file

@ -1,11 +1,11 @@
/* eslint-disable react/no-multi-comp */
const bindAll = require('lodash.bindall');
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const PropTypes = require('prop-types');
const React = require('react');
const intl = require('react-intl');
const intl = require('../../lib/intl.jsx');
const intlShape = require('../../lib/intl-shape');
const Card = require('../../components/card/card.jsx');
const Checkbox = require('../../components/forms/checkbox.jsx');

View file

@ -1,14 +1,13 @@
/* eslint-disable react/no-multi-comp */
const bindAll = require('lodash.bindall');
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const {injectIntl, FormattedMessage} = require('react-intl');
const omit = require('lodash.omit');
const PropTypes = require('prop-types');
const React = require('react');
const api = require('../../lib/api');
const countryData = require('../../lib/country-data');
const intl = require('../../lib/intl.jsx');
const intlShape = require('../../lib/intl-shape');
const Avatar = require('../../components/avatar/avatar.jsx');
const Button = require('../../components/forms/button.jsx');
@ -174,7 +173,7 @@ class UsernameStep extends React.Component {
{this.props.title ? (
this.props.title
) : (
<intl.FormattedMessage id="registration.usernameStepTitle" />
<FormattedMessage id="registration.usernameStepTitle" />
)}
</h2>
<p className="description">
@ -182,9 +181,9 @@ class UsernameStep extends React.Component {
this.props.description
) : (
<span>
<intl.FormattedMessage id="registration.usernameStepDescription" />&nbsp;
<FormattedMessage id="registration.usernameStepDescription" />&nbsp;
<b>
<intl.FormattedMessage id="registration.usernameStepRealName" />
<FormattedMessage id="registration.usernameStepRealName" />
</b>
</span>
)}
@ -281,7 +280,7 @@ class UsernameStep extends React.Component {
/>
<GeneralError name="all" />
<NextStepButton
text={<intl.FormattedMessage id="registration.nextStep" />}
text={<FormattedMessage id="registration.nextStep" />}
waiting={this.props.waiting || this.state.waiting}
/>
</Form>
@ -298,7 +297,7 @@ class UsernameStep extends React.Component {
UsernameStep.propTypes = {
activeStep: PropTypes.number,
description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
intl: intl.intlShape,
intl: intlShape,
onNextStep: PropTypes.func,
showPassword: PropTypes.bool,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
@ -339,7 +338,7 @@ class ChoosePasswordStep extends React.Component {
{this.props.intl.formatMessage({id: 'registration.choosePasswordStepTitle'})}
</h2>
<p className="description">
<intl.FormattedMessage id="registration.choosePasswordStepDescription" />
<FormattedMessage id="registration.choosePasswordStepDescription" />
<Tooltip
tipContent={
this.props.intl.formatMessage({id: 'registration.choosePasswordStepTooltip'})
@ -384,7 +383,7 @@ class ChoosePasswordStep extends React.Component {
onChange={this.handleChangeShowPassword}
/>
<NextStepButton
text={<intl.FormattedMessage id="registration.nextStep" />}
text={<FormattedMessage id="registration.nextStep" />}
waiting={this.props.waiting || this.state.waiting}
/>
</Form>
@ -501,12 +500,12 @@ class DemographicsStep extends React.Component {
return (
<Slide className="registration-step demographics-step">
<h2>
<intl.FormattedMessage id="registration.personalStepTitle" />
<FormattedMessage id="registration.personalStepTitle" />
</h2>
<p className="description">
{this.props.description ?
this.props.description :
<intl.FormattedMessage id="registration.personalStepDescription" />
<FormattedMessage id="registration.personalStepDescription" />
}
<Tooltip
tipContent={
@ -589,7 +588,7 @@ class DemographicsStep extends React.Component {
valueLabel="I'm a robot!"
/>
<NextStepButton
text={<intl.FormattedMessage id="registration.nextStep" />}
text={<FormattedMessage id="registration.nextStep" />}
waiting={this.props.waiting}
/>
</Form>
@ -629,10 +628,10 @@ const IntlDemographicsStep = injectIntl(DemographicsStep);
const NameStep = props => (
<Slide className="registration-step name-step">
<h2>
<intl.FormattedHTMLMessage id="teacherRegistration.nameStepTitle" />
<FormattedMessage id="teacherRegistration.nameStepTitleNew" />
</h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.nameStepDescription" />
<FormattedMessage id="teacherRegistration.nameStepDescription" />
<Tooltip
tipContent={
props.intl.formatMessage({id: 'registration.nameStepTooltip'})
@ -675,7 +674,7 @@ const NameStep = props => (
}}
/>
<NextStepButton
text={<intl.FormattedMessage id="registration.nextStep" />}
text={<FormattedMessage id="registration.nextStep" />}
waiting={props.waiting}
/>
</Form>
@ -748,10 +747,10 @@ class OrganizationStep extends React.Component {
return (
<Slide className="registration-step organization-step">
<h2>
<intl.FormattedMessage id="teacherRegistration.organization" />
<FormattedMessage id="teacherRegistration.organization" />
</h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.privacyDescription" />
<FormattedMessage id="teacherRegistration.privacyDescription" />
<Tooltip
tipContent={
this.props.intl.formatMessage({id: 'registration.nameStepTooltip'})
@ -792,9 +791,9 @@ class OrganizationStep extends React.Component {
}}
/>
<div className="organization-type">
<b className="row-label"><intl.FormattedMessage id="teacherRegistration.orgType" /></b>
<b className="row-label"><FormattedMessage id="teacherRegistration.orgType" /></b>
<p className="help-text">
<intl.FormattedMessage id="teacherRegistration.checkAll" />
<FormattedMessage id="teacherRegistration.checkAll" />
</p>
<CheckboxGroup
required
@ -833,9 +832,9 @@ class OrganizationStep extends React.Component {
/>
</div>
<div className="url-input">
<b className="row-label"><intl.FormattedMessage id="general.website" /></b>
<b className="row-label"><FormattedMessage id="general.website" /></b>
<p className="help-text">
<intl.FormattedMessage id="teacherRegistration.notRequired" />
<FormattedMessage id="teacherRegistration.notRequired" />
</p>
<Input
name="organization.url"
@ -853,7 +852,7 @@ class OrganizationStep extends React.Component {
/>
</div>
<NextStepButton
text={<intl.FormattedMessage id="registration.nextStep" />}
text={<FormattedMessage id="registration.nextStep" />}
waiting={this.props.waiting}
/>
</Form>
@ -906,10 +905,10 @@ class AddressStep extends React.Component {
return (
<Slide className="registration-step address-step">
<h2>
<intl.FormattedMessage id="teacherRegistration.addressStepTitle" />
<FormattedMessage id="teacherRegistration.addressStepTitle" />
</h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.privacyDescription" />
<FormattedMessage id="teacherRegistration.privacyDescription" />
<Tooltip
tipContent={
this.props.intl.formatMessage({id: 'registration.nameStepTooltip'})
@ -988,10 +987,10 @@ class AddressStep extends React.Component {
/> : []
}
<b className="row-label">
<intl.FormattedMessage id="teacherRegistration.zipCode" />
<FormattedMessage id="teacherRegistration.zipCode" />
</b>
{this.state.countryChoice === 'us' ? [] : <p className="help-text">
<intl.FormattedMessage id="teacherRegistration.notRequired" />
<FormattedMessage id="teacherRegistration.notRequired" />
</p>}
<Input
name="address.zip"
@ -1008,7 +1007,7 @@ class AddressStep extends React.Component {
/>
<GeneralError name="all" />
<NextStepButton
text={<intl.FormattedMessage id="registration.nextStep" />}
text={<FormattedMessage id="registration.nextStep" />}
waiting={this.props.waiting || this.state.waiting}
/>
</Form>
@ -1062,10 +1061,10 @@ class UseScratchStep extends React.Component {
return (
<Slide className="registration-step usescratch-step">
<h2>
<intl.FormattedMessage id="teacherRegistration.useScratchStepTitle" />
<FormattedMessage id="teacherRegistration.useScratchStepTitle" />
</h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.useScratchStepDescription" />
<FormattedMessage id="teacherRegistration.useScratchStepDescription" />
<Tooltip
tipContent={
this.props.intl.formatMessage({id: 'registration.nameStepTooltip'})
@ -1098,7 +1097,7 @@ class UseScratchStep extends React.Component {
maxCharacters={this.props.maxCharacters}
/>
<NextStepButton
text={<intl.FormattedMessage id="registration.nextStep" />}
text={<FormattedMessage id="registration.nextStep" />}
waiting={this.props.waiting}
/>
</Form>
@ -1192,10 +1191,10 @@ class EmailStep extends React.Component {
return (
<Slide className="registration-step email-step">
<h2>
<intl.FormattedMessage id="teacherRegistration.emailStepTitle" />
<FormattedMessage id="teacherRegistration.emailStepTitle" />
</h2>
<p className="description">
<intl.FormattedMessage id="teacherRegistration.emailStepDescription" />
<FormattedMessage id="teacherRegistration.emailStepDescription" />
<Tooltip
tipContent={
this.props.intl.formatMessage({id: 'registration.nameStepTooltip'})
@ -1237,7 +1236,7 @@ class EmailStep extends React.Component {
/>
<GeneralError name="all" />
<NextStepButton
text={<intl.FormattedMessage id="registration.nextStep" />}
text={<FormattedMessage id="registration.nextStep" />}
waiting={this.props.waiting}
/>
</Form>
@ -1272,6 +1271,7 @@ EmailStep.defaultProps = {
const IntlEmailStep = injectIntl(EmailStep);
const EducatorResourcesLink = chunks => <a href="/educators#resources">{chunks}</a>;
/*
* TEACHER APPROVAL STEP
@ -1279,17 +1279,17 @@ const IntlEmailStep = injectIntl(EmailStep);
const TeacherApprovalStep = props => (
<Slide className="registration-step last-step">
<h2>
<intl.FormattedMessage id="registration.lastStepTitle" />
<FormattedMessage id="registration.lastStepTitle" />
</h2>
<p className="description">
<intl.FormattedMessage id="registration.lastStepDescription" />
<FormattedMessage id="registration.lastStepDescription" />
</p>
{props.confirmed || !props.email ?
[] : (
<Card className="confirm">
<h4><intl.FormattedMessage id="registration.confirmYourEmail" /></h4>
<h4><FormattedMessage id="registration.confirmYourEmail" /></h4>
<p>
<intl.FormattedMessage id="registration.confirmYourEmailDescription" /><br />
<FormattedMessage id="registration.confirmYourEmailDescription" /><br />
<strong>{props.email}</strong>
</p>
</Card>
@ -1297,16 +1297,19 @@ const TeacherApprovalStep = props => (
}
{props.invited ?
<Card className="wait">
<h4><intl.FormattedMessage id="registration.waitForApproval" /></h4>
<h4><FormattedMessage id="registration.waitForApproval" /></h4>
<p>
<intl.FormattedMessage id="registration.waitForApprovalDescription" />
<FormattedMessage id="registration.waitForApprovalDescription" />
</p>
</Card> : []
}
<Card className="resources">
<h4><intl.FormattedMessage id="registration.checkOutResources" /></h4>
<h4><FormattedMessage id="registration.checkOutResources" /></h4>
<p>
<intl.FormattedHTMLMessage id="registration.checkOutResourcesDescription" />
<FormattedMessage
id="registration.checkOutResourcesDescriptionHTML"
values={{a: EducatorResourcesLink}}
/>
</p>
</Card>
</Slide>

View file

@ -0,0 +1,39 @@
const React = require('react');
const {useEffect, useState} = React;
const PropTypes = require('prop-types');
const {FormattedRelativeTime} = require('react-intl');
const {selectUnit} = require('../../lib/select-unit');
const RelativeTime = ({value}) => {
const [selectedUnit, setSelectedUnit] = useState(selectUnit(value));
useEffect(() => {
// It is unlikely that users will leave this running for days. Don't
// auto-update beyond hours.
if (!['second', 'minute', 'hour'].includes(selectedUnit.unit)) return;
const timerId = setInterval(() => {
const nextSelectedUnit = selectUnit(value);
if (selectedUnit.value !== nextSelectedUnit.value ||
selectUnit.unit !== nextSelectedUnit.unit) {
setSelectedUnit(nextSelectedUnit);
}
}, 10000);
return () => clearTimeout(timerId);
}, [value, selectedUnit]);
return (
<FormattedRelativeTime
value={selectedUnit.value}
unit={selectedUnit.unit}
numeric="auto"
/>
);
};
RelativeTime.propTypes = {
value: PropTypes.instanceOf(Date)
};
module.exports = RelativeTime;

View file

@ -1,9 +1,9 @@
const classNames = require('classnames');
const FormattedRelative = require('react-intl').FormattedRelative;
const PropTypes = require('prop-types');
const React = require('react');
const FlexRow = require('../flex-row/flex-row.jsx');
const RelativeTime = require('../relative-time/relative-time.jsx');
require('./social-message.scss');
@ -24,7 +24,7 @@ const SocialMessage = props => (
</div>
</div>
<span className="social-message-date">
<FormattedRelative value={new Date(props.datetime)} />
<RelativeTime value={new Date(props.datetime)} />
</span>
</FlexRow>
</props.as>

View file

@ -41,7 +41,7 @@ a.social-messages-profile-link {
color: $type-gray;
&:hover {
color: $link-blue;
color: $link-purple;
}
}

View file

@ -29,7 +29,7 @@
.step-number {
display: inline-flex;
border-radius: 2rem;
background-color: $ui-blue;
background-color: $ui-purple-dark;
width: 2rem;
height: 2rem;
color: $ui-white;

View file

@ -20,6 +20,7 @@ const SubNavigation = props => (
'sub-nav-align-right': props.align === 'right'
}
)}
role={props.role}
>
{props.children}
</div>
@ -27,6 +28,7 @@ const SubNavigation = props => (
SubNavigation.propTypes = {
align: PropTypes.string,
role: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string
};

View file

@ -1,5 +1,5 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const {useRef} = require('react');
const React = require('react');
const SubNavigation = require('../../components/subnavigation/subnavigation.jsx');
@ -10,17 +10,94 @@ require('./tabs.scss');
* Container for a custom, horizontal list of navigation elements
* that can be displayed within a view or component.
*/
const Tabs = props => (
<div className="tab-background">
<SubNavigation className={classNames('tabs', props.className)}>
{props.children}
</SubNavigation>
</div>
);
const Tabs = ({items, activeTabName}) => {
const tabElementRefs = useRef({});
const itemsRendered = items.map(({name, onTrigger, getContent}) => {
const isActive = name === activeTabName;
let tabRef;
if (tabElementRefs.current[name]) {
tabRef = tabElementRefs.current[name];
} else {
tabRef = React.createRef();
tabElementRefs.current[name] = tabRef;
}
return (
<button
role="tab"
aria-selected={`${isActive ? 'true' : 'false'}`}
className={`${isActive ? 'active' : ''}`}
onClick={onTrigger}
tabIndex={isActive ? 0 : -1}
key={name}
ref={tabRef}
>
{getContent(isActive)}
</button>
);
});
const handleKeyDown = event => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' '].includes(event.key)) {
return;
}
event.preventDefault();
const focusedIndex = Object.values(tabElementRefs.current)
.findIndex(tabElementRef =>
document.activeElement === tabElementRef.current
);
if (event.key === 'ArrowLeft') {
let nextIndex;
if (focusedIndex === 0) {
nextIndex = Object.values(tabElementRefs.current).length - 1;
} else {
nextIndex = focusedIndex - 1;
}
Object.values(tabElementRefs.current)[nextIndex].current.focus();
} else if (event.key === 'ArrowRight') {
let nextIndex;
if (focusedIndex === Object.values(tabElementRefs.current).length - 1) {
nextIndex = 0;
} else {
nextIndex = focusedIndex + 1;
}
Object.values(tabElementRefs.current)[nextIndex].current.focus();
} else if (event.key === 'Home') {
Object.values(tabElementRefs.current)[0].current.focus();
} else if (event.key === 'End') {
const lastTab = Object.values(tabElementRefs.current).length - 1;
Object.values(tabElementRefs.current)[lastTab].current.focus();
} else if (event.key === 'Enter' || event.key === ' ') {
items[focusedIndex].onTrigger();
}
};
return (
<div
className="tab-background"
onKeyDown={handleKeyDown}// eslint-disable-line
>
<SubNavigation
role="tablist"
className="tabs"
>
{itemsRendered}
</SubNavigation>
</div>
);
};
Tabs.propTypes = {
children: PropTypes.node,
className: PropTypes.string
items: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
onTrigger: PropTypes.func.isRequired,
getContent: PropTypes.func.isRequired
})
).isRequired,
activeTabName: PropTypes.string.isRequired
};
module.exports = Tabs;

View file

@ -14,13 +14,14 @@
justify-content: center;
}
.tabs li {
.tabs button {
margin: 0;
border: 0;
border-radius: 0;
width: $cols2;
text-align: center;
color: $header-gray;
background-color: transparent;
&.active {
border-bottom: 3px solid $ui-aqua;

View file

@ -33,6 +33,6 @@
margin-left: 10px;
background-color: $ui-white;
padding: 13px 20px;
color: $ui-blue;
color: $ui-purple-dark;
}
}

View file

@ -25,7 +25,7 @@
margin-top: $arrow-border-width * 0.5;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-blue;
background-color: $ui-blue-dark;
padding: 1rem;
width: 13.75rem;
text-align: left;
@ -43,7 +43,7 @@
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-blue;
background-color: $ui-blue-dark;
width: $arrow-border-width;
height: $arrow-border-width;

View file

@ -13,7 +13,7 @@ const TTTTile = props => (
<div className="ttt-tile-tutorial">
<div className="ttt-tile-image">
<img
alt=""
alt={props.thumbImageDescription}
className="ttt-tile-image-img"
src={props.thumbImage}
/>
@ -33,6 +33,7 @@ TTTTile.propTypes = {
description: PropTypes.string,
onClick: PropTypes.func,
thumbImage: PropTypes.string.isRequired,
thumbImageDescription: PropTypes.string,
title: PropTypes.string.isRequired
};

View file

@ -82,22 +82,6 @@
font-size: .875rem;
}
.ttt-tile-guides {
margin: auto;
border-top: 1px dashed $ui-border;
border-radius: 0 0 1rem 1rem;
cursor: pointer;
padding: 1.25rem 0;
color: $link-blue;
font-size: .75rem;
font-weight: 500;
&:hover {
background-color: $ui-blue-10percent;
}
}
.ttt-tile-open-modal {
display: inline-block;
padding: 0 .25rem;

View file

@ -1,13 +1,12 @@
import 'regenerator-runtime/runtime'; // Needed for async/await
const jar = require('./lib/jar');
import intlPolyfill from './lib/intl-polyfill';
/**
* -----------------------------------------------------------------------------
* L10N
* -----------------------------------------------------------------------------
*/
(async () => {
(() => {
/*
* Bind locale code from cookie if available. Uses navigator language API as a fallback.
*
@ -37,7 +36,6 @@ import intlPolyfill from './lib/intl-polyfill';
window._locale = updateLocale();
document.documentElement.lang = window._locale;
await intlPolyfill(window._locale);
})();
/**

View file

@ -17,6 +17,7 @@
"general.contactUs": "Contact Us",
"general.getHelp": "Get Help",
"general.contact": "Contact",
"general.cookies": "Cookies",
"general.done": "Done",
"general.downloadPDF": "Download PDF",
"general.emailUs": "Email Us",
@ -74,6 +75,8 @@
"general.download": "Download",
"general.password": "Password",
"general.press": "Press",
"general.projectsSelected": "Projects Tab Selected",
"general.projectsNotS": "Projects",
"general.privacyPolicy": "Privacy Policy",
"general.projects": "Projects",
"general.profile": "Profile",
@ -91,6 +94,8 @@
"general.startOver": "Start over",
"general.statistics": "Statistics",
"general.studios": "Studios",
"general.studiosSelected": "Studios Tab Selected",
"general.studiosNotS": "Studios",
"general.support": "Resources",
"general.ideas": "Ideas",
"general.tipsWindow": "Tips Window",
@ -111,13 +116,21 @@
"general.seeAllComments": "See all comments",
"general.all": "All",
"general.allSelected": "All Selected",
"general.animations": "Animations",
"general.animationsSelected": "Animations Selected",
"general.art": "Art",
"general.artSelected": "Art Selected",
"general.games": "Games",
"general.gamesSelected": "Games Selected",
"general.music": "Music",
"general.musicSelected": "Music Selected",
"general.results": "Results",
"general.resultsSelected": "Results Selected",
"general.stories": "Stories",
"general.storiesSelected": "Stories Selected",
"general.tutorials": "Tutorials",
"general.tutorialsSelected": "Tutorials Selected",
"general.teacherAccounts": "Teacher Accounts",
@ -166,6 +179,7 @@
"registration.cantCreateAccount": "Scratch could not create your account.",
"registration.checkOutResources": "Get Started with Resources",
"registration.checkOutResourcesDescription": "Explore materials for educators and facilitators written by the Scratch Team, including <a href='/educators#resources'>tips, tutorials, and guides</a>.",
"registration.checkOutResourcesDescriptionHTML": "Explore materials for educators and facilitators written by the Scratch Team, including <a>tips, tutorials, and guides</a>.",
"registration.choosePasswordStepDescription": "Type in a new password for your account. You will use this password the next time you log into Scratch.",
"registration.choosePasswordStepTitle": "Create a password",
"registration.choosePasswordStepTooltip": "Don't use your name or anything that's easy for someone else to guess.",
@ -439,7 +453,8 @@
"bluetooth.enableLocationServicesTitle": "Make sure you have location services enabled on Chromebooks or Android tablets",
"bluetooth.enableLocationServicesText": "Bluetooth can be used to provide location data to the app. In addition to granting the Scratch App permission to access location, location must be enabled in your general device settings. Search for 'Location' in your settings, and make sure it is on. On Chromebooks search for 'Location' in the Google Play Store Android preferences.",
"privacyBanner.update": "The Scratch privacy policy has been updated, effective May 25, 2023. You can see the new policy <a>here</a>.",
"renameAccount.accountBlocked": "Account Blocked",
"renameAccount.toRecover": "To recover a access to your account, change your username.",
"renameAccount.yourScratchAccount": "Your scratch account has been temporarily blocked because your username appears to contain personal information.",

View file

@ -1,9 +1,9 @@
// this file should only be `required` in the format-time
// when Intl.RelativeTimeFormat is not available (Safari < 14), but
// we're not currently able to do the code splitting in www, and it
// is always included. To reduce the amount of data that's loaded limit
// the number of languages loaded to just the top few that are still using
// safari <14. These seven account for most uses.
// This file polyfills the required Intl objects and locale data.
// The react-intl library uses PluralRules, RelativeTimeFormat, NumberFormat,
// and DateTimeFormat. Even if browsers support these objects, it is possible
// that the browser does not support the specific locale.
// There are a small number of Scratch locales that do not have polyfill locale
// data available. See /src/lib/locales.js for how they are handled.
// relativetimeformat depends on locale which also needs to be polyfilled in
// safari <14
// The plural rules is required for safari 12.
@ -11,6 +11,8 @@ import 'regenerator-runtime/runtime'; // Needed for async/await
import {shouldPolyfill as shouldPolyfillLocale} from '@formatjs/intl-locale/should-polyfill';
import {shouldPolyfill as shouldPolyfillRelativeTimeFormat} from '@formatjs/intl-relativetimeformat/should-polyfill';
import {shouldPolyfill as shouldPolyfillPluralRules} from '@formatjs/intl-pluralrules/should-polyfill';
import {shouldPolyfill as shouldPolyfillNumberFormat} from '@formatjs/intl-numberformat/should-polyfill';
import {shouldPolyfill as shouldPolyfillDateTimeFormat} from '@formatjs/intl-datetimeformat/should-polyfill';
/**
* polyfill all the parts needed from intl
* @param {string} locale currently selected locale
@ -19,293 +21,458 @@ import {shouldPolyfill as shouldPolyfillPluralRules} from '@formatjs/intl-plural
const intlPolyfill = async function (locale) {
if (!(shouldPolyfillLocale() ||
shouldPolyfillPluralRules(locale) ||
shouldPolyfillRelativeTimeFormat(locale))) {
shouldPolyfillRelativeTimeFormat(locale) ||
shouldPolyfillNumberFormat(locale) ||
shouldPolyfillDateTimeFormat(locale))) {
return;
}
if (shouldPolyfillRelativeTimeFormat(locale)) {
await import('@formatjs/intl-relativetimeformat/polyfill');
if (shouldPolyfillLocale()) {
await import('@formatjs/intl-locale/polyfill-force');
}
if (shouldPolyfillPluralRules(locale)) {
await import('@formatjs/intl-pluralrules/polyfill');
await import('@formatjs/intl-pluralrules/polyfill-force');
}
if (shouldPolyfillLocale(locale)) {
await import('@formatjs/intl-locale/polyfill');
if (shouldPolyfillRelativeTimeFormat(locale)) {
await import('@formatjs/intl-relativetimeformat/polyfill-force');
}
if (shouldPolyfillNumberFormat(locale)) {
await import('@formatjs/intl-numberformat/polyfill-force');
}
if (shouldPolyfillDateTimeFormat(locale)) {
await import('@formatjs/intl-datetimeformat/polyfill-force');
}
switch (locale.toLowerCase().split('-')[0]) {
case 'af':
await import('@formatjs/intl-relativetimeformat/locale-data/af');
await import('@formatjs/intl-pluralrules/locale-data/af');
await import('@formatjs/intl-relativetimeformat/locale-data/af');
await import('@formatjs/intl-numberformat/locale-data/af');
await import('@formatjs/intl-datetimeformat/locale-data/af');
break;
case 'ar':
await import('@formatjs/intl-relativetimeformat/locale-data/ar');
await import('@formatjs/intl-pluralrules/locale-data/ar');
await import('@formatjs/intl-relativetimeformat/locale-data/ar');
await import('@formatjs/intl-numberformat/locale-data/ar');
await import('@formatjs/intl-datetimeformat/locale-data/ar');
break;
case 'am':
await import('@formatjs/intl-relativetimeformat/locale-data/am');
await import('@formatjs/intl-pluralrules/locale-data/am');
await import('@formatjs/intl-relativetimeformat/locale-data/am');
await import('@formatjs/intl-numberformat/locale-data/am');
await import('@formatjs/intl-datetimeformat/locale-data/am');
break;
case 'an':
await import('@formatjs/intl-relativetimeformat/locale-data/en');
await import('@formatjs/intl-pluralrules/locale-data/an');
case 'ast':
await import('@formatjs/intl-pluralrules/locale-data/ast');
await import('@formatjs/intl-relativetimeformat/locale-data/ast');
await import('@formatjs/intl-numberformat/locale-data/ast');
await import('@formatjs/intl-datetimeformat/locale-data/ast');
break;
case 'az':
await import('@formatjs/intl-relativetimeformat/locale-data/az');
await import('@formatjs/intl-pluralrules/locale-data/az');
await import('@formatjs/intl-relativetimeformat/locale-data/az');
await import('@formatjs/intl-numberformat/locale-data/az');
await import('@formatjs/intl-datetimeformat/locale-data/az');
break;
case 'id':
await import('@formatjs/intl-relativetimeformat/locale-data/id');
await import('@formatjs/intl-pluralrules/locale-data/id');
await import('@formatjs/intl-relativetimeformat/locale-data/id');
await import('@formatjs/intl-numberformat/locale-data/id');
await import('@formatjs/intl-datetimeformat/locale-data/id');
break;
case 'bn':
await import('@formatjs/intl-relativetimeformat/locale-data/bn');
await import('@formatjs/intl-pluralrules/locale-data/bn');
await import('@formatjs/intl-relativetimeformat/locale-data/bn');
await import('@formatjs/intl-numberformat/locale-data/bn');
await import('@formatjs/intl-datetimeformat/locale-data/bn');
break;
case 'be':
await import('@formatjs/intl-relativetimeformat/locale-data/be');
await import('@formatjs/intl-pluralrules/locale-data/be');
await import('@formatjs/intl-relativetimeformat/locale-data/be');
await import('@formatjs/intl-numberformat/locale-data/be');
await import('@formatjs/intl-datetimeformat/locale-data/be');
break;
case 'bg':
await import('@formatjs/intl-relativetimeformat/locale-data/bg');
await import('@formatjs/intl-pluralrules/locale-data/bg');
await import('@formatjs/intl-relativetimeformat/locale-data/bg');
await import('@formatjs/intl-numberformat/locale-data/bg');
await import('@formatjs/intl-datetimeformat/locale-data/bg');
break;
case 'ca':
await import('@formatjs/intl-relativetimeformat/locale-data/ca');
await import('@formatjs/intl-pluralrules/locale-data/ca');
await import('@formatjs/intl-relativetimeformat/locale-data/ca');
await import('@formatjs/intl-numberformat/locale-data/ca');
await import('@formatjs/intl-datetimeformat/locale-data/ca');
break;
case 'cs':
await import('@formatjs/intl-relativetimeformat/locale-data/cs');
await import('@formatjs/intl-pluralrules/locale-data/cs');
await import('@formatjs/intl-relativetimeformat/locale-data/cs');
await import('@formatjs/intl-numberformat/locale-data/cs');
await import('@formatjs/intl-datetimeformat/locale-data/cs');
break;
case 'cy':
await import('@formatjs/intl-relativetimeformat/locale-data/cy');
await import('@formatjs/intl-pluralrules/locale-data/cy');
await import('@formatjs/intl-relativetimeformat/locale-data/cy');
await import('@formatjs/intl-numberformat/locale-data/cy');
await import('@formatjs/intl-datetimeformat/locale-data/cy');
break;
case 'da':
await import('@formatjs/intl-relativetimeformat/locale-data/da');
await import('@formatjs/intl-pluralrules/locale-data/da');
await import('@formatjs/intl-relativetimeformat/locale-data/da');
await import('@formatjs/intl-numberformat/locale-data/da');
await import('@formatjs/intl-datetimeformat/locale-data/da');
break;
case 'de':
await import('@formatjs/intl-relativetimeformat/locale-data/de');
await import('@formatjs/intl-pluralrules/locale-data/de');
await import('@formatjs/intl-relativetimeformat/locale-data/de');
await import('@formatjs/intl-numberformat/locale-data/de');
await import('@formatjs/intl-datetimeformat/locale-data/de');
break;
case 'et':
await import('@formatjs/intl-relativetimeformat/locale-data/et');
await import('@formatjs/intl-pluralrules/locale-data/et');
await import('@formatjs/intl-relativetimeformat/locale-data/et');
await import('@formatjs/intl-numberformat/locale-data/et');
await import('@formatjs/intl-datetimeformat/locale-data/et');
break;
case 'el':
await import('@formatjs/intl-relativetimeformat/locale-data/el');
await import('@formatjs/intl-pluralrules/locale-data/el');
await import('@formatjs/intl-relativetimeformat/locale-data/el');
await import('@formatjs/intl-numberformat/locale-data/el');
await import('@formatjs/intl-datetimeformat/locale-data/el');
break;
case 'en':
await import('@formatjs/intl-relativetimeformat/locale-data/en');
await import('@formatjs/intl-pluralrules/locale-data/en');
await import('@formatjs/intl-relativetimeformat/locale-data/en');
await import('@formatjs/intl-numberformat/locale-data/en');
await import('@formatjs/intl-datetimeformat/locale-data/en');
break;
case 'es':
case 'rap':
case 'qu':
await import('@formatjs/intl-relativetimeformat/locale-data/es');
await import('@formatjs/intl-pluralrules/locale-data/es');
await import('@formatjs/intl-relativetimeformat/locale-data/es');
await import('@formatjs/intl-numberformat/locale-data/es');
await import('@formatjs/intl-datetimeformat/locale-data/es');
break;
case 'eo':
await import('@formatjs/intl-pluralrules/locale-data/eo');
await import('@formatjs/intl-relativetimeformat/locale-data/eo');
await import('@formatjs/intl-numberformat/locale-data/eo');
await import('@formatjs/intl-datetimeformat/locale-data/eo');
break;
case 'eu':
await import('@formatjs/intl-relativetimeformat/locale-data/eu');
await import('@formatjs/intl-pluralrules/locale-data/eu');
await import('@formatjs/intl-relativetimeformat/locale-data/eu');
await import('@formatjs/intl-numberformat/locale-data/eu');
await import('@formatjs/intl-datetimeformat/locale-data/eu');
break;
case 'fa':
await import('@formatjs/intl-relativetimeformat/locale-data/fa');
await import('@formatjs/intl-pluralrules/locale-data/fa');
await import('@formatjs/intl-relativetimeformat/locale-data/fa');
await import('@formatjs/intl-numberformat/locale-data/fa');
await import('@formatjs/intl-datetimeformat/locale-data/fa');
break;
case 'fil':
await import('@formatjs/intl-pluralrules/locale-data/fil');
await import('@formatjs/intl-relativetimeformat/locale-data/fil');
await import('@formatjs/intl-numberformat/locale-data/fil');
await import('@formatjs/intl-datetimeformat/locale-data/fil');
break;
case 'fr':
case 'ht':
await import('@formatjs/intl-relativetimeformat/locale-data/fr');
await import('@formatjs/intl-pluralrules/locale-data/fr');
await import('@formatjs/intl-relativetimeformat/locale-data/fr');
await import('@formatjs/intl-numberformat/locale-data/fr');
await import('@formatjs/intl-datetimeformat/locale-data/fr');
break;
case 'fy':
await import('@formatjs/intl-relativetimeformat/locale-data/fy');
await import('@formatjs/intl-pluralrules/locale-data/fy');
await import('@formatjs/intl-relativetimeformat/locale-data/fy');
await import('@formatjs/intl-numberformat/locale-data/fy');
await import('@formatjs/intl-datetimeformat/locale-data/fy');
break;
case 'ga':
await import('@formatjs/intl-relativetimeformat/locale-data/ga');
await import('@formatjs/intl-pluralrules/locale-data/ga');
await import('@formatjs/intl-relativetimeformat/locale-data/ga');
await import('@formatjs/intl-numberformat/locale-data/ga');
await import('@formatjs/intl-datetimeformat/locale-data/ga');
break;
case 'gd':
await import('@formatjs/intl-relativetimeformat/locale-data/gd');
await import('@formatjs/intl-pluralrules/locale-data/gd');
await import('@formatjs/intl-relativetimeformat/locale-data/gd');
await import('@formatjs/intl-numberformat/locale-data/gd');
await import('@formatjs/intl-datetimeformat/locale-data/gd');
break;
case 'gl':
await import('@formatjs/intl-relativetimeformat/locale-data/gl');
await import('@formatjs/intl-pluralrules/locale-data/gl');
await import('@formatjs/intl-relativetimeformat/locale-data/gl');
await import('@formatjs/intl-numberformat/locale-data/gl');
await import('@formatjs/intl-datetimeformat/locale-data/gl');
break;
case 'ko':
await import('@formatjs/intl-relativetimeformat/locale-data/ko');
await import('@formatjs/intl-pluralrules/locale-data/ko');
await import('@formatjs/intl-relativetimeformat/locale-data/ko');
await import('@formatjs/intl-numberformat/locale-data/ko');
await import('@formatjs/intl-datetimeformat/locale-data/ko');
break;
case 'ha':
await import('@formatjs/intl-pluralrules/locale-data/ha');
await import('@formatjs/intl-relativetimeformat/locale-data/ha');
await import('@formatjs/intl-numberformat/locale-data/ha');
await import('@formatjs/intl-datetimeformat/locale-data/ha');
break;
case 'hy':
await import('@formatjs/intl-relativetimeformat/locale-data/hy');
await import('@formatjs/intl-pluralrules/locale-data/hy');
await import('@formatjs/intl-relativetimeformat/locale-data/hy');
await import('@formatjs/intl-numberformat/locale-data/hy');
await import('@formatjs/intl-datetimeformat/locale-data/hy');
break;
case 'he':
await import('@formatjs/intl-relativetimeformat/locale-data/he');
await import('@formatjs/intl-pluralrules/locale-data/he');
await import('@formatjs/intl-relativetimeformat/locale-data/he');
await import('@formatjs/intl-numberformat/locale-data/he');
await import('@formatjs/intl-datetimeformat/locale-data/he');
break;
case 'hr':
await import('@formatjs/intl-relativetimeformat/locale-data/hr');
await import('@formatjs/intl-pluralrules/locale-data/hr');
await import('@formatjs/intl-relativetimeformat/locale-data/hr');
await import('@formatjs/intl-numberformat/locale-data/hr');
await import('@formatjs/intl-datetimeformat/locale-data/hr');
break;
case 'xh':
await import('@formatjs/intl-relativetimeformat/locale-data/xh');
await import('@formatjs/intl-pluralrules/locale-data/xh');
await import('@formatjs/intl-relativetimeformat/locale-data/xh');
await import('@formatjs/intl-numberformat/locale-data/xh');
await import('@formatjs/intl-datetimeformat/locale-data/xh');
break;
case 'zu':
await import('@formatjs/intl-relativetimeformat/locale-data/zu');
await import('@formatjs/intl-pluralrules/locale-data/zu');
await import('@formatjs/intl-relativetimeformat/locale-data/zu');
await import('@formatjs/intl-numberformat/locale-data/zu');
await import('@formatjs/intl-datetimeformat/locale-data/zu');
break;
case 'is':
await import('@formatjs/intl-relativetimeformat/locale-data/is');
await import('@formatjs/intl-pluralrules/locale-data/is');
await import('@formatjs/intl-relativetimeformat/locale-data/is');
await import('@formatjs/intl-numberformat/locale-data/is');
await import('@formatjs/intl-datetimeformat/locale-data/is');
break;
case 'it':
await import('@formatjs/intl-relativetimeformat/locale-data/it');
await import('@formatjs/intl-pluralrules/locale-data/it');
await import('@formatjs/intl-relativetimeformat/locale-data/it');
await import('@formatjs/intl-numberformat/locale-data/it');
await import('@formatjs/intl-datetimeformat/locale-data/it');
break;
case 'ka':
await import('@formatjs/intl-relativetimeformat/locale-data/ka');
await import('@formatjs/intl-pluralrules/locale-data/ka');
await import('@formatjs/intl-relativetimeformat/locale-data/ka');
await import('@formatjs/intl-numberformat/locale-data/ka');
await import('@formatjs/intl-datetimeformat/locale-data/ka');
break;
case 'kk':
await import('@formatjs/intl-relativetimeformat/locale-data/kk');
await import('@formatjs/intl-pluralrules/locale-data/kk');
await import('@formatjs/intl-relativetimeformat/locale-data/kk');
await import('@formatjs/intl-numberformat/locale-data/kk');
await import('@formatjs/intl-datetimeformat/locale-data/kk');
break;
case 'qu':
await import('@formatjs/intl-pluralrules/locale-data/en');
await import('@formatjs/intl-relativetimeformat/locale-data/qu');
await import('@formatjs/intl-numberformat/locale-data/qu');
await import('@formatjs/intl-datetimeformat/locale-data/qu');
break;
case 'sw':
await import('@formatjs/intl-relativetimeformat/locale-data/sw');
await import('@formatjs/intl-pluralrules/locale-data/sw');
await import('@formatjs/intl-relativetimeformat/locale-data/sw');
await import('@formatjs/intl-numberformat/locale-data/sw');
await import('@formatjs/intl-datetimeformat/locale-data/sw');
break;
case 'ku':
await import('@formatjs/intl-relativetimeformat/locale-data/ku');
await import('@formatjs/intl-pluralrules/locale-data/ku');
await import('@formatjs/intl-relativetimeformat/locale-data/ku');
await import('@formatjs/intl-numberformat/locale-data/ku');
await import('@formatjs/intl-datetimeformat/locale-data/ku');
break;
case 'ckb':
await import('@formatjs/intl-relativetimeformat/locale-data/ckb');
await import('@formatjs/intl-pluralrules/locale-data/ckb');
await import('@formatjs/intl-relativetimeformat/locale-data/ckb');
await import('@formatjs/intl-numberformat/locale-data/ckb');
await import('@formatjs/intl-datetimeformat/locale-data/ckb');
break;
case 'lv':
await import('@formatjs/intl-relativetimeformat/locale-data/lv');
await import('@formatjs/intl-pluralrules/locale-data/lv');
await import('@formatjs/intl-relativetimeformat/locale-data/lv');
await import('@formatjs/intl-numberformat/locale-data/lv');
await import('@formatjs/intl-datetimeformat/locale-data/lv');
break;
case 'lt':
await import('@formatjs/intl-relativetimeformat/locale-data/lt');
await import('@formatjs/intl-pluralrules/locale-data/lt');
await import('@formatjs/intl-relativetimeformat/locale-data/lt');
await import('@formatjs/intl-numberformat/locale-data/lt');
await import('@formatjs/intl-datetimeformat/locale-data/lt');
break;
case 'hu':
await import('@formatjs/intl-relativetimeformat/locale-data/hu');
await import('@formatjs/intl-pluralrules/locale-data/hu');
await import('@formatjs/intl-relativetimeformat/locale-data/hu');
await import('@formatjs/intl-numberformat/locale-data/hu');
await import('@formatjs/intl-datetimeformat/locale-data/hu');
break;
case 'mi':
await import('@formatjs/intl-relativetimeformat/locale-data/mi');
await import('@formatjs/intl-pluralrules/locale-data/en');
await import('@formatjs/intl-relativetimeformat/locale-data/mi');
await import('@formatjs/intl-numberformat/locale-data/mi');
await import('@formatjs/intl-datetimeformat/locale-data/mi');
break;
case 'mn':
await import('@formatjs/intl-relativetimeformat/locale-data/mn');
await import('@formatjs/intl-pluralrules/locale-data/mn');
await import('@formatjs/intl-relativetimeformat/locale-data/mn');
await import('@formatjs/intl-numberformat/locale-data/mn');
await import('@formatjs/intl-datetimeformat/locale-data/mn');
break;
case 'nl':
await import('@formatjs/intl-relativetimeformat/locale-data/nl');
await import('@formatjs/intl-pluralrules/locale-data/nl');
await import('@formatjs/intl-relativetimeformat/locale-data/nl');
await import('@formatjs/intl-numberformat/locale-data/nl');
await import('@formatjs/intl-datetimeformat/locale-data/nl');
break;
case 'ja':
await import('@formatjs/intl-relativetimeformat/locale-data/ja');
await import('@formatjs/intl-pluralrules/locale-data/ja');
await import('@formatjs/intl-relativetimeformat/locale-data/ja');
await import('@formatjs/intl-numberformat/locale-data/ja');
await import('@formatjs/intl-datetimeformat/locale-data/ja');
break;
case 'nb':
await import('@formatjs/intl-relativetimeformat/locale-data/nb');
await import('@formatjs/intl-pluralrules/locale-data/nb');
await import('@formatjs/intl-relativetimeformat/locale-data/nb');
await import('@formatjs/intl-numberformat/locale-data/nb');
await import('@formatjs/intl-datetimeformat/locale-data/nb');
break;
case 'nn':
await import('@formatjs/intl-relativetimeformat/locale-data/nn');
await import('@formatjs/intl-pluralrules/locale-data/nn');
await import('@formatjs/intl-relativetimeformat/locale-data/nn');
await import('@formatjs/intl-numberformat/locale-data/nn');
await import('@formatjs/intl-datetimeformat/locale-data/nn');
break;
case 'or':
await import('@formatjs/intl-relativetimeformat/locale-data/or');
await import('@formatjs/intl-pluralrules/locale-data/or');
await import('@formatjs/intl-relativetimeformat/locale-data/or');
await import('@formatjs/intl-numberformat/locale-data/or');
await import('@formatjs/intl-datetimeformat/locale-data/or');
break;
case 'uz':
await import('@formatjs/intl-relativetimeformat/locale-data/uz');
await import('@formatjs/intl-pluralrules/locale-data/uz');
await import('@formatjs/intl-relativetimeformat/locale-data/uz');
await import('@formatjs/intl-numberformat/locale-data/uz');
await import('@formatjs/intl-datetimeformat/locale-data/uz');
break;
case 'th':
await import('@formatjs/intl-relativetimeformat/locale-data/th');
await import('@formatjs/intl-pluralrules/locale-data/th');
await import('@formatjs/intl-relativetimeformat/locale-data/th');
await import('@formatjs/intl-numberformat/locale-data/th');
await import('@formatjs/intl-datetimeformat/locale-data/th');
break;
case 'km':
await import('@formatjs/intl-relativetimeformat/locale-data/km');
await import('@formatjs/intl-pluralrules/locale-data/km');
await import('@formatjs/intl-relativetimeformat/locale-data/km');
await import('@formatjs/intl-numberformat/locale-data/km');
await import('@formatjs/intl-datetimeformat/locale-data/km');
break;
case 'pl':
await import('@formatjs/intl-relativetimeformat/locale-data/pl');
await import('@formatjs/intl-pluralrules/locale-data/pl');
await import('@formatjs/intl-relativetimeformat/locale-data/pl');
await import('@formatjs/intl-numberformat/locale-data/pl');
await import('@formatjs/intl-datetimeformat/locale-data/pl');
break;
case 'pt':
await import('@formatjs/intl-relativetimeformat/locale-data/pt');
await import('@formatjs/intl-pluralrules/locale-data/pt');
await import('@formatjs/intl-relativetimeformat/locale-data/pt');
await import('@formatjs/intl-numberformat/locale-data/pt');
await import('@formatjs/intl-datetimeformat/locale-data/pt');
break;
case 'ro':
await import('@formatjs/intl-relativetimeformat/locale-data/ro');
await import('@formatjs/intl-pluralrules/locale-data/ro');
await import('@formatjs/intl-relativetimeformat/locale-data/ro');
await import('@formatjs/intl-numberformat/locale-data/ro');
await import('@formatjs/intl-datetimeformat/locale-data/ro');
break;
case 'ru':
await import('@formatjs/intl-relativetimeformat/locale-data/ru');
await import('@formatjs/intl-pluralrules/locale-data/ru');
await import('@formatjs/intl-relativetimeformat/locale-data/ru');
await import('@formatjs/intl-numberformat/locale-data/ru');
await import('@formatjs/intl-datetimeformat/locale-data/ru');
break;
case 'nso':
await import('@formatjs/intl-relativetimeformat/locale-data/en');
await import('@formatjs/intl-pluralrules/locale-data/nso');
await import('@formatjs/intl-relativetimeformat/locale-data/en');
await import('@formatjs/intl-numberformat/locale-data/en');
await import('@formatjs/intl-datetimeformat/locale-data/en');
break;
case 'tn':
await import('@formatjs/intl-relativetimeformat/locale-data/en');
await import('@formatjs/intl-pluralrules/locale-data/tn');
await import('@formatjs/intl-relativetimeformat/locale-data/en');
await import('@formatjs/intl-numberformat/locale-data/en');
await import('@formatjs/intl-datetimeformat/locale-data/en');
break;
case 'sk':
await import('@formatjs/intl-relativetimeformat/locale-data/sk');
await import('@formatjs/intl-pluralrules/locale-data/sk');
await import('@formatjs/intl-relativetimeformat/locale-data/sk');
await import('@formatjs/intl-numberformat/locale-data/sk');
await import('@formatjs/intl-datetimeformat/locale-data/sk');
break;
case 'sl':
await import('@formatjs/intl-relativetimeformat/locale-data/sl');
await import('@formatjs/intl-pluralrules/locale-data/sl');
await import('@formatjs/intl-relativetimeformat/locale-data/sl');
await import('@formatjs/intl-numberformat/locale-data/sl');
await import('@formatjs/intl-datetimeformat/locale-data/sl');
break;
case 'sr':
await import('@formatjs/intl-relativetimeformat/locale-data/sr');
await import('@formatjs/intl-pluralrules/locale-data/sr');
await import('@formatjs/intl-relativetimeformat/locale-data/sr');
await import('@formatjs/intl-numberformat/locale-data/sr');
await import('@formatjs/intl-datetimeformat/locale-data/sr');
break;
case 'fi':
await import('@formatjs/intl-relativetimeformat/locale-data/fi');
await import('@formatjs/intl-pluralrules/locale-data/fi');
await import('@formatjs/intl-relativetimeformat/locale-data/fi');
await import('@formatjs/intl-numberformat/locale-data/fi');
await import('@formatjs/intl-datetimeformat/locale-data/fi');
break;
case 'sv':
await import('@formatjs/intl-relativetimeformat/locale-data/sv');
await import('@formatjs/intl-pluralrules/locale-data/sv');
await import('@formatjs/intl-relativetimeformat/locale-data/sv');
await import('@formatjs/intl-numberformat/locale-data/sv');
await import('@formatjs/intl-datetimeformat/locale-data/sv');
break;
case 'vi':
await import('@formatjs/intl-relativetimeformat/locale-data/vi');
await import('@formatjs/intl-pluralrules/locale-data/vi');
await import('@formatjs/intl-relativetimeformat/locale-data/vi');
await import('@formatjs/intl-numberformat/locale-data/vi');
await import('@formatjs/intl-datetimeformat/locale-data/vi');
break;
case 'tr':
await import('@formatjs/intl-relativetimeformat/locale-data/tr');
await import('@formatjs/intl-pluralrules/locale-data/tr');
await import('@formatjs/intl-relativetimeformat/locale-data/tr');
await import('@formatjs/intl-numberformat/locale-data/tr');
await import('@formatjs/intl-datetimeformat/locale-data/tr');
break;
case 'uk':
await import('@formatjs/intl-relativetimeformat/locale-data/uk');
await import('@formatjs/intl-pluralrules/locale-data/uk');
await import('@formatjs/intl-relativetimeformat/locale-data/uk');
await import('@formatjs/intl-numberformat/locale-data/uk');
await import('@formatjs/intl-datetimeformat/locale-data/uk');
break;
case 'zh':
await import('@formatjs/intl-relativetimeformat/locale-data/zh');
await import('@formatjs/intl-pluralrules/locale-data/zh');
await import('@formatjs/intl-relativetimeformat/locale-data/zh');
await import('@formatjs/intl-numberformat/locale-data/zh');
await import('@formatjs/intl-datetimeformat/locale-data/zh');
break;
default:
await import('@formatjs/intl-relativetimeformat/locale-data/en');
await import('@formatjs/intl-pluralrules/locale-data/en');
await import('@formatjs/intl-relativetimeformat/locale-data/en');
await import('@formatjs/intl-numberformat/locale-data/en');
await import('@formatjs/intl-datetimeformat/locale-data/en');
break;
}
};

10
src/lib/intl-shape.js Normal file
View file

@ -0,0 +1,10 @@
const PropTypes = require('prop-types');
// intlShape was removed in react-intl@3 and replaced with a TypeScript interface.
// These are some of the commonly used properties from the intl object.
const intlShape = PropTypes.shape({
locale: PropTypes.string.isRequired,
formatMessage: PropTypes.func.isRequired
});
module.exports = intlShape;

View file

@ -1,7 +0,0 @@
const ReactIntl = require('react-intl');
// Add locale data to react intl for all supported languages
const localeData = require('scratch-l10n').localeData;
ReactIntl.addLocaleData(localeData);
module.exports = ReactIntl;

44
src/lib/locales.js Normal file
View file

@ -0,0 +1,44 @@
/**
* Scratch has some locales that are not recognized by Intl. Use an appropriate alternative for these locales.
* @param {string} locale Scratch's locale
* @returns {string} the locale to use in IntlProvider
*/
const scratchLocaleToIntlLocale = locale => {
switch (locale) {
case 'ab':
return 'ru';
case 'an':
case 'rap':
return 'es';
case 'ht':
case 'oc':
return 'fr';
default:
return locale;
}
};
/**
* Gets the locale for the current window.
* @returns {string} locale
*/
const getLocale = () => {
// Get locale from global namespace (see "init.js")
let locale = window._locale || 'en';
if (typeof window._messages !== 'undefined') {
if (typeof window._messages[locale] === 'undefined') {
// Fall back on the split
locale = locale.split('-')[0];
}
if (typeof window._messages[locale] === 'undefined') {
// Language appears to not be supported fall back to 'en'
locale = 'en';
}
}
return locale;
};
module.exports = {
getLocale,
scratchLocaleToIntlLocale
};

View file

@ -2,11 +2,13 @@
const React = require('react'); // eslint-disable-line
const ReactDOM = require('react-dom');
const StoreProvider = require('react-redux').Provider;
const IntlProvider = require('react-intl').IntlProvider;
const IntlProvider = require('./intl.jsx').IntlProvider;
const {getLocale, scratchLocaleToIntlLocale} = require('./locales.js');
const permissionsActions = require('../redux/permissions.js');
const sessionActions = require('../redux/session.js');
const configureStore = require('./configure-store.js');
import intlPolyfill from '../lib/intl-polyfill';
require('../main.scss');
@ -20,38 +22,35 @@ require('../main.scss');
*/
const render = (jsx, element, reducers, initialState, enhancer) => {
// Get locale and messages from global namespace (see "init.js")
let locale = window._locale || 'en';
const locale = getLocale();
let messages = {};
if (typeof window._messages !== 'undefined') {
if (typeof window._messages[locale] === 'undefined') {
// Fall back on the split
locale = locale.split('-')[0];
}
if (typeof window._messages[locale] === 'undefined') {
// Language appears to not be supported fall back to 'en'
locale = 'en';
}
messages = window._messages[locale];
}
const intlLocale = scratchLocaleToIntlLocale(locale);
// react-intl needs Intl before rendering
intlPolyfill(intlLocale).then(() => {
const store = configureStore(reducers, initialState, enhancer);
const store = configureStore(reducers, initialState, enhancer);
// Render view component
ReactDOM.render(
<StoreProvider store={store}>
<IntlProvider
locale={locale}
messages={messages}
>
{jsx}
</IntlProvider>
</StoreProvider>,
element
);
// Get initial session & permissions
store.dispatch(permissionsActions.getPermissions());
store.dispatch(sessionActions.refreshSession());
// Render view component
ReactDOM.render(
<StoreProvider store={store}>
<IntlProvider
locale={intlLocale}
messages={messages}
textComponent="span"
>
{jsx}
</IntlProvider>
</StoreProvider>,
element
);
// Get initial session & permissions
store.dispatch(permissionsActions.getPermissions());
store.dispatch(sessionActions.refreshSession());
});
};
module.exports = render;

82
src/lib/select-unit.js Normal file
View file

@ -0,0 +1,82 @@
const MILLISECONDS_PER_SECOND = 1000;
const SECONDS_PER_MINUTE = 60;
const MINUTES_PER_HOUR = 60;
const HOURS_PER_DAY = 24;
/**
* Determines the best unit (seconds, minutes, hours, days, months, or years)
* and value to represent the time difference between two dates.
* @param {Date} time - date to calculate relative units
* @param {Date} [relativeTo] - optional relative date, defaults to now
* @returns {object} calculated best unit and value
*/
const selectUnit = (time, relativeTo) => {
// Default to now
if (!relativeTo) relativeTo = new Date();
const seconds = (time - relativeTo) / MILLISECONDS_PER_SECOND;
if (Math.abs(seconds) < SECONDS_PER_MINUTE) {
return {
value: Math.trunc(seconds),
unit: 'second'
};
}
const minutes = seconds / SECONDS_PER_MINUTE;
if (Math.abs(minutes) < MINUTES_PER_HOUR) {
return {
value: Math.trunc(minutes),
unit: 'minute'
};
}
const hours = minutes / MINUTES_PER_HOUR;
if (Math.abs(hours) < HOURS_PER_DAY) {
return {
value: Math.trunc(hours),
unit: 'hour'
};
}
const days = hours / HOURS_PER_DAY;
let years = time.getFullYear() - relativeTo.getFullYear();
let months = time.getMonth() - relativeTo.getMonth() + (12 * years);
// Handle calendar months different but less than a complete month elapsed
if (months > 0 && time.getDate() < relativeTo.getDate()) months--;
if (months < 0 && time.getDate() > relativeTo.getDate()) months++;
if (Math.abs(months) < 1) {
return {
value: Math.trunc(days),
unit: 'day'
};
}
// Handle calendar years different but less than a complete year elapsed
if (years > 0 && (time.getMonth() < relativeTo.getMonth() ||
(time.getMonth() === relativeTo.getMonth() && time.getDate() < relativeTo.getDate()))) {
years--;
}
if (years < 0 && (time.getMonth() > relativeTo.getMonth() ||
(time.getMonth() === relativeTo.getMonth() && time.getDate() > relativeTo.getDate()))) {
years++;
}
if (Math.abs(years) < 1) {
return {
value: months,
unit: 'month'
};
}
return {
value: years,
unit: 'year'
};
};
module.exports = {
selectUnit
};

View file

@ -1,18 +0,0 @@
const initSentry = () => {
// initialize Sentry instance, making sure it hasn't been initialized already
if (!window.Sentry && `${process.env.SENTRY_DSN}` !== '') {
const Sentry = require('@sentry/browser');
Sentry.init({
dsn: `${process.env.SENTRY_DSN}`,
// Do not collect global onerror, only collect specifically from React error boundaries.
// TryCatch plugin also includes errors from setTimeouts (i.e. the VM)
integrations: integrations => integrations.filter(i =>
!(i.name === 'GlobalHandlers' || i.name === 'TryCatch'))
});
window.Sentry = Sentry; // Allow GUI access to Sentry via window
}
};
module.exports = initSentry;

View file

@ -6,7 +6,7 @@ html,
body {
display: block;
margin: 0;
background-color: $ui-blue-dark;
background-color: $ui-purple-dark;
padding: 0;
color: $type-gray;
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
@ -91,19 +91,19 @@ strong {
/* Links */
a {
cursor: pointer;
color: $ui-blue;
color: $ui-purple-dark;
font-weight: bold;
&:link,
&:visited,
&:active {
text-decoration: none;
color: $link-blue;
color: $link-purple;
}
&:hover {
text-decoration: none;
color: $ui-blue-dark;
color: $ui-purple-darker;
}
}
@ -137,6 +137,10 @@ p {
font-weight: normal;
}
input {
outline-color: $ui-purple-dark;
}
::selection {
background-color: $ui-blue-25percent;
}

View file

@ -13,6 +13,8 @@ const Types = keyMirror({
});
const banGoodListPaths = [
'/ip_ban_appeal',
'/vpn_required',
'/accounts/banned-response',
'/accounts/bad-username',
'/community_guidelines',
@ -67,6 +69,19 @@ module.exports.setStatus = status => ({
const handleSessionResponse = (dispatch, body) => {
if (typeof body === 'undefined') return dispatch(module.exports.setSessionError('No session content'));
if (
body.vpn_required &&
banGoodListPaths.every(goodPath => window.location.pathname.indexOf(goodPath) === -1)
) {
window.location = '/vpn_required/';
return;
} else if (
body.banned &&
body.redirectURL &&
banGoodListPaths.every(goodPath => window.location.pathname.indexOf(goodPath) === -1)
) {
window.location = body.redirectURL;
return;
} else if (
body.user &&
body.user.banned &&
banGoodListPaths.every(goodPath => window.location.pathname.indexOf(goodPath) === -1)
@ -145,7 +160,6 @@ module.exports.selectToken = state => get(state, ['session', 'session', 'user',
module.exports.selectIsAdmin = state => get(state, ['session', 'session', 'permissions', 'admin'], false);
module.exports.selectIsSocial = state => get(state, ['session', 'session', 'permissions', 'social'], false);
module.exports.selectIsEducator = state => get(state, ['session', 'session', 'permissions', 'educator'], false);
module.exports.selectBannedUser = state => get(state, ['session', 'session', 'user'], false);
module.exports.selectProjectCommentsGloballyEnabled = state =>
get(state, ['session', 'session', 'flags', 'project_comments_enabled'], false);
module.exports.selectStudioCommentsGloballyEnabled = state =>

View file

@ -138,6 +138,13 @@
"title": "Contact Us",
"viewportWidth": "device-width"
},
{
"name": "cookies",
"pattern": "^/cookies/?$",
"routeAlias": "/cookies/?",
"view": "cookies/cookies",
"title": "Cookie Policy"
},
{
"name": "credits",
"pattern": "^/credits/?$",
@ -211,7 +218,7 @@
{
"name": "jobs-redirect",
"pattern": "^/jobs/?(\\?.*)?$",
"redirect": "https://www.scratchfoundation.org/opportunities/"
"redirect": "https://www.scratchfoundation.org/careers"
},
{
"name": "join",

View file

@ -28,6 +28,15 @@ module.exports = {
og_image_height: 860,
// Analytics & Monitoring
ga_tracker: process.env.GA_TRACKER || '',
gtm_id: process.env.GTM_ID || ''
// ----------------------
// Google Tag Manager ID
// Looks like 'GTM-XXXXXXX'
gtm_id: process.env.GTM_ID || '',
// Google Tag Manager env & auth info for alterative GTM environments
// Looks like '&gtm_auth=0123456789abcdefghijklm&gtm_preview=env-00&gtm_cookies_win=x'
// Taken from the middle of: GTM -> Admin -> Environments -> (environment) -> Get Snippet
// Blank for production
gtm_env_auth: process.env.GTM_ENV_AUTH || ''
};

View file

@ -8,8 +8,9 @@
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','<%- htmlWebpackPlugin.options.gtm_id %>');</script>
'https://www.googletagmanager.com/gtm.js?id='+i+dl+'<%= htmlWebpackPlugin.options.gtm_env_auth %>';
f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','<%- htmlWebpackPlugin.options.gtm_id %>');
</script>
<!-- End Google Tag Manager -->
<% } %>
@ -46,28 +47,12 @@
<!-- Polyfills -->
<script src="/js/polyfill.min.js"></script>
<!-- Analytics (GA) -->
<script>
/* eslint-disable */
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '<%- htmlWebpackPlugin.options.ga_tracker %>', {
'sampleRate': 10
});
ga('send', 'pageview');
window.GA_ID = '<%- htmlWebpackPlugin.options.ga_tracker %>';
/* eslint-enable */
</script>
</head>
<body>
<% if (htmlWebpackPlugin.options.gtm_id) { %>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=<%- htmlWebpackPlugin.options.gtm_id %>" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=<%- htmlWebpackPlugin.options.gtm_id %><%= htmlWebpackPlugin.options.gtm_env_auth %>" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<% } %>
<noscript>

View file

@ -5,10 +5,12 @@ const render = require('../../lib/render.jsx');
const Button = require('../../components/forms/button.jsx');
const Page = require('../../components/page/www/page.jsx');
const Video = require('../../components/video/video.jsx');
const injectIntl = require('react-intl').injectIntl;
require('./about.scss');
const About = () => (
const tedLink = chunks => <a href="https://www.ted.com/talks/mitch_resnick_let_s_teach_kids_to_code">{chunks}</a>;
const About = injectIntl(({intl}) => (
<div className="inner about">
<h1><FormattedMessage id="general.aboutScratch" /></h1>
@ -56,14 +58,22 @@ const About = () => (
<li>
<h2><FormattedMessage id="about.literacy" /></h2>
<iframe
allowFullScreen
mozallowfullscreen={'true'}
scrolling="no"
src="https://embed-ssl.ted.com/talks/mitch_resnick_let_s_teach_kids_to_code.html"
webkitallowfullscreen={'true'}
/>
<p><FormattedMessage id="about.literacyDescription" /></p>
<a href="https://www.ted.com/talks/mitch_resnick_let_s_teach_kids_to_code">
<img
alt={intl.formatMessage(
{id: 'about.literacyImageDescription'}
)}
src="/images/about/ted-thumbnail.jpg"
/>
</a>
<p>
<FormattedMessage
id="about.literacyDescription"
values={{
a: tedLink
}}
/>
</p>
</li>
<li>
@ -176,7 +186,7 @@ const About = () => (
),
annualReportLink: (
<a
href="/annual-report"
href="https://www.scratchfoundation.org/annualreport"
>
<FormattedMessage id="about.annualReportLinkText" />
</a>
@ -198,7 +208,7 @@ const About = () => (
<a href="/educators"><FormattedMessage id="about.learnMoreEducators" /></a>
</li>
<li>
<a href="/annual-report"><FormattedMessage id="about.learnMoreAnnualReport" /></a>
<a href="https://www.scratchfoundation.org/annualreport"><FormattedMessage id="about.learnMoreAnnualReport" /></a>
</li>
</ul>
</li>
@ -241,6 +251,6 @@ const About = () => (
</ul>
</div>
</div>
);
));
render(<Page><About /></Page>, document.getElementById('app'));

View file

@ -79,7 +79,7 @@
.about-button {
margin-right: .75rem;
background-color: $ui-blue;
background-color: $ui-purple-dark;
color: $ui-white;
font-size: 1rem;

View file

@ -19,7 +19,8 @@
"about.learnMoreEducators": "Information for Educators",
"about.learnMoreAnnualReport": "Annual Report",
"about.literacy": "Learn to Code, Code to Learn",
"about.literacyDescription": "The ability to code computer programs is an important part of literacy in todays society. When people learn to code in Scratch, they learn important strategies for solving problems, designing projects, and communicating ideas.",
"about.literacyImageDescription": "An image of Mitch Resnick giving a TED talk titled \"Let's Teach Kids to Code.\" A play button is in the center of the image.",
"about.literacyDescription": "In this <a>TED talk</a>, Scratch founder Mitch Resnick describes why the ability to code computer programs is an important part of literacy in todays society. When people learn to code in Scratch, they learn important strategies for solving problems, designing projects, and communicating ideas.",
"about.schools": "Scratch in Schools",
"about.schoolsDescription": "Students are learning with Scratch at all levels (from elementary school to college) and across disciplines (such as math, computer science, language arts, social studies). Educator resources are available on the {scratchForEducatorsLink} page.",
"about.scratchForEducatorsLinkText": "Scratch For Educators",

View file

@ -4,8 +4,8 @@ const React = require('react');
const MediaQuery = require('react-responsive').default;
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const intlShape = require('../../../lib/intl-shape');
const render = require('../../../lib/render.jsx');
const frameless = require('../../../lib/frameless');

View file

@ -4,8 +4,8 @@ const React = require('react');
const MediaQuery = require('react-responsive').default;
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const intlShape = require('../../../lib/intl-shape');
const render = require('../../../lib/render.jsx');
const frameless = require('../../../lib/frameless');
@ -2129,6 +2129,7 @@ class AnnualReport extends React.Component {
comment={this.props.intl.formatMessage(
{id: 'annualReport.2020.communityQuote2Text'}
)}
datetimeCreated="2020-01-01"
/>
</div>
</div>

View file

@ -4,8 +4,8 @@ const React = require('react');
const MediaQuery = require('react-responsive').default;
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const intlShape = require('../../../lib/intl-shape');
const render = require('../../../lib/render.jsx');
const frameless = require('../../../lib/frameless');

View file

@ -75,7 +75,7 @@ p a {
a, a:link, a:visited, a:active{
cursor: pointer;
color: $motion-blue-3;
color: $ui-purple-dark;
}
.bold {

View file

@ -1,8 +1,9 @@
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const FormattedMessage = require('react-intl').FormattedMessage;
const React = require('react');
const intlShape = require('../../lib/intl-shape');
const Page = require('../../components/page/www/page.jsx');
const render = require('../../lib/render.jsx');
@ -34,7 +35,7 @@ class Boost extends ExtensionLanding {
renderCopy={
<FlexRow className="extension-copy">
<h1><img
alt=""
alt="Boost"
className="headline-icon"
src="/images/boost/boost.svg"
/>LEGO BOOST</h1>

View file

@ -3,7 +3,7 @@
.boost {
.extension-header {
background-color: $ui-orange;
background-color: $ui-magenta-dark;
background-image: url("/images/boost/boost-pattern.svg");
}
}

View file

@ -1,7 +1,6 @@
const React = require('react');
const injectIntl = require('react-intl').injectIntl;
const FormattedMessage = require('react-intl').FormattedMessage;
const FormattedHTMLMessage = require('react-intl').FormattedHTMLMessage;
const render = require('../../lib/render.jsx');
const TitleBanner = require('../../components/title-banner/title-banner.jsx');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
@ -10,6 +9,10 @@ const Page = require('../../components/page/www/page.jsx');
require('./camp.scss');
const MainStudio = chunks => <a href="https://scratch.mit.edu/studios/4160302/">{chunks}</a>;
const FinalStudio = chunks => <a href="https://scratch.mit.edu/studios/4160301/">{chunks}</a>;
const CounselorsStudio = chunks => <a href="https://scratch.mit.edu/studios/4160300/">{chunks}</a>;
const Camp = injectIntl(() => (
<div>
<TitleBanner className="masthead mod-blue-bg">
@ -40,7 +43,10 @@ const Camp = injectIntl(() => (
<center>
<h2><FormattedMessage id="camp.welcome" /></h2>
<p id="intro">
<FormattedHTMLMessage id="camp.welcomeIntro" />
<FormattedMessage
id="camp.welcomeIntroHTML"
values={{br: <br />}}
/>
</p>
</center>
<center>
@ -63,7 +69,10 @@ const Camp = injectIntl(() => (
<div className="sidebar column">
<h3><FormattedMessage id="camp.particpateTitle" /></h3>
<p>
<FormattedHTMLMessage id="camp.part1Particpate" />
<FormattedMessage
id="camp.part1ParticpateHTML"
values={{a: MainStudio}}
/>
</p>
</div>
</FlexRow>
@ -86,7 +95,10 @@ const Camp = injectIntl(() => (
<div className="sidebar column">
<h3><FormattedMessage id="camp.particpateTitle" /></h3>
<p>
<FormattedHTMLMessage id="camp.part2Particpate" />
<FormattedMessage
id="camp.part2ParticpateHTML"
values={{a: MainStudio}}
/>
</p>
</div>
</FlexRow>
@ -109,7 +121,10 @@ const Camp = injectIntl(() => (
<div className="sidebar column">
<h3><FormattedMessage id="camp.particpateTitle" /></h3>
<p>
<FormattedHTMLMessage id="camp.part3Particpate" />
<FormattedMessage
id="camp.part3ParticpateHTML"
values={{a: FinalStudio}}
/>
</p>
</div>
</FlexRow>
@ -132,7 +147,10 @@ const Camp = injectIntl(() => (
src="/images/camp/dolphin.svg"
/>
<p>
<FormattedHTMLMessage id="camp.infoCounselors" />
<FormattedMessage
id="camp.infoCounselorsHTML"
values={{a: CounselorsStudio}}
/>
</p>
</div>
<div>

Some files were not shown because too many files have changed in this diff Show more