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
NODE= NODE_OPTIONS=--max_old_space_size=8000 node
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
TAP=./node_modules/.bin/tap
WATCH= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/watch
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:

View file

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

View file

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

View file

@ -33,8 +33,8 @@
}
input {
// 100% minus border and padding
margin-bottom: 12px;
// 100% minus border and padding
width: calc(100% - 30px);
}
@ -88,7 +88,7 @@
content: "";
}
}
@media only screen and (max-width: $tablet - 1) {
min-width: 160px;
}

View file

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

View file

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

View file

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

View file

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

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

View file

@ -2,6 +2,26 @@
.login {
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 {
padding-top: 5px;
@ -15,7 +35,7 @@
.spinner {
margin: 0 .8rem;
width: 1rem;
height: 1rem;
vertical-align: middle;
}
.submit-button {
@ -24,13 +44,19 @@
a {
margin-top: 15px;
color: $ui-white;
&:link,
&:visited,
&:active {
color: $ui-white;
}
&:hover {
background-color: transparent;
}
}
.error {
border: 1px solid $active-dark-gray;
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 */
}
.studio-selector-button {
display: flex;
position: relative;
transition: all .5s;
margin: .21875rem .21875rem;
border-radius: .5rem;
background-color: $ui-white;
cursor: pointer;
padding: 0;
width: 16.1875rem; /* 259px */
height: 2.5rem;
box-sizing: border-box;
justify-content: space-between;
}
.studio-selector-button-text {
@ -112,8 +114,11 @@
*/
margin: .575rem 2.18375rem .175rem .6875rem;
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;
white-space: nowrap;
font-family: "Helvetica Neue";
font-size: .875rem;
font-weight: regular;
@ -160,30 +165,30 @@
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;
height: 1.4rem;
transform-origin: center;
}
.studio-status-icon--img {
width: 1.4rem;
height: 1.4rem;
.studio-status-icon-with-animation {
animation-name: bump;
animation-duration: .25s;
animation-timing-function: cubic-bezier(.3, -3, .6, 3);
animation-iteration-count: 1;
}
.action-button-text .spinner-smooth {
margin: .2125rem auto;
width: 1.875rem;
height: 1rem;
}
.studio-status-icon .spinner-smooth {
position: unset; /* don't understand why neither relative nor absolute work */
}
.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 */
@keyframes bump {
0% {
transform: scale(0);
opacity: 0;
-webkit-transform: scale(0);
}
100% {
transform: scale(1);
opacity: 1;
-webkit-transform: scale(1);
}
}

View file

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

View file

@ -1,29 +1,36 @@
const truncateAtWordBoundary = require('../../../lib/truncate').truncateAtWordBoundary;
const PropTypes = require('prop-types');
const React = require('react');
const classNames = require('classnames');
const Spinner = require('../../spinner/spinner.jsx');
const AnimateHOC = require('./animate-hoc.jsx');
require('./modal.scss');
const StudioButton = ({
hasRequestOutstanding,
id,
includesProject,
title,
onToggleStudio
onClick,
wasClicked
}) => {
const checkmark = (
<img
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"
/>
);
const plus = (
<img
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"
/>
);
@ -35,8 +42,7 @@ const StudioButton = ({
{'studio-selector-button-selected':
includesProject && !hasRequestOutstanding}
)}
data-id={id}
onClick={onToggleStudio}
onClick={onClick}
>
<div
className={classNames(
@ -44,17 +50,18 @@ const StudioButton = ({
{'studio-selector-button-text-selected': includesProject || hasRequestOutstanding},
{'studio-selector-button-text-unselected': !includesProject && !hasRequestOutstanding}
)}
title={title}
>
{truncateAtWordBoundary(title, 25)}
{title}
</div>
<div
className={classNames(
'studio-status-icon',
{'studio-status-icon-unselected': !includesProject}
{'studio-status-icon-unselected': !includesProject && !hasRequestOutstanding}
)}
>
{(hasRequestOutstanding ?
(<Spinner mode="smooth" />) :
<Spinner /> :
(includesProject ? checkmark : plus))}
</div>
</div>
@ -63,10 +70,10 @@ const StudioButton = ({
StudioButton.propTypes = {
hasRequestOutstanding: PropTypes.bool,
id: PropTypes.number,
includesProject: PropTypes.bool,
onToggleStudio: PropTypes.func,
title: PropTypes.string
onClick: PropTypes.func,
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');
ReactModal.setAppElement(document.getElementById('view'));
ReactModal.setAppElement(document.getElementById('app'));
/**
* Container for pop up windows (See: registration window)
@ -25,7 +25,7 @@ class Modal extends React.Component {
render () {
return (
<ReactModal
appElement={document.getElementById('view')}
appElement={document.getElementById('app')}
className={{
base: 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 ? (
<div className="action-button-text">
<Spinner mode="smooth" />
<Spinner />
<FormattedMessage id="report.sending" />
</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 messageCountActions = require('../../../redux/message-count.js');
const navigationActions = require('../../../redux/navigation.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 Dropdown = require('../../dropdown/dropdown.jsx');
const Form = require('../../forms/form.jsx');
const Input = require('../../forms/input.jsx');
const log = require('../../../lib/log.js');
const Login = require('../../login/login.jsx');
const Modal = require('../../modal/base/modal.jsx');
const LoginDropdown = require('../../login/login-dropdown.jsx');
const CanceledDeletionModal = require('../../login/canceled-deletion-modal.jsx');
const NavigationBox = require('../base/navigation.jsx');
const Registration = require('../../registration/registration.jsx');
const AccountNav = require('./accountnav.jsx');
require('./navigation.scss');
@ -29,34 +27,16 @@ class Navigation extends React.Component {
super(props);
bindAll(this, [
'getProfileUrl',
'handleJoinClick',
'handleLoginClick',
'handleCloseLogin',
'handleLogIn',
'handleLogOut',
'handleAccountNavClick',
'handleCloseAccountNav',
'showCanceledDeletion',
'handleCloseCanceledDeletion',
'handleCloseRegistration',
'handleCompleteRegistration',
'handleSearchSubmit'
]);
this.state = {
accountNavOpen: false,
canceledDeletionOpen: false,
loginOpen: false,
loginError: null,
registrationOpen: false,
messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
};
}
componentDidMount () {
if (this.props.session.session.user) {
if (this.props.user) {
const intervalId = setInterval(() => {
this.props.dispatch(
messageCountActions.getCount(this.props.session.session.user.username)
);
this.props.getMessageCount(this.props.user.username);
}, 120000); // check for new messages every 2 mins.
this.setState({ // eslint-disable-line react/no-did-mount-set-state
messageCountIntervalId: intervalId
@ -64,16 +44,11 @@ class Navigation extends React.Component {
}
}
componentDidUpdate (prevProps) {
if (prevProps.session.session.user !== this.props.session.session.user) {
this.setState({ // eslint-disable-line react/no-did-update-set-state
loginOpen: false,
accountNavOpen: false
});
if (this.props.session.session.user) {
if (prevProps.user !== this.props.user) {
this.props.closeAccountMenus();
if (this.props.user) {
const intervalId = setInterval(() => {
this.props.dispatch(
messageCountActions.getCount(this.props.session.session.user.username)
);
this.props.getMessageCount(this.props.user.username);
}, 120000); // check for new messages every 2 mins.
this.setState({ // eslint-disable-line react/no-did-update-set-state
messageCountIntervalId: intervalId
@ -81,7 +56,7 @@ class Navigation extends React.Component {
} else {
// clear message count check, and set to default id.
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
messageCountIntervalId: -1
});
@ -92,102 +67,25 @@ class Navigation extends React.Component {
// clear message interval if it exists
if (this.state.messageCountIntervalId !== -1) {
clearInterval(this.state.messageCountIntervalId);
this.props.dispatch(messageCountActions.setCount(0));
this.props.setMessageCount(0);
this.setState({
messageCountIntervalId: -1
});
}
}
getProfileUrl () {
if (!this.props.session.session.user) return;
return `/users/${this.props.session.session.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();
if (!this.props.user) return;
return `/users/${this.props.user.username}/`;
}
handleSearchSubmit (formData) {
window.location.href = `/search/projects?q=${encodeURIComponent(formData.q)}`;
}
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 (
<NavigationBox
className={classNames({
'logged-in': this.props.session.session.user
'logged-in': this.props.user
})}
>
<ul>
@ -235,7 +133,7 @@ class Navigation extends React.Component {
</Form>
</li>
{this.props.session.status === sessionActions.Status.FETCHED ? (
this.props.session.session.user ? [
this.props.user ? [
<li
className="link right messages"
key="messages"
@ -268,66 +166,18 @@ class Navigation extends React.Component {
className="link right account-nav"
key="account-nav"
>
<a
className={classNames({
'user-info': true,
'open': this.state.accountNavOpen
})}
href="#"
onClick={this.handleAccountNavClick}
>
<Avatar
alt=""
src={this.props.session.session.user.thumbnailUrl}
/>
<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>
<AccountNav
classroomId={this.props.user.classroomId}
isEducator={this.props.permissions.educator}
isOpen={this.props.accountNavOpen}
isStudent={this.props.permissions.student}
profileUrl={this.getProfileUrl()}
thumbnailUrl={this.props.user.thumbnailUrl}
username={this.props.user.username}
onClick={this.props.handleToggleAccountNav}
onClickLogout={this.props.handleLogOut}
onClose={this.props.handleCloseAccountNav}
/>
</li>
] : [
<li
@ -336,16 +186,13 @@ class Navigation extends React.Component {
>
<a
href="#"
onClick={this.handleJoinClick}
onClick={this.props.handleOpenRegistration}
>
<FormattedMessage id="general.joinScratch" />
</a>
</li>,
<Registration
isOpen={this.state.registrationOpen}
key="registration"
onRegistrationDone={this.handleCompleteRegistration}
onRequestClose={this.handleCloseRegistration}
/>,
<li
className="link right login-item"
@ -355,53 +202,31 @@ class Navigation extends React.Component {
className="ignore-react-onclickoutside"
href="#"
key="login-link"
onClick={this.handleLoginClick}
onClick={this.props.handleToggleLoginOpen}
>
<FormattedMessage id="general.signIn" />
</a>
<Dropdown
className="login-dropdown with-arrow"
isOpen={this.state.loginOpen}
<LoginDropdown
key="login-dropdown"
onRequestClose={this.handleCloseLogin}
>
<Login
error={this.state.loginError}
onLogIn={this.handleLogIn}
/>
</Dropdown>
/>
</li>
]) : []}
</ul>
<Modal
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>
<CanceledDeletionModal />
</NavigationBox>
);
}
}
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,
permissions: PropTypes.shape({
admin: PropTypes.bool,
@ -412,16 +237,15 @@ Navigation.propTypes = {
}),
searchTerm: PropTypes.string,
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
}),
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 = {
@ -431,12 +255,48 @@ Navigation.defaultProps = {
};
const mapStateToProps = state => ({
accountNavOpen: state.navigation && state.navigation.accountNavOpen,
session: state.session,
permissions: state.permissions,
searchTerm: state.navigation.searchTerm,
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);

View file

@ -163,74 +163,6 @@
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
@ -242,20 +174,6 @@
&.login-item {
margin-left: 0;
}
&.account-nav {
margin-left: 0;
> a {
.avatar {
margin-right: 0;
}
&:after {
display: none;
}
}
}
}
.create,
@ -280,20 +198,6 @@
&.login-item {
margin-left: 0;
}
&.account-nav {
margin-left: 0;
> a {
.avatar {
margin-right: 0;
}
&:after {
display: none;
}
}
}
}
.discuss,
@ -313,8 +217,7 @@
width: $cols8;
> ul > li {
&.login-item,
&.account-nav {
&.login-item {
margin-left: 0;
}
}

View file

@ -1,8 +1,10 @@
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
const connect = require('react-redux').connect;
const IframeModal = require('../modal/iframe/modal.jsx');
const navigationActions = require('../../redux/navigation.js');
require('./registration.scss');
@ -26,7 +28,7 @@ class Registration extends React.Component {
handleMessage (e) {
if (e.origin !== window.location.origin) 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') {
this.registrationIframe.contentWindow.location.reload();
}
@ -47,16 +49,32 @@ class Registration extends React.Component {
}}
isOpen={this.props.isOpen}
src="/accounts/standalone-registration/"
onRequestClose={this.props.onRequestClose}
onRequestClose={this.props.handleCloseRegistration}
/>
);
}
}
Registration.propTypes = {
isOpen: PropTypes.bool,
onRegistrationDone: PropTypes.func,
onRequestClose: PropTypes.func
handleCloseRegistration: PropTypes.func,
handleCompleteRegistration: 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 PropTypes = require('prop-types');
const classNames = require('classnames');
require('./spinner.scss');
// Adapted from http://tobiasahlin.com/spinkit/
const Spinner = ({
mode
}) => {
const spinnerClassName = (mode === 'smooth' ? 'spinner-smooth' : 'spinner');
const spinnerDivCount = (mode === 'smooth' ? 24 : 12);
return (
<div className={spinnerClassName}>
{range(1, spinnerDivCount + 1).map(id => (
<div
className={`circle${id} circle`}
key={`circle${id}`}
/>
))}
</div>
);
className,
color
}) => (
<img
alt="loading animation"
className={classNames('studio-status-icon-spinner', className)}
src={`/svgs/modal/spinner-${color}.svg`}
/>
);
Spinner.defaultProps = {
color: 'white'
};
Spinner.propTypes = {
mode: PropTypes.string
className: PropTypes.string,
color: PropTypes.oneOf(['white', 'blue', 'transparent-gray'])
};
module.exports = Spinner;

View file

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

View file

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

View file

@ -5,10 +5,10 @@
$thumbnail-width: 220px;
$thumbnail-inner-width: 204px;
$project-height: 208px;
$gallery-height: 164px;
margin: 0 auto;
padding: 12px 0;
justify-content: flex-start;
@ -16,14 +16,13 @@
.thumbnail {
margin: 7px;
border-radius: 4px;
box-shadow: 0 0 0 1px $active-gray;
background-color: $ui-white;
padding-bottom: 4px;
width: $thumbnail-width;
.thumbnail-image {
margin: 8px auto;
border-radius: 4px;
box-shadow: 0 0 0 1px $active-gray;
background-color: $ui-white;
width: $thumbnail-inner-width;
}
@ -45,10 +44,18 @@
.thumbnail-title {
float: left;
max-width: 164px;
overflow: hidden;
.thumbnail-creator a {
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();
document.documentElement.lang = window._locale;
})();
/**

View file

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

View file

@ -37,7 +37,7 @@ const render = (jsx, element, reducers, initialState, enhancer) => {
}
const allReducers = reducer(reducers);
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || redux.compose;
const enhancers = enhancer ?
composeEnhancers(

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 defaults = require('lodash.defaults');
const api = require('../lib/api');
const log = require('../lib/log.js');
const sessionActions = require('./session.js');
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) => {
if (typeof state === 'undefined') {
state = '';
state = module.exports.getInitialState();
}
switch (action.type) {
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:
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 => ({
type: Types.SET_SEARCH_TERM,
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 permissionsReducer = require('./permissions.js').permissionsReducer;
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`.
@ -18,8 +19,9 @@ const sessionReducer = require('./session.js').sessionReducer;
module.exports = opts => {
opts = opts || {};
return combineReducers(defaults(opts, {
session: sessionReducer,
messageCount: messageCountReducer,
navigation: navigationReducer,
permissions: permissionsReducer,
messageCount: messageCountReducer
session: sessionReducer
}));
};

View file

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

View file

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

View file

@ -29,7 +29,7 @@ const Credits = () => (
/>
<span className="name">Carl Bowman</span>
</li>
<li>
<img
alt="Karishma Avatar"
@ -86,6 +86,14 @@ const Credits = () => (
<span className="name">DD Liu</span>
</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>
<img
alt="Shruti Avatar"
@ -446,6 +454,6 @@ const Credits = () => (
</p>
</div>
);
render(<Page><Credits /></Page>, document.getElementById('app'));

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,8 @@
const FormattedDate = require('react-intl').FormattedDate;
const injectIntl = require('react-intl').injectIntl;
const PropTypes = require('prop-types');
const intlShape = require('react-intl').intlShape;
const MediaQuery = require('react-responsive').default;
const React = require('react');
const Formsy = require('formsy-react').default;
const classNames = require('classnames');
@ -27,6 +29,18 @@ const ExtensionChip = require('./extension-chip.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
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 = ({
assetHost,
backpackOptions,
@ -35,6 +49,7 @@ const PreviewPresentation = ({
extensions,
faved,
favoriteCount,
intl,
isFullScreen,
isLoggedIn,
isShared,
@ -70,7 +85,7 @@ const PreviewPresentation = ({
<ShareBanner shared={isShared} />
{ projectInfo && projectInfo.author && projectInfo.author.id && (
<Formsy>
<Formsy onKeyPress={onKeyPress}>
<div className="inner">
<FlexRow className="preview-row">
<FlexRow className="project-header">
@ -88,10 +103,9 @@ const PreviewPresentation = ({
handleUpdate={onUpdate}
name="title"
validationErrors={{
maxLength: 'Sorry title is too long'
// maxLength: props.intl.formatMessage({
// id: 'project.titleMaxLength'
// })
maxLength: intl.formatMessage({
id: 'preview.titleMaxLength'
})
}}
validations={{
maxLength: 100
@ -99,7 +113,10 @@ const PreviewPresentation = ({
value={projectInfo.title}
/> :
<React.Fragment>
<div className="project-title">{projectInfo.title}</div>
<div
className="project-title no-edit"
title={projectInfo.title}
>{projectInfo.title}</div>
{'by '}
<a href={`/users/${projectInfo.author.username}`}>
{projectInfo.author.username}
@ -141,6 +158,21 @@ const PreviewPresentation = ({
<RemixCredit projectInfo={parentInfo} />
<RemixCredit projectInfo={originalInfo} />
{/* 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">
<div className="project-textlabel">
Instructions
@ -295,19 +327,21 @@ const PreviewPresentation = ({
</FlexRow>
</FlexRow>
</FlexRow>
<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}
/>
))}
<MediaQuery minWidth={frameless.tablet}>
<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>
</FlexRow>
</MediaQuery>
</div>
<div className="project-lower-container">
<div className="inner">
@ -365,6 +399,7 @@ PreviewPresentation.propTypes = {
extensions: PropTypes.arrayOf(PropTypes.object),
faved: PropTypes.bool,
favoriteCount: PropTypes.number,
intl: intlShape,
isFullScreen: PropTypes.bool,
isLoggedIn: 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 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 navigationActions = require('../../redux/navigation.js');
const previewActions = require('../../redux/preview.js');
const GUI = require('scratch-gui');
@ -31,7 +35,6 @@ class Preview extends React.Component {
'handleFavoriteToggle',
'handleLoadMore',
'handleLoveToggle',
'handlePermissions',
'handlePopState',
'handleReportClick',
'handleReportClose',
@ -39,11 +42,11 @@ class Preview extends React.Component {
'handleAddToStudioClick',
'handleAddToStudioClose',
'handleSeeInside',
'handleUpdateProjectTitle',
'handleUpdate',
'initCounts',
'isShared',
'pushHistory',
'userOwnsProject'
'renderLogin'
]);
const pathname = window.location.pathname.toLowerCase();
const parts = pathname.split('/').filter(Boolean);
@ -51,7 +54,6 @@ class Preview extends React.Component {
// parts[1]: either :id or 'editor'
// parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen'
this.state = {
editable: false,
extensions: [],
favoriteCount: 0,
loveCount: 0,
@ -86,7 +88,6 @@ class Preview extends React.Component {
if (this.props.projectInfo.id !== prevProps.projectInfo.id) {
this.getExtensions(this.state.projectId);
this.initCounts(this.props.projectInfo.stats.favorites, this.props.projectInfo.stats.loves);
this.handlePermissions();
if (this.props.projectInfo.remix.parent !== null) {
this.props.getParentInfo(this.props.projectInfo.remix.parent);
}
@ -189,8 +190,8 @@ class Preview extends React.Component {
);
}
}
handleToggleStudio (event) {
const studioId = parseInt(event.currentTarget.dataset.id, 10);
handleToggleStudio (id) {
const studioId = parseInt(id, 10);
if (isNaN(studioId)) { // sanity check in case event had no integer data-id
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 () {
this.props.setPlayer(false);
}
handleShare () {
// This is just a placeholder, but enables the button in the editor
}
handleUpdate (jsonData) {
this.props.updateProject(
this.props.projectInfo.id,
@ -259,32 +257,32 @@ class Preview extends React.Component {
this.props.user.token
);
}
handleUpdateProjectTitle (title) {
this.handleUpdate({
title: title
});
}
initCounts (favorites, loves) {
this.setState({
favoriteCount: favorites,
loveCount: loves
});
}
isShared () {
renderLogin ({onClose}) {
return (
// if we don't have projectInfo assume shared until we know otherwise
Object.keys(this.props.projectInfo).length === 0 || (
this.props.projectInfo.history &&
this.props.projectInfo.history.shared.length > 0
)
);
}
isLoggedIn () {
return (
this.props.sessionStatus === sessionActions.Status.FETCHED &&
Object.keys(this.props.user).length > 0
);
}
userOwnsProject () {
return (
this.isLoggedIn() &&
Object.keys(this.props.projectInfo).length > 0 &&
this.props.user.id === this.props.projectInfo.author.id
<ConnectedLogin
key="login-dropdown-presentation"
/* eslint-disable react/jsx-no-bind */
onLogIn={(formData, callback) => {
this.props.handleLogIn(formData, result => {
if (result.success === true) {
onClose();
}
callback(result);
});
}}
/* eslint-ensable react/jsx-no-bind */
/>
);
}
render () {
@ -296,13 +294,13 @@ class Preview extends React.Component {
assetHost={this.props.assetHost}
backpackOptions={this.props.backpackOptions}
comments={this.props.comments}
editable={this.state.editable}
editable={this.props.isEditable}
extensions={this.state.extensions}
faved={this.props.faved}
favoriteCount={this.state.favoriteCount}
isFullScreen={this.state.isFullScreen}
isLoggedIn={this.isLoggedIn()}
isShared={this.isShared()}
isLoggedIn={this.props.isLoggedIn}
isShared={this.props.isShared}
loveCount={this.state.loveCount}
loved={this.props.loved}
originalInfo={this.props.original}
@ -315,8 +313,7 @@ class Preview extends React.Component {
replies={this.props.replies}
reportOpen={this.state.reportOpen}
studios={this.props.studios}
user={this.props.user}
userOwnsProject={this.userOwnsProject()}
userOwnsProject={this.props.userOwnsProject}
onAddToStudioClicked={this.handleAddToStudioClick}
onAddToStudioClosed={this.handleAddToStudioClose}
onFavoriteClicked={this.handleFavoriteToggle}
@ -330,16 +327,28 @@ class Preview extends React.Component {
onUpdate={this.handleUpdate}
/>
</Page> :
<IntlGUI
enableCommunity
hideIntro
assetHost={this.props.assetHost}
backpackOptions={this.props.backpackOptions}
basePath="/"
className="gui"
projectHost={this.props.projectHost}
projectId={this.state.projectId}
/>
<React.Fragment>
<IntlGUI
enableCommunity
hideIntro
assetHost={this.props.assetHost}
backpackOptions={this.props.backpackOptions}
basePath="/"
className="gui"
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,
getRemixes: 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,
original: projectShape,
parent: projectShape,
@ -389,7 +405,8 @@ Preview.propTypes = {
dateJoined: PropTypes.string,
email: PropTypes.string,
classroomId: PropTypes.string
})
}),
userOwnsProject: PropTypes.bool
};
Preview.defaultProps = {
@ -437,26 +454,64 @@ const consolidateStudiosInfo = (curatedStudios, projectStudios, currentStudioIds
return consolidatedStudios;
};
const mapStateToProps = state => ({
projectInfo: state.preview.projectInfo,
comments: state.preview.comments,
faved: state.preview.faved,
loved: state.preview.loved,
original: state.preview.original,
parent: state.preview.parent,
remixes: state.preview.remixes,
replies: state.preview.replies,
sessionStatus: state.session.status,
projectStudios: state.preview.projectStudios,
studios: consolidateStudiosInfo(state.preview.curatedStudios,
state.preview.projectStudios, state.preview.currentStudioIds,
state.preview.status.studioRequests),
user: state.session.session.user,
playerMode: state.scratchGui.mode.isPlayerOnly,
fullScreen: state.scratchGui.mode.isFullScreen
});
const mapStateToProps = state => {
const projectInfoPresent = Object.keys(state.preview.projectInfo).length > 0;
const userPresent = state.session.session.user &&
Object.keys(state.session.session.user).length > 0;
const isLoggedIn = state.session.status === sessionActions.Status.FETCHED &&
userPresent;
const authorPresent = projectInfoPresent && state.preview.projectInfo.author &&
Object.keys(state.preview.projectInfo.author).length > 0;
return {
comments: state.preview.comments,
faved: state.preview.faved,
fullScreen: state.scratchGui.mode.isFullScreen,
// project is editable iff logged in user is the author of the project, or
// logged in user is an admin.
isEditable: isLoggedIn &&
((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 => ({
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 => {
dispatch(previewActions.getOriginalInfo(id));
},
@ -497,9 +552,6 @@ const mapDispatchToProps = dispatch => ({
setLovedStatus: (loved, id, username, token) => {
dispatch(previewActions.setLovedStatus(loved, id, username, token));
},
refreshSession: () => {
dispatch(sessionActions.refreshSession());
},
reportProject: (id, formData) => {
dispatch(previewActions.reportProject(id, formData));
},

View file

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

View file

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

View file

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

View file

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

View file

@ -37,7 +37,7 @@ class Search extends React.Component {
this.state.mode = 'popular';
this.state.offset = 0;
this.state.loadMore = false;
let mode = '';
const query = window.location.search;
const m = query.lastIndexOf('mode=');
@ -54,15 +54,24 @@ class Search extends React.Component {
if (ACCEPTABLE_MODES.indexOf(mode) !== -1) {
this.state.mode = mode;
}
}
componentDidMount () {
const query = window.location.search;
const q = query.lastIndexOf('q=');
let term = '';
if (q !== -1) {
term = query.substring(q + 2, query.length).toLowerCase();
}
const query = decodeURIComponent(window.location.search);
let term = query;
const stripQueryValue = function (queryTerm) {
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) {
term = term.substring(0, term.indexOf('/'));
}
@ -232,7 +241,7 @@ Search.propTypes = {
};
const mapStateToProps = state => ({
searchTerm: state.navigation
searchTerm: state.navigation.searchTerm
});
const WrappedSearch = injectIntl(Search);

View file

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

View file

@ -7,8 +7,8 @@ const React = require('react');
const api = require('../../lib/api');
const injectIntl = require('../../lib/intl.jsx').injectIntl;
const intlShape = require('../../lib/intl.jsx').intlShape;
const log = require('../../lib/log.js');
const sessionStatus = require('../../redux/session').Status;
const navigationActions = require('../../redux/navigation.js');
const Deck = require('../../components/deck/deck.jsx');
const Progression = require('../../components/progression/progression.jsx');
@ -24,7 +24,6 @@ class StudentCompleteRegistration extends React.Component {
super(props);
bindAll(this, [
'handleAdvanceStep',
'handleLogOut',
'handleRegister',
'handleGoToClass'
]);
@ -61,21 +60,9 @@ class StudentCompleteRegistration extends React.Component {
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) {
this.setState({waiting: true});
formData = defaults({}, formData || {}, this.state.formData);
const submittedData = {
birth_month: formData.user.birth.month,
@ -87,7 +74,7 @@ class StudentCompleteRegistration extends React.Component {
if (this.props.must_reset_password) {
submittedData.password = formData.user.password;
}
api({
host: '',
uri: '/classes/student_update_registration/',
@ -147,7 +134,7 @@ class StudentCompleteRegistration extends React.Component {
classroom={this.state.classroom}
studentUsername={this.props.studentUsername}
waiting={this.state.waiting}
onHandleLogOut={this.handleLogOut}
onHandleLogOut={this.props.handleLogOut}
onNextStep={this.handleAdvanceStep}
/>
{this.props.must_reset_password ?
@ -178,6 +165,7 @@ class StudentCompleteRegistration extends React.Component {
StudentCompleteRegistration.propTypes = {
classroomId: PropTypes.number.isRequired,
handleLogOut: PropTypes.func,
intl: intlShape,
must_reset_password: PropTypes.bool.isRequired,
newStudent: PropTypes.bool.isRequired,
@ -199,6 +187,16 @@ const mapStateToProps = state => ({
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'));

View file

@ -25,5 +25,9 @@
"wedo2.updateLinkText": "Make sure you have installed the latest version of Scratch Link.",
"wedo2.legacyInfoTitle": "Using Scratch 2.0?",
"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 () {
return (
<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">
<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
id="wedo2.headerText"
values={{
@ -48,19 +54,31 @@ class Wedo2 extends ExtensionLanding {
</FlexRow>
<ExtensionRequirements>
<span>
<img src="/svgs/extensions/windows.svg" />
<img
alt=""
src="/svgs/extensions/windows.svg"
/>
Windows 10+
</span>
<span>
<img src="/svgs/extensions/mac.svg" />
<img
alt=""
src="/svgs/extensions/mac.svg"
/>
macOS 10.13+
</span>
<span>
<img src="/svgs/extensions/bluetooth.svg" />
<img
alt=""
src="/svgs/extensions/bluetooth.svg"
/>
Bluetooth
</span>
<span>
<img src="/svgs/extensions/scratch-link.svg" />
<img
alt=""
src="/svgs/extensions/scratch-link.svg"
/>
Scratch Link
</span>
</ExtensionRequirements>
@ -80,6 +98,7 @@ class Wedo2 extends ExtensionLanding {
<Step number={1}>
<div className="step-image">
<img
alt=""
className="screenshot"
src="/images/wedo2/wedo2-connect-1.png"
/>
@ -104,6 +123,7 @@ class Wedo2 extends ExtensionLanding {
<Step number={2}>
<div className="step-image">
<img
alt={this.props.intl.formatMessage({id: 'extensionInstallation.addExtension'})}
className="screenshot"
src="/images/wedo2/wedo2-connect-2.png"
/>
@ -125,7 +145,10 @@ class Wedo2 extends ExtensionLanding {
<FormattedMessage id="wedo2.plugMotorIn" />
</span>
<div className="step-image">
<img src="/images/wedo2/wedo2-motor.png" />
<img
alt=""
src="/images/wedo2/wedo2-motor.png"
/>
</div>
</Step>
<Step
@ -143,7 +166,10 @@ class Wedo2 extends ExtensionLanding {
/>
</span>
<div className="step-image">
<img src="/images/wedo2/wedo2-motor-turn-block.png" />
<img
alt=""
src="/images/wedo2/wedo2-motor-turn-block.png"
/>
</div>
</Step>
</Steps>
@ -153,18 +179,21 @@ class Wedo2 extends ExtensionLanding {
<ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239284992"
description={this.props.intl.formatMessage({id: 'wedo2.starter1Description'})}
imageAlt={this.props.intl.formatMessage({id: 'wedo2.imgAltStarter1'})}
imageSrc="/images/wedo2/wedo2-starter1.png"
title={this.props.intl.formatMessage({id: 'wedo2.starter1Title'})}
/>
<ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239284997"
description={this.props.intl.formatMessage({id: 'wedo2.starter2Description'})}
imageAlt={this.props.intl.formatMessage({id: 'wedo2.imgAltStarter2'})}
imageSrc="/images/wedo2/wedo2-starter2.png"
title={this.props.intl.formatMessage({id: 'wedo2.starter2Title'})}
/>
<ProjectCard
cardUrl="https://beta.scratch.mit.edu/#239285001"
description={this.props.intl.formatMessage({id: 'wedo2.starter3Description'})}
imageAlt={this.props.intl.formatMessage({id: 'wedo2.imgAltStarter3'})}
imageSrc="/images/wedo2/wedo2-starter3.png"
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 => {
clickXpath('//a[@class="mystuff-icon"]')
clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => findByXpath('//div[@class="box-head"]/h2'))
.then((element) => element.getText('h2'))
.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 => {
clickXpath('//a[@class="mystuff-icon"]')
clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => findByXpath('//a[@data-control="edit"]'))
.then((element) => element.getText('span'))
.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 => {
clickXpath('//a[@class="mystuff-icon"]')
clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => clickXpath('//a[@data-control="edit"]'))
.then(() => driver.getCurrentUrl())
.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 => {
clickXpath('//a[@class="mystuff-icon"]')
clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => findByXpath('//div[@data-control="add-to"]'))
.then((element) => element.getText('span'))
.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 => {
clickXpath('//a[@class="mystuff-icon"]')
clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => clickXpath('//form[@id="new_studio"]/button[@type="submit"]'))
.then(() => findByXpath('//div[@id="show-add-project"]'))
.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 => {
clickXpath('//a[@class="mystuff-icon"]')
clickXpath('//a[contains(@class, "mystuff-icon")]')
.then(() => clickText('+ New Project'))
.then(() => driver.getCurrentUrl())
.then(function (u) {

View file

@ -79,7 +79,8 @@ tap.test('checkFeaturedStudiosRowWhenSignedOut', function (t) {
// checks that the link for a studio makes sense
tap.test('checkFeaturedStudiosRowLinkWhenSignedOut', function (t) {
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))
.then(function (element) {
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 => {
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(() => findText('Sign in'))
.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(() => clickXpath('//button[contains(@class, "button") and ' +
'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((text) => t.match(text.toLowerCase(), username.substring(0, 10).toLowerCase(),
'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 => {
clickXpath('//a[@class="user-info"]')
clickXpath('//a[contains(@class, "user-info")]')
.then(() => clickText('Sign out'))
.then(() => findText('Sign in'))
.then((element) => t.ok(element, 'Sign in reappeared on the page after signing out'))