Merge pull request #2123 from LLK/release/september-2018

[Master] Release for September 2018
This commit is contained in:
Ray Schamp 2018-10-03 14:16:45 -04:00 committed by GitHub
commit dded9e9c0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 1348 additions and 765 deletions

View file

@ -1,11 +1,25 @@
ESLINT=./node_modules/.bin/eslint ESLINT=./node_modules/.bin/eslint
NODE= NODE_OPTIONS=--max_old_space_size=8000 node NODE= NODE_OPTIONS=--max_old_space_size=8000 node
SASSLINT=./node_modules/.bin/sass-lint -v SASSLINT=./node_modules/.bin/sass-lint -v
SCRATCH_DOCKER_CONFIG=./node_modules/.bin/docker_config.sh
S3CMD=s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600 S3CMD=s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600
TAP=./node_modules/.bin/tap TAP=./node_modules/.bin/tap
WATCH= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/watch WATCH= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/watch
WEBPACK= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/webpack WEBPACK= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/webpack
# ------------------------------------
$(SCRATCH_DOCKER_CONFIG):
npm install scratch-docker
docker-up: $(SCRATCH_DOCKER_CONFIG)
$(SCRATCH_DOCKER_CONFIG) network create
docker-compose up
docker-down:
docker-compose down
# ------------------------------------ # ------------------------------------
build: build:

View file

@ -4,8 +4,9 @@ volumes:
runtime_data: runtime_data:
networks: networks:
scratch-api_scratch_network: default:
external: true external:
name: scratchapi_scratch_network
services: services:
app: app:
@ -13,7 +14,7 @@ services:
hostname: scratch-www-app hostname: scratch-www-app
environment: environment:
- API_HOST=http://localhost:8491 - API_HOST=http://localhost:8491
- FALLBACK=http://localhost:8080 - FALLBACK=http://scratchr2-app:8080
- USE_DOCKER_WATCHOPTIONS=true - USE_DOCKER_WATCHOPTIONS=true
build: build:
context: ./ context: ./
@ -35,5 +36,3 @@ services:
- runtime_data:/runtime - runtime_data:/runtime
ports: ports:
- "8333:8333" - "8333:8333"
networks:
- scratch-api_scratch_network

View file

@ -31,6 +31,7 @@
"lodash.defaults": "4.0.1", "lodash.defaults": "4.0.1",
"newrelic": "1.25.4", "newrelic": "1.25.4",
"raven": "0.10.0", "raven": "0.10.0",
"scratch-docker": "^1.0.2",
"scratch-parser": "^4.2.0", "scratch-parser": "^4.2.0",
"scratch-storage": "^0.5.1" "scratch-storage": "^0.5.1"
}, },
@ -77,7 +78,6 @@
"lodash.merge": "3.3.2", "lodash.merge": "3.3.2",
"lodash.omit": "3.1.0", "lodash.omit": "3.1.0",
"lodash.range": "3.0.1", "lodash.range": "3.0.1",
"lodash.truncate": "4.4.2",
"minilog": "2.0.8", "minilog": "2.0.8",
"node-dir": "0.1.16", "node-dir": "0.1.16",
"node-sass": "4.6.1", "node-sass": "4.6.1",
@ -100,7 +100,7 @@
"redux-thunk": "2.0.1", "redux-thunk": "2.0.1",
"sass-lint": "1.5.1", "sass-lint": "1.5.1",
"sass-loader": "6.0.6", "sass-loader": "6.0.6",
"scratch-gui": "latest", "scratch-gui": "develop",
"scratchr2_translations": "git://github.com/LLK/scratchr2_translations.git#master", "scratchr2_translations": "git://github.com/LLK/scratchr2_translations.git#master",
"slick-carousel": "1.6.0", "slick-carousel": "1.6.0",
"source-map-support": "0.3.2", "source-map-support": "0.3.2",

View file

@ -33,8 +33,8 @@
} }
input { input {
// 100% minus border and padding
margin-bottom: 12px; margin-bottom: 12px;
// 100% minus border and padding
width: calc(100% - 30px); width: calc(100% - 30px);
} }

View file

@ -10,7 +10,11 @@
padding: 4rem 0; padding: 4rem 0;
} }
h2 { h1 {
font-size: 2rem;
}
h1, h2 {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -85,15 +89,15 @@
margin-bottom: 5rem; margin-bottom: 5rem;
align-items: flex-start; align-items: flex-start;
h2 { h1, h2 {
display: flex; display: flex;
margin-bottom: 2rem; margin-bottom: 2rem;
color: $ui-white; color: $ui-white;
}
h2 img { img {
padding-right: .5rem; padding-right: .5rem;
max-height: 100%; max-height: 100%;
}
} }
span { span {

View file

@ -35,7 +35,10 @@ const InstallScratchLink = ({
<FormattedMessage id="installScratchLink.windowsDownload" /> : <FormattedMessage id="installScratchLink.windowsDownload" /> :
<FormattedMessage id="installScratchLink.macosDownload" /> <FormattedMessage id="installScratchLink.macosDownload" />
} }
<img src="/svgs/extensions/download-white.svg" /> <img
alt=""
src="/svgs/extensions/download-white.svg"
/>
</button> </button>
</a> </a>
</Step> </Step>
@ -50,6 +53,7 @@ const InstallScratchLink = ({
</span> </span>
<div className="step-image"> <div className="step-image">
<img <img
alt=""
className="screenshot" className="screenshot"
src={`/images/scratchlink/${ src={`/images/scratchlink/${
currentOS === OS_ENUM.WINDOWS ? 'windows' : 'mac' currentOS === OS_ENUM.WINDOWS ? 'windows' : 'mac'

View file

@ -8,7 +8,10 @@ const ProjectCard = props => (
target="_blank" target="_blank"
> >
<div className="project-card-image"> <div className="project-card-image">
<img src={props.imageSrc} /> <img
alt={props.imageAlt}
src={props.imageSrc}
/>
</div> </div>
<div className="project-card-info"> <div className="project-card-info">
<h4>{props.title}</h4> <h4>{props.title}</h4>
@ -20,6 +23,7 @@ const ProjectCard = props => (
ProjectCard.propTypes = { ProjectCard.propTypes = {
cardUrl: PropTypes.string, cardUrl: PropTypes.string,
description: PropTypes.string, description: PropTypes.string,
imageAlt: PropTypes.string,
imageSrc: PropTypes.string, imageSrc: PropTypes.string,
title: PropTypes.string title: PropTypes.string
}; };

View file

@ -3,7 +3,7 @@ const connect = require('react-redux').connect;
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const sessionActions = require('../../redux/session.js'); const navigationActions = require('../../redux/navigation.js');
const IframeModal = require('../modal/iframe/modal.jsx'); const IframeModal = require('../modal/iframe/modal.jsx');
const Registration = require('../registration/registration.jsx'); const Registration = require('../registration/registration.jsx');
@ -15,10 +15,7 @@ class Intro extends React.Component {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'handleShowVideo', 'handleShowVideo',
'handleCloseVideo', 'handleCloseVideo'
'handleJoinClick',
'handleCloseRegistration',
'handleCompleteRegistration'
]); ]);
this.state = { this.state = {
videoOpen: false videoOpen: false
@ -30,17 +27,6 @@ class Intro extends React.Component {
handleCloseVideo () { handleCloseVideo () {
this.setState({videoOpen: false}); this.setState({videoOpen: false});
} }
handleJoinClick (e) {
e.preventDefault();
this.setState({registrationOpen: true});
}
handleCloseRegistration () {
this.setState({registrationOpen: false});
}
handleCompleteRegistration () {
this.props.dispatch(sessionActions.refreshSession());
this.closeRegistration();
}
render () { render () {
return ( return (
<div className="intro"> <div className="intro">
@ -92,7 +78,7 @@ class Intro extends React.Component {
<a <a
className="sprite sprite-3" className="sprite sprite-3"
href="#" href="#"
onClick={this.handleJoinClick} onClick={this.props.handleOpenRegistration}
> >
<img <img
alt="Gobo" alt="Gobo"
@ -111,10 +97,7 @@ class Intro extends React.Component {
<div className="text subtext">{this.props.messages['intro.itsFree']}</div> <div className="text subtext">{this.props.messages['intro.itsFree']}</div>
</a> </a>
<Registration <Registration
isOpen={this.state.registrationOpen}
key="registration" key="registration"
onRegistrationDone={this.handleCompleteRegistration}
onRequestClose={this.handleCloseRegistration}
/> />
</div> </div>
<div <div
@ -160,7 +143,7 @@ class Intro extends React.Component {
} }
Intro.propTypes = { Intro.propTypes = {
dispatch: PropTypes.func.isRequired, handleOpenRegistration: PropTypes.func,
messages: PropTypes.shape({ messages: PropTypes.shape({
'intro.aboutScratch': PropTypes.string, 'intro.aboutScratch': PropTypes.string,
'intro.forEducators': PropTypes.string, 'intro.forEducators': PropTypes.string,
@ -194,6 +177,17 @@ const mapStateToProps = state => ({
session: state.session session: state.session
}); });
const ConnectedIntro = connect(mapStateToProps)(Intro); const mapDispatchToProps = dispatch => ({
handleOpenRegistration: event => {
event.preventDefault();
dispatch(navigationActions.handleOpenRegistration());
}
});
const ConnectedIntro = connect(
mapStateToProps,
mapDispatchToProps
)(Intro);
module.exports = ConnectedIntro; module.exports = ConnectedIntro;

View file

@ -0,0 +1,60 @@
const React = require('react');
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 navigationActions = require('../../redux/navigation.js');
const Modal = require('../modal/base/modal.jsx');
const CanceledDeletionModal = ({
canceledDeletionOpen,
handleCloseCanceledDeletion,
intl
}) => (
<Modal
isOpen={canceledDeletionOpen}
style={{
content: {
padding: 15
}
}}
onRequestClose={handleCloseCanceledDeletion}
>
<h4><FormattedMessage id="general.noDeletionTitle" /></h4>
<p>
<FormattedMessage
id="general.noDeletionDescription"
values={{
resetLink: <a href="/accounts/password_reset/">
{intl.formatMessage({id: 'general.noDeletionLink'})}
</a>
}}
/>
</p>
</Modal>
);
CanceledDeletionModal.propTypes = {
canceledDeletionOpen: PropTypes.bool,
handleCloseCanceledDeletion: PropTypes.func,
intl: intlShape
};
const mapStateToProps = state => ({
canceledDeletionOpen: state.navigation && state.navigation.canceledDeletionOpen
});
const mapDispatchToProps = dispatch => ({
handleCloseCanceledDeletion: () => {
dispatch(navigationActions.setCanceledDeletionOpen(false));
}
});
const ConnectedCanceledDeletionModal = connect(
mapStateToProps,
mapDispatchToProps
)(CanceledDeletionModal);
module.exports = injectIntl(ConnectedCanceledDeletionModal);

View file

@ -0,0 +1,34 @@
const PropTypes = require('prop-types');
const React = require('react');
const connect = require('react-redux').connect;
const Login = require('./login.jsx');
require('./login-dropdown.scss');
const ConnectedLogin = ({
error,
onLogIn
}) => (
<Login
error={error}
key="login-dropdown-presentation"
onLogIn={onLogIn}
/>
);
ConnectedLogin.propTypes = {
error: PropTypes.string,
onLogIn: PropTypes.func
};
const mapStateToProps = state => ({
error: state.navigation && state.navigation.loginError
});
const mapDispatchToProps = () => ({});
module.exports = connect(
mapStateToProps,
mapDispatchToProps
)(ConnectedLogin);

View file

@ -0,0 +1,50 @@
const PropTypes = require('prop-types');
const React = require('react');
const connect = require('react-redux').connect;
const navigationActions = require('../../redux/navigation.js');
const Dropdown = require('../dropdown/dropdown.jsx');
const ConnectedLogin = require('./connected-login.jsx');
require('./login-dropdown.scss');
const LoginDropdown = ({
isOpen,
onClose,
onLogIn
}) => (
<Dropdown
className={'with-arrow'}
isOpen={isOpen}
key="login-dropdown"
onRequestClose={onClose}
>
<ConnectedLogin
onLogIn={onLogIn}
/>
</Dropdown>
);
LoginDropdown.propTypes = {
isOpen: PropTypes.bool,
onClose: PropTypes.func,
onLogIn: PropTypes.func
};
const mapStateToProps = state => ({
isOpen: state.navigation && state.navigation.loginOpen
});
const mapDispatchToProps = dispatch => ({
onClose: () => {
dispatch(navigationActions.setLoginOpen(false));
},
onLogIn: (formData, callback) => {
dispatch(navigationActions.handleLogIn(formData, callback));
}
});
module.exports = connect(
mapStateToProps,
mapDispatchToProps
)(LoginDropdown);

View file

View file

@ -3,8 +3,6 @@ const FormattedMessage = require('react-intl').FormattedMessage;
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const log = require('../../lib/log.js');
const Form = require('../forms/form.jsx'); const Form = require('../forms/form.jsx');
const Input = require('../forms/input.jsx'); const Input = require('../forms/input.jsx');
const Button = require('../forms/button.jsx'); const Button = require('../forms/button.jsx');
@ -24,8 +22,7 @@ class Login extends React.Component {
} }
handleSubmit (formData) { handleSubmit (formData) {
this.setState({waiting: true}); this.setState({waiting: true});
this.props.onLogIn(formData, err => { this.props.onLogIn(formData, () => {
if (err) log.error(err);
this.setState({waiting: false}); this.setState({waiting: false});
}); });
} }
@ -48,9 +45,6 @@ class Login extends React.Component {
key="usernameInput" key="usernameInput"
maxLength="30" maxLength="30"
name="username" name="username"
ref={input => {
this.username = input;
}}
type="text" type="text"
/> />
<label <label
@ -63,9 +57,6 @@ class Login extends React.Component {
required required
key="passwordInput" key="passwordInput"
name="password" name="password"
ref={input => {
this.password = input;
}}
type="password" type="password"
/> />
{this.state.waiting ? [ {this.state.waiting ? [
@ -75,7 +66,10 @@ class Login extends React.Component {
key="submitButton" key="submitButton"
type="submit" type="submit"
> >
<Spinner /> <Spinner
className="spinner"
color="blue"
/>
</Button> </Button>
] : [ ] : [
<Button <Button

View file

@ -2,6 +2,26 @@
.login { .login {
padding: 10px; padding: 10px;
width: 200px;
line-height: 1.5rem;
white-space: normal; // override any parent, such as in gui, who sets nowrap
color: $type-white;
font-size: .8125rem;
.button {
padding: .75em;
}
.row {
margin-bottom: 1.25rem;
}
.input {
margin-bottom: 12px;
// 100% minus border and padding
width: calc(100% - 30px);
height: 2.25rem;
}
label { label {
padding-top: 5px; padding-top: 5px;
@ -15,7 +35,7 @@
.spinner { .spinner {
margin: 0 .8rem; margin: 0 .8rem;
width: 1rem; width: 1rem;
height: 1rem; vertical-align: middle;
} }
.submit-button { .submit-button {
@ -24,13 +44,19 @@
a { a {
margin-top: 15px; margin-top: 15px;
color: $ui-white;
&:link,
&:visited,
&:active {
color: $ui-white;
}
&:hover { &:hover {
background-color: transparent; background-color: transparent;
} }
} }
.error { .error {
border: 1px solid $active-dark-gray; border: 1px solid $active-dark-gray;
border-radius: 5px; border-radius: 5px;

View file

@ -0,0 +1,48 @@
const React = require('react');
const PropTypes = require('prop-types');
/**
* Higher-order component for building an animated studio button
* it is used to decorate the onToggleStudio function with noticing
* when the button has first been clicked.
* This is needed so the buttons don't play the animation when they are
* first rendered but when they are first clicked.
* @param {React.Component} Component a studio button component
* @return {React.Component} a wrapped studio button component
*/
const AnimateHOC = Component => {
class WrappedComponent extends React.Component {
constructor (props) {
super(props);
this.state = {
wasClicked: false
};
this.handleClick = this.handleClick.bind(this);
}
handleClick () {
this.setState({ // else tell the state that the button has been clicked
wasClicked: true
}, () => this.props.onClick(this.props.id)); // callback after state has been updated
}
render () {
const {wasClicked} = this.state;
return (<Component
{...this.props}
wasClicked={wasClicked}
onClick={this.handleClick}
/>);
}
}
WrappedComponent.propTypes = {
id: PropTypes.number,
onClick: PropTypes.func
};
return WrappedComponent;
};
module.exports = AnimateHOC;

View file

@ -89,18 +89,20 @@
pointer-events: none; /* pass clicks through to buttons underneath */ pointer-events: none; /* pass clicks through to buttons underneath */
} }
.studio-selector-button { .studio-selector-button {
display: flex; display: flex;
position: relative; position: relative;
transition: all .5s;
margin: .21875rem .21875rem; margin: .21875rem .21875rem;
border-radius: .5rem; border-radius: .5rem;
background-color: $ui-white; background-color: $ui-white;
cursor: pointer;
padding: 0; padding: 0;
width: 16.1875rem; /* 259px */ width: 16.1875rem; /* 259px */
height: 2.5rem; height: 2.5rem;
box-sizing: border-box; box-sizing: border-box;
justify-content: space-between; justify-content: space-between;
} }
.studio-selector-button-text { .studio-selector-button-text {
@ -112,8 +114,11 @@
*/ */
margin: .575rem 2.18375rem .175rem .6875rem; margin: .575rem 2.18375rem .175rem .6875rem;
width: 13.3125rem; width: 13.3125rem;
height: 1rem; /* diff from spec, in case we ever do valign to middle */ height: 1.25rem; /* diff from spec, in case we ever do valign to middle; changed to match line-height because else with overflow hidden it cuts off some letters */
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.25rem; line-height: 1.25rem;
white-space: nowrap;
font-family: "Helvetica Neue"; font-family: "Helvetica Neue";
font-size: .875rem; font-size: .875rem;
font-weight: regular; font-weight: regular;
@ -160,30 +165,30 @@
background-color: $ui-blue; background-color: $ui-blue;
} }
.studio-status-icon-plus-img { .studio-status-icon-plus-img,
.studio-status-icon-checkmark-img {
animation-direction: normal;
width: 1.4rem; width: 1.4rem;
height: 1.4rem; height: 1.4rem;
transform-origin: center;
} }
.studio-status-icon--img { .studio-status-icon-with-animation {
width: 1.4rem; animation-name: bump;
height: 1.4rem; animation-duration: .25s;
animation-timing-function: cubic-bezier(.3, -3, .6, 3);
animation-iteration-count: 1;
} }
.action-button-text .spinner-smooth { @keyframes bump {
margin: .2125rem auto; 0% {
width: 1.875rem; transform: scale(0);
height: 1rem; opacity: 0;
} -webkit-transform: scale(0);
}
.studio-status-icon .spinner-smooth { 100% {
position: unset; /* don't understand why neither relative nor absolute work */ transform: scale(1);
} opacity: 1;
-webkit-transform: scale(1);
.studio-status-icon .spinner-smooth .circle { }
/* overlay spinner on circle */
position: absolute;
margin: .1875rem; /* stay within boundaries of circle */
width: 75%; /* stay within boundaries of circle */
height: 75%; /* stay within boundaries of circle */
} }

View file

@ -31,7 +31,7 @@ const AddToStudioModalPresentation = ({
includesProject={studio.includesProject} includesProject={studio.includesProject}
key={studio.id} key={studio.id}
title={studio.title} title={studio.title}
onToggleStudio={onToggleStudio} onClick={onToggleStudio}
/> />
)); ));
@ -83,7 +83,7 @@ const AddToStudioModalPresentation = ({
type="submit" type="submit"
> >
<div className="action-button-text"> <div className="action-button-text">
<Spinner mode="smooth" /> <Spinner />
<FormattedMessage id="addToStudio.finishing" /> <FormattedMessage id="addToStudio.finishing" />
</div> </div>
</Button> </Button>

View file

@ -1,29 +1,36 @@
const truncateAtWordBoundary = require('../../../lib/truncate').truncateAtWordBoundary;
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const classNames = require('classnames'); const classNames = require('classnames');
const Spinner = require('../../spinner/spinner.jsx'); const Spinner = require('../../spinner/spinner.jsx');
const AnimateHOC = require('./animate-hoc.jsx');
require('./modal.scss'); require('./modal.scss');
const StudioButton = ({ const StudioButton = ({
hasRequestOutstanding, hasRequestOutstanding,
id,
includesProject, includesProject,
title, title,
onToggleStudio onClick,
wasClicked
}) => { }) => {
const checkmark = ( const checkmark = (
<img <img
alt="checkmark-icon" alt="checkmark-icon"
className="studio-status-icon-checkmark-img" className={classNames(
'studio-status-icon-checkmark-img',
{'studio-status-icon-with-animation': wasClicked}
)}
src="/svgs/modal/confirm.svg" src="/svgs/modal/confirm.svg"
/> />
); );
const plus = ( const plus = (
<img <img
alt="plus-icon" alt="plus-icon"
className="studio-status-icon-plus-img" className={classNames(
'studio-status-icon-plus-img',
{'studio-status-icon-with-animation': wasClicked}
)}
src="/svgs/modal/add.svg" src="/svgs/modal/add.svg"
/> />
); );
@ -35,8 +42,7 @@ const StudioButton = ({
{'studio-selector-button-selected': {'studio-selector-button-selected':
includesProject && !hasRequestOutstanding} includesProject && !hasRequestOutstanding}
)} )}
data-id={id} onClick={onClick}
onClick={onToggleStudio}
> >
<div <div
className={classNames( className={classNames(
@ -44,17 +50,18 @@ const StudioButton = ({
{'studio-selector-button-text-selected': includesProject || hasRequestOutstanding}, {'studio-selector-button-text-selected': includesProject || hasRequestOutstanding},
{'studio-selector-button-text-unselected': !includesProject && !hasRequestOutstanding} {'studio-selector-button-text-unselected': !includesProject && !hasRequestOutstanding}
)} )}
title={title}
> >
{truncateAtWordBoundary(title, 25)} {title}
</div> </div>
<div <div
className={classNames( className={classNames(
'studio-status-icon', 'studio-status-icon',
{'studio-status-icon-unselected': !includesProject} {'studio-status-icon-unselected': !includesProject && !hasRequestOutstanding}
)} )}
> >
{(hasRequestOutstanding ? {(hasRequestOutstanding ?
(<Spinner mode="smooth" />) : <Spinner /> :
(includesProject ? checkmark : plus))} (includesProject ? checkmark : plus))}
</div> </div>
</div> </div>
@ -63,10 +70,10 @@ const StudioButton = ({
StudioButton.propTypes = { StudioButton.propTypes = {
hasRequestOutstanding: PropTypes.bool, hasRequestOutstanding: PropTypes.bool,
id: PropTypes.number,
includesProject: PropTypes.bool, includesProject: PropTypes.bool,
onToggleStudio: PropTypes.func, onClick: PropTypes.func,
title: PropTypes.string title: PropTypes.string,
wasClicked: PropTypes.bool
}; };
module.exports = StudioButton; module.exports = AnimateHOC(StudioButton);

View file

@ -7,7 +7,7 @@ const ReactModal = require('react-modal');
require('./modal.scss'); require('./modal.scss');
ReactModal.setAppElement(document.getElementById('view')); ReactModal.setAppElement(document.getElementById('app'));
/** /**
* Container for pop up windows (See: registration window) * Container for pop up windows (See: registration window)
@ -25,7 +25,7 @@ class Modal extends React.Component {
render () { render () {
return ( return (
<ReactModal <ReactModal
appElement={document.getElementById('view')} appElement={document.getElementById('app')}
className={{ className={{
base: classNames('modal-content', this.props.className), base: classNames('modal-content', this.props.className),
afterOpen: classNames('modal-content', this.props.className), afterOpen: classNames('modal-content', this.props.className),

View file

@ -224,7 +224,7 @@ class ReportModal extends React.Component {
> >
{isWaiting ? ( {isWaiting ? (
<div className="action-button-text"> <div className="action-button-text">
<Spinner mode="smooth" /> <Spinner />
<FormattedMessage id="report.sending" /> <FormattedMessage id="report.sending" />
</div> </div>
) : ( ) : (

View file

@ -0,0 +1,102 @@
const classNames = require('classnames');
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const PropTypes = require('prop-types');
const React = require('react');
const Avatar = require('../../avatar/avatar.jsx');
const Dropdown = require('../../dropdown/dropdown.jsx');
require('./accountnav.scss');
const AccountNav = ({
classroomId,
isEducator,
isOpen,
isStudent,
profileUrl,
thumbnailUrl,
username,
onClick,
onClickLogout,
onClose
}) => (
<div className="account-nav">
<a
className={classNames([
'ignore-react-onclickoutside',
'user-info',
{open: isOpen}
])}
href="#"
onClick={onClick}
>
<Avatar
alt=""
src={thumbnailUrl}
/>
<span className="profile-name">
{username}
</span>
</a>
<Dropdown
as="ul"
className={process.env.SCRATCH_ENV}
isOpen={isOpen}
onRequestClose={onClose}
>
<li>
<a href={profileUrl}>
<FormattedMessage id="general.profile" />
</a>
</li>
<li>
<a href="/mystuff/">
<FormattedMessage id="general.myStuff" />
</a>
</li>
{isEducator ? [
<li key="my-classes-li">
<a href="/educators/classes/">
<FormattedMessage id="general.myClasses" />
</a>
</li>
] : []}
{isStudent ? [
<li key="my-class-li">
<a href={`/classes/${classroomId}/`}>
<FormattedMessage id="general.myClass" />
</a>
</li>
] : []}
<li>
<a href="/accounts/settings/">
<FormattedMessage id="general.accountSettings" />
</a>
</li>
<li className="divider">
<a
href="#"
onClick={onClickLogout}
>
<FormattedMessage id="navigation.signOut" />
</a>
</li>
</Dropdown>
</div>
);
AccountNav.propTypes = {
classroomId: PropTypes.string,
isEducator: PropTypes.bool,
isOpen: PropTypes.bool,
isStudent: PropTypes.bool,
onClick: PropTypes.func,
onClickLogout: PropTypes.func,
onClose: PropTypes.func,
profileUrl: PropTypes.string,
thumbnailUrl: PropTypes.string,
username: PropTypes.string
};
module.exports = injectIntl(AccountNav);

View file

@ -0,0 +1,98 @@
@import "../../../colors";
@import "../../../frameless";
.account-nav {
.user-info {
display: inline-block;
padding: 14px 15px 4px 15px;
max-width: 260px;
height: 33px;
overflow: hidden;
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
color: $type-white;
font-size: .8125rem;
font-weight: normal;
.avatar {
margin-right: 10px;
border-radius: 3px;
width: 24px;
height: 24px;
vertical-align: middle;
}
&:hover {
background-color: $active-gray;
}
&.open {
background-color: $active-gray;
}
&:after {
display: inline-block;
margin-left: 8px;
background-image: url("/images/dropdown.png");
background-repeat: no-repeat;
background-position: center center;
background-size: 50%;
width: 20px;
height: 20px;
vertical-align: middle;
content: " ";
}
}
.dropdown {
top: 50px;
padding: 0;
padding-top: 5px;
width: 100%;
box-sizing: border-box;
}
}
//4 columns
@media only screen and (max-width: $mobile - 1) {
.account-nav {
margin-left: 0;
.user-info {
.avatar {
margin-right: 0;
}
&:after {
display: none;
}
}
}
}
//6 columns
@media only screen and (min-width: $mobile) and (max-width: $tablet - 1) {
.account-nav {
margin-left: 0;
.user-info {
.avatar {
margin-right: 0;
}
&:after {
display: none;
}
}
}
}
//8 columns
@media only screen and (min-width: $tablet) and (max-width: $desktop - 1) {
.account-nav {
margin-left: 0;
}
}

View file

@ -8,19 +8,17 @@ const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const messageCountActions = require('../../../redux/message-count.js'); const messageCountActions = require('../../../redux/message-count.js');
const navigationActions = require('../../../redux/navigation.js');
const sessionActions = require('../../../redux/session.js'); const sessionActions = require('../../../redux/session.js');
const api = require('../../../lib/api');
const Avatar = require('../../avatar/avatar.jsx');
const Button = require('../../forms/button.jsx'); const Button = require('../../forms/button.jsx');
const Dropdown = require('../../dropdown/dropdown.jsx');
const Form = require('../../forms/form.jsx'); const Form = require('../../forms/form.jsx');
const Input = require('../../forms/input.jsx'); const Input = require('../../forms/input.jsx');
const log = require('../../../lib/log.js'); const LoginDropdown = require('../../login/login-dropdown.jsx');
const Login = require('../../login/login.jsx'); const CanceledDeletionModal = require('../../login/canceled-deletion-modal.jsx');
const Modal = require('../../modal/base/modal.jsx');
const NavigationBox = require('../base/navigation.jsx'); const NavigationBox = require('../base/navigation.jsx');
const Registration = require('../../registration/registration.jsx'); const Registration = require('../../registration/registration.jsx');
const AccountNav = require('./accountnav.jsx');
require('./navigation.scss'); require('./navigation.scss');
@ -29,34 +27,16 @@ class Navigation extends React.Component {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'getProfileUrl', 'getProfileUrl',
'handleJoinClick',
'handleLoginClick',
'handleCloseLogin',
'handleLogIn',
'handleLogOut',
'handleAccountNavClick',
'handleCloseAccountNav',
'showCanceledDeletion',
'handleCloseCanceledDeletion',
'handleCloseRegistration',
'handleCompleteRegistration',
'handleSearchSubmit' 'handleSearchSubmit'
]); ]);
this.state = { this.state = {
accountNavOpen: false,
canceledDeletionOpen: false,
loginOpen: false,
loginError: null,
registrationOpen: false,
messageCountIntervalId: -1 // javascript method interval id for getting messsage count. messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
}; };
} }
componentDidMount () { componentDidMount () {
if (this.props.session.session.user) { if (this.props.user) {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
this.props.dispatch( this.props.getMessageCount(this.props.user.username);
messageCountActions.getCount(this.props.session.session.user.username)
);
}, 120000); // check for new messages every 2 mins. }, 120000); // check for new messages every 2 mins.
this.setState({ // eslint-disable-line react/no-did-mount-set-state this.setState({ // eslint-disable-line react/no-did-mount-set-state
messageCountIntervalId: intervalId messageCountIntervalId: intervalId
@ -64,16 +44,11 @@ class Navigation extends React.Component {
} }
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.session.session.user !== this.props.session.session.user) { if (prevProps.user !== this.props.user) {
this.setState({ // eslint-disable-line react/no-did-update-set-state this.props.closeAccountMenus();
loginOpen: false, if (this.props.user) {
accountNavOpen: false
});
if (this.props.session.session.user) {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
this.props.dispatch( this.props.getMessageCount(this.props.user.username);
messageCountActions.getCount(this.props.session.session.user.username)
);
}, 120000); // check for new messages every 2 mins. }, 120000); // check for new messages every 2 mins.
this.setState({ // eslint-disable-line react/no-did-update-set-state this.setState({ // eslint-disable-line react/no-did-update-set-state
messageCountIntervalId: intervalId messageCountIntervalId: intervalId
@ -81,7 +56,7 @@ class Navigation extends React.Component {
} else { } else {
// clear message count check, and set to default id. // clear message count check, and set to default id.
clearInterval(this.state.messageCountIntervalId); clearInterval(this.state.messageCountIntervalId);
this.props.dispatch(messageCountActions.setCount(0)); this.props.setMessageCount(0);
this.setState({ // eslint-disable-line react/no-did-update-set-state this.setState({ // eslint-disable-line react/no-did-update-set-state
messageCountIntervalId: -1 messageCountIntervalId: -1
}); });
@ -92,102 +67,25 @@ class Navigation extends React.Component {
// clear message interval if it exists // clear message interval if it exists
if (this.state.messageCountIntervalId !== -1) { if (this.state.messageCountIntervalId !== -1) {
clearInterval(this.state.messageCountIntervalId); clearInterval(this.state.messageCountIntervalId);
this.props.dispatch(messageCountActions.setCount(0)); this.props.setMessageCount(0);
this.setState({ this.setState({
messageCountIntervalId: -1 messageCountIntervalId: -1
}); });
} }
} }
getProfileUrl () { getProfileUrl () {
if (!this.props.session.session.user) return; if (!this.props.user) return;
return `/users/${this.props.session.session.user.username}/`; return `/users/${this.props.user.username}/`;
}
handleJoinClick (e) {
e.preventDefault();
this.setState({registrationOpen: true});
}
handleLoginClick (e) {
e.preventDefault();
this.setState({loginOpen: !this.state.loginOpen});
}
handleCloseLogin () {
this.setState({loginOpen: false});
}
handleLogIn (formData, callback) {
this.setState({loginError: null});
formData.useMessages = true;
api({
method: 'post',
host: '',
uri: '/accounts/login/',
json: formData,
useCsrf: true
}, (err, body) => {
if (err) this.setState({loginError: err.message});
if (body) {
body = body[0];
if (body.success) {
this.handleCloseLogin();
body.messages.map(message => { // eslint-disable-line array-callback-return
if (message.message === 'canceled-deletion') {
this.showCanceledDeletion();
}
});
this.props.dispatch(sessionActions.refreshSession());
} else {
if (body.redirect) {
window.location = body.redirect;
}
// Update login error message to a friendlier one if it exists
this.setState({loginError: body.msg});
}
}
// JS error already logged by api mixin
callback();
});
}
handleLogOut (e) {
e.preventDefault();
api({
host: '',
method: 'post',
uri: '/accounts/logout/',
useCsrf: true
}, err => {
if (err) log.error(err);
this.handleCloseLogin();
window.location = '/';
});
}
handleAccountNavClick (e) {
e.preventDefault();
this.setState({accountNavOpen: true});
}
handleCloseAccountNav () {
this.setState({accountNavOpen: false});
}
showCanceledDeletion () {
this.setState({canceledDeletionOpen: true});
}
handleCloseCanceledDeletion () {
this.setState({canceledDeletionOpen: false});
}
handleCloseRegistration () {
this.setState({registrationOpen: false});
}
handleCompleteRegistration () {
this.props.dispatch(sessionActions.refreshSession());
this.handleCloseRegistration();
} }
handleSearchSubmit (formData) { handleSearchSubmit (formData) {
window.location.href = `/search/projects?q=${encodeURIComponent(formData.q)}`; window.location.href = `/search/projects?q=${encodeURIComponent(formData.q)}`;
} }
render () { render () {
const createLink = this.props.session.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home'; const createLink = this.props.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
return ( return (
<NavigationBox <NavigationBox
className={classNames({ className={classNames({
'logged-in': this.props.session.session.user 'logged-in': this.props.user
})} })}
> >
<ul> <ul>
@ -235,7 +133,7 @@ class Navigation extends React.Component {
</Form> </Form>
</li> </li>
{this.props.session.status === sessionActions.Status.FETCHED ? ( {this.props.session.status === sessionActions.Status.FETCHED ? (
this.props.session.session.user ? [ this.props.user ? [
<li <li
className="link right messages" className="link right messages"
key="messages" key="messages"
@ -268,66 +166,18 @@ class Navigation extends React.Component {
className="link right account-nav" className="link right account-nav"
key="account-nav" key="account-nav"
> >
<a <AccountNav
className={classNames({ classroomId={this.props.user.classroomId}
'user-info': true, isEducator={this.props.permissions.educator}
'open': this.state.accountNavOpen isOpen={this.props.accountNavOpen}
})} isStudent={this.props.permissions.student}
href="#" profileUrl={this.getProfileUrl()}
onClick={this.handleAccountNavClick} thumbnailUrl={this.props.user.thumbnailUrl}
> username={this.props.user.username}
<Avatar onClick={this.props.handleToggleAccountNav}
alt="" onClickLogout={this.props.handleLogOut}
src={this.props.session.session.user.thumbnailUrl} onClose={this.props.handleCloseAccountNav}
/> />
<span className="profile-name">
{this.props.session.session.user.username}
</span>
</a>
<Dropdown
as="ul"
className={process.env.SCRATCH_ENV}
isOpen={this.state.accountNavOpen}
onRequestClose={this.handleCloseAccountNav}
>
<li>
<a href={this.getProfileUrl()}>
<FormattedMessage id="general.profile" />
</a>
</li>
<li>
<a href="/mystuff/">
<FormattedMessage id="general.myStuff" />
</a>
</li>
{this.props.permissions.educator ? [
<li key="my-classes-li">
<a href="/educators/classes/">
<FormattedMessage id="general.myClasses" />
</a>
</li>
] : []}
{this.props.permissions.student ? [
<li key="my-class-li">
<a href={`/classes/${this.props.session.session.user.classroomId}/`}>
<FormattedMessage id="general.myClass" />
</a>
</li>
] : []}
<li>
<a href="/accounts/settings/">
<FormattedMessage id="general.accountSettings" />
</a>
</li>
<li className="divider">
<a
href="#"
onClick={this.handleLogOut}
>
<FormattedMessage id="navigation.signOut" />
</a>
</li>
</Dropdown>
</li> </li>
] : [ ] : [
<li <li
@ -336,16 +186,13 @@ class Navigation extends React.Component {
> >
<a <a
href="#" href="#"
onClick={this.handleJoinClick} onClick={this.props.handleOpenRegistration}
> >
<FormattedMessage id="general.joinScratch" /> <FormattedMessage id="general.joinScratch" />
</a> </a>
</li>, </li>,
<Registration <Registration
isOpen={this.state.registrationOpen}
key="registration" key="registration"
onRegistrationDone={this.handleCompleteRegistration}
onRequestClose={this.handleCloseRegistration}
/>, />,
<li <li
className="link right login-item" className="link right login-item"
@ -355,53 +202,31 @@ class Navigation extends React.Component {
className="ignore-react-onclickoutside" className="ignore-react-onclickoutside"
href="#" href="#"
key="login-link" key="login-link"
onClick={this.handleLoginClick} onClick={this.props.handleToggleLoginOpen}
> >
<FormattedMessage id="general.signIn" /> <FormattedMessage id="general.signIn" />
</a> </a>
<Dropdown <LoginDropdown
className="login-dropdown with-arrow"
isOpen={this.state.loginOpen}
key="login-dropdown" key="login-dropdown"
onRequestClose={this.handleCloseLogin} />
>
<Login
error={this.state.loginError}
onLogIn={this.handleLogIn}
/>
</Dropdown>
</li> </li>
]) : []} ]) : []}
</ul> </ul>
<Modal <CanceledDeletionModal />
isOpen={this.state.canceledDeletionOpen}
style={{
content: {
padding: 15
}
}}
onRequestClose={this.handleCloseCanceledDeletion}
>
<h4>Your Account Will Not Be Deleted</h4>
<h4><FormattedMessage id="general.noDeletionTitle" /></h4>
<p>
<FormattedMessage
id="general.noDeletionDescription"
values={{
resetLink: <a href="/accounts/password_reset/">
{this.props.intl.formatMessage({id: 'general.noDeletionLink'})}
</a>
}}
/>
</p>
</Modal>
</NavigationBox> </NavigationBox>
); );
} }
} }
Navigation.propTypes = { Navigation.propTypes = {
dispatch: PropTypes.func, accountNavOpen: PropTypes.bool,
closeAccountMenus: PropTypes.func,
getMessageCount: PropTypes.func,
handleCloseAccountNav: PropTypes.func,
handleLogOut: PropTypes.func,
handleOpenRegistration: PropTypes.func,
handleToggleAccountNav: PropTypes.func,
handleToggleLoginOpen: PropTypes.func,
intl: intlShape, intl: intlShape,
permissions: PropTypes.shape({ permissions: PropTypes.shape({
admin: PropTypes.bool, admin: PropTypes.bool,
@ -412,16 +237,15 @@ Navigation.propTypes = {
}), }),
searchTerm: PropTypes.string, searchTerm: PropTypes.string,
session: PropTypes.shape({ session: PropTypes.shape({
session: PropTypes.shape({
user: PropTypes.shape({
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
thumbnailUrl: PropTypes.string,
username: PropTypes.string
})
}),
status: PropTypes.string status: PropTypes.string
}), }),
unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) setMessageCount: PropTypes.func,
unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
user: PropTypes.shape({
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
thumbnailUrl: PropTypes.string,
username: PropTypes.string
})
}; };
Navigation.defaultProps = { Navigation.defaultProps = {
@ -431,12 +255,48 @@ Navigation.defaultProps = {
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
accountNavOpen: state.navigation && state.navigation.accountNavOpen,
session: state.session, session: state.session,
permissions: state.permissions, permissions: state.permissions,
searchTerm: state.navigation.searchTerm,
unreadMessageCount: state.messageCount.messageCount, unreadMessageCount: state.messageCount.messageCount,
searchTerm: state.navigation user: state.session && state.session.session && state.session.session.user
}); });
const ConnectedNavigation = connect(mapStateToProps)(Navigation); const mapDispatchToProps = dispatch => ({
closeAccountMenus: () => {
dispatch(navigationActions.closeAccountMenus());
},
getMessageCount: username => {
dispatch(messageCountActions.getCount(username));
},
handleToggleAccountNav: event => {
event.preventDefault();
dispatch(navigationActions.handleToggleAccountNav());
},
handleCloseAccountNav: () => {
dispatch(navigationActions.setAccountNavOpen(false));
},
handleOpenRegistration: event => {
event.preventDefault();
dispatch(navigationActions.setRegistrationOpen(true));
},
handleLogOut: event => {
event.preventDefault();
dispatch(navigationActions.handleLogOut());
},
handleToggleLoginOpen: event => {
event.preventDefault();
dispatch(navigationActions.toggleLoginOpen());
},
setMessageCount: newCount => {
dispatch(messageCountActions.setCount(newCount));
}
});
const ConnectedNavigation = connect(
mapStateToProps,
mapDispatchToProps
)(Navigation);
module.exports = injectIntl(ConnectedNavigation); module.exports = injectIntl(ConnectedNavigation);

View file

@ -163,74 +163,6 @@
background-image: url("/images/mystuff.png"); background-image: url("/images/mystuff.png");
} }
} }
.login-dropdown {
width: 200px;
.button {
padding: .75em;
}
}
.dropdown {
.row {
margin-bottom: 1.25rem;
input {
margin: 0;
height: 2.25rem;
}
}
}
.account-nav {
.user-info {
padding-top: 14px;
max-width: 260px;
}
> a {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
font-size: .8125rem;
font-weight: normal;
.avatar {
margin-right: 10px;
border-radius: 3px;
width: 24px;
height: 24px;
vertical-align: middle;
}
&.open {
background-color: $active-gray;
}
&:after {
display: inline-block;
margin-left: 8px;
background-image: url("/images/dropdown.png");
background-repeat: no-repeat;
background-position: center center;
background-size: 50%;
width: 20px;
height: 20px;
vertical-align: middle;
content: " ";
}
}
.dropdown {
top: 50px;
padding: 0;
padding-top: 5px;
width: 100%;
box-sizing: border-box;
}
}
} }
//4 columns //4 columns
@ -242,20 +174,6 @@
&.login-item { &.login-item {
margin-left: 0; margin-left: 0;
} }
&.account-nav {
margin-left: 0;
> a {
.avatar {
margin-right: 0;
}
&:after {
display: none;
}
}
}
} }
.create, .create,
@ -280,20 +198,6 @@
&.login-item { &.login-item {
margin-left: 0; margin-left: 0;
} }
&.account-nav {
margin-left: 0;
> a {
.avatar {
margin-right: 0;
}
&:after {
display: none;
}
}
}
} }
.discuss, .discuss,
@ -313,8 +217,7 @@
width: $cols8; width: $cols8;
> ul > li { > ul > li {
&.login-item, &.login-item {
&.account-nav {
margin-left: 0; margin-left: 0;
} }
} }

View file

@ -1,8 +1,10 @@
const bindAll = require('lodash.bindall'); const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const connect = require('react-redux').connect;
const IframeModal = require('../modal/iframe/modal.jsx'); const IframeModal = require('../modal/iframe/modal.jsx');
const navigationActions = require('../../redux/navigation.js');
require('./registration.scss'); require('./registration.scss');
@ -26,7 +28,7 @@ class Registration extends React.Component {
handleMessage (e) { handleMessage (e) {
if (e.origin !== window.location.origin) return; if (e.origin !== window.location.origin) return;
if (e.source !== this.registrationIframe.contentWindow) return; if (e.source !== this.registrationIframe.contentWindow) return;
if (e.data === 'registration-done') this.props.onRegistrationDone(); if (e.data === 'registration-done') this.props.handleCompleteRegistration();
if (e.data === 'registration-relaunch') { if (e.data === 'registration-relaunch') {
this.registrationIframe.contentWindow.location.reload(); this.registrationIframe.contentWindow.location.reload();
} }
@ -47,16 +49,32 @@ class Registration extends React.Component {
}} }}
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
src="/accounts/standalone-registration/" src="/accounts/standalone-registration/"
onRequestClose={this.props.onRequestClose} onRequestClose={this.props.handleCloseRegistration}
/> />
); );
} }
} }
Registration.propTypes = { Registration.propTypes = {
isOpen: PropTypes.bool, handleCloseRegistration: PropTypes.func,
onRegistrationDone: PropTypes.func, handleCompleteRegistration: PropTypes.func,
onRequestClose: PropTypes.func isOpen: PropTypes.bool
}; };
module.exports = Registration; const mapStateToProps = state => ({
isOpen: state.navigation.registrationOpen
});
const mapDispatchToProps = dispatch => ({
handleCloseRegistration: () => {
dispatch(navigationActions.setRegistrationOpen(false));
},
handleCompleteRegistration: () => {
dispatch(navigationActions.handleCompleteRegistration());
}
});
module.exports = connect(
mapStateToProps,
mapDispatchToProps
)(Registration);

View file

@ -1,29 +1,28 @@
const range = require('lodash.range');
const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types');
const classNames = require('classnames');
require('./spinner.scss'); require('./spinner.scss');
// Adapted from http://tobiasahlin.com/spinkit/ // Adapted from http://tobiasahlin.com/spinkit/
const Spinner = ({ const Spinner = ({
mode className,
}) => { color
const spinnerClassName = (mode === 'smooth' ? 'spinner-smooth' : 'spinner'); }) => (
const spinnerDivCount = (mode === 'smooth' ? 24 : 12); <img
return ( alt="loading animation"
<div className={spinnerClassName}> className={classNames('studio-status-icon-spinner', className)}
{range(1, spinnerDivCount + 1).map(id => ( src={`/svgs/modal/spinner-${color}.svg`}
<div />
className={`circle${id} circle`} );
key={`circle${id}`}
/> Spinner.defaultProps = {
))} color: 'white'
</div>
);
}; };
Spinner.propTypes = { Spinner.propTypes = {
mode: PropTypes.string className: PropTypes.string,
color: PropTypes.oneOf(['white', 'blue', 'transparent-gray'])
}; };
module.exports = Spinner; module.exports = Spinner;

View file

@ -1,118 +1,44 @@
@import "../../colors"; .studio-status-icon-spinner {
/* This class can be used on an icon that should spin.
.spinner { It first plays the intro animation, then spins forever. */
position: relative; animation-name: intro, spin;
margin: 0 auto; animation-duration: .25s, .5s;
width: 20px; animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear;
height: 20px; animation-delay: 0s, .25s;
animation-iteration-count: 1, infinite;
.circle { animation-direction: normal;
position: absolute; width: 1.4rem; /* standard is 1.4 rem but can be overwritten by parent */
top: 0; height: 1.4rem;
left: 0; -webkit-animation-name: intro, spin;
width: 100%; -webkit-animation-duration: .25s, .5s;
height: 100%; -webkit-animation-iteration-count: 1, infinite;
-webkit-animation-delay: 0s, .25s;
&:before { -webkit-animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear;
display: block; transform-origin: center;
animation: circleFadeDelay 1.2s infinite ease-in-out both;
margin: 0 auto;
border-radius: 100%;
background-color: $ui-gray;
width: 15%;
height: 15%;
content: "";
.white & {
background-color: $ui-blue-dark;
}
}
}
@for $i from 1 through 12 {
$rotation: 30deg * ($i - 1);
$delay: -1.3s + $i * .1;
.circle#{$i} {
transform: rotate($rotation);
&:before {
animation-delay: $delay;
}
}
}
} }
@keyframes circleFadeDelay { @keyframes intro {
0%, 0% {
39%, transform: scale(0);
opacity: 0;
-webkit-transform: scale(0);
}
100% { 100% {
opacity: 0; transform: scale(1);
}
40% {
opacity: 1; opacity: 1;
-webkit-transform: scale(1);
} }
} }
@keyframes spin {
/*********************/ 0% {
/* type === "smooth" */ transform: rotate(0);
/*********************/ -webkit-transform: rotate(0);
.spinner-smooth {
position: relative;
margin: 0 auto;
width: 20px;
height: 20px;
.circle {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
&:before {
display: block;
animation: circleFadeDelaySmooth 1.8s infinite ease-in-out both;
margin: 0 auto;
border-radius: 100%;
background-color: $ui-white;
width: 30%;
height: 20%;
content: "";
.white & {
background-color: darken($ui-blue, 8%);
}
}
} }
@for $i from 1 through 24 { 100% {
$rotation: 15deg * ($i - 1); transform: rotate(359deg);
$delay: -1.9s + $i * .075; -webkit-transform: rotate(359deg);
.circle#{$i} {
transform: rotate($rotation);
&:before {
animation-delay: $delay;
}
}
}
}
@keyframes circleFadeDelaySmooth {
0%,
35% {
opacity: 0;
},
40% {
opacity: 1;
} }
} }

View file

@ -74,6 +74,7 @@ const Thumbnail = props => {
<a <a
href={props.href} href={props.href}
key="titleElement" key="titleElement"
title={props.title}
> >
{props.title} {props.title}
</a> </a>

View file

@ -16,14 +16,13 @@
.thumbnail { .thumbnail {
margin: 7px; margin: 7px;
border-radius: 4px;
box-shadow: 0 0 0 1px $active-gray;
background-color: $ui-white;
padding-bottom: 4px;
width: $thumbnail-width; width: $thumbnail-width;
.thumbnail-image { .thumbnail-image {
margin: 8px auto; margin: 8px auto;
border-radius: 4px;
box-shadow: 0 0 0 1px $active-gray;
background-color: $ui-white;
width: $thumbnail-inner-width; width: $thumbnail-inner-width;
} }
@ -45,10 +44,18 @@
.thumbnail-title { .thumbnail-title {
float: left; float: left;
max-width: 164px; max-width: 164px;
overflow: hidden;
.thumbnail-creator a { .thumbnail-creator a {
color: $type-gray; color: $type-gray;
} }
a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: break-word;
}
} }
} }

View file

@ -35,6 +35,7 @@ const Raven = require('raven-js');
}; };
window._locale = updateLocale(); window._locale = updateLocale();
document.documentElement.lang = window._locale;
})(); })();
/** /**

View file

@ -106,6 +106,8 @@
"navigation.signOut": "Sign out", "navigation.signOut": "Sign out",
"extensionHeader.requirements": "Requirements", "extensionHeader.requirements": "Requirements",
"extensionInstallation.addExtension": "In the editor, click on the \"Add Extensions\" button on the lower left.",
"oschooser.choose": "Choose your OS:", "oschooser.choose": "Choose your OS:",

View file

@ -1,9 +0,0 @@
const lodashTruncate = require('lodash.truncate');
/*
* Function that applies regex for word boundaries, replaces removed string
* with indication of ellipsis (...)
*/
module.exports.truncateAtWordBoundary = (str, length) => (
lodashTruncate(str, {length: length, separator: /[.,:;]*\s+/})
);

View file

@ -1,22 +1,153 @@
const keyMirror = require('keymirror'); const keyMirror = require('keymirror');
const defaults = require('lodash.defaults');
const api = require('../lib/api');
const log = require('../lib/log.js');
const sessionActions = require('./session.js');
const Types = keyMirror({ const Types = keyMirror({
SET_SEARCH_TERM: null SET_SEARCH_TERM: null,
SET_ACCOUNT_NAV_OPEN: null,
TOGGLE_ACCOUNT_NAV_OPEN: null,
SET_LOGIN_ERROR: null,
SET_LOGIN_OPEN: null,
TOGGLE_LOGIN_OPEN: null,
SET_CANCELED_DELETION_OPEN: null,
SET_REGISTRATION_OPEN: null
}); });
module.exports.getInitialState = () => ({
accountNavOpen: false,
canceledDeletionOpen: false,
loginError: null,
loginOpen: false,
registrationOpen: false,
searchTerm: ''
});
module.exports.navigationReducer = (state, action) => { module.exports.navigationReducer = (state, action) => {
if (typeof state === 'undefined') { if (typeof state === 'undefined') {
state = ''; state = module.exports.getInitialState();
} }
switch (action.type) { switch (action.type) {
case Types.SET_SEARCH_TERM: case Types.SET_SEARCH_TERM:
return action.searchTerm; return defaults({searchTerm: action.searchTerm}, state);
case Types.SET_ACCOUNT_NAV_OPEN:
return defaults({accountNavOpen: action.isOpen}, state);
case Types.TOGGLE_ACCOUNT_NAV_OPEN:
return defaults({accountNavOpen: !state.accountNavOpen}, state);
case Types.SET_LOGIN_ERROR:
return defaults({loginError: action.loginError}, state);
case Types.SET_LOGIN_OPEN:
return defaults({loginOpen: action.isOpen}, state);
case Types.TOGGLE_LOGIN_OPEN:
return defaults({loginOpen: !state.loginOpen}, state);
case Types.SET_CANCELED_DELETION_OPEN:
return defaults({canceledDeletionOpen: action.isOpen}, state);
case Types.SET_REGISTRATION_OPEN:
return defaults({registrationOpen: action.isOpen}, state);
default: default:
return state; return state;
} }
}; };
module.exports.setAccountNavOpen = isOpen => ({
type: Types.SET_ACCOUNT_NAV_OPEN,
isOpen: isOpen
});
module.exports.handleToggleAccountNav = () => ({
type: Types.TOGGLE_ACCOUNT_NAV_OPEN
});
module.exports.setCanceledDeletionOpen = isOpen => ({
type: Types.SET_CANCELED_DELETION_OPEN,
isOpen: isOpen
});
module.exports.setLoginError = loginError => ({
type: Types.SET_LOGIN_ERROR,
loginError: loginError
});
module.exports.setLoginOpen = isOpen => ({
type: Types.SET_LOGIN_OPEN,
isOpen: isOpen
});
module.exports.toggleLoginOpen = () => ({
type: Types.TOGGLE_LOGIN_OPEN
});
module.exports.setRegistrationOpen = isOpen => ({
type: Types.SET_REGISTRATION_OPEN,
isOpen: isOpen
});
module.exports.setSearchTerm = searchTerm => ({ module.exports.setSearchTerm = searchTerm => ({
type: Types.SET_SEARCH_TERM, type: Types.SET_SEARCH_TERM,
searchTerm: searchTerm searchTerm: searchTerm
}); });
module.exports.handleCompleteRegistration = () => (dispatch => {
dispatch(sessionActions.refreshSession());
dispatch(module.exports.setRegistrationOpen(false));
});
module.exports.closeAccountMenus = () => (dispatch => {
dispatch(module.exports.setAccountNavOpen(false));
dispatch(module.exports.setRegistrationOpen(false));
});
module.exports.handleLogIn = (formData, callback) => (dispatch => {
dispatch(module.exports.setLoginError(null));
formData.useMessages = true; // NOTE: this may or may not be being used anywhere else
api({
method: 'post',
host: '',
uri: '/accounts/login/',
json: formData,
useCsrf: true
}, (err, body) => {
if (err) dispatch(module.exports.setLoginError(err.message));
if (body) {
body = body[0];
if (body.success) {
dispatch(module.exports.setLoginOpen(false));
body.messages.forEach(message => {
if (message.message === 'canceled-deletion') {
dispatch(module.exports.setCanceledDeletionOpen(true));
}
});
dispatch(sessionActions.refreshSession());
callback({success: true});
} else {
if (body.redirect) {
window.location = body.redirect;
}
// Update login error message to a friendlier one if it exists
dispatch(module.exports.setLoginError(body.msg));
// JS error already logged by api mixin
callback({success: false});
}
} else {
// JS error already logged by api mixin
callback({success: false});
}
});
});
module.exports.handleLogOut = () => (dispatch => {
api({
host: '',
method: 'post',
uri: '/accounts/logout/',
useCsrf: true
}, err => {
if (err) log.error(err);
dispatch(module.exports.setLoginOpen(false));
dispatch(module.exports.setAccountNavOpen(false));
window.location = '/';
});
});

View file

@ -4,6 +4,7 @@ const defaults = require('lodash.defaults');
const messageCountReducer = require('./message-count.js').messageCountReducer; const messageCountReducer = require('./message-count.js').messageCountReducer;
const permissionsReducer = require('./permissions.js').permissionsReducer; const permissionsReducer = require('./permissions.js').permissionsReducer;
const sessionReducer = require('./session.js').sessionReducer; const sessionReducer = require('./session.js').sessionReducer;
const navigationReducer = require('./navigation.js').navigationReducer;
/** /**
* Returns a combined reducer to be used for a page in `render.jsx`. * Returns a combined reducer to be used for a page in `render.jsx`.
@ -18,8 +19,9 @@ const sessionReducer = require('./session.js').sessionReducer;
module.exports = opts => { module.exports = opts => {
opts = opts || {}; opts = opts || {};
return combineReducers(defaults(opts, { return combineReducers(defaults(opts, {
session: sessionReducer, messageCount: messageCountReducer,
navigation: navigationReducer,
permissions: permissionsReducer, permissions: permissionsReducer,
messageCount: messageCountReducer session: sessionReducer
})); }));
}; };

View file

@ -77,13 +77,13 @@ module.exports.getActivity = (username, token) => (dispatch => {
api({ api({
uri: `/users/${username}/following/users/activity?limit=5`, uri: `/users/${username}/following/users/activity?limit=5`,
authentication: token authentication: token
}, (err, body) => { }, (err, body, res) => {
if (err) { if (err) {
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
dispatch(module.exports.setError(err)); dispatch(module.exports.setError(err));
return; return;
} }
if (typeof body === 'undefined') { if (typeof body === 'undefined' || res.statusCode !== 200) {
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
dispatch(module.exports.setError('No session content')); dispatch(module.exports.setError('No session content'));
return; return;
@ -100,13 +100,13 @@ module.exports.getFeaturedGlobal = () => (dispatch => {
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.FETCHING)); dispatch(module.exports.setFetchStatus('featured', module.exports.Status.FETCHING));
api({ api({
uri: '/proxy/featured' uri: '/proxy/featured'
}, (err, body) => { }, (err, body, res) => {
if (err) { if (err) {
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
dispatch(module.exports.setError(err)); dispatch(module.exports.setError(err));
return; return;
} }
if (typeof body === 'undefined') { if (typeof body === 'undefined' || res.statusCode !== 200) {
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
dispatch(module.exports.setError('No session content')); dispatch(module.exports.setError('No session content'));
return; return;
@ -126,13 +126,13 @@ module.exports.getSharedByFollowing = (username, token) => (dispatch => {
api({ api({
uri: `/users/${username}/following/users/projects`, uri: `/users/${username}/following/users/projects`,
authentication: token authentication: token
}, (err, body) => { }, (err, body, res) => {
if (err) { if (err) {
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.Status.ERROR)); dispatch(module.exports.setFetchStatus('shared', module.exports.Status.Status.ERROR));
dispatch(module.exports.setError(err)); dispatch(module.exports.setError(err));
return; return;
} }
if (typeof body === 'undefined') { if (typeof body === 'undefined' || res.statusCode !== 200) {
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('shared', module.exports.Status.ERROR));
dispatch(module.exports.setError('No session content')); dispatch(module.exports.setError('No session content'));
return; return;
@ -152,13 +152,13 @@ module.exports.getInStudiosFollowing = (username, token) => (dispatch => {
api({ api({
uri: `/users/${username}/following/studios/projects`, uri: `/users/${username}/following/studios/projects`,
authentication: token authentication: token
}, (err, body) => { }, (err, body, res) => {
if (err) { if (err) {
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
dispatch(module.exports.setError(err)); dispatch(module.exports.setError(err));
return; return;
} }
if (typeof body === 'undefined') { if (typeof body === 'undefined' || res.statusCode !== 200) {
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
dispatch(module.exports.setError('No session content')); dispatch(module.exports.setError('No session content'));
return; return;
@ -178,13 +178,13 @@ module.exports.getLovedByFollowing = (username, token) => (dispatch => {
api({ api({
uri: `/users/${username}/following/users/loves`, uri: `/users/${username}/following/users/loves`,
authentication: token authentication: token
}, (err, body) => { }, (err, body, res) => {
if (err) { if (err) {
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
dispatch(module.exports.setError(err)); dispatch(module.exports.setError(err));
return; return;
} }
if (typeof body === 'undefined') { if (typeof body === 'undefined' || res.statusCode !== 200) {
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR)); dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
dispatch(module.exports.setError('No session content')); dispatch(module.exports.setError('No session content'));
return; return;

View file

@ -35,8 +35,10 @@ const Components = () => (
<Box title="Carousel component in a box!"> <Box title="Carousel component in a box!">
<Carousel /> <Carousel />
</Box> </Box>
<h1>This is a Spinner</h1> <h1>This is a blue Spinner</h1>
<Spinner /> <Spinner
color="blue"
/>
<h1>Colors</h1> <h1>Colors</h1>
<div className="colors"> <div className="colors">
<span className="ui-blue">$ui-blue</span> <span className="ui-blue">$ui-blue</span>

View file

@ -86,6 +86,14 @@ const Credits = () => (
<span className="name">DD Liu</span> <span className="name">DD Liu</span>
</li> </li>
<li>
<img
alt="Katelyn Avatar"
src="//cdn.scratch.mit.edu/get_image/user/34607790_170x170.png"
/>
<span className="name">Katelyn Mann</span>
</li>
<li> <li>
<img <img
alt="Shruti Avatar" alt="Shruti Avatar"

View file

@ -31,9 +31,15 @@ class EV3 extends ExtensionLanding {
render () { render () {
return ( return (
<div className="extension-landing ev3"> <div className="extension-landing ev3">
<ExtensionHeader imageSrc="/images/ev3/ev3-illustration.png"> <ExtensionHeader
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltEv3Illustration'})}
imageSrc="/images/ev3/ev3-illustration.png"
>
<FlexRow className="column extension-copy"> <FlexRow className="column extension-copy">
<h2><img src="/images/ev3/ev3.svg" />LEGO MINDSTORMS EV3</h2> <h1><img
alt=""
src="/images/ev3/ev3.svg"
/>LEGO MINDSTORMS EV3</h1>
<FormattedMessage <FormattedMessage
id="ev3.headerText" id="ev3.headerText"
values={{ values={{
@ -51,11 +57,17 @@ class EV3 extends ExtensionLanding {
</FlexRow> </FlexRow>
<ExtensionRequirements> <ExtensionRequirements>
<span> <span>
<img src="/svgs/extensions/windows.svg" /> <img
alt=""
src="/svgs/extensions/windows.svg"
/>
Windows 10+ Windows 10+
</span> </span>
<span> <span>
<img src="/svgs/extensions/mac.svg" /> <img
alt=""
src="/svgs/extensions/mac.svg"
/>
macOS 10.13+ macOS 10.13+
</span> </span>
<span> <span>
@ -63,7 +75,10 @@ class EV3 extends ExtensionLanding {
Bluetooth Bluetooth
</span> </span>
<span> <span>
<img src="/svgs/extensions/scratch-link.svg" /> <img
alt=""
src="/svgs/extensions/scratch-link.svg"
/>
Scratch Link Scratch Link
</span> </span>
</ExtensionRequirements> </ExtensionRequirements>
@ -82,13 +97,17 @@ class EV3 extends ExtensionLanding {
<Steps> <Steps>
<Step number={1}> <Step number={1}>
<div className="step-image"> <div className="step-image">
<img src="/images/ev3/ev3-connect-1.png" /> <img
alt=""
src="/images/ev3/ev3-connect-1.png"
/>
</div> </div>
<p><FormattedMessage id="ev3.turnOnEV3" /></p> <p><FormattedMessage id="ev3.turnOnEV3" /></p>
</Step> </Step>
<Step number={2}> <Step number={2}>
<div className="step-image"> <div className="step-image">
<img <img
alt=""
className="screenshot" className="screenshot"
src="/images/ev3/ev3-connect-2.png" src="/images/ev3/ev3-connect-2.png"
/> />
@ -113,6 +132,7 @@ class EV3 extends ExtensionLanding {
<Step number={3}> <Step number={3}>
<div className="step-image"> <div className="step-image">
<img <img
alt={this.props.intl.formatMessage({id: 'extensionInstallation.addExtension'})}
className="screenshot" className="screenshot"
src="/images/ev3/ev3-connect-3.png" src="/images/ev3/ev3-connect-3.png"
/> />
@ -125,19 +145,30 @@ class EV3 extends ExtensionLanding {
<Steps> <Steps>
<Step> <Step>
<div className="step-image"> <div className="step-image">
<img src="/images/ev3/ev3-accept-connection.png" /> <img
alt={this.props.intl.formatMessage({id: 'ev3.imgAltAcceptConnection'})}
src="/images/ev3/ev3-accept-connection.png"
/>
</div> </div>
<p><FormattedMessage id="ev3.acceptConnection" /></p> <p><FormattedMessage id="ev3.acceptConnection" /></p>
</Step> </Step>
<Step> <Step>
<div className="step-image"> <div className="step-image">
<img src="/images/ev3/ev3-pin.png" /> <img
alt={this.props.intl.formatMessage({id: 'ev3.imgAltAcceptPasscode'})}
src="/images/ev3/ev3-pin.png"
/>
</div> </div>
<p><FormattedMessage id="ev3.acceptPasscode" /></p> <p><FormattedMessage id="ev3.acceptPasscode" /></p>
</Step> </Step>
<Step> <Step>
<div className="step-image"> <div className="step-image">
<img <img
alt={this.props.intl.formatMessage({id: `ev3.imgAlt${
this.state.OS === OS_ENUM.WINDOWS ?
'WaitForWindows' :
'EnterPasscodeMac'
}`})}
className="screenshot" className="screenshot"
src={`/images/ev3/${ src={`/images/ev3/${
this.state.OS === OS_ENUM.WINDOWS ? this.state.OS === OS_ENUM.WINDOWS ?
@ -176,7 +207,10 @@ class EV3 extends ExtensionLanding {
/> />
</span> </span>
<div className="step-image"> <div className="step-image">
<img src="/images/ev3/ev3-motor-port-a.png" /> <img
alt={this.props.intl.formatMessage({id: 'ev3.imgAltPlugInMotor'})}
src="/images/ev3/ev3-motor-port-a.png"
/>
</div> </div>
</Step> </Step>
<Step <Step
@ -194,7 +228,10 @@ class EV3 extends ExtensionLanding {
/> />
</span> </span>
<div className="step-image"> <div className="step-image">
<img src="/images/ev3/motor-turn-block.png" /> <img
alt=""
src="/images/ev3/motor-turn-block.png"
/>
</div> </div>
</Step> </Step>
</Steps> </Steps>
@ -204,18 +241,21 @@ class EV3 extends ExtensionLanding {
<ProjectCard <ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239075992" cardUrl="https://beta.scratch.mit.edu/#239075992"
description={this.props.intl.formatMessage({id: 'ev3.waveHelloDescription'})} description={this.props.intl.formatMessage({id: 'ev3.waveHelloDescription'})}
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltWaveHello'})}
imageSrc="/images/ev3/starter-wave-hello.png" imageSrc="/images/ev3/starter-wave-hello.png"
title={this.props.intl.formatMessage({id: 'ev3.waveHelloTitle'})} title={this.props.intl.formatMessage({id: 'ev3.waveHelloTitle'})}
/> />
<ProjectCard <ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239076020" cardUrl="https://beta.scratch.mit.edu/#239076020"
description={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentDescription'})} description={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentDescription'})}
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltDistanceInstrument'})}
imageSrc="/images/ev3/starter-distance-instrument.png" imageSrc="/images/ev3/starter-distance-instrument.png"
title={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentTitle'})} title={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentTitle'})}
/> />
<ProjectCard <ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239076044" cardUrl="https://beta.scratch.mit.edu/#239076044"
description={this.props.intl.formatMessage({id: 'ev3.spaceTacosDescription'})} description={this.props.intl.formatMessage({id: 'ev3.spaceTacosDescription'})}
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltSpaceTacos'})}
imageSrc="/images/ev3/starter-flying-game.png" imageSrc="/images/ev3/starter-flying-game.png"
title={this.props.intl.formatMessage({id: 'ev3.spaceTacosTitle'})} title={this.props.intl.formatMessage({id: 'ev3.spaceTacosTitle'})}
/> />

View file

@ -34,5 +34,14 @@
"ev3.otherComputerConnectedText": "Only one computer can be connected to an EV3 at a time. If you have another computer connected to your EV3, disconnect the EV3 or close Scratch on that computer and try again.", "ev3.otherComputerConnectedText": "Only one computer can be connected to an EV3 at a time. If you have another computer connected to your EV3, disconnect the EV3 or close Scratch on that computer and try again.",
"ev3.updateFirmwareTitle": "Try updating your EV3 firmware", "ev3.updateFirmwareTitle": "Try updating your EV3 firmware",
"ev3.updateFirmwareText": "We recommend updating to EV3 firmware version 1.10E or above. See {firmwareUpdateLink}.", "ev3.updateFirmwareText": "We recommend updating to EV3 firmware version 1.10E or above. See {firmwareUpdateLink}.",
"ev3.firmwareUpdateText": "firmware update instructions from LEGO" "ev3.firmwareUpdateText": "firmware update instructions from LEGO",
"ev3.imgAltEv3Illustration": "Illustration of an EV3 hub, featuring some examples of interacting with it.",
"ev3.imgAltAcceptConnection": "Use the buttons on your EV3 to accept the connection.",
"ev3.imgAltAcceptPasscode": "Use the center button on your EV3 to accept the passcode.",
"ev3.imgAltWaitForWindows": "Windows will notify you when the EV3 is ready.",
"ev3.imgAltEnterPasscodeMac": "Enter the passcode into the connection request window opening on your Mac.",
"ev3.imgAltPlugInMotor": "To find port A: hold the EV3 with the screen and buttons facing you, with the screen above the buttons. Port A is on top, and it is the left-most one",
"ev3.imgAltWaveHello": "A Scratch project with a waving fairy.",
"ev3.imgAltDistanceInstrument": "A Scratch project with a guitar.",
"ev3.imgAltSpaceTacos": "A Scratch project with Scratch Cat and a taco in space."
} }

View file

@ -35,14 +35,6 @@ const Jobs = () => (
MIT Media Lab, Cambridge, MA MIT Media Lab, Cambridge, MA
</span> </span>
</li> </li>
<li>
<a href="https://scratch.mit.edu/jobs/moderator">
Community Moderator
</a>
<span>
Remote
</span>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -28,5 +28,11 @@
"microbit.otherComputerConnectedTitle": "Make sure no other computer is connected to your micro:bit", "microbit.otherComputerConnectedTitle": "Make sure no other computer is connected to your micro:bit",
"microbit.otherComputerConnectedText": "Only one computer can be connected to an micro:bit at a time. If you have another computer connected to your micro:bit, disconnect the micro:bit or close Scratch on that computer and try again.", "microbit.otherComputerConnectedText": "Only one computer can be connected to an micro:bit at a time. If you have another computer connected to your micro:bit, disconnect the micro:bit or close Scratch on that computer and try again.",
"microbit.resetButtonTitle": "Make sure you arent hitting the “reset” button", "microbit.resetButtonTitle": "Make sure you arent hitting the “reset” button",
"microbit.resetButtonText": "Sometimes while using the micro:bit you can accidentally press the “reset” button on the back in-between the USB and power ports. Make sure you keep your fingers (and toes) away from it while using Scratch!" "microbit.resetButtonText": "Sometimes while using the micro:bit you can accidentally press the “reset” button on the back in-between the USB and power ports. Make sure you keep your fingers (and toes) away from it while using Scratch!",
"microbit.imgAltMicrobitIllustration": "Illustration of the micro:bit circuit board.",
"microbit.imgAltDragDropHex": "Drag and drop the HEX file from the folder you downloaded it to to the micro:bit.",
"microbit.imgAltDisplayH": "A micro:bit displaying an H.",
"microbit.imgAltHeartBeat" : "A Scratch project with a heart.",
"microbit.imgAltTiltGuitar": "A Scratch project with a guitar.",
"microbit.imgAltOceanAdventure": "A Scratch project with a clown fish and a saxophone under water."
} }

View file

@ -31,9 +31,15 @@ class MicroBit extends ExtensionLanding {
render () { render () {
return ( return (
<div className="extension-landing microbit"> <div className="extension-landing microbit">
<ExtensionHeader imageSrc="/images/microbit/microbit-heart.png"> <ExtensionHeader
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltMicrobitIllustration'})}
imageSrc="/images/microbit/microbit-heart.png"
>
<FlexRow className="column extension-copy"> <FlexRow className="column extension-copy">
<h2><img src="/images/microbit/microbit.svg" />micro:bit</h2> <h1><img
alt=""
src="/images/microbit/microbit.svg"
/>micro:bit</h1>
<FormattedMessage <FormattedMessage
id="microbit.headerText" id="microbit.headerText"
values={{ values={{
@ -51,19 +57,31 @@ class MicroBit extends ExtensionLanding {
</FlexRow> </FlexRow>
<ExtensionRequirements> <ExtensionRequirements>
<span> <span>
<img src="/svgs/extensions/windows.svg" /> <img
alt=""
src="/svgs/extensions/windows.svg"
/>
Windows 10+ Windows 10+
</span> </span>
<span> <span>
<img src="/svgs/extensions/mac.svg" /> <img
alt=""
src="/svgs/extensions/mac.svg"
/>
macOS 10.13+ macOS 10.13+
</span> </span>
<span> <span>
<img src="/svgs/extensions/bluetooth.svg" /> <img
alt=""
src="/svgs/extensions/bluetooth.svg"
/>
Bluetooth 4.0 Bluetooth 4.0
</span> </span>
<span> <span>
<img src="/svgs/extensions/scratch-link.svg" /> <img
alt=""
src="/svgs/extensions/scratch-link.svg"
/>
Scratch Link Scratch Link
</span> </span>
</ExtensionRequirements> </ExtensionRequirements>
@ -82,7 +100,10 @@ class MicroBit extends ExtensionLanding {
<Steps> <Steps>
<Step number={1}> <Step number={1}>
<div className="step-image"> <div className="step-image">
<img src="/images/microbit/mbit-usb.png" /> <img
alt=""
src="/images/microbit/mbit-usb.png"
/>
</div> </div>
<p> <p>
<FormattedMessage id="microbit.connectUSB" /> <FormattedMessage id="microbit.connectUSB" />
@ -90,7 +111,10 @@ class MicroBit extends ExtensionLanding {
</Step> </Step>
<Step number={2}> <Step number={2}>
<div className="step-image"> <div className="step-image">
<img src="/images/microbit/mbit-hex-download.png" /> <img
alt=""
src="/images/microbit/mbit-hex-download.png"
/>
</div> </div>
<a <a
download download
@ -103,6 +127,7 @@ class MicroBit extends ExtensionLanding {
<Step number={3}> <Step number={3}>
<div className="step-image"> <div className="step-image">
<img <img
alt={this.props.intl.formatMessage({id: 'microbit.imgAltDragDropHex'})}
src={`/images/microbit/${ src={`/images/microbit/${
this.state.OS === OS_ENUM.WINDOWS ? 'win' : 'mac' this.state.OS === OS_ENUM.WINDOWS ? 'win' : 'mac'
}-copy-hex.png`} }-copy-hex.png`}
@ -120,13 +145,17 @@ class MicroBit extends ExtensionLanding {
<Steps> <Steps>
<Step number={1}> <Step number={1}>
<div className="step-image"> <div className="step-image">
<img src="/images/microbit/mbit-connect-1.png" /> <img
alt=""
src="/images/microbit/mbit-connect-1.png"
/>
</div> </div>
<p><FormattedMessage id="microbit.powerMicrobit" /></p> <p><FormattedMessage id="microbit.powerMicrobit" /></p>
</Step> </Step>
<Step number={2}> <Step number={2}>
<div className="step-image"> <div className="step-image">
<img <img
alt=""
className="screenshot" className="screenshot"
src="/images/microbit/mbit-connect-2.png" src="/images/microbit/mbit-connect-2.png"
/> />
@ -151,6 +180,7 @@ class MicroBit extends ExtensionLanding {
<Step number={3}> <Step number={3}>
<div className="step-image"> <div className="step-image">
<img <img
alt={this.props.intl.formatMessage({id: 'extensionInstallation.addExtension'})}
className="screenshot" className="screenshot"
src="/images/microbit/mbit-connect-3.png" src="/images/microbit/mbit-connect-3.png"
/> />
@ -181,7 +211,10 @@ class MicroBit extends ExtensionLanding {
/> />
</span> </span>
<div className="step-image"> <div className="step-image">
<img src="/images/microbit/display-hello-block.png" /> <img
alt=""
src="/images/microbit/display-hello-block.png"
/>
</div> </div>
</Step> </Step>
<Step <Step
@ -199,7 +232,10 @@ class MicroBit extends ExtensionLanding {
/> />
</span> </span>
<div className="step-image"> <div className="step-image">
<img src="/images/microbit/mbit-display-h.png" /> <img
alt={this.props.intl.formatMessage({id: 'microbit.imgAltDisplayH'})}
src="/images/microbit/mbit-display-h.png"
/>
</div> </div>
</Step> </Step>
</Steps> </Steps>
@ -209,18 +245,21 @@ class MicroBit extends ExtensionLanding {
<ProjectCard <ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239075756" cardUrl="https://beta.scratch.mit.edu/#239075756"
description={this.props.intl.formatMessage({id: 'microbit.heartBeatDescription'})} description={this.props.intl.formatMessage({id: 'microbit.heartBeatDescription'})}
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltHeartBeat'})}
imageSrc="/images/microbit/starter-heart.png" imageSrc="/images/microbit/starter-heart.png"
title={this.props.intl.formatMessage({id: 'microbit.heartBeat'})} title={this.props.intl.formatMessage({id: 'microbit.heartBeat'})}
/> />
<ProjectCard <ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239075950" cardUrl="https://beta.scratch.mit.edu/#239075950"
description={this.props.intl.formatMessage({id: 'microbit.tiltGuitarDescription'})} description={this.props.intl.formatMessage({id: 'microbit.tiltGuitarDescription'})}
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltTiltGuitar'})}
imageSrc="/images/microbit/starter-guitar.png" imageSrc="/images/microbit/starter-guitar.png"
title={this.props.intl.formatMessage({id: 'microbit.tiltGuitar'})} title={this.props.intl.formatMessage({id: 'microbit.tiltGuitar'})}
/> />
<ProjectCard <ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239075973" cardUrl="https://beta.scratch.mit.edu/#239075973"
description={this.props.intl.formatMessage({id: 'microbit.oceanAdventureDescription'})} description={this.props.intl.formatMessage({id: 'microbit.oceanAdventureDescription'})}
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltOceanAdventure'})}
imageSrc="/images/microbit/starter-fish.png" imageSrc="/images/microbit/starter-fish.png"
title={this.props.intl.formatMessage({id: 'microbit.oceanAdventure'})} title={this.props.intl.formatMessage({id: 'microbit.oceanAdventure'})}
/> />

View file

@ -26,6 +26,7 @@
font-size: .875rem; font-size: .875rem;
justify-content: center; justify-content: center;
flex-flow: column; flex-flow: column;
align-items: flex-start;
} }
.extension-status { .extension-status {

View file

@ -1,6 +1,7 @@
{ {
"addToStudio.title": "Add to Studio", "addToStudio.title": "Add to Studio",
"addToStudio.finishing": "Finishing up...", "addToStudio.finishing": "Finishing up...",
"preview.titleMaxLength": "Title is too long",
"preview.musicExtensionChip": "Music", "preview.musicExtensionChip": "Music",
"preview.penExtensionChip": "Pen", "preview.penExtensionChip": "Pen",
"preview.speechExtensionChip": "Google Speech", "preview.speechExtensionChip": "Google Speech",

View file

@ -1,6 +1,8 @@
const FormattedDate = require('react-intl').FormattedDate; const FormattedDate = require('react-intl').FormattedDate;
const injectIntl = require('react-intl').injectIntl; const injectIntl = require('react-intl').injectIntl;
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const intlShape = require('react-intl').intlShape;
const MediaQuery = require('react-responsive').default;
const React = require('react'); const React = require('react');
const Formsy = require('formsy-react').default; const Formsy = require('formsy-react').default;
const classNames = require('classnames'); const classNames = require('classnames');
@ -27,6 +29,18 @@ const ExtensionChip = require('./extension-chip.jsx');
const projectShape = require('./projectshape.jsx').projectShape; const projectShape = require('./projectshape.jsx').projectShape;
require('./preview.scss'); require('./preview.scss');
const frameless = require('../../lib/frameless');
// disable enter key submission on formsy input fields; otherwise formsy thinks
// we meant to trigger the "See inside" button. Instead, treat these keypresses
// as a blur, which will trigger a save.
const onKeyPress = e => {
if (e.target.type === 'text' && e.which === 13 /* Enter */) {
e.preventDefault();
e.target.blur();
}
};
const PreviewPresentation = ({ const PreviewPresentation = ({
assetHost, assetHost,
backpackOptions, backpackOptions,
@ -35,6 +49,7 @@ const PreviewPresentation = ({
extensions, extensions,
faved, faved,
favoriteCount, favoriteCount,
intl,
isFullScreen, isFullScreen,
isLoggedIn, isLoggedIn,
isShared, isShared,
@ -70,7 +85,7 @@ const PreviewPresentation = ({
<ShareBanner shared={isShared} /> <ShareBanner shared={isShared} />
{ projectInfo && projectInfo.author && projectInfo.author.id && ( { projectInfo && projectInfo.author && projectInfo.author.id && (
<Formsy> <Formsy onKeyPress={onKeyPress}>
<div className="inner"> <div className="inner">
<FlexRow className="preview-row"> <FlexRow className="preview-row">
<FlexRow className="project-header"> <FlexRow className="project-header">
@ -88,10 +103,9 @@ const PreviewPresentation = ({
handleUpdate={onUpdate} handleUpdate={onUpdate}
name="title" name="title"
validationErrors={{ validationErrors={{
maxLength: 'Sorry title is too long' maxLength: intl.formatMessage({
// maxLength: props.intl.formatMessage({ id: 'preview.titleMaxLength'
// id: 'project.titleMaxLength' })
// })
}} }}
validations={{ validations={{
maxLength: 100 maxLength: 100
@ -99,7 +113,10 @@ const PreviewPresentation = ({
value={projectInfo.title} value={projectInfo.title}
/> : /> :
<React.Fragment> <React.Fragment>
<div className="project-title">{projectInfo.title}</div> <div
className="project-title no-edit"
title={projectInfo.title}
>{projectInfo.title}</div>
{'by '} {'by '}
<a href={`/users/${projectInfo.author.username}`}> <a href={`/users/${projectInfo.author.username}`}>
{projectInfo.author.username} {projectInfo.author.username}
@ -141,6 +158,21 @@ const PreviewPresentation = ({
<RemixCredit projectInfo={parentInfo} /> <RemixCredit projectInfo={parentInfo} />
<RemixCredit projectInfo={originalInfo} /> <RemixCredit projectInfo={originalInfo} />
{/* eslint-disable max-len */} {/* eslint-disable max-len */}
<MediaQuery maxWidth={frameless.tablet - 1}>
<FlexRow className="preview-row">
<FlexRow className="extension-list">
{extensions && extensions.map(extension => (
<ExtensionChip
extensionL10n={extension.l10nId}
extensionName={extension.name}
hasStatus={extension.hasStatus}
iconURI={extension.icon && `/svgs/project/${extension.icon}`}
key={extension.name || extension.l10nId}
/>
))}
</FlexRow>
</FlexRow>
</MediaQuery>
<FlexRow className="description-block"> <FlexRow className="description-block">
<div className="project-textlabel"> <div className="project-textlabel">
Instructions Instructions
@ -295,19 +327,21 @@ const PreviewPresentation = ({
</FlexRow> </FlexRow>
</FlexRow> </FlexRow>
</FlexRow> </FlexRow>
<FlexRow className="preview-row"> <MediaQuery minWidth={frameless.tablet}>
<FlexRow className="extension-list"> <FlexRow className="preview-row">
{extensions && extensions.map(extension => ( <FlexRow className="extension-list">
<ExtensionChip {extensions && extensions.map(extension => (
extensionL10n={extension.l10nId} <ExtensionChip
extensionName={extension.name} extensionL10n={extension.l10nId}
hasStatus={extension.hasStatus} extensionName={extension.name}
iconURI={extension.icon && `/svgs/project/${extension.icon}`} hasStatus={extension.hasStatus}
key={extension.name || extension.l10nId} iconURI={extension.icon && `/svgs/project/${extension.icon}`}
/> key={extension.name || extension.l10nId}
))} />
))}
</FlexRow>
</FlexRow> </FlexRow>
</FlexRow> </MediaQuery>
</div> </div>
<div className="project-lower-container"> <div className="project-lower-container">
<div className="inner"> <div className="inner">
@ -365,6 +399,7 @@ PreviewPresentation.propTypes = {
extensions: PropTypes.arrayOf(PropTypes.object), extensions: PropTypes.arrayOf(PropTypes.object),
faved: PropTypes.bool, faved: PropTypes.bool,
favoriteCount: PropTypes.number, favoriteCount: PropTypes.number,
intl: intlShape,
isFullScreen: PropTypes.bool, isFullScreen: PropTypes.bool,
isLoggedIn: PropTypes.bool, isLoggedIn: PropTypes.bool,
isShared: PropTypes.bool, isShared: PropTypes.bool,

View file

@ -15,8 +15,12 @@ const EXTENSION_INFO = require('../../lib/extensions.js').default;
const PreviewPresentation = require('./presentation.jsx'); const PreviewPresentation = require('./presentation.jsx');
const projectShape = require('./projectshape.jsx').projectShape; const projectShape = require('./projectshape.jsx').projectShape;
const Registration = require('../../components/registration/registration.jsx');
const ConnectedLogin = require('../../components/login/connected-login.jsx');
const CanceledDeletionModal = require('../../components/login/canceled-deletion-modal.jsx');
const sessionActions = require('../../redux/session.js'); const sessionActions = require('../../redux/session.js');
const navigationActions = require('../../redux/navigation.js');
const previewActions = require('../../redux/preview.js'); const previewActions = require('../../redux/preview.js');
const GUI = require('scratch-gui'); const GUI = require('scratch-gui');
@ -31,7 +35,6 @@ class Preview extends React.Component {
'handleFavoriteToggle', 'handleFavoriteToggle',
'handleLoadMore', 'handleLoadMore',
'handleLoveToggle', 'handleLoveToggle',
'handlePermissions',
'handlePopState', 'handlePopState',
'handleReportClick', 'handleReportClick',
'handleReportClose', 'handleReportClose',
@ -39,11 +42,11 @@ class Preview extends React.Component {
'handleAddToStudioClick', 'handleAddToStudioClick',
'handleAddToStudioClose', 'handleAddToStudioClose',
'handleSeeInside', 'handleSeeInside',
'handleUpdateProjectTitle',
'handleUpdate', 'handleUpdate',
'initCounts', 'initCounts',
'isShared',
'pushHistory', 'pushHistory',
'userOwnsProject' 'renderLogin'
]); ]);
const pathname = window.location.pathname.toLowerCase(); const pathname = window.location.pathname.toLowerCase();
const parts = pathname.split('/').filter(Boolean); const parts = pathname.split('/').filter(Boolean);
@ -51,7 +54,6 @@ class Preview extends React.Component {
// parts[1]: either :id or 'editor' // parts[1]: either :id or 'editor'
// parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen' // parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen'
this.state = { this.state = {
editable: false,
extensions: [], extensions: [],
favoriteCount: 0, favoriteCount: 0,
loveCount: 0, loveCount: 0,
@ -86,7 +88,6 @@ class Preview extends React.Component {
if (this.props.projectInfo.id !== prevProps.projectInfo.id) { if (this.props.projectInfo.id !== prevProps.projectInfo.id) {
this.getExtensions(this.state.projectId); this.getExtensions(this.state.projectId);
this.initCounts(this.props.projectInfo.stats.favorites, this.props.projectInfo.stats.loves); this.initCounts(this.props.projectInfo.stats.favorites, this.props.projectInfo.stats.loves);
this.handlePermissions();
if (this.props.projectInfo.remix.parent !== null) { if (this.props.projectInfo.remix.parent !== null) {
this.props.getParentInfo(this.props.projectInfo.remix.parent); this.props.getParentInfo(this.props.projectInfo.remix.parent);
} }
@ -189,8 +190,8 @@ class Preview extends React.Component {
); );
} }
} }
handleToggleStudio (event) { handleToggleStudio (id) {
const studioId = parseInt(event.currentTarget.dataset.id, 10); const studioId = parseInt(id, 10);
if (isNaN(studioId)) { // sanity check in case event had no integer data-id if (isNaN(studioId)) { // sanity check in case event had no integer data-id
return; return;
} }
@ -242,15 +243,12 @@ class Preview extends React.Component {
})); }));
} }
} }
handlePermissions () {
// TODO: handle admins and mods
if (this.props.projectInfo.author.username === this.props.user.username) {
this.setState({editable: true});
}
}
handleSeeInside () { handleSeeInside () {
this.props.setPlayer(false); this.props.setPlayer(false);
} }
handleShare () {
// This is just a placeholder, but enables the button in the editor
}
handleUpdate (jsonData) { handleUpdate (jsonData) {
this.props.updateProject( this.props.updateProject(
this.props.projectInfo.id, this.props.projectInfo.id,
@ -259,32 +257,32 @@ class Preview extends React.Component {
this.props.user.token this.props.user.token
); );
} }
handleUpdateProjectTitle (title) {
this.handleUpdate({
title: title
});
}
initCounts (favorites, loves) { initCounts (favorites, loves) {
this.setState({ this.setState({
favoriteCount: favorites, favoriteCount: favorites,
loveCount: loves loveCount: loves
}); });
} }
isShared () { renderLogin ({onClose}) {
return ( return (
// if we don't have projectInfo assume shared until we know otherwise <ConnectedLogin
Object.keys(this.props.projectInfo).length === 0 || ( key="login-dropdown-presentation"
this.props.projectInfo.history && /* eslint-disable react/jsx-no-bind */
this.props.projectInfo.history.shared.length > 0 onLogIn={(formData, callback) => {
) this.props.handleLogIn(formData, result => {
); if (result.success === true) {
} onClose();
isLoggedIn () { }
return ( callback(result);
this.props.sessionStatus === sessionActions.Status.FETCHED && });
Object.keys(this.props.user).length > 0 }}
); /* eslint-ensable react/jsx-no-bind */
} />
userOwnsProject () {
return (
this.isLoggedIn() &&
Object.keys(this.props.projectInfo).length > 0 &&
this.props.user.id === this.props.projectInfo.author.id
); );
} }
render () { render () {
@ -296,13 +294,13 @@ class Preview extends React.Component {
assetHost={this.props.assetHost} assetHost={this.props.assetHost}
backpackOptions={this.props.backpackOptions} backpackOptions={this.props.backpackOptions}
comments={this.props.comments} comments={this.props.comments}
editable={this.state.editable} editable={this.props.isEditable}
extensions={this.state.extensions} extensions={this.state.extensions}
faved={this.props.faved} faved={this.props.faved}
favoriteCount={this.state.favoriteCount} favoriteCount={this.state.favoriteCount}
isFullScreen={this.state.isFullScreen} isFullScreen={this.state.isFullScreen}
isLoggedIn={this.isLoggedIn()} isLoggedIn={this.props.isLoggedIn}
isShared={this.isShared()} isShared={this.props.isShared}
loveCount={this.state.loveCount} loveCount={this.state.loveCount}
loved={this.props.loved} loved={this.props.loved}
originalInfo={this.props.original} originalInfo={this.props.original}
@ -315,8 +313,7 @@ class Preview extends React.Component {
replies={this.props.replies} replies={this.props.replies}
reportOpen={this.state.reportOpen} reportOpen={this.state.reportOpen}
studios={this.props.studios} studios={this.props.studios}
user={this.props.user} userOwnsProject={this.props.userOwnsProject}
userOwnsProject={this.userOwnsProject()}
onAddToStudioClicked={this.handleAddToStudioClick} onAddToStudioClicked={this.handleAddToStudioClick}
onAddToStudioClosed={this.handleAddToStudioClose} onAddToStudioClosed={this.handleAddToStudioClose}
onFavoriteClicked={this.handleFavoriteToggle} onFavoriteClicked={this.handleFavoriteToggle}
@ -330,16 +327,28 @@ class Preview extends React.Component {
onUpdate={this.handleUpdate} onUpdate={this.handleUpdate}
/> />
</Page> : </Page> :
<IntlGUI <React.Fragment>
enableCommunity <IntlGUI
hideIntro enableCommunity
assetHost={this.props.assetHost} hideIntro
backpackOptions={this.props.backpackOptions} assetHost={this.props.assetHost}
basePath="/" backpackOptions={this.props.backpackOptions}
className="gui" basePath="/"
projectHost={this.props.projectHost} className="gui"
projectId={this.state.projectId} projectHost={this.props.projectHost}
/> projectId={this.state.projectId}
projectTitle={this.props.projectInfo.title}
renderLogin={this.renderLogin}
onLogOut={this.props.handleLogOut}
onOpenRegistration={this.props.handleOpenRegistration}
onShare={this.handleShare}
onToggleLoginOpen={this.props.handleToggleLoginOpen}
onUpdateProjectTitle={this.handleUpdateProjectTitle}
/>
<Registration />
<CanceledDeletionModal />
</React.Fragment>
); );
} }
} }
@ -362,6 +371,13 @@ Preview.propTypes = {
getProjectStudios: PropTypes.func.isRequired, getProjectStudios: PropTypes.func.isRequired,
getRemixes: PropTypes.func.isRequired, getRemixes: PropTypes.func.isRequired,
getTopLevelComments: PropTypes.func.isRequired, getTopLevelComments: PropTypes.func.isRequired,
handleLogIn: PropTypes.func,
handleLogOut: PropTypes.func,
handleOpenRegistration: PropTypes.func,
handleToggleLoginOpen: PropTypes.func,
isEditable: PropTypes.bool,
isLoggedIn: PropTypes.bool,
isShared: PropTypes.bool,
loved: PropTypes.bool, loved: PropTypes.bool,
original: projectShape, original: projectShape,
parent: projectShape, parent: projectShape,
@ -389,7 +405,8 @@ Preview.propTypes = {
dateJoined: PropTypes.string, dateJoined: PropTypes.string,
email: PropTypes.string, email: PropTypes.string,
classroomId: PropTypes.string classroomId: PropTypes.string
}) }),
userOwnsProject: PropTypes.bool
}; };
Preview.defaultProps = { Preview.defaultProps = {
@ -437,26 +454,64 @@ const consolidateStudiosInfo = (curatedStudios, projectStudios, currentStudioIds
return consolidatedStudios; return consolidatedStudios;
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => {
projectInfo: state.preview.projectInfo, const projectInfoPresent = Object.keys(state.preview.projectInfo).length > 0;
comments: state.preview.comments, const userPresent = state.session.session.user &&
faved: state.preview.faved, Object.keys(state.session.session.user).length > 0;
loved: state.preview.loved, const isLoggedIn = state.session.status === sessionActions.Status.FETCHED &&
original: state.preview.original, userPresent;
parent: state.preview.parent, const authorPresent = projectInfoPresent && state.preview.projectInfo.author &&
remixes: state.preview.remixes, Object.keys(state.preview.projectInfo.author).length > 0;
replies: state.preview.replies,
sessionStatus: state.session.status, return {
projectStudios: state.preview.projectStudios, comments: state.preview.comments,
studios: consolidateStudiosInfo(state.preview.curatedStudios, faved: state.preview.faved,
state.preview.projectStudios, state.preview.currentStudioIds, fullScreen: state.scratchGui.mode.isFullScreen,
state.preview.status.studioRequests), // project is editable iff logged in user is the author of the project, or
user: state.session.session.user, // logged in user is an admin.
playerMode: state.scratchGui.mode.isPlayerOnly, isEditable: isLoggedIn &&
fullScreen: state.scratchGui.mode.isFullScreen ((authorPresent && state.preview.projectInfo.author.username === state.session.session.user.username) ||
}); state.permissions.admin === true),
isLoggedIn: isLoggedIn,
// if we don't have projectInfo, assume it's shared until we know otherwise
isShared: !projectInfoPresent || (
state.preview.projectInfo.history &&
state.preview.projectInfo.history.shared &&
state.preview.projectInfo.history.shared.length > 0),
loved: state.preview.loved,
original: state.preview.original,
parent: state.preview.parent,
playerMode: state.scratchGui.mode.isPlayerOnly,
projectInfo: state.preview.projectInfo,
projectStudios: state.preview.projectStudios,
remixes: state.preview.remixes,
replies: state.preview.replies,
sessionStatus: state.session.status, // check if used
studios: consolidateStudiosInfo(state.preview.curatedStudios,
state.preview.projectStudios, state.preview.currentStudioIds,
state.preview.status.studioRequests),
user: state.session.session.user,
userOwnsProject: isLoggedIn && authorPresent &&
state.session.session.user.id === state.preview.projectInfo.author.id
};
};
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
handleOpenRegistration: event => {
event.preventDefault();
dispatch(navigationActions.setRegistrationOpen(true));
},
handleLogIn: (formData, callback) => {
dispatch(navigationActions.handleLogIn(formData, callback));
},
handleLogOut: event => {
event.preventDefault();
dispatch(navigationActions.handleLogOut());
},
handleToggleLoginOpen: event => {
event.preventDefault();
dispatch(navigationActions.toggleLoginOpen());
},
getOriginalInfo: id => { getOriginalInfo: id => {
dispatch(previewActions.getOriginalInfo(id)); dispatch(previewActions.getOriginalInfo(id));
}, },
@ -497,9 +552,6 @@ const mapDispatchToProps = dispatch => ({
setLovedStatus: (loved, id, username, token) => { setLovedStatus: (loved, id, username, token) => {
dispatch(previewActions.setLovedStatus(loved, id, username, token)); dispatch(previewActions.setLovedStatus(loved, id, username, token));
}, },
refreshSession: () => {
dispatch(sessionActions.refreshSession());
},
reportProject: (id, formData) => { reportProject: (id, formData) => {
dispatch(previewActions.reportProject(id, formData)); dispatch(previewActions.reportProject(id, formData));
}, },

View file

@ -6,6 +6,12 @@ $player-width: 482px;
$player-height: 406px; $player-height: 406px;
$stage-width: 480px; $stage-width: 480px;
/* screen sizes */
$small: "screen and (max-width : #{$mobile}-1)";
$medium: "screen and (min-width : #{$mobile}) and (max-width : #{$tablet}-1)";
$big: "screen and (min-width : #{$tablet})";
$medium-and-small: "screen and (max-width : #{$tablet}-1)";
/* override view padding for share banner */ /* override view padding for share banner */
#view { #view {
padding: 0; padding: 0;
@ -29,16 +35,26 @@ $stage-width: 480px;
&.has-error { &.has-error {
.validation-message { .validation-message {
transform: translate(22rem, 0); right: 0;
} }
} }
&.no-edit {
/* titles of projects you don't own should not
show the full title if this is too long */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
} }
.project-header { .project-header {
margin-right: 2rem; margin-right: 2rem;
min-width: 0;
flex-grow: 1; flex-grow: 1;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
flex-wrap: nowrap;
.inplace-input { .inplace-input {
height: calc(3rem - 4px); height: calc(3rem - 4px);
@ -63,18 +79,18 @@ $stage-width: 480px;
.title { .title {
margin-left: 1rem; margin-left: 1rem;
min-width: 0;
text-align: left; text-align: left;
font-size: .8rem; font-size: .8rem;
flex-grow: 1; flex-grow: 1;
} }
.validation-message { .validation-message {
$arrow-border-width: 1rem;
display: block; display: block;
position: absolute; position: absolute;
top: 0; z-index: 1;
left: 0; $arrow-border-width: 1rem;
margin-left: $arrow-border-width; margin-top: $arrow-border-width;
border: 1px solid $active-gray; border: 1px solid $active-gray;
border-radius: 5px; border-radius: 5px;
background-color: $ui-orange; background-color: $ui-orange;
@ -85,13 +101,18 @@ $stage-width: 480px;
color: $type-white; color: $type-white;
font-size: 1rem; font-size: 1rem;
@media #{$medium-and-small} {
margin-top: calc($arrow-border-width / 2);
width: 100%;
}
&:before { &:before {
display: block; display: block;
position: absolute; position: absolute;
top: 1rem; top: -.5rem;
left: -$arrow-border-width / 2; left: calc(50% - calc(#{$arrow-border-width} / 2));
transform: rotate(45deg); transform: rotate(135deg);
border-bottom: 1px solid $active-gray; border-bottom: 1px solid $active-gray;
border-left: 1px solid $active-gray; border-left: 1px solid $active-gray;
@ -102,6 +123,10 @@ $stage-width: 480px;
height: $arrow-border-width; height: $arrow-border-width;
content: ""; content: "";
@media #{$medium-and-small} {
display: none;
}
} }
} }
@ -119,12 +144,16 @@ $stage-width: 480px;
} }
} }
.project-buttons {
flex-shrink: 0;
}
.button { .button {
margin-left: 1rem; margin-left: 1rem;
} }
.comments-container { .comments-container {
width: 60%; width: 65%;
} }
.remix-button, .remix-button,
@ -174,12 +203,15 @@ $stage-width: 480px;
} }
.project-notes { .project-notes {
// not 1.5rem because of stage padding
margin-left: 1rem; margin-left: 1rem;
height: $player-height; height: $player-height;
align-items: flex-start; align-items: flex-start;
flex: 1; flex: 1;
flex-flow: column; flex-flow: column;
> .description-block:first-child {
margin-top: 1rem;
}
} }
.share-date { .share-date {
@ -193,6 +225,7 @@ $stage-width: 480px;
.subactions { .subactions {
margin-left: 1.5rem; margin-left: 1.5rem;
justify-content: flex-end; justify-content: flex-end;
align-items: flex-start;
flex: 1; flex: 1;
} }
@ -368,6 +401,7 @@ $stage-width: 480px;
.action-buttons { .action-buttons {
display: flex; display: flex;
margin-top: 0;
color: $type-white; color: $type-white;
font-size: .8rem; font-size: .8rem;
font-weight: 500; font-weight: 500;
@ -442,6 +476,7 @@ $stage-width: 480px;
content: ""; content: "";
} }
} }
.studio-button { .studio-button {
&:before { &:before {
background-image: url("/svgs/project/studio-add-white.svg"); background-image: url("/svgs/project/studio-add-white.svg");
@ -480,14 +515,23 @@ $stage-width: 480px;
.extension-list { .extension-list {
justify-content: flex-start; justify-content: flex-start;
flex-direction: row;
@media #{$medium-and-small} {
justify-content: center;
}
} }
.remix-list, .remix-list,
.studio-list { .studio-list {
flex-direction: column; flex-direction: column;
.project { .list-title {
margin-bottom: 1.5rem; margin-left: 1rem;
font-size: 1.2rem;
font-weight: bold;
align-self: flex-start;
} }
.creator-image img { .creator-image img {

View file

@ -19,6 +19,7 @@ const RemixCredit = props => {
{projectInfo.author.username} {projectInfo.author.username}
</a> for the original project <a </a> for the original project <a
href={`/preview/${projectInfo.id}`} href={`/preview/${projectInfo.id}`}
title={projectInfo.title}
> >
{projectInfo.title} {projectInfo.title}
</a>. </a>.

View file

@ -9,7 +9,7 @@ const RemixList = props => {
if (remixes.length === 0) return null; if (remixes.length === 0) return null;
return ( return (
<FlexRow className="remix-list"> <FlexRow className="remix-list">
<div className="project-title"> <div className="list-title">
Remixes Remixes
</div> </div>
{remixes.length === 0 ? ( {remixes.length === 0 ? (

View file

@ -8,8 +8,8 @@ const StudioList = props => {
const studios = props.studios; const studios = props.studios;
if (studios.length === 0) return null; if (studios.length === 0) return null;
return ( return (
<FlexRow className="remix-list"> <FlexRow className="studio-list">
<div className="project-title"> <div className="list-title">
Studios Studios
</div> </div>
{studios.length === 0 ? ( {studios.length === 0 ? (

View file

@ -57,12 +57,21 @@ class Search extends React.Component {
} }
componentDidMount () { componentDidMount () {
const query = window.location.search; const query = decodeURIComponent(window.location.search);
const q = query.lastIndexOf('q='); let term = query;
let term = '';
if (q !== -1) { const stripQueryValue = function (queryTerm) {
term = query.substring(q + 2, query.length).toLowerCase(); const queryIndex = query.indexOf('q=');
} if (queryIndex !== -1) {
queryTerm = query.substring(queryIndex + 2, query.length).toLowerCase();
}
return queryTerm;
};
// Strip off the initial "?q="
term = stripQueryValue(term);
// Strip off user entered "?q="
term = stripQueryValue(term);
while (term.indexOf('/') > -1) { while (term.indexOf('/') > -1) {
term = term.substring(0, term.indexOf('/')); term = term.substring(0, term.indexOf('/'));
} }
@ -232,7 +241,7 @@ Search.propTypes = {
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
searchTerm: state.navigation searchTerm: state.navigation.searchTerm
}); });
const WrappedSearch = injectIntl(Search); const WrappedSearch = injectIntl(Search);

View file

@ -495,7 +495,6 @@ class SplashPresentation extends React.Component { // eslint-disable-line react/
</MediaQuery> </MediaQuery>
]) : [] ]) : []
} }
{featured} {featured}
{this.props.isAdmin ? [ {this.props.isAdmin ? [
@ -555,7 +554,7 @@ SplashPresentation.propTypes = {
isAdmin: PropTypes.bool.isRequired, isAdmin: PropTypes.bool.isRequired,
isEducator: PropTypes.bool.isRequired, isEducator: PropTypes.bool.isRequired,
lovedByFollowing: PropTypes.arrayOf(PropTypes.object), lovedByFollowing: PropTypes.arrayOf(PropTypes.object),
news: PropTypes.object, // eslint-disable-line react/forbid-prop-types news: PropTypes.arrayOf(PropTypes.object),
onDismiss: PropTypes.func.isRequired, onDismiss: PropTypes.func.isRequired,
onHideEmailConfirmationModal: PropTypes.func.isRequired, onHideEmailConfirmationModal: PropTypes.func.isRequired,
onRefreshHomepageCache: PropTypes.func.isRequired, onRefreshHomepageCache: PropTypes.func.isRequired,

View file

@ -7,8 +7,8 @@ const React = require('react');
const api = require('../../lib/api'); const api = require('../../lib/api');
const injectIntl = require('../../lib/intl.jsx').injectIntl; const injectIntl = require('../../lib/intl.jsx').injectIntl;
const intlShape = require('../../lib/intl.jsx').intlShape; const intlShape = require('../../lib/intl.jsx').intlShape;
const log = require('../../lib/log.js');
const sessionStatus = require('../../redux/session').Status; const sessionStatus = require('../../redux/session').Status;
const navigationActions = require('../../redux/navigation.js');
const Deck = require('../../components/deck/deck.jsx'); const Deck = require('../../components/deck/deck.jsx');
const Progression = require('../../components/progression/progression.jsx'); const Progression = require('../../components/progression/progression.jsx');
@ -24,7 +24,6 @@ class StudentCompleteRegistration extends React.Component {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'handleAdvanceStep', 'handleAdvanceStep',
'handleLogOut',
'handleRegister', 'handleRegister',
'handleGoToClass' 'handleGoToClass'
]); ]);
@ -61,18 +60,6 @@ class StudentCompleteRegistration extends React.Component {
formData: defaults({}, formData, this.state.formData) formData: defaults({}, formData, this.state.formData)
}); });
} }
handleLogOut (e) {
e.preventDefault();
api({
host: '',
method: 'post',
uri: '/accounts/logout/',
useCsrf: true
}, err => {
if (err) return log.error(err);
window.location = '/';
});
}
handleRegister (formData) { handleRegister (formData) {
this.setState({waiting: true}); this.setState({waiting: true});
@ -147,7 +134,7 @@ class StudentCompleteRegistration extends React.Component {
classroom={this.state.classroom} classroom={this.state.classroom}
studentUsername={this.props.studentUsername} studentUsername={this.props.studentUsername}
waiting={this.state.waiting} waiting={this.state.waiting}
onHandleLogOut={this.handleLogOut} onHandleLogOut={this.props.handleLogOut}
onNextStep={this.handleAdvanceStep} onNextStep={this.handleAdvanceStep}
/> />
{this.props.must_reset_password ? {this.props.must_reset_password ?
@ -178,6 +165,7 @@ class StudentCompleteRegistration extends React.Component {
StudentCompleteRegistration.propTypes = { StudentCompleteRegistration.propTypes = {
classroomId: PropTypes.number.isRequired, classroomId: PropTypes.number.isRequired,
handleLogOut: PropTypes.func,
intl: intlShape, intl: intlShape,
must_reset_password: PropTypes.bool.isRequired, must_reset_password: PropTypes.bool.isRequired,
newStudent: PropTypes.bool.isRequired, newStudent: PropTypes.bool.isRequired,
@ -199,6 +187,16 @@ const mapStateToProps = state => ({
studentUsername: state.session.session.user && state.session.session.user.username studentUsername: state.session.session.user && state.session.session.user.username
}); });
const ConnectedStudentCompleteRegistration = connect(mapStateToProps)(IntlStudentCompleteRegistration); const mapDispatchToProps = dispatch => ({
handleLogOut: event => {
event.preventDefault();
dispatch(navigationActions.handleLogOut());
}
});
const ConnectedStudentCompleteRegistration = connect(
mapStateToProps,
mapDispatchToProps
)(IntlStudentCompleteRegistration);
render(<ConnectedStudentCompleteRegistration />, document.getElementById('app')); render(<ConnectedStudentCompleteRegistration />, document.getElementById('app'));

View file

@ -25,5 +25,9 @@
"wedo2.updateLinkText": "Make sure you have installed the latest version of Scratch Link.", "wedo2.updateLinkText": "Make sure you have installed the latest version of Scratch Link.",
"wedo2.legacyInfoTitle": "Using Scratch 2.0?", "wedo2.legacyInfoTitle": "Using Scratch 2.0?",
"wedo2.legacyInfoText": "Visit our page about {wedoLegacyLink}.", "wedo2.legacyInfoText": "Visit our page about {wedoLegacyLink}.",
"wedo2.legacyLinkText": "using LEGO WeDo with Scratch 2.0" "wedo2.legacyLinkText": "using LEGO WeDo with Scratch 2.0",
"wedo2.imgAltWeDoIllustration": "An illustration of a WeDo2 featuring a tilt sensor and a motor.",
"wedo2.imgAltStarter1": "A Scratch project with a dog and a taco.",
"wedo2.imgAltStarter2": "A Scratch project with a toad playing instruments in space.",
"wedo2.imgAltStarter3": "A Scratch project with dinosaurs."
} }

View file

@ -28,9 +28,15 @@ class Wedo2 extends ExtensionLanding {
render () { render () {
return ( return (
<div className="extension-landing wedo2"> <div className="extension-landing wedo2">
<ExtensionHeader imageSrc="/images/wedo2/wedo2-illustration.png"> <ExtensionHeader
imageAlt={this.props.intl.formatMessage({id: 'wedo2.imgAltWeDoIllustration'})}
imageSrc="/images/wedo2/wedo2-illustration.png"
>
<FlexRow className="column extension-copy"> <FlexRow className="column extension-copy">
<h2><img src="/images/wedo2/wedo2.svg" />LEGO WeDo 2.0</h2> <h1><img
alt=""
src="/images/wedo2/wedo2.svg"
/>LEGO WeDo 2.0</h1>
<FormattedMessage <FormattedMessage
id="wedo2.headerText" id="wedo2.headerText"
values={{ values={{
@ -48,19 +54,31 @@ class Wedo2 extends ExtensionLanding {
</FlexRow> </FlexRow>
<ExtensionRequirements> <ExtensionRequirements>
<span> <span>
<img src="/svgs/extensions/windows.svg" /> <img
alt=""
src="/svgs/extensions/windows.svg"
/>
Windows 10+ Windows 10+
</span> </span>
<span> <span>
<img src="/svgs/extensions/mac.svg" /> <img
alt=""
src="/svgs/extensions/mac.svg"
/>
macOS 10.13+ macOS 10.13+
</span> </span>
<span> <span>
<img src="/svgs/extensions/bluetooth.svg" /> <img
alt=""
src="/svgs/extensions/bluetooth.svg"
/>
Bluetooth Bluetooth
</span> </span>
<span> <span>
<img src="/svgs/extensions/scratch-link.svg" /> <img
alt=""
src="/svgs/extensions/scratch-link.svg"
/>
Scratch Link Scratch Link
</span> </span>
</ExtensionRequirements> </ExtensionRequirements>
@ -80,6 +98,7 @@ class Wedo2 extends ExtensionLanding {
<Step number={1}> <Step number={1}>
<div className="step-image"> <div className="step-image">
<img <img
alt=""
className="screenshot" className="screenshot"
src="/images/wedo2/wedo2-connect-1.png" src="/images/wedo2/wedo2-connect-1.png"
/> />
@ -104,6 +123,7 @@ class Wedo2 extends ExtensionLanding {
<Step number={2}> <Step number={2}>
<div className="step-image"> <div className="step-image">
<img <img
alt={this.props.intl.formatMessage({id: 'extensionInstallation.addExtension'})}
className="screenshot" className="screenshot"
src="/images/wedo2/wedo2-connect-2.png" src="/images/wedo2/wedo2-connect-2.png"
/> />
@ -125,7 +145,10 @@ class Wedo2 extends ExtensionLanding {
<FormattedMessage id="wedo2.plugMotorIn" /> <FormattedMessage id="wedo2.plugMotorIn" />
</span> </span>
<div className="step-image"> <div className="step-image">
<img src="/images/wedo2/wedo2-motor.png" /> <img
alt=""
src="/images/wedo2/wedo2-motor.png"
/>
</div> </div>
</Step> </Step>
<Step <Step
@ -143,7 +166,10 @@ class Wedo2 extends ExtensionLanding {
/> />
</span> </span>
<div className="step-image"> <div className="step-image">
<img src="/images/wedo2/wedo2-motor-turn-block.png" /> <img
alt=""
src="/images/wedo2/wedo2-motor-turn-block.png"
/>
</div> </div>
</Step> </Step>
</Steps> </Steps>
@ -153,18 +179,21 @@ class Wedo2 extends ExtensionLanding {
<ProjectCard <ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239284992" cardUrl="https://beta.scratch.mit.edu/#239284992"
description={this.props.intl.formatMessage({id: 'wedo2.starter1Description'})} description={this.props.intl.formatMessage({id: 'wedo2.starter1Description'})}
imageAlt={this.props.intl.formatMessage({id: 'wedo2.imgAltStarter1'})}
imageSrc="/images/wedo2/wedo2-starter1.png" imageSrc="/images/wedo2/wedo2-starter1.png"
title={this.props.intl.formatMessage({id: 'wedo2.starter1Title'})} title={this.props.intl.formatMessage({id: 'wedo2.starter1Title'})}
/> />
<ProjectCard <ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239284997" cardUrl="https://beta.scratch.mit.edu/#239284997"
description={this.props.intl.formatMessage({id: 'wedo2.starter2Description'})} description={this.props.intl.formatMessage({id: 'wedo2.starter2Description'})}
imageAlt={this.props.intl.formatMessage({id: 'wedo2.imgAltStarter2'})}
imageSrc="/images/wedo2/wedo2-starter2.png" imageSrc="/images/wedo2/wedo2-starter2.png"
title={this.props.intl.formatMessage({id: 'wedo2.starter2Title'})} title={this.props.intl.formatMessage({id: 'wedo2.starter2Title'})}
/> />
<ProjectCard <ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239285001" cardUrl="https://beta.scratch.mit.edu/#239285001"
description={this.props.intl.formatMessage({id: 'wedo2.starter3Description'})} description={this.props.intl.formatMessage({id: 'wedo2.starter3Description'})}
imageAlt={this.props.intl.formatMessage({id: 'wedo2.imgAltStarter3'})}
imageSrc="/images/wedo2/wedo2-starter3.png" imageSrc="/images/wedo2/wedo2-starter3.png"
title={this.props.intl.formatMessage({id: 'wedo2.starter3Title'})} title={this.props.intl.formatMessage({id: 'wedo2.starter3Title'})}
/> />

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>spinner-blue</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="spinner-blue" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M15,10 C15,7.23857625 12.7614237,5 10,5 C7.23857625,5 5,7.23857625 5,10 C5,12.7614237 7.23857625,15 10,15" id="Oval-2" stroke="#4D97FF" stroke-width="2.5"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 686 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>spinner-transparent-gray</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="spinner-transparent-gray" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.15">
<path d="M15,10 C15,7.23857625 12.7614237,5 10,5 C7.23857625,5 5,7.23857625 5,10 C5,12.7614237 7.23857625,15 10,15" id="Oval-2" stroke="#000000" stroke-width="2.5"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 732 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>spinner-white</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="spinner-white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M15,10 C15,7.23857625 12.7614237,5 10,5 C7.23857625,5 5,7.23857625 5,10 C5,12.7614237 7.23857625,15 10,15" id="Oval-2" stroke="#FFFFFF" stroke-width="2.5"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 688 B

View file

@ -1 +0,0 @@
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><path d="M15 10a5 5 0 1 0-5 5" stroke="#FFF" stroke-width="2.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 213 B

View file

@ -52,7 +52,7 @@ test('Sign in to Scratch using scratchr2 navbar', t => {
}); });
test('Sign in to Scratch & verify My Stuff structure (tabs, title)', t => { test('Sign in to Scratch & verify My Stuff structure (tabs, title)', t => {
clickXpath('//a[@class="mystuff-icon"]') clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => findByXpath('//div[@class="box-head"]/h2')) .then(() => findByXpath('//div[@class="box-head"]/h2'))
.then((element) => element.getText('h2')) .then((element) => element.getText('h2'))
.then((text) => t.equal('My Stuff', text, 'title should be My Stuff')) .then((text) => t.equal('My Stuff', text, 'title should be My Stuff'))
@ -75,7 +75,7 @@ test('Sign in to Scratch & verify My Stuff structure (tabs, title)', t => {
}); });
test('clicking See Inside should take you to the editor', t => { test('clicking See Inside should take you to the editor', t => {
clickXpath('//a[@class="mystuff-icon"]') clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => findByXpath('//a[@data-control="edit"]')) .then(() => findByXpath('//a[@data-control="edit"]'))
.then((element) => element.getText('span')) .then((element) => element.getText('span'))
.then((text) => t.equal(text, 'See inside', 'there should be a "See inside" button')) .then((text) => t.equal(text, 'See inside', 'there should be a "See inside" button'))
@ -89,7 +89,7 @@ test('clicking See Inside should take you to the editor', t => {
}); });
test('clicking a project title should take you to the project page', t => { test('clicking a project title should take you to the project page', t => {
clickXpath('//a[@class="mystuff-icon"]') clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => clickXpath('//a[@data-control="edit"]')) .then(() => clickXpath('//a[@data-control="edit"]'))
.then(() => driver.getCurrentUrl()) .then(() => driver.getCurrentUrl())
.then(function (u) { .then(function (u) {
@ -100,7 +100,7 @@ test('clicking a project title should take you to the project page', t => {
}); });
test('Add To button should bring up a list of studios', t => { test('Add To button should bring up a list of studios', t => {
clickXpath('//a[@class="mystuff-icon"]') clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => findByXpath('//div[@data-control="add-to"]')) .then(() => findByXpath('//div[@data-control="add-to"]'))
.then((element) => element.getText('span')) .then((element) => element.getText('span'))
.then((text) => t.equal(text, 'Add to', 'there should be an "Add to" button')) .then((text) => t.equal(text, 'Add to', 'there should be an "Add to" button'))
@ -115,7 +115,7 @@ test('Add To button should bring up a list of studios', t => {
}); });
test('+ New Studio button should take you to the studio page', t => { test('+ New Studio button should take you to the studio page', t => {
clickXpath('//a[@class="mystuff-icon"]') clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => clickXpath('//form[@id="new_studio"]/button[@type="submit"]')) .then(() => clickXpath('//form[@id="new_studio"]/button[@type="submit"]'))
.then(() => findByXpath('//div[@id="show-add-project"]')) .then(() => findByXpath('//div[@id="show-add-project"]'))
.then((element) => element.getText('span')) .then((element) => element.getText('span'))
@ -130,7 +130,7 @@ test('+ New Studio button should take you to the studio page', t => {
}); });
test('+ New Project button should open the editor', t => { test('+ New Project button should open the editor', t => {
clickXpath('//a[@class="mystuff-icon"]') clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => clickText('+ New Project')) .then(() => clickText('+ New Project'))
.then(() => driver.getCurrentUrl()) .then(() => driver.getCurrentUrl())
.then(function (u) { .then(function (u) {

View file

@ -79,7 +79,8 @@ tap.test('checkFeaturedStudiosRowWhenSignedOut', function (t) {
// checks that the link for a studio makes sense // checks that the link for a studio makes sense
tap.test('checkFeaturedStudiosRowLinkWhenSignedOut', function (t) { tap.test('checkFeaturedStudiosRowLinkWhenSignedOut', function (t) {
var xPathLink = '//div[contains(@class, "thumbnail") and contains(@class, "gallery") ' + var xPathLink = '//div[contains(@class, "thumbnail") and contains(@class, "gallery") ' +
'and contains(@class, "slick-slide") and contains(@class, "slick-active")]/a[@class="thumbnail-image"]'; 'and contains(@class, "slick-slide") ' +
'and contains(@class, "slick-active")]/a[@class="thumbnail-image"]';
driver.findElement(webdriver.By.xpath(xPathLink)) driver.findElement(webdriver.By.xpath(xPathLink))
.then(function (element) { .then(function (element) {
element.getAttribute('href') element.getAttribute('href')

View file

@ -54,7 +54,7 @@ test('Sign in to Scratch using scratchr2 navbar', t => {
test('Sign out of Scratch using scratchr2 navbar', t => { test('Sign out of Scratch using scratchr2 navbar', t => {
clickXpath('//span[contains(@class, "user-name")' + clickXpath('//span[contains(@class, "user-name")' +
' and contains(@class, "dropdown-toggle")]/img[@class="user-icon"]') ' and contains(@class, "dropdown-toggle")]/img[contains(@class, "user-icon")]')
.then(() => clickXpath('//input[@value="Sign out"]')) .then(() => clickXpath('//input[@value="Sign out"]'))
.then(() => findText('Sign in')) .then(() => findText('Sign in'))
.then((element) => t.ok(element, 'Sign in reappeared on the page after signing out')) .then((element) => t.ok(element, 'Sign in reappeared on the page after signing out'))

View file

@ -43,7 +43,7 @@ test('Sign in to Scratch using scratch-www navbar', t => {
.then((element) => element.sendKeys(password)) .then((element) => element.sendKeys(password))
.then(() => clickXpath('//button[contains(@class, "button") and ' + .then(() => clickXpath('//button[contains(@class, "button") and ' +
'contains(@class, "submit-button") and contains(@class, "white")]')) 'contains(@class, "submit-button") and contains(@class, "white")]'))
.then(() => findByXpath('//span[@class="profile-name"]')) .then(() => findByXpath('//span[contains(@class, "profile-name")]'))
.then((element) => element.getText()) .then((element) => element.getText())
.then((text) => t.match(text.toLowerCase(), username.substring(0, 10).toLowerCase(), .then((text) => t.match(text.toLowerCase(), username.substring(0, 10).toLowerCase(),
'first part of username should be displayed in navbar')) 'first part of username should be displayed in navbar'))
@ -51,7 +51,7 @@ test('Sign in to Scratch using scratch-www navbar', t => {
}); });
test('Sign out of Scratch using scratch-www navbar', t => { test('Sign out of Scratch using scratch-www navbar', t => {
clickXpath('//a[@class="user-info"]') clickXpath('//a[contains(@class, "user-info")]')
.then(() => clickText('Sign out')) .then(() => clickText('Sign out'))
.then(() => findText('Sign in')) .then(() => findText('Sign in'))
.then((element) => t.ok(element, 'Sign in reappeared on the page after signing out')) .then((element) => t.ok(element, 'Sign in reappeared on the page after signing out'))