mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-30 02:56:20 -05:00
Merge pull request #2123 from LLK/release/september-2018
[Master] Release for September 2018
This commit is contained in:
commit
dded9e9c0f
64 changed files with 1348 additions and 765 deletions
14
Makefile
14
Makefile
|
@ -1,11 +1,25 @@
|
||||||
ESLINT=./node_modules/.bin/eslint
|
ESLINT=./node_modules/.bin/eslint
|
||||||
NODE= NODE_OPTIONS=--max_old_space_size=8000 node
|
NODE= NODE_OPTIONS=--max_old_space_size=8000 node
|
||||||
SASSLINT=./node_modules/.bin/sass-lint -v
|
SASSLINT=./node_modules/.bin/sass-lint -v
|
||||||
|
SCRATCH_DOCKER_CONFIG=./node_modules/.bin/docker_config.sh
|
||||||
S3CMD=s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600
|
S3CMD=s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600
|
||||||
TAP=./node_modules/.bin/tap
|
TAP=./node_modules/.bin/tap
|
||||||
WATCH= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/watch
|
WATCH= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/watch
|
||||||
WEBPACK= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/webpack
|
WEBPACK= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/webpack
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------
|
||||||
|
|
||||||
|
$(SCRATCH_DOCKER_CONFIG):
|
||||||
|
npm install scratch-docker
|
||||||
|
|
||||||
|
docker-up: $(SCRATCH_DOCKER_CONFIG)
|
||||||
|
$(SCRATCH_DOCKER_CONFIG) network create
|
||||||
|
docker-compose up
|
||||||
|
|
||||||
|
docker-down:
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
# ------------------------------------
|
# ------------------------------------
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|
|
@ -4,8 +4,9 @@ volumes:
|
||||||
runtime_data:
|
runtime_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
scratch-api_scratch_network:
|
default:
|
||||||
external: true
|
external:
|
||||||
|
name: scratchapi_scratch_network
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
|
@ -13,7 +14,7 @@ services:
|
||||||
hostname: scratch-www-app
|
hostname: scratch-www-app
|
||||||
environment:
|
environment:
|
||||||
- API_HOST=http://localhost:8491
|
- API_HOST=http://localhost:8491
|
||||||
- FALLBACK=http://localhost:8080
|
- FALLBACK=http://scratchr2-app:8080
|
||||||
- USE_DOCKER_WATCHOPTIONS=true
|
- USE_DOCKER_WATCHOPTIONS=true
|
||||||
build:
|
build:
|
||||||
context: ./
|
context: ./
|
||||||
|
@ -35,5 +36,3 @@ services:
|
||||||
- runtime_data:/runtime
|
- runtime_data:/runtime
|
||||||
ports:
|
ports:
|
||||||
- "8333:8333"
|
- "8333:8333"
|
||||||
networks:
|
|
||||||
- scratch-api_scratch_network
|
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"lodash.defaults": "4.0.1",
|
"lodash.defaults": "4.0.1",
|
||||||
"newrelic": "1.25.4",
|
"newrelic": "1.25.4",
|
||||||
"raven": "0.10.0",
|
"raven": "0.10.0",
|
||||||
|
"scratch-docker": "^1.0.2",
|
||||||
"scratch-parser": "^4.2.0",
|
"scratch-parser": "^4.2.0",
|
||||||
"scratch-storage": "^0.5.1"
|
"scratch-storage": "^0.5.1"
|
||||||
},
|
},
|
||||||
|
@ -77,7 +78,6 @@
|
||||||
"lodash.merge": "3.3.2",
|
"lodash.merge": "3.3.2",
|
||||||
"lodash.omit": "3.1.0",
|
"lodash.omit": "3.1.0",
|
||||||
"lodash.range": "3.0.1",
|
"lodash.range": "3.0.1",
|
||||||
"lodash.truncate": "4.4.2",
|
|
||||||
"minilog": "2.0.8",
|
"minilog": "2.0.8",
|
||||||
"node-dir": "0.1.16",
|
"node-dir": "0.1.16",
|
||||||
"node-sass": "4.6.1",
|
"node-sass": "4.6.1",
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
"redux-thunk": "2.0.1",
|
"redux-thunk": "2.0.1",
|
||||||
"sass-lint": "1.5.1",
|
"sass-lint": "1.5.1",
|
||||||
"sass-loader": "6.0.6",
|
"sass-loader": "6.0.6",
|
||||||
"scratch-gui": "latest",
|
"scratch-gui": "develop",
|
||||||
"scratchr2_translations": "git://github.com/LLK/scratchr2_translations.git#master",
|
"scratchr2_translations": "git://github.com/LLK/scratchr2_translations.git#master",
|
||||||
"slick-carousel": "1.6.0",
|
"slick-carousel": "1.6.0",
|
||||||
"source-map-support": "0.3.2",
|
"source-map-support": "0.3.2",
|
||||||
|
|
|
@ -33,8 +33,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
// 100% minus border and padding
|
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
// 100% minus border and padding
|
||||||
width: calc(100% - 30px);
|
width: calc(100% - 30px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,11 @@
|
||||||
padding: 4rem 0;
|
padding: 4rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2 {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,16 +89,16 @@
|
||||||
margin-bottom: 5rem;
|
margin-bottom: 5rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
h2 {
|
h1, h2 {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
}
|
|
||||||
|
|
||||||
h2 img {
|
img {
|
||||||
padding-right: .5rem;
|
padding-right: .5rem;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
|
|
@ -35,7 +35,10 @@ const InstallScratchLink = ({
|
||||||
<FormattedMessage id="installScratchLink.windowsDownload" /> :
|
<FormattedMessage id="installScratchLink.windowsDownload" /> :
|
||||||
<FormattedMessage id="installScratchLink.macosDownload" />
|
<FormattedMessage id="installScratchLink.macosDownload" />
|
||||||
}
|
}
|
||||||
<img src="/svgs/extensions/download-white.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/download-white.svg"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</Step>
|
</Step>
|
||||||
|
@ -50,6 +53,7 @@ const InstallScratchLink = ({
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
className="screenshot"
|
className="screenshot"
|
||||||
src={`/images/scratchlink/${
|
src={`/images/scratchlink/${
|
||||||
currentOS === OS_ENUM.WINDOWS ? 'windows' : 'mac'
|
currentOS === OS_ENUM.WINDOWS ? 'windows' : 'mac'
|
||||||
|
|
|
@ -8,7 +8,10 @@ const ProjectCard = props => (
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div className="project-card-image">
|
<div className="project-card-image">
|
||||||
<img src={props.imageSrc} />
|
<img
|
||||||
|
alt={props.imageAlt}
|
||||||
|
src={props.imageSrc}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="project-card-info">
|
<div className="project-card-info">
|
||||||
<h4>{props.title}</h4>
|
<h4>{props.title}</h4>
|
||||||
|
@ -20,6 +23,7 @@ const ProjectCard = props => (
|
||||||
ProjectCard.propTypes = {
|
ProjectCard.propTypes = {
|
||||||
cardUrl: PropTypes.string,
|
cardUrl: PropTypes.string,
|
||||||
description: PropTypes.string,
|
description: PropTypes.string,
|
||||||
|
imageAlt: PropTypes.string,
|
||||||
imageSrc: PropTypes.string,
|
imageSrc: PropTypes.string,
|
||||||
title: PropTypes.string
|
title: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@ const connect = require('react-redux').connect;
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
const sessionActions = require('../../redux/session.js');
|
const navigationActions = require('../../redux/navigation.js');
|
||||||
|
|
||||||
const IframeModal = require('../modal/iframe/modal.jsx');
|
const IframeModal = require('../modal/iframe/modal.jsx');
|
||||||
const Registration = require('../registration/registration.jsx');
|
const Registration = require('../registration/registration.jsx');
|
||||||
|
@ -15,10 +15,7 @@ class Intro extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'handleShowVideo',
|
'handleShowVideo',
|
||||||
'handleCloseVideo',
|
'handleCloseVideo'
|
||||||
'handleJoinClick',
|
|
||||||
'handleCloseRegistration',
|
|
||||||
'handleCompleteRegistration'
|
|
||||||
]);
|
]);
|
||||||
this.state = {
|
this.state = {
|
||||||
videoOpen: false
|
videoOpen: false
|
||||||
|
@ -30,17 +27,6 @@ class Intro extends React.Component {
|
||||||
handleCloseVideo () {
|
handleCloseVideo () {
|
||||||
this.setState({videoOpen: false});
|
this.setState({videoOpen: false});
|
||||||
}
|
}
|
||||||
handleJoinClick (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({registrationOpen: true});
|
|
||||||
}
|
|
||||||
handleCloseRegistration () {
|
|
||||||
this.setState({registrationOpen: false});
|
|
||||||
}
|
|
||||||
handleCompleteRegistration () {
|
|
||||||
this.props.dispatch(sessionActions.refreshSession());
|
|
||||||
this.closeRegistration();
|
|
||||||
}
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className="intro">
|
<div className="intro">
|
||||||
|
@ -92,7 +78,7 @@ class Intro extends React.Component {
|
||||||
<a
|
<a
|
||||||
className="sprite sprite-3"
|
className="sprite sprite-3"
|
||||||
href="#"
|
href="#"
|
||||||
onClick={this.handleJoinClick}
|
onClick={this.props.handleOpenRegistration}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Gobo"
|
alt="Gobo"
|
||||||
|
@ -111,10 +97,7 @@ class Intro extends React.Component {
|
||||||
<div className="text subtext">{this.props.messages['intro.itsFree']}</div>
|
<div className="text subtext">{this.props.messages['intro.itsFree']}</div>
|
||||||
</a>
|
</a>
|
||||||
<Registration
|
<Registration
|
||||||
isOpen={this.state.registrationOpen}
|
|
||||||
key="registration"
|
key="registration"
|
||||||
onRegistrationDone={this.handleCompleteRegistration}
|
|
||||||
onRequestClose={this.handleCloseRegistration}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -160,7 +143,7 @@ class Intro extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
Intro.propTypes = {
|
Intro.propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
handleOpenRegistration: PropTypes.func,
|
||||||
messages: PropTypes.shape({
|
messages: PropTypes.shape({
|
||||||
'intro.aboutScratch': PropTypes.string,
|
'intro.aboutScratch': PropTypes.string,
|
||||||
'intro.forEducators': PropTypes.string,
|
'intro.forEducators': PropTypes.string,
|
||||||
|
@ -194,6 +177,17 @@ const mapStateToProps = state => ({
|
||||||
session: state.session
|
session: state.session
|
||||||
});
|
});
|
||||||
|
|
||||||
const ConnectedIntro = connect(mapStateToProps)(Intro);
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
handleOpenRegistration: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.handleOpenRegistration());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const ConnectedIntro = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Intro);
|
||||||
|
|
||||||
module.exports = ConnectedIntro;
|
module.exports = ConnectedIntro;
|
||||||
|
|
60
src/components/login/canceled-deletion-modal.jsx
Normal file
60
src/components/login/canceled-deletion-modal.jsx
Normal 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);
|
34
src/components/login/connected-login.jsx
Normal file
34
src/components/login/connected-login.jsx
Normal 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);
|
50
src/components/login/login-dropdown.jsx
Normal file
50
src/components/login/login-dropdown.jsx
Normal 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);
|
0
src/components/login/login-dropdown.scss
Normal file
0
src/components/login/login-dropdown.scss
Normal file
|
@ -3,8 +3,6 @@ const FormattedMessage = require('react-intl').FormattedMessage;
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
const log = require('../../lib/log.js');
|
|
||||||
|
|
||||||
const Form = require('../forms/form.jsx');
|
const Form = require('../forms/form.jsx');
|
||||||
const Input = require('../forms/input.jsx');
|
const Input = require('../forms/input.jsx');
|
||||||
const Button = require('../forms/button.jsx');
|
const Button = require('../forms/button.jsx');
|
||||||
|
@ -24,8 +22,7 @@ class Login extends React.Component {
|
||||||
}
|
}
|
||||||
handleSubmit (formData) {
|
handleSubmit (formData) {
|
||||||
this.setState({waiting: true});
|
this.setState({waiting: true});
|
||||||
this.props.onLogIn(formData, err => {
|
this.props.onLogIn(formData, () => {
|
||||||
if (err) log.error(err);
|
|
||||||
this.setState({waiting: false});
|
this.setState({waiting: false});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -48,9 +45,6 @@ class Login extends React.Component {
|
||||||
key="usernameInput"
|
key="usernameInput"
|
||||||
maxLength="30"
|
maxLength="30"
|
||||||
name="username"
|
name="username"
|
||||||
ref={input => {
|
|
||||||
this.username = input;
|
|
||||||
}}
|
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
|
@ -63,9 +57,6 @@ class Login extends React.Component {
|
||||||
required
|
required
|
||||||
key="passwordInput"
|
key="passwordInput"
|
||||||
name="password"
|
name="password"
|
||||||
ref={input => {
|
|
||||||
this.password = input;
|
|
||||||
}}
|
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
{this.state.waiting ? [
|
{this.state.waiting ? [
|
||||||
|
@ -75,7 +66,10 @@ class Login extends React.Component {
|
||||||
key="submitButton"
|
key="submitButton"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<Spinner />
|
<Spinner
|
||||||
|
className="spinner"
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
] : [
|
] : [
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -2,6 +2,26 @@
|
||||||
|
|
||||||
.login {
|
.login {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
width: 200px;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
white-space: normal; // override any parent, such as in gui, who sets nowrap
|
||||||
|
color: $type-white;
|
||||||
|
font-size: .8125rem;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: .75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
// 100% minus border and padding
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
height: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
|
@ -15,7 +35,7 @@
|
||||||
.spinner {
|
.spinner {
|
||||||
margin: 0 .8rem;
|
margin: 0 .8rem;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-button {
|
.submit-button {
|
||||||
|
@ -24,13 +44,19 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:link,
|
||||||
|
&:visited,
|
||||||
|
&:active {
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
border: 1px solid $active-dark-gray;
|
border: 1px solid $active-dark-gray;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
48
src/components/modal/addtostudio/animate-hoc.jsx
Normal file
48
src/components/modal/addtostudio/animate-hoc.jsx
Normal 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;
|
|
@ -89,18 +89,20 @@
|
||||||
pointer-events: none; /* pass clicks through to buttons underneath */
|
pointer-events: none; /* pass clicks through to buttons underneath */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.studio-selector-button {
|
.studio-selector-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
transition: all .5s;
|
||||||
margin: .21875rem .21875rem;
|
margin: .21875rem .21875rem;
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
background-color: $ui-white;
|
background-color: $ui-white;
|
||||||
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 16.1875rem; /* 259px */
|
width: 16.1875rem; /* 259px */
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-selector-button-text {
|
.studio-selector-button-text {
|
||||||
|
@ -112,8 +114,11 @@
|
||||||
*/
|
*/
|
||||||
margin: .575rem 2.18375rem .175rem .6875rem;
|
margin: .575rem 2.18375rem .175rem .6875rem;
|
||||||
width: 13.3125rem;
|
width: 13.3125rem;
|
||||||
height: 1rem; /* diff from spec, in case we ever do valign to middle */
|
height: 1.25rem; /* diff from spec, in case we ever do valign to middle; changed to match line-height because else with overflow hidden it cuts off some letters */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
font-family: "Helvetica Neue";
|
font-family: "Helvetica Neue";
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
font-weight: regular;
|
font-weight: regular;
|
||||||
|
@ -160,30 +165,30 @@
|
||||||
background-color: $ui-blue;
|
background-color: $ui-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-status-icon-plus-img {
|
.studio-status-icon-plus-img,
|
||||||
|
.studio-status-icon-checkmark-img {
|
||||||
|
animation-direction: normal;
|
||||||
width: 1.4rem;
|
width: 1.4rem;
|
||||||
height: 1.4rem;
|
height: 1.4rem;
|
||||||
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-status-icon--img {
|
.studio-status-icon-with-animation {
|
||||||
width: 1.4rem;
|
animation-name: bump;
|
||||||
height: 1.4rem;
|
animation-duration: .25s;
|
||||||
|
animation-timing-function: cubic-bezier(.3, -3, .6, 3);
|
||||||
|
animation-iteration-count: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button-text .spinner-smooth {
|
@keyframes bump {
|
||||||
margin: .2125rem auto;
|
0% {
|
||||||
width: 1.875rem;
|
transform: scale(0);
|
||||||
height: 1rem;
|
opacity: 0;
|
||||||
}
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
.studio-status-icon .spinner-smooth {
|
100% {
|
||||||
position: unset; /* don't understand why neither relative nor absolute work */
|
transform: scale(1);
|
||||||
}
|
opacity: 1;
|
||||||
|
-webkit-transform: scale(1);
|
||||||
.studio-status-icon .spinner-smooth .circle {
|
}
|
||||||
/* overlay spinner on circle */
|
|
||||||
position: absolute;
|
|
||||||
margin: .1875rem; /* stay within boundaries of circle */
|
|
||||||
width: 75%; /* stay within boundaries of circle */
|
|
||||||
height: 75%; /* stay within boundaries of circle */
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ const AddToStudioModalPresentation = ({
|
||||||
includesProject={studio.includesProject}
|
includesProject={studio.includesProject}
|
||||||
key={studio.id}
|
key={studio.id}
|
||||||
title={studio.title}
|
title={studio.title}
|
||||||
onToggleStudio={onToggleStudio}
|
onClick={onToggleStudio}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ const AddToStudioModalPresentation = ({
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<div className="action-button-text">
|
<div className="action-button-text">
|
||||||
<Spinner mode="smooth" />
|
<Spinner />
|
||||||
<FormattedMessage id="addToStudio.finishing" />
|
<FormattedMessage id="addToStudio.finishing" />
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,29 +1,36 @@
|
||||||
const truncateAtWordBoundary = require('../../../lib/truncate').truncateAtWordBoundary;
|
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const classNames = require('classnames');
|
const classNames = require('classnames');
|
||||||
|
|
||||||
const Spinner = require('../../spinner/spinner.jsx');
|
const Spinner = require('../../spinner/spinner.jsx');
|
||||||
|
const AnimateHOC = require('./animate-hoc.jsx');
|
||||||
|
|
||||||
require('./modal.scss');
|
require('./modal.scss');
|
||||||
|
|
||||||
const StudioButton = ({
|
const StudioButton = ({
|
||||||
hasRequestOutstanding,
|
hasRequestOutstanding,
|
||||||
id,
|
|
||||||
includesProject,
|
includesProject,
|
||||||
title,
|
title,
|
||||||
onToggleStudio
|
onClick,
|
||||||
|
wasClicked
|
||||||
}) => {
|
}) => {
|
||||||
const checkmark = (
|
const checkmark = (
|
||||||
<img
|
<img
|
||||||
alt="checkmark-icon"
|
alt="checkmark-icon"
|
||||||
className="studio-status-icon-checkmark-img"
|
className={classNames(
|
||||||
|
'studio-status-icon-checkmark-img',
|
||||||
|
{'studio-status-icon-with-animation': wasClicked}
|
||||||
|
)}
|
||||||
src="/svgs/modal/confirm.svg"
|
src="/svgs/modal/confirm.svg"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const plus = (
|
const plus = (
|
||||||
<img
|
<img
|
||||||
alt="plus-icon"
|
alt="plus-icon"
|
||||||
className="studio-status-icon-plus-img"
|
className={classNames(
|
||||||
|
'studio-status-icon-plus-img',
|
||||||
|
{'studio-status-icon-with-animation': wasClicked}
|
||||||
|
)}
|
||||||
src="/svgs/modal/add.svg"
|
src="/svgs/modal/add.svg"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -35,8 +42,7 @@ const StudioButton = ({
|
||||||
{'studio-selector-button-selected':
|
{'studio-selector-button-selected':
|
||||||
includesProject && !hasRequestOutstanding}
|
includesProject && !hasRequestOutstanding}
|
||||||
)}
|
)}
|
||||||
data-id={id}
|
onClick={onClick}
|
||||||
onClick={onToggleStudio}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -44,17 +50,18 @@ const StudioButton = ({
|
||||||
{'studio-selector-button-text-selected': includesProject || hasRequestOutstanding},
|
{'studio-selector-button-text-selected': includesProject || hasRequestOutstanding},
|
||||||
{'studio-selector-button-text-unselected': !includesProject && !hasRequestOutstanding}
|
{'studio-selector-button-text-unselected': !includesProject && !hasRequestOutstanding}
|
||||||
)}
|
)}
|
||||||
|
title={title}
|
||||||
>
|
>
|
||||||
{truncateAtWordBoundary(title, 25)}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'studio-status-icon',
|
'studio-status-icon',
|
||||||
{'studio-status-icon-unselected': !includesProject}
|
{'studio-status-icon-unselected': !includesProject && !hasRequestOutstanding}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{(hasRequestOutstanding ?
|
{(hasRequestOutstanding ?
|
||||||
(<Spinner mode="smooth" />) :
|
<Spinner /> :
|
||||||
(includesProject ? checkmark : plus))}
|
(includesProject ? checkmark : plus))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,10 +70,10 @@ const StudioButton = ({
|
||||||
|
|
||||||
StudioButton.propTypes = {
|
StudioButton.propTypes = {
|
||||||
hasRequestOutstanding: PropTypes.bool,
|
hasRequestOutstanding: PropTypes.bool,
|
||||||
id: PropTypes.number,
|
|
||||||
includesProject: PropTypes.bool,
|
includesProject: PropTypes.bool,
|
||||||
onToggleStudio: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
title: PropTypes.string
|
title: PropTypes.string,
|
||||||
|
wasClicked: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = StudioButton;
|
module.exports = AnimateHOC(StudioButton);
|
||||||
|
|
|
@ -7,7 +7,7 @@ const ReactModal = require('react-modal');
|
||||||
|
|
||||||
require('./modal.scss');
|
require('./modal.scss');
|
||||||
|
|
||||||
ReactModal.setAppElement(document.getElementById('view'));
|
ReactModal.setAppElement(document.getElementById('app'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Container for pop up windows (See: registration window)
|
* Container for pop up windows (See: registration window)
|
||||||
|
@ -25,7 +25,7 @@ class Modal extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<ReactModal
|
||||||
appElement={document.getElementById('view')}
|
appElement={document.getElementById('app')}
|
||||||
className={{
|
className={{
|
||||||
base: classNames('modal-content', this.props.className),
|
base: classNames('modal-content', this.props.className),
|
||||||
afterOpen: classNames('modal-content', this.props.className),
|
afterOpen: classNames('modal-content', this.props.className),
|
||||||
|
|
|
@ -224,7 +224,7 @@ class ReportModal extends React.Component {
|
||||||
>
|
>
|
||||||
{isWaiting ? (
|
{isWaiting ? (
|
||||||
<div className="action-button-text">
|
<div className="action-button-text">
|
||||||
<Spinner mode="smooth" />
|
<Spinner />
|
||||||
<FormattedMessage id="report.sending" />
|
<FormattedMessage id="report.sending" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
102
src/components/navigation/www/accountnav.jsx
Normal file
102
src/components/navigation/www/accountnav.jsx
Normal 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);
|
98
src/components/navigation/www/accountnav.scss
Normal file
98
src/components/navigation/www/accountnav.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,19 +8,17 @@ const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
const messageCountActions = require('../../../redux/message-count.js');
|
const messageCountActions = require('../../../redux/message-count.js');
|
||||||
|
const navigationActions = require('../../../redux/navigation.js');
|
||||||
const sessionActions = require('../../../redux/session.js');
|
const sessionActions = require('../../../redux/session.js');
|
||||||
|
|
||||||
const api = require('../../../lib/api');
|
|
||||||
const Avatar = require('../../avatar/avatar.jsx');
|
|
||||||
const Button = require('../../forms/button.jsx');
|
const Button = require('../../forms/button.jsx');
|
||||||
const Dropdown = require('../../dropdown/dropdown.jsx');
|
|
||||||
const Form = require('../../forms/form.jsx');
|
const Form = require('../../forms/form.jsx');
|
||||||
const Input = require('../../forms/input.jsx');
|
const Input = require('../../forms/input.jsx');
|
||||||
const log = require('../../../lib/log.js');
|
const LoginDropdown = require('../../login/login-dropdown.jsx');
|
||||||
const Login = require('../../login/login.jsx');
|
const CanceledDeletionModal = require('../../login/canceled-deletion-modal.jsx');
|
||||||
const Modal = require('../../modal/base/modal.jsx');
|
|
||||||
const NavigationBox = require('../base/navigation.jsx');
|
const NavigationBox = require('../base/navigation.jsx');
|
||||||
const Registration = require('../../registration/registration.jsx');
|
const Registration = require('../../registration/registration.jsx');
|
||||||
|
const AccountNav = require('./accountnav.jsx');
|
||||||
|
|
||||||
require('./navigation.scss');
|
require('./navigation.scss');
|
||||||
|
|
||||||
|
@ -29,34 +27,16 @@ class Navigation extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'getProfileUrl',
|
'getProfileUrl',
|
||||||
'handleJoinClick',
|
|
||||||
'handleLoginClick',
|
|
||||||
'handleCloseLogin',
|
|
||||||
'handleLogIn',
|
|
||||||
'handleLogOut',
|
|
||||||
'handleAccountNavClick',
|
|
||||||
'handleCloseAccountNav',
|
|
||||||
'showCanceledDeletion',
|
|
||||||
'handleCloseCanceledDeletion',
|
|
||||||
'handleCloseRegistration',
|
|
||||||
'handleCompleteRegistration',
|
|
||||||
'handleSearchSubmit'
|
'handleSearchSubmit'
|
||||||
]);
|
]);
|
||||||
this.state = {
|
this.state = {
|
||||||
accountNavOpen: false,
|
|
||||||
canceledDeletionOpen: false,
|
|
||||||
loginOpen: false,
|
|
||||||
loginError: null,
|
|
||||||
registrationOpen: false,
|
|
||||||
messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
|
messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
if (this.props.session.session.user) {
|
if (this.props.user) {
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
this.props.dispatch(
|
this.props.getMessageCount(this.props.user.username);
|
||||||
messageCountActions.getCount(this.props.session.session.user.username)
|
|
||||||
);
|
|
||||||
}, 120000); // check for new messages every 2 mins.
|
}, 120000); // check for new messages every 2 mins.
|
||||||
this.setState({ // eslint-disable-line react/no-did-mount-set-state
|
this.setState({ // eslint-disable-line react/no-did-mount-set-state
|
||||||
messageCountIntervalId: intervalId
|
messageCountIntervalId: intervalId
|
||||||
|
@ -64,16 +44,11 @@ class Navigation extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (prevProps.session.session.user !== this.props.session.session.user) {
|
if (prevProps.user !== this.props.user) {
|
||||||
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
this.props.closeAccountMenus();
|
||||||
loginOpen: false,
|
if (this.props.user) {
|
||||||
accountNavOpen: false
|
|
||||||
});
|
|
||||||
if (this.props.session.session.user) {
|
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
this.props.dispatch(
|
this.props.getMessageCount(this.props.user.username);
|
||||||
messageCountActions.getCount(this.props.session.session.user.username)
|
|
||||||
);
|
|
||||||
}, 120000); // check for new messages every 2 mins.
|
}, 120000); // check for new messages every 2 mins.
|
||||||
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
||||||
messageCountIntervalId: intervalId
|
messageCountIntervalId: intervalId
|
||||||
|
@ -81,7 +56,7 @@ class Navigation extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
// clear message count check, and set to default id.
|
// clear message count check, and set to default id.
|
||||||
clearInterval(this.state.messageCountIntervalId);
|
clearInterval(this.state.messageCountIntervalId);
|
||||||
this.props.dispatch(messageCountActions.setCount(0));
|
this.props.setMessageCount(0);
|
||||||
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
||||||
messageCountIntervalId: -1
|
messageCountIntervalId: -1
|
||||||
});
|
});
|
||||||
|
@ -92,102 +67,25 @@ class Navigation extends React.Component {
|
||||||
// clear message interval if it exists
|
// clear message interval if it exists
|
||||||
if (this.state.messageCountIntervalId !== -1) {
|
if (this.state.messageCountIntervalId !== -1) {
|
||||||
clearInterval(this.state.messageCountIntervalId);
|
clearInterval(this.state.messageCountIntervalId);
|
||||||
this.props.dispatch(messageCountActions.setCount(0));
|
this.props.setMessageCount(0);
|
||||||
this.setState({
|
this.setState({
|
||||||
messageCountIntervalId: -1
|
messageCountIntervalId: -1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getProfileUrl () {
|
getProfileUrl () {
|
||||||
if (!this.props.session.session.user) return;
|
if (!this.props.user) return;
|
||||||
return `/users/${this.props.session.session.user.username}/`;
|
return `/users/${this.props.user.username}/`;
|
||||||
}
|
|
||||||
handleJoinClick (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({registrationOpen: true});
|
|
||||||
}
|
|
||||||
handleLoginClick (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({loginOpen: !this.state.loginOpen});
|
|
||||||
}
|
|
||||||
handleCloseLogin () {
|
|
||||||
this.setState({loginOpen: false});
|
|
||||||
}
|
|
||||||
handleLogIn (formData, callback) {
|
|
||||||
this.setState({loginError: null});
|
|
||||||
formData.useMessages = true;
|
|
||||||
api({
|
|
||||||
method: 'post',
|
|
||||||
host: '',
|
|
||||||
uri: '/accounts/login/',
|
|
||||||
json: formData,
|
|
||||||
useCsrf: true
|
|
||||||
}, (err, body) => {
|
|
||||||
if (err) this.setState({loginError: err.message});
|
|
||||||
if (body) {
|
|
||||||
body = body[0];
|
|
||||||
if (body.success) {
|
|
||||||
this.handleCloseLogin();
|
|
||||||
body.messages.map(message => { // eslint-disable-line array-callback-return
|
|
||||||
if (message.message === 'canceled-deletion') {
|
|
||||||
this.showCanceledDeletion();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.props.dispatch(sessionActions.refreshSession());
|
|
||||||
} else {
|
|
||||||
if (body.redirect) {
|
|
||||||
window.location = body.redirect;
|
|
||||||
}
|
|
||||||
// Update login error message to a friendlier one if it exists
|
|
||||||
this.setState({loginError: body.msg});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// JS error already logged by api mixin
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
handleLogOut (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
api({
|
|
||||||
host: '',
|
|
||||||
method: 'post',
|
|
||||||
uri: '/accounts/logout/',
|
|
||||||
useCsrf: true
|
|
||||||
}, err => {
|
|
||||||
if (err) log.error(err);
|
|
||||||
this.handleCloseLogin();
|
|
||||||
window.location = '/';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
handleAccountNavClick (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({accountNavOpen: true});
|
|
||||||
}
|
|
||||||
handleCloseAccountNav () {
|
|
||||||
this.setState({accountNavOpen: false});
|
|
||||||
}
|
|
||||||
showCanceledDeletion () {
|
|
||||||
this.setState({canceledDeletionOpen: true});
|
|
||||||
}
|
|
||||||
handleCloseCanceledDeletion () {
|
|
||||||
this.setState({canceledDeletionOpen: false});
|
|
||||||
}
|
|
||||||
handleCloseRegistration () {
|
|
||||||
this.setState({registrationOpen: false});
|
|
||||||
}
|
|
||||||
handleCompleteRegistration () {
|
|
||||||
this.props.dispatch(sessionActions.refreshSession());
|
|
||||||
this.handleCloseRegistration();
|
|
||||||
}
|
}
|
||||||
handleSearchSubmit (formData) {
|
handleSearchSubmit (formData) {
|
||||||
window.location.href = `/search/projects?q=${encodeURIComponent(formData.q)}`;
|
window.location.href = `/search/projects?q=${encodeURIComponent(formData.q)}`;
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
const createLink = this.props.session.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
|
const createLink = this.props.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
|
||||||
return (
|
return (
|
||||||
<NavigationBox
|
<NavigationBox
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'logged-in': this.props.session.session.user
|
'logged-in': this.props.user
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -235,7 +133,7 @@ class Navigation extends React.Component {
|
||||||
</Form>
|
</Form>
|
||||||
</li>
|
</li>
|
||||||
{this.props.session.status === sessionActions.Status.FETCHED ? (
|
{this.props.session.status === sessionActions.Status.FETCHED ? (
|
||||||
this.props.session.session.user ? [
|
this.props.user ? [
|
||||||
<li
|
<li
|
||||||
className="link right messages"
|
className="link right messages"
|
||||||
key="messages"
|
key="messages"
|
||||||
|
@ -268,66 +166,18 @@ class Navigation extends React.Component {
|
||||||
className="link right account-nav"
|
className="link right account-nav"
|
||||||
key="account-nav"
|
key="account-nav"
|
||||||
>
|
>
|
||||||
<a
|
<AccountNav
|
||||||
className={classNames({
|
classroomId={this.props.user.classroomId}
|
||||||
'user-info': true,
|
isEducator={this.props.permissions.educator}
|
||||||
'open': this.state.accountNavOpen
|
isOpen={this.props.accountNavOpen}
|
||||||
})}
|
isStudent={this.props.permissions.student}
|
||||||
href="#"
|
profileUrl={this.getProfileUrl()}
|
||||||
onClick={this.handleAccountNavClick}
|
thumbnailUrl={this.props.user.thumbnailUrl}
|
||||||
>
|
username={this.props.user.username}
|
||||||
<Avatar
|
onClick={this.props.handleToggleAccountNav}
|
||||||
alt=""
|
onClickLogout={this.props.handleLogOut}
|
||||||
src={this.props.session.session.user.thumbnailUrl}
|
onClose={this.props.handleCloseAccountNav}
|
||||||
/>
|
/>
|
||||||
<span className="profile-name">
|
|
||||||
{this.props.session.session.user.username}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<Dropdown
|
|
||||||
as="ul"
|
|
||||||
className={process.env.SCRATCH_ENV}
|
|
||||||
isOpen={this.state.accountNavOpen}
|
|
||||||
onRequestClose={this.handleCloseAccountNav}
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a href={this.getProfileUrl()}>
|
|
||||||
<FormattedMessage id="general.profile" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/mystuff/">
|
|
||||||
<FormattedMessage id="general.myStuff" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{this.props.permissions.educator ? [
|
|
||||||
<li key="my-classes-li">
|
|
||||||
<a href="/educators/classes/">
|
|
||||||
<FormattedMessage id="general.myClasses" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
] : []}
|
|
||||||
{this.props.permissions.student ? [
|
|
||||||
<li key="my-class-li">
|
|
||||||
<a href={`/classes/${this.props.session.session.user.classroomId}/`}>
|
|
||||||
<FormattedMessage id="general.myClass" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
] : []}
|
|
||||||
<li>
|
|
||||||
<a href="/accounts/settings/">
|
|
||||||
<FormattedMessage id="general.accountSettings" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="divider">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={this.handleLogOut}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="navigation.signOut" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</Dropdown>
|
|
||||||
</li>
|
</li>
|
||||||
] : [
|
] : [
|
||||||
<li
|
<li
|
||||||
|
@ -336,16 +186,13 @@ class Navigation extends React.Component {
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
onClick={this.handleJoinClick}
|
onClick={this.props.handleOpenRegistration}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="general.joinScratch" />
|
<FormattedMessage id="general.joinScratch" />
|
||||||
</a>
|
</a>
|
||||||
</li>,
|
</li>,
|
||||||
<Registration
|
<Registration
|
||||||
isOpen={this.state.registrationOpen}
|
|
||||||
key="registration"
|
key="registration"
|
||||||
onRegistrationDone={this.handleCompleteRegistration}
|
|
||||||
onRequestClose={this.handleCloseRegistration}
|
|
||||||
/>,
|
/>,
|
||||||
<li
|
<li
|
||||||
className="link right login-item"
|
className="link right login-item"
|
||||||
|
@ -355,53 +202,31 @@ class Navigation extends React.Component {
|
||||||
className="ignore-react-onclickoutside"
|
className="ignore-react-onclickoutside"
|
||||||
href="#"
|
href="#"
|
||||||
key="login-link"
|
key="login-link"
|
||||||
onClick={this.handleLoginClick}
|
onClick={this.props.handleToggleLoginOpen}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="general.signIn" />
|
<FormattedMessage id="general.signIn" />
|
||||||
</a>
|
</a>
|
||||||
<Dropdown
|
<LoginDropdown
|
||||||
className="login-dropdown with-arrow"
|
|
||||||
isOpen={this.state.loginOpen}
|
|
||||||
key="login-dropdown"
|
key="login-dropdown"
|
||||||
onRequestClose={this.handleCloseLogin}
|
|
||||||
>
|
|
||||||
<Login
|
|
||||||
error={this.state.loginError}
|
|
||||||
onLogIn={this.handleLogIn}
|
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
|
||||||
</li>
|
</li>
|
||||||
]) : []}
|
]) : []}
|
||||||
</ul>
|
</ul>
|
||||||
<Modal
|
<CanceledDeletionModal />
|
||||||
isOpen={this.state.canceledDeletionOpen}
|
|
||||||
style={{
|
|
||||||
content: {
|
|
||||||
padding: 15
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onRequestClose={this.handleCloseCanceledDeletion}
|
|
||||||
>
|
|
||||||
<h4>Your Account Will Not Be Deleted</h4>
|
|
||||||
<h4><FormattedMessage id="general.noDeletionTitle" /></h4>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id="general.noDeletionDescription"
|
|
||||||
values={{
|
|
||||||
resetLink: <a href="/accounts/password_reset/">
|
|
||||||
{this.props.intl.formatMessage({id: 'general.noDeletionLink'})}
|
|
||||||
</a>
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</Modal>
|
|
||||||
</NavigationBox>
|
</NavigationBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigation.propTypes = {
|
Navigation.propTypes = {
|
||||||
dispatch: PropTypes.func,
|
accountNavOpen: PropTypes.bool,
|
||||||
|
closeAccountMenus: PropTypes.func,
|
||||||
|
getMessageCount: PropTypes.func,
|
||||||
|
handleCloseAccountNav: PropTypes.func,
|
||||||
|
handleLogOut: PropTypes.func,
|
||||||
|
handleOpenRegistration: PropTypes.func,
|
||||||
|
handleToggleAccountNav: PropTypes.func,
|
||||||
|
handleToggleLoginOpen: PropTypes.func,
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
permissions: PropTypes.shape({
|
permissions: PropTypes.shape({
|
||||||
admin: PropTypes.bool,
|
admin: PropTypes.bool,
|
||||||
|
@ -412,16 +237,15 @@ Navigation.propTypes = {
|
||||||
}),
|
}),
|
||||||
searchTerm: PropTypes.string,
|
searchTerm: PropTypes.string,
|
||||||
session: PropTypes.shape({
|
session: PropTypes.shape({
|
||||||
session: PropTypes.shape({
|
status: PropTypes.string
|
||||||
|
}),
|
||||||
|
setMessageCount: PropTypes.func,
|
||||||
|
unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
thumbnailUrl: PropTypes.string,
|
thumbnailUrl: PropTypes.string,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
})
|
})
|
||||||
}),
|
|
||||||
status: PropTypes.string
|
|
||||||
}),
|
|
||||||
unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Navigation.defaultProps = {
|
Navigation.defaultProps = {
|
||||||
|
@ -431,12 +255,48 @@ Navigation.defaultProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
accountNavOpen: state.navigation && state.navigation.accountNavOpen,
|
||||||
session: state.session,
|
session: state.session,
|
||||||
permissions: state.permissions,
|
permissions: state.permissions,
|
||||||
|
searchTerm: state.navigation.searchTerm,
|
||||||
unreadMessageCount: state.messageCount.messageCount,
|
unreadMessageCount: state.messageCount.messageCount,
|
||||||
searchTerm: state.navigation
|
user: state.session && state.session.session && state.session.session.user
|
||||||
});
|
});
|
||||||
|
|
||||||
const ConnectedNavigation = connect(mapStateToProps)(Navigation);
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
closeAccountMenus: () => {
|
||||||
|
dispatch(navigationActions.closeAccountMenus());
|
||||||
|
},
|
||||||
|
getMessageCount: username => {
|
||||||
|
dispatch(messageCountActions.getCount(username));
|
||||||
|
},
|
||||||
|
handleToggleAccountNav: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.handleToggleAccountNav());
|
||||||
|
},
|
||||||
|
handleCloseAccountNav: () => {
|
||||||
|
dispatch(navigationActions.setAccountNavOpen(false));
|
||||||
|
},
|
||||||
|
handleOpenRegistration: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.setRegistrationOpen(true));
|
||||||
|
},
|
||||||
|
handleLogOut: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.handleLogOut());
|
||||||
|
},
|
||||||
|
handleToggleLoginOpen: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.toggleLoginOpen());
|
||||||
|
},
|
||||||
|
setMessageCount: newCount => {
|
||||||
|
dispatch(messageCountActions.setCount(newCount));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ConnectedNavigation = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Navigation);
|
||||||
|
|
||||||
module.exports = injectIntl(ConnectedNavigation);
|
module.exports = injectIntl(ConnectedNavigation);
|
||||||
|
|
|
@ -163,74 +163,6 @@
|
||||||
background-image: url("/images/mystuff.png");
|
background-image: url("/images/mystuff.png");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-dropdown {
|
|
||||||
width: 200px;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
padding: .75em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
.row {
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
|
|
||||||
input {
|
|
||||||
margin: 0;
|
|
||||||
height: 2.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-nav {
|
|
||||||
.user-info {
|
|
||||||
padding-top: 14px;
|
|
||||||
max-width: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> a {
|
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: .8125rem;
|
|
||||||
font-weight: normal;
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
margin-right: 10px;
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
background-color: $active-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 8px;
|
|
||||||
|
|
||||||
background-image: url("/images/dropdown.png");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center center;
|
|
||||||
background-size: 50%;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
vertical-align: middle;
|
|
||||||
content: " ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
top: 50px;
|
|
||||||
padding: 0;
|
|
||||||
padding-top: 5px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//4 columns
|
//4 columns
|
||||||
|
@ -242,20 +174,6 @@
|
||||||
&.login-item {
|
&.login-item {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.account-nav {
|
|
||||||
margin-left: 0;
|
|
||||||
|
|
||||||
> a {
|
|
||||||
.avatar {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.create,
|
.create,
|
||||||
|
@ -280,20 +198,6 @@
|
||||||
&.login-item {
|
&.login-item {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.account-nav {
|
|
||||||
margin-left: 0;
|
|
||||||
|
|
||||||
> a {
|
|
||||||
.avatar {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.discuss,
|
.discuss,
|
||||||
|
@ -313,8 +217,7 @@
|
||||||
width: $cols8;
|
width: $cols8;
|
||||||
|
|
||||||
> ul > li {
|
> ul > li {
|
||||||
&.login-item,
|
&.login-item {
|
||||||
&.account-nav {
|
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
const bindAll = require('lodash.bindall');
|
const bindAll = require('lodash.bindall');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
const connect = require('react-redux').connect;
|
||||||
|
|
||||||
const IframeModal = require('../modal/iframe/modal.jsx');
|
const IframeModal = require('../modal/iframe/modal.jsx');
|
||||||
|
const navigationActions = require('../../redux/navigation.js');
|
||||||
|
|
||||||
require('./registration.scss');
|
require('./registration.scss');
|
||||||
|
|
||||||
|
@ -26,7 +28,7 @@ class Registration extends React.Component {
|
||||||
handleMessage (e) {
|
handleMessage (e) {
|
||||||
if (e.origin !== window.location.origin) return;
|
if (e.origin !== window.location.origin) return;
|
||||||
if (e.source !== this.registrationIframe.contentWindow) return;
|
if (e.source !== this.registrationIframe.contentWindow) return;
|
||||||
if (e.data === 'registration-done') this.props.onRegistrationDone();
|
if (e.data === 'registration-done') this.props.handleCompleteRegistration();
|
||||||
if (e.data === 'registration-relaunch') {
|
if (e.data === 'registration-relaunch') {
|
||||||
this.registrationIframe.contentWindow.location.reload();
|
this.registrationIframe.contentWindow.location.reload();
|
||||||
}
|
}
|
||||||
|
@ -47,16 +49,32 @@ class Registration extends React.Component {
|
||||||
}}
|
}}
|
||||||
isOpen={this.props.isOpen}
|
isOpen={this.props.isOpen}
|
||||||
src="/accounts/standalone-registration/"
|
src="/accounts/standalone-registration/"
|
||||||
onRequestClose={this.props.onRequestClose}
|
onRequestClose={this.props.handleCloseRegistration}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Registration.propTypes = {
|
Registration.propTypes = {
|
||||||
isOpen: PropTypes.bool,
|
handleCloseRegistration: PropTypes.func,
|
||||||
onRegistrationDone: PropTypes.func,
|
handleCompleteRegistration: PropTypes.func,
|
||||||
onRequestClose: PropTypes.func
|
isOpen: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Registration;
|
const mapStateToProps = state => ({
|
||||||
|
isOpen: state.navigation.registrationOpen
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
handleCloseRegistration: () => {
|
||||||
|
dispatch(navigationActions.setRegistrationOpen(false));
|
||||||
|
},
|
||||||
|
handleCompleteRegistration: () => {
|
||||||
|
dispatch(navigationActions.handleCompleteRegistration());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Registration);
|
||||||
|
|
|
@ -1,29 +1,28 @@
|
||||||
const range = require('lodash.range');
|
|
||||||
const PropTypes = require('prop-types');
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const classNames = require('classnames');
|
||||||
|
|
||||||
require('./spinner.scss');
|
require('./spinner.scss');
|
||||||
|
|
||||||
// Adapted from http://tobiasahlin.com/spinkit/
|
// Adapted from http://tobiasahlin.com/spinkit/
|
||||||
const Spinner = ({
|
const Spinner = ({
|
||||||
mode
|
className,
|
||||||
}) => {
|
color
|
||||||
const spinnerClassName = (mode === 'smooth' ? 'spinner-smooth' : 'spinner');
|
}) => (
|
||||||
const spinnerDivCount = (mode === 'smooth' ? 24 : 12);
|
<img
|
||||||
return (
|
alt="loading animation"
|
||||||
<div className={spinnerClassName}>
|
className={classNames('studio-status-icon-spinner', className)}
|
||||||
{range(1, spinnerDivCount + 1).map(id => (
|
src={`/svgs/modal/spinner-${color}.svg`}
|
||||||
<div
|
|
||||||
className={`circle${id} circle`}
|
|
||||||
key={`circle${id}`}
|
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
</div>
|
|
||||||
);
|
Spinner.defaultProps = {
|
||||||
|
color: 'white'
|
||||||
};
|
};
|
||||||
|
|
||||||
Spinner.propTypes = {
|
Spinner.propTypes = {
|
||||||
mode: PropTypes.string
|
className: PropTypes.string,
|
||||||
|
color: PropTypes.oneOf(['white', 'blue', 'transparent-gray'])
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Spinner;
|
module.exports = Spinner;
|
||||||
|
|
|
@ -1,118 +1,44 @@
|
||||||
@import "../../colors";
|
.studio-status-icon-spinner {
|
||||||
|
/* This class can be used on an icon that should spin.
|
||||||
.spinner {
|
It first plays the intro animation, then spins forever. */
|
||||||
position: relative;
|
animation-name: intro, spin;
|
||||||
margin: 0 auto;
|
animation-duration: .25s, .5s;
|
||||||
width: 20px;
|
animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear;
|
||||||
height: 20px;
|
animation-delay: 0s, .25s;
|
||||||
|
animation-iteration-count: 1, infinite;
|
||||||
.circle {
|
animation-direction: normal;
|
||||||
position: absolute;
|
width: 1.4rem; /* standard is 1.4 rem but can be overwritten by parent */
|
||||||
top: 0;
|
height: 1.4rem;
|
||||||
left: 0;
|
-webkit-animation-name: intro, spin;
|
||||||
width: 100%;
|
-webkit-animation-duration: .25s, .5s;
|
||||||
height: 100%;
|
-webkit-animation-iteration-count: 1, infinite;
|
||||||
|
-webkit-animation-delay: 0s, .25s;
|
||||||
&:before {
|
-webkit-animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear;
|
||||||
display: block;
|
transform-origin: center;
|
||||||
animation: circleFadeDelay 1.2s infinite ease-in-out both;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: $ui-gray;
|
|
||||||
width: 15%;
|
|
||||||
height: 15%;
|
|
||||||
content: "";
|
|
||||||
|
|
||||||
.white & {
|
|
||||||
background-color: $ui-blue-dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@for $i from 1 through 12 {
|
|
||||||
$rotation: 30deg * ($i - 1);
|
|
||||||
$delay: -1.3s + $i * .1;
|
|
||||||
|
|
||||||
.circle#{$i} {
|
|
||||||
transform: rotate($rotation);
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
animation-delay: $delay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes circleFadeDelay {
|
@keyframes intro {
|
||||||
0%,
|
0% {
|
||||||
39%,
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
transform: scale(1);
|
||||||
}
|
|
||||||
|
|
||||||
40% {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
-webkit-transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
/*********************/
|
0% {
|
||||||
/* type === "smooth" */
|
transform: rotate(0);
|
||||||
/*********************/
|
-webkit-transform: rotate(0);
|
||||||
|
|
||||||
.spinner-smooth {
|
|
||||||
position: relative;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
|
|
||||||
.circle {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
display: block;
|
|
||||||
animation: circleFadeDelaySmooth 1.8s infinite ease-in-out both;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: $ui-white;
|
|
||||||
width: 30%;
|
|
||||||
height: 20%;
|
|
||||||
content: "";
|
|
||||||
|
|
||||||
.white & {
|
|
||||||
background-color: darken($ui-blue, 8%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@for $i from 1 through 24 {
|
100% {
|
||||||
$rotation: 15deg * ($i - 1);
|
transform: rotate(359deg);
|
||||||
$delay: -1.9s + $i * .075;
|
-webkit-transform: rotate(359deg);
|
||||||
|
|
||||||
.circle#{$i} {
|
|
||||||
transform: rotate($rotation);
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
animation-delay: $delay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes circleFadeDelaySmooth {
|
|
||||||
0%,
|
|
||||||
35% {
|
|
||||||
opacity: 0;
|
|
||||||
},
|
|
||||||
40% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,7 @@ const Thumbnail = props => {
|
||||||
<a
|
<a
|
||||||
href={props.href}
|
href={props.href}
|
||||||
key="titleElement"
|
key="titleElement"
|
||||||
|
title={props.title}
|
||||||
>
|
>
|
||||||
{props.title}
|
{props.title}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -16,14 +16,13 @@
|
||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
margin: 7px;
|
margin: 7px;
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 0 0 1px $active-gray;
|
|
||||||
background-color: $ui-white;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
width: $thumbnail-width;
|
width: $thumbnail-width;
|
||||||
|
|
||||||
.thumbnail-image {
|
.thumbnail-image {
|
||||||
margin: 8px auto;
|
margin: 8px auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 0 0 1px $active-gray;
|
||||||
|
background-color: $ui-white;
|
||||||
width: $thumbnail-inner-width;
|
width: $thumbnail-inner-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,10 +44,18 @@
|
||||||
.thumbnail-title {
|
.thumbnail-title {
|
||||||
float: left;
|
float: left;
|
||||||
max-width: 164px;
|
max-width: 164px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.thumbnail-creator a {
|
.thumbnail-creator a {
|
||||||
color: $type-gray;
|
color: $type-gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ const Raven = require('raven-js');
|
||||||
};
|
};
|
||||||
|
|
||||||
window._locale = updateLocale();
|
window._locale = updateLocale();
|
||||||
|
document.documentElement.lang = window._locale;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -106,6 +106,8 @@
|
||||||
"navigation.signOut": "Sign out",
|
"navigation.signOut": "Sign out",
|
||||||
|
|
||||||
"extensionHeader.requirements": "Requirements",
|
"extensionHeader.requirements": "Requirements",
|
||||||
|
"extensionInstallation.addExtension": "In the editor, click on the \"Add Extensions\" button on the lower left.",
|
||||||
|
|
||||||
|
|
||||||
"oschooser.choose": "Choose your OS:",
|
"oschooser.choose": "Choose your OS:",
|
||||||
|
|
||||||
|
|
|
@ -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+/})
|
|
||||||
);
|
|
|
@ -1,22 +1,153 @@
|
||||||
const keyMirror = require('keymirror');
|
const keyMirror = require('keymirror');
|
||||||
|
const defaults = require('lodash.defaults');
|
||||||
|
|
||||||
|
const api = require('../lib/api');
|
||||||
|
const log = require('../lib/log.js');
|
||||||
|
const sessionActions = require('./session.js');
|
||||||
|
|
||||||
const Types = keyMirror({
|
const Types = keyMirror({
|
||||||
SET_SEARCH_TERM: null
|
SET_SEARCH_TERM: null,
|
||||||
|
SET_ACCOUNT_NAV_OPEN: null,
|
||||||
|
TOGGLE_ACCOUNT_NAV_OPEN: null,
|
||||||
|
SET_LOGIN_ERROR: null,
|
||||||
|
SET_LOGIN_OPEN: null,
|
||||||
|
TOGGLE_LOGIN_OPEN: null,
|
||||||
|
SET_CANCELED_DELETION_OPEN: null,
|
||||||
|
SET_REGISTRATION_OPEN: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports.getInitialState = () => ({
|
||||||
|
accountNavOpen: false,
|
||||||
|
canceledDeletionOpen: false,
|
||||||
|
loginError: null,
|
||||||
|
loginOpen: false,
|
||||||
|
registrationOpen: false,
|
||||||
|
searchTerm: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports.navigationReducer = (state, action) => {
|
module.exports.navigationReducer = (state, action) => {
|
||||||
if (typeof state === 'undefined') {
|
if (typeof state === 'undefined') {
|
||||||
state = '';
|
state = module.exports.getInitialState();
|
||||||
}
|
}
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case Types.SET_SEARCH_TERM:
|
case Types.SET_SEARCH_TERM:
|
||||||
return action.searchTerm;
|
return defaults({searchTerm: action.searchTerm}, state);
|
||||||
|
case Types.SET_ACCOUNT_NAV_OPEN:
|
||||||
|
return defaults({accountNavOpen: action.isOpen}, state);
|
||||||
|
case Types.TOGGLE_ACCOUNT_NAV_OPEN:
|
||||||
|
return defaults({accountNavOpen: !state.accountNavOpen}, state);
|
||||||
|
case Types.SET_LOGIN_ERROR:
|
||||||
|
return defaults({loginError: action.loginError}, state);
|
||||||
|
case Types.SET_LOGIN_OPEN:
|
||||||
|
return defaults({loginOpen: action.isOpen}, state);
|
||||||
|
case Types.TOGGLE_LOGIN_OPEN:
|
||||||
|
return defaults({loginOpen: !state.loginOpen}, state);
|
||||||
|
case Types.SET_CANCELED_DELETION_OPEN:
|
||||||
|
return defaults({canceledDeletionOpen: action.isOpen}, state);
|
||||||
|
case Types.SET_REGISTRATION_OPEN:
|
||||||
|
return defaults({registrationOpen: action.isOpen}, state);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.setAccountNavOpen = isOpen => ({
|
||||||
|
type: Types.SET_ACCOUNT_NAV_OPEN,
|
||||||
|
isOpen: isOpen
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.handleToggleAccountNav = () => ({
|
||||||
|
type: Types.TOGGLE_ACCOUNT_NAV_OPEN
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setCanceledDeletionOpen = isOpen => ({
|
||||||
|
type: Types.SET_CANCELED_DELETION_OPEN,
|
||||||
|
isOpen: isOpen
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setLoginError = loginError => ({
|
||||||
|
type: Types.SET_LOGIN_ERROR,
|
||||||
|
loginError: loginError
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setLoginOpen = isOpen => ({
|
||||||
|
type: Types.SET_LOGIN_OPEN,
|
||||||
|
isOpen: isOpen
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.toggleLoginOpen = () => ({
|
||||||
|
type: Types.TOGGLE_LOGIN_OPEN
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setRegistrationOpen = isOpen => ({
|
||||||
|
type: Types.SET_REGISTRATION_OPEN,
|
||||||
|
isOpen: isOpen
|
||||||
|
});
|
||||||
|
|
||||||
module.exports.setSearchTerm = searchTerm => ({
|
module.exports.setSearchTerm = searchTerm => ({
|
||||||
type: Types.SET_SEARCH_TERM,
|
type: Types.SET_SEARCH_TERM,
|
||||||
searchTerm: searchTerm
|
searchTerm: searchTerm
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports.handleCompleteRegistration = () => (dispatch => {
|
||||||
|
dispatch(sessionActions.refreshSession());
|
||||||
|
dispatch(module.exports.setRegistrationOpen(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.closeAccountMenus = () => (dispatch => {
|
||||||
|
dispatch(module.exports.setAccountNavOpen(false));
|
||||||
|
dispatch(module.exports.setRegistrationOpen(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.handleLogIn = (formData, callback) => (dispatch => {
|
||||||
|
dispatch(module.exports.setLoginError(null));
|
||||||
|
formData.useMessages = true; // NOTE: this may or may not be being used anywhere else
|
||||||
|
api({
|
||||||
|
method: 'post',
|
||||||
|
host: '',
|
||||||
|
uri: '/accounts/login/',
|
||||||
|
json: formData,
|
||||||
|
useCsrf: true
|
||||||
|
}, (err, body) => {
|
||||||
|
if (err) dispatch(module.exports.setLoginError(err.message));
|
||||||
|
if (body) {
|
||||||
|
body = body[0];
|
||||||
|
if (body.success) {
|
||||||
|
dispatch(module.exports.setLoginOpen(false));
|
||||||
|
body.messages.forEach(message => {
|
||||||
|
if (message.message === 'canceled-deletion') {
|
||||||
|
dispatch(module.exports.setCanceledDeletionOpen(true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dispatch(sessionActions.refreshSession());
|
||||||
|
callback({success: true});
|
||||||
|
} else {
|
||||||
|
if (body.redirect) {
|
||||||
|
window.location = body.redirect;
|
||||||
|
}
|
||||||
|
// Update login error message to a friendlier one if it exists
|
||||||
|
dispatch(module.exports.setLoginError(body.msg));
|
||||||
|
// JS error already logged by api mixin
|
||||||
|
callback({success: false});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// JS error already logged by api mixin
|
||||||
|
callback({success: false});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.handleLogOut = () => (dispatch => {
|
||||||
|
api({
|
||||||
|
host: '',
|
||||||
|
method: 'post',
|
||||||
|
uri: '/accounts/logout/',
|
||||||
|
useCsrf: true
|
||||||
|
}, err => {
|
||||||
|
if (err) log.error(err);
|
||||||
|
dispatch(module.exports.setLoginOpen(false));
|
||||||
|
dispatch(module.exports.setAccountNavOpen(false));
|
||||||
|
window.location = '/';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ const defaults = require('lodash.defaults');
|
||||||
const messageCountReducer = require('./message-count.js').messageCountReducer;
|
const messageCountReducer = require('./message-count.js').messageCountReducer;
|
||||||
const permissionsReducer = require('./permissions.js').permissionsReducer;
|
const permissionsReducer = require('./permissions.js').permissionsReducer;
|
||||||
const sessionReducer = require('./session.js').sessionReducer;
|
const sessionReducer = require('./session.js').sessionReducer;
|
||||||
|
const navigationReducer = require('./navigation.js').navigationReducer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a combined reducer to be used for a page in `render.jsx`.
|
* Returns a combined reducer to be used for a page in `render.jsx`.
|
||||||
|
@ -18,8 +19,9 @@ const sessionReducer = require('./session.js').sessionReducer;
|
||||||
module.exports = opts => {
|
module.exports = opts => {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
return combineReducers(defaults(opts, {
|
return combineReducers(defaults(opts, {
|
||||||
session: sessionReducer,
|
messageCount: messageCountReducer,
|
||||||
|
navigation: navigationReducer,
|
||||||
permissions: permissionsReducer,
|
permissions: permissionsReducer,
|
||||||
messageCount: messageCountReducer
|
session: sessionReducer
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
|
@ -77,13 +77,13 @@ module.exports.getActivity = (username, token) => (dispatch => {
|
||||||
api({
|
api({
|
||||||
uri: `/users/${username}/following/users/activity?limit=5`,
|
uri: `/users/${username}/following/users/activity?limit=5`,
|
||||||
authentication: token
|
authentication: token
|
||||||
}, (err, body) => {
|
}, (err, body, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError(err));
|
dispatch(module.exports.setError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof body === 'undefined') {
|
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||||
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('activity', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError('No session content'));
|
dispatch(module.exports.setError('No session content'));
|
||||||
return;
|
return;
|
||||||
|
@ -100,13 +100,13 @@ module.exports.getFeaturedGlobal = () => (dispatch => {
|
||||||
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.FETCHING));
|
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.FETCHING));
|
||||||
api({
|
api({
|
||||||
uri: '/proxy/featured'
|
uri: '/proxy/featured'
|
||||||
}, (err, body) => {
|
}, (err, body, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError(err));
|
dispatch(module.exports.setError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof body === 'undefined') {
|
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||||
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('featured', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError('No session content'));
|
dispatch(module.exports.setError('No session content'));
|
||||||
return;
|
return;
|
||||||
|
@ -126,13 +126,13 @@ module.exports.getSharedByFollowing = (username, token) => (dispatch => {
|
||||||
api({
|
api({
|
||||||
uri: `/users/${username}/following/users/projects`,
|
uri: `/users/${username}/following/users/projects`,
|
||||||
authentication: token
|
authentication: token
|
||||||
}, (err, body) => {
|
}, (err, body, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.Status.ERROR));
|
||||||
dispatch(module.exports.setError(err));
|
dispatch(module.exports.setError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof body === 'undefined') {
|
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||||
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('shared', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError('No session content'));
|
dispatch(module.exports.setError('No session content'));
|
||||||
return;
|
return;
|
||||||
|
@ -152,13 +152,13 @@ module.exports.getInStudiosFollowing = (username, token) => (dispatch => {
|
||||||
api({
|
api({
|
||||||
uri: `/users/${username}/following/studios/projects`,
|
uri: `/users/${username}/following/studios/projects`,
|
||||||
authentication: token
|
authentication: token
|
||||||
}, (err, body) => {
|
}, (err, body, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError(err));
|
dispatch(module.exports.setError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof body === 'undefined') {
|
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError('No session content'));
|
dispatch(module.exports.setError('No session content'));
|
||||||
return;
|
return;
|
||||||
|
@ -178,13 +178,13 @@ module.exports.getLovedByFollowing = (username, token) => (dispatch => {
|
||||||
api({
|
api({
|
||||||
uri: `/users/${username}/following/users/loves`,
|
uri: `/users/${username}/following/users/loves`,
|
||||||
authentication: token
|
authentication: token
|
||||||
}, (err, body) => {
|
}, (err, body, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError(err));
|
dispatch(module.exports.setError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof body === 'undefined') {
|
if (typeof body === 'undefined' || res.statusCode !== 200) {
|
||||||
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError('No session content'));
|
dispatch(module.exports.setError('No session content'));
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -35,8 +35,10 @@ const Components = () => (
|
||||||
<Box title="Carousel component in a box!">
|
<Box title="Carousel component in a box!">
|
||||||
<Carousel />
|
<Carousel />
|
||||||
</Box>
|
</Box>
|
||||||
<h1>This is a Spinner</h1>
|
<h1>This is a blue Spinner</h1>
|
||||||
<Spinner />
|
<Spinner
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
<h1>Colors</h1>
|
<h1>Colors</h1>
|
||||||
<div className="colors">
|
<div className="colors">
|
||||||
<span className="ui-blue">$ui-blue</span>
|
<span className="ui-blue">$ui-blue</span>
|
||||||
|
|
|
@ -86,6 +86,14 @@ const Credits = () => (
|
||||||
<span className="name">DD Liu</span>
|
<span className="name">DD Liu</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<img
|
||||||
|
alt="Katelyn Avatar"
|
||||||
|
src="//cdn.scratch.mit.edu/get_image/user/34607790_170x170.png"
|
||||||
|
/>
|
||||||
|
<span className="name">Katelyn Mann</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<img
|
<img
|
||||||
alt="Shruti Avatar"
|
alt="Shruti Avatar"
|
||||||
|
|
|
@ -31,9 +31,15 @@ class EV3 extends ExtensionLanding {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className="extension-landing ev3">
|
<div className="extension-landing ev3">
|
||||||
<ExtensionHeader imageSrc="/images/ev3/ev3-illustration.png">
|
<ExtensionHeader
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltEv3Illustration'})}
|
||||||
|
imageSrc="/images/ev3/ev3-illustration.png"
|
||||||
|
>
|
||||||
<FlexRow className="column extension-copy">
|
<FlexRow className="column extension-copy">
|
||||||
<h2><img src="/images/ev3/ev3.svg" />LEGO MINDSTORMS EV3</h2>
|
<h1><img
|
||||||
|
alt=""
|
||||||
|
src="/images/ev3/ev3.svg"
|
||||||
|
/>LEGO MINDSTORMS EV3</h1>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="ev3.headerText"
|
id="ev3.headerText"
|
||||||
values={{
|
values={{
|
||||||
|
@ -51,11 +57,17 @@ class EV3 extends ExtensionLanding {
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
<ExtensionRequirements>
|
<ExtensionRequirements>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/windows.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/windows.svg"
|
||||||
|
/>
|
||||||
Windows 10+
|
Windows 10+
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/mac.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/mac.svg"
|
||||||
|
/>
|
||||||
macOS 10.13+
|
macOS 10.13+
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
|
@ -63,7 +75,10 @@ class EV3 extends ExtensionLanding {
|
||||||
Bluetooth
|
Bluetooth
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/scratch-link.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/scratch-link.svg"
|
||||||
|
/>
|
||||||
Scratch Link
|
Scratch Link
|
||||||
</span>
|
</span>
|
||||||
</ExtensionRequirements>
|
</ExtensionRequirements>
|
||||||
|
@ -82,13 +97,17 @@ class EV3 extends ExtensionLanding {
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step number={1}>
|
<Step number={1}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/ev3-connect-1.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/ev3/ev3-connect-1.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p><FormattedMessage id="ev3.turnOnEV3" /></p>
|
<p><FormattedMessage id="ev3.turnOnEV3" /></p>
|
||||||
</Step>
|
</Step>
|
||||||
<Step number={2}>
|
<Step number={2}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
className="screenshot"
|
className="screenshot"
|
||||||
src="/images/ev3/ev3-connect-2.png"
|
src="/images/ev3/ev3-connect-2.png"
|
||||||
/>
|
/>
|
||||||
|
@ -113,6 +132,7 @@ class EV3 extends ExtensionLanding {
|
||||||
<Step number={3}>
|
<Step number={3}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'extensionInstallation.addExtension'})}
|
||||||
className="screenshot"
|
className="screenshot"
|
||||||
src="/images/ev3/ev3-connect-3.png"
|
src="/images/ev3/ev3-connect-3.png"
|
||||||
/>
|
/>
|
||||||
|
@ -125,19 +145,30 @@ class EV3 extends ExtensionLanding {
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step>
|
<Step>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/ev3-accept-connection.png" />
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'ev3.imgAltAcceptConnection'})}
|
||||||
|
src="/images/ev3/ev3-accept-connection.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p><FormattedMessage id="ev3.acceptConnection" /></p>
|
<p><FormattedMessage id="ev3.acceptConnection" /></p>
|
||||||
</Step>
|
</Step>
|
||||||
<Step>
|
<Step>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/ev3-pin.png" />
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'ev3.imgAltAcceptPasscode'})}
|
||||||
|
src="/images/ev3/ev3-pin.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p><FormattedMessage id="ev3.acceptPasscode" /></p>
|
<p><FormattedMessage id="ev3.acceptPasscode" /></p>
|
||||||
</Step>
|
</Step>
|
||||||
<Step>
|
<Step>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: `ev3.imgAlt${
|
||||||
|
this.state.OS === OS_ENUM.WINDOWS ?
|
||||||
|
'WaitForWindows' :
|
||||||
|
'EnterPasscodeMac'
|
||||||
|
}`})}
|
||||||
className="screenshot"
|
className="screenshot"
|
||||||
src={`/images/ev3/${
|
src={`/images/ev3/${
|
||||||
this.state.OS === OS_ENUM.WINDOWS ?
|
this.state.OS === OS_ENUM.WINDOWS ?
|
||||||
|
@ -176,7 +207,10 @@ class EV3 extends ExtensionLanding {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/ev3-motor-port-a.png" />
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'ev3.imgAltPlugInMotor'})}
|
||||||
|
src="/images/ev3/ev3-motor-port-a.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Step>
|
</Step>
|
||||||
<Step
|
<Step
|
||||||
|
@ -194,7 +228,10 @@ class EV3 extends ExtensionLanding {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/ev3/motor-turn-block.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/ev3/motor-turn-block.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
@ -204,18 +241,21 @@ class EV3 extends ExtensionLanding {
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://beta.scratch.mit.edu/#239075992"
|
cardUrl="https://beta.scratch.mit.edu/#239075992"
|
||||||
description={this.props.intl.formatMessage({id: 'ev3.waveHelloDescription'})}
|
description={this.props.intl.formatMessage({id: 'ev3.waveHelloDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltWaveHello'})}
|
||||||
imageSrc="/images/ev3/starter-wave-hello.png"
|
imageSrc="/images/ev3/starter-wave-hello.png"
|
||||||
title={this.props.intl.formatMessage({id: 'ev3.waveHelloTitle'})}
|
title={this.props.intl.formatMessage({id: 'ev3.waveHelloTitle'})}
|
||||||
/>
|
/>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://beta.scratch.mit.edu/#239076020"
|
cardUrl="https://beta.scratch.mit.edu/#239076020"
|
||||||
description={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentDescription'})}
|
description={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltDistanceInstrument'})}
|
||||||
imageSrc="/images/ev3/starter-distance-instrument.png"
|
imageSrc="/images/ev3/starter-distance-instrument.png"
|
||||||
title={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentTitle'})}
|
title={this.props.intl.formatMessage({id: 'ev3.distanceInstrumentTitle'})}
|
||||||
/>
|
/>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://beta.scratch.mit.edu/#239076044"
|
cardUrl="https://beta.scratch.mit.edu/#239076044"
|
||||||
description={this.props.intl.formatMessage({id: 'ev3.spaceTacosDescription'})}
|
description={this.props.intl.formatMessage({id: 'ev3.spaceTacosDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'ev3.imgAltSpaceTacos'})}
|
||||||
imageSrc="/images/ev3/starter-flying-game.png"
|
imageSrc="/images/ev3/starter-flying-game.png"
|
||||||
title={this.props.intl.formatMessage({id: 'ev3.spaceTacosTitle'})}
|
title={this.props.intl.formatMessage({id: 'ev3.spaceTacosTitle'})}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -34,5 +34,14 @@
|
||||||
"ev3.otherComputerConnectedText": "Only one computer can be connected to an EV3 at a time. If you have another computer connected to your EV3, disconnect the EV3 or close Scratch on that computer and try again.",
|
"ev3.otherComputerConnectedText": "Only one computer can be connected to an EV3 at a time. If you have another computer connected to your EV3, disconnect the EV3 or close Scratch on that computer and try again.",
|
||||||
"ev3.updateFirmwareTitle": "Try updating your EV3 firmware",
|
"ev3.updateFirmwareTitle": "Try updating your EV3 firmware",
|
||||||
"ev3.updateFirmwareText": "We recommend updating to EV3 firmware version 1.10E or above. See {firmwareUpdateLink}.",
|
"ev3.updateFirmwareText": "We recommend updating to EV3 firmware version 1.10E or above. See {firmwareUpdateLink}.",
|
||||||
"ev3.firmwareUpdateText": "firmware update instructions from LEGO"
|
"ev3.firmwareUpdateText": "firmware update instructions from LEGO",
|
||||||
|
"ev3.imgAltEv3Illustration": "Illustration of an EV3 hub, featuring some examples of interacting with it.",
|
||||||
|
"ev3.imgAltAcceptConnection": "Use the buttons on your EV3 to accept the connection.",
|
||||||
|
"ev3.imgAltAcceptPasscode": "Use the center button on your EV3 to accept the passcode.",
|
||||||
|
"ev3.imgAltWaitForWindows": "Windows will notify you when the EV3 is ready.",
|
||||||
|
"ev3.imgAltEnterPasscodeMac": "Enter the passcode into the connection request window opening on your Mac.",
|
||||||
|
"ev3.imgAltPlugInMotor": "To find port A: hold the EV3 with the screen and buttons facing you, with the screen above the buttons. Port A is on top, and it is the left-most one",
|
||||||
|
"ev3.imgAltWaveHello": "A Scratch project with a waving fairy.",
|
||||||
|
"ev3.imgAltDistanceInstrument": "A Scratch project with a guitar.",
|
||||||
|
"ev3.imgAltSpaceTacos": "A Scratch project with Scratch Cat and a taco in space."
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,14 +35,6 @@ const Jobs = () => (
|
||||||
MIT Media Lab, Cambridge, MA
|
MIT Media Lab, Cambridge, MA
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href="https://scratch.mit.edu/jobs/moderator">
|
|
||||||
Community Moderator
|
|
||||||
</a>
|
|
||||||
<span>
|
|
||||||
Remote
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,5 +28,11 @@
|
||||||
"microbit.otherComputerConnectedTitle": "Make sure no other computer is connected to your micro:bit",
|
"microbit.otherComputerConnectedTitle": "Make sure no other computer is connected to your micro:bit",
|
||||||
"microbit.otherComputerConnectedText": "Only one computer can be connected to an micro:bit at a time. If you have another computer connected to your micro:bit, disconnect the micro:bit or close Scratch on that computer and try again.",
|
"microbit.otherComputerConnectedText": "Only one computer can be connected to an micro:bit at a time. If you have another computer connected to your micro:bit, disconnect the micro:bit or close Scratch on that computer and try again.",
|
||||||
"microbit.resetButtonTitle": "Make sure you aren’t hitting the “reset” button",
|
"microbit.resetButtonTitle": "Make sure you aren’t 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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,9 +31,15 @@ class MicroBit extends ExtensionLanding {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className="extension-landing microbit">
|
<div className="extension-landing microbit">
|
||||||
<ExtensionHeader imageSrc="/images/microbit/microbit-heart.png">
|
<ExtensionHeader
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltMicrobitIllustration'})}
|
||||||
|
imageSrc="/images/microbit/microbit-heart.png"
|
||||||
|
>
|
||||||
<FlexRow className="column extension-copy">
|
<FlexRow className="column extension-copy">
|
||||||
<h2><img src="/images/microbit/microbit.svg" />micro:bit</h2>
|
<h1><img
|
||||||
|
alt=""
|
||||||
|
src="/images/microbit/microbit.svg"
|
||||||
|
/>micro:bit</h1>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="microbit.headerText"
|
id="microbit.headerText"
|
||||||
values={{
|
values={{
|
||||||
|
@ -51,19 +57,31 @@ class MicroBit extends ExtensionLanding {
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
<ExtensionRequirements>
|
<ExtensionRequirements>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/windows.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/windows.svg"
|
||||||
|
/>
|
||||||
Windows 10+
|
Windows 10+
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/mac.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/mac.svg"
|
||||||
|
/>
|
||||||
macOS 10.13+
|
macOS 10.13+
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/bluetooth.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/bluetooth.svg"
|
||||||
|
/>
|
||||||
Bluetooth 4.0
|
Bluetooth 4.0
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/scratch-link.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/scratch-link.svg"
|
||||||
|
/>
|
||||||
Scratch Link
|
Scratch Link
|
||||||
</span>
|
</span>
|
||||||
</ExtensionRequirements>
|
</ExtensionRequirements>
|
||||||
|
@ -82,7 +100,10 @@ class MicroBit extends ExtensionLanding {
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step number={1}>
|
<Step number={1}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/mbit-usb.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/microbit/mbit-usb.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage id="microbit.connectUSB" />
|
<FormattedMessage id="microbit.connectUSB" />
|
||||||
|
@ -90,7 +111,10 @@ class MicroBit extends ExtensionLanding {
|
||||||
</Step>
|
</Step>
|
||||||
<Step number={2}>
|
<Step number={2}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/mbit-hex-download.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/microbit/mbit-hex-download.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
download
|
download
|
||||||
|
@ -103,6 +127,7 @@ class MicroBit extends ExtensionLanding {
|
||||||
<Step number={3}>
|
<Step number={3}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'microbit.imgAltDragDropHex'})}
|
||||||
src={`/images/microbit/${
|
src={`/images/microbit/${
|
||||||
this.state.OS === OS_ENUM.WINDOWS ? 'win' : 'mac'
|
this.state.OS === OS_ENUM.WINDOWS ? 'win' : 'mac'
|
||||||
}-copy-hex.png`}
|
}-copy-hex.png`}
|
||||||
|
@ -120,13 +145,17 @@ class MicroBit extends ExtensionLanding {
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step number={1}>
|
<Step number={1}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/mbit-connect-1.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/microbit/mbit-connect-1.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p><FormattedMessage id="microbit.powerMicrobit" /></p>
|
<p><FormattedMessage id="microbit.powerMicrobit" /></p>
|
||||||
</Step>
|
</Step>
|
||||||
<Step number={2}>
|
<Step number={2}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
className="screenshot"
|
className="screenshot"
|
||||||
src="/images/microbit/mbit-connect-2.png"
|
src="/images/microbit/mbit-connect-2.png"
|
||||||
/>
|
/>
|
||||||
|
@ -151,6 +180,7 @@ class MicroBit extends ExtensionLanding {
|
||||||
<Step number={3}>
|
<Step number={3}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'extensionInstallation.addExtension'})}
|
||||||
className="screenshot"
|
className="screenshot"
|
||||||
src="/images/microbit/mbit-connect-3.png"
|
src="/images/microbit/mbit-connect-3.png"
|
||||||
/>
|
/>
|
||||||
|
@ -181,7 +211,10 @@ class MicroBit extends ExtensionLanding {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/display-hello-block.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/microbit/display-hello-block.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Step>
|
</Step>
|
||||||
<Step
|
<Step
|
||||||
|
@ -199,7 +232,10 @@ class MicroBit extends ExtensionLanding {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/microbit/mbit-display-h.png" />
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'microbit.imgAltDisplayH'})}
|
||||||
|
src="/images/microbit/mbit-display-h.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
@ -209,18 +245,21 @@ class MicroBit extends ExtensionLanding {
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://beta.scratch.mit.edu/#239075756"
|
cardUrl="https://beta.scratch.mit.edu/#239075756"
|
||||||
description={this.props.intl.formatMessage({id: 'microbit.heartBeatDescription'})}
|
description={this.props.intl.formatMessage({id: 'microbit.heartBeatDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltHeartBeat'})}
|
||||||
imageSrc="/images/microbit/starter-heart.png"
|
imageSrc="/images/microbit/starter-heart.png"
|
||||||
title={this.props.intl.formatMessage({id: 'microbit.heartBeat'})}
|
title={this.props.intl.formatMessage({id: 'microbit.heartBeat'})}
|
||||||
/>
|
/>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://beta.scratch.mit.edu/#239075950"
|
cardUrl="https://beta.scratch.mit.edu/#239075950"
|
||||||
description={this.props.intl.formatMessage({id: 'microbit.tiltGuitarDescription'})}
|
description={this.props.intl.formatMessage({id: 'microbit.tiltGuitarDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltTiltGuitar'})}
|
||||||
imageSrc="/images/microbit/starter-guitar.png"
|
imageSrc="/images/microbit/starter-guitar.png"
|
||||||
title={this.props.intl.formatMessage({id: 'microbit.tiltGuitar'})}
|
title={this.props.intl.formatMessage({id: 'microbit.tiltGuitar'})}
|
||||||
/>
|
/>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://beta.scratch.mit.edu/#239075973"
|
cardUrl="https://beta.scratch.mit.edu/#239075973"
|
||||||
description={this.props.intl.formatMessage({id: 'microbit.oceanAdventureDescription'})}
|
description={this.props.intl.formatMessage({id: 'microbit.oceanAdventureDescription'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'microbit.imgAltOceanAdventure'})}
|
||||||
imageSrc="/images/microbit/starter-fish.png"
|
imageSrc="/images/microbit/starter-fish.png"
|
||||||
title={this.props.intl.formatMessage({id: 'microbit.oceanAdventure'})}
|
title={this.props.intl.formatMessage({id: 'microbit.oceanAdventure'})}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.extension-status {
|
.extension-status {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"addToStudio.title": "Add to Studio",
|
"addToStudio.title": "Add to Studio",
|
||||||
"addToStudio.finishing": "Finishing up...",
|
"addToStudio.finishing": "Finishing up...",
|
||||||
|
"preview.titleMaxLength": "Title is too long",
|
||||||
"preview.musicExtensionChip": "Music",
|
"preview.musicExtensionChip": "Music",
|
||||||
"preview.penExtensionChip": "Pen",
|
"preview.penExtensionChip": "Pen",
|
||||||
"preview.speechExtensionChip": "Google Speech",
|
"preview.speechExtensionChip": "Google Speech",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
const FormattedDate = require('react-intl').FormattedDate;
|
const FormattedDate = require('react-intl').FormattedDate;
|
||||||
const injectIntl = require('react-intl').injectIntl;
|
const injectIntl = require('react-intl').injectIntl;
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
|
const intlShape = require('react-intl').intlShape;
|
||||||
|
const MediaQuery = require('react-responsive').default;
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const Formsy = require('formsy-react').default;
|
const Formsy = require('formsy-react').default;
|
||||||
const classNames = require('classnames');
|
const classNames = require('classnames');
|
||||||
|
@ -27,6 +29,18 @@ const ExtensionChip = require('./extension-chip.jsx');
|
||||||
const projectShape = require('./projectshape.jsx').projectShape;
|
const projectShape = require('./projectshape.jsx').projectShape;
|
||||||
require('./preview.scss');
|
require('./preview.scss');
|
||||||
|
|
||||||
|
const frameless = require('../../lib/frameless');
|
||||||
|
|
||||||
|
// disable enter key submission on formsy input fields; otherwise formsy thinks
|
||||||
|
// we meant to trigger the "See inside" button. Instead, treat these keypresses
|
||||||
|
// as a blur, which will trigger a save.
|
||||||
|
const onKeyPress = e => {
|
||||||
|
if (e.target.type === 'text' && e.which === 13 /* Enter */) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.target.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const PreviewPresentation = ({
|
const PreviewPresentation = ({
|
||||||
assetHost,
|
assetHost,
|
||||||
backpackOptions,
|
backpackOptions,
|
||||||
|
@ -35,6 +49,7 @@ const PreviewPresentation = ({
|
||||||
extensions,
|
extensions,
|
||||||
faved,
|
faved,
|
||||||
favoriteCount,
|
favoriteCount,
|
||||||
|
intl,
|
||||||
isFullScreen,
|
isFullScreen,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
isShared,
|
isShared,
|
||||||
|
@ -70,7 +85,7 @@ const PreviewPresentation = ({
|
||||||
<ShareBanner shared={isShared} />
|
<ShareBanner shared={isShared} />
|
||||||
|
|
||||||
{ projectInfo && projectInfo.author && projectInfo.author.id && (
|
{ projectInfo && projectInfo.author && projectInfo.author.id && (
|
||||||
<Formsy>
|
<Formsy onKeyPress={onKeyPress}>
|
||||||
<div className="inner">
|
<div className="inner">
|
||||||
<FlexRow className="preview-row">
|
<FlexRow className="preview-row">
|
||||||
<FlexRow className="project-header">
|
<FlexRow className="project-header">
|
||||||
|
@ -88,10 +103,9 @@ const PreviewPresentation = ({
|
||||||
handleUpdate={onUpdate}
|
handleUpdate={onUpdate}
|
||||||
name="title"
|
name="title"
|
||||||
validationErrors={{
|
validationErrors={{
|
||||||
maxLength: 'Sorry title is too long'
|
maxLength: intl.formatMessage({
|
||||||
// maxLength: props.intl.formatMessage({
|
id: 'preview.titleMaxLength'
|
||||||
// id: 'project.titleMaxLength'
|
})
|
||||||
// })
|
|
||||||
}}
|
}}
|
||||||
validations={{
|
validations={{
|
||||||
maxLength: 100
|
maxLength: 100
|
||||||
|
@ -99,7 +113,10 @@ const PreviewPresentation = ({
|
||||||
value={projectInfo.title}
|
value={projectInfo.title}
|
||||||
/> :
|
/> :
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="project-title">{projectInfo.title}</div>
|
<div
|
||||||
|
className="project-title no-edit"
|
||||||
|
title={projectInfo.title}
|
||||||
|
>{projectInfo.title}</div>
|
||||||
{'by '}
|
{'by '}
|
||||||
<a href={`/users/${projectInfo.author.username}`}>
|
<a href={`/users/${projectInfo.author.username}`}>
|
||||||
{projectInfo.author.username}
|
{projectInfo.author.username}
|
||||||
|
@ -141,6 +158,21 @@ const PreviewPresentation = ({
|
||||||
<RemixCredit projectInfo={parentInfo} />
|
<RemixCredit projectInfo={parentInfo} />
|
||||||
<RemixCredit projectInfo={originalInfo} />
|
<RemixCredit projectInfo={originalInfo} />
|
||||||
{/* eslint-disable max-len */}
|
{/* eslint-disable max-len */}
|
||||||
|
<MediaQuery maxWidth={frameless.tablet - 1}>
|
||||||
|
<FlexRow className="preview-row">
|
||||||
|
<FlexRow className="extension-list">
|
||||||
|
{extensions && extensions.map(extension => (
|
||||||
|
<ExtensionChip
|
||||||
|
extensionL10n={extension.l10nId}
|
||||||
|
extensionName={extension.name}
|
||||||
|
hasStatus={extension.hasStatus}
|
||||||
|
iconURI={extension.icon && `/svgs/project/${extension.icon}`}
|
||||||
|
key={extension.name || extension.l10nId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FlexRow>
|
||||||
|
</FlexRow>
|
||||||
|
</MediaQuery>
|
||||||
<FlexRow className="description-block">
|
<FlexRow className="description-block">
|
||||||
<div className="project-textlabel">
|
<div className="project-textlabel">
|
||||||
Instructions
|
Instructions
|
||||||
|
@ -295,6 +327,7 @@ const PreviewPresentation = ({
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
|
<MediaQuery minWidth={frameless.tablet}>
|
||||||
<FlexRow className="preview-row">
|
<FlexRow className="preview-row">
|
||||||
<FlexRow className="extension-list">
|
<FlexRow className="extension-list">
|
||||||
{extensions && extensions.map(extension => (
|
{extensions && extensions.map(extension => (
|
||||||
|
@ -308,6 +341,7 @@ const PreviewPresentation = ({
|
||||||
))}
|
))}
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
|
</MediaQuery>
|
||||||
</div>
|
</div>
|
||||||
<div className="project-lower-container">
|
<div className="project-lower-container">
|
||||||
<div className="inner">
|
<div className="inner">
|
||||||
|
@ -365,6 +399,7 @@ PreviewPresentation.propTypes = {
|
||||||
extensions: PropTypes.arrayOf(PropTypes.object),
|
extensions: PropTypes.arrayOf(PropTypes.object),
|
||||||
faved: PropTypes.bool,
|
faved: PropTypes.bool,
|
||||||
favoriteCount: PropTypes.number,
|
favoriteCount: PropTypes.number,
|
||||||
|
intl: intlShape,
|
||||||
isFullScreen: PropTypes.bool,
|
isFullScreen: PropTypes.bool,
|
||||||
isLoggedIn: PropTypes.bool,
|
isLoggedIn: PropTypes.bool,
|
||||||
isShared: PropTypes.bool,
|
isShared: PropTypes.bool,
|
||||||
|
|
|
@ -15,8 +15,12 @@ const EXTENSION_INFO = require('../../lib/extensions.js').default;
|
||||||
|
|
||||||
const PreviewPresentation = require('./presentation.jsx');
|
const PreviewPresentation = require('./presentation.jsx');
|
||||||
const projectShape = require('./projectshape.jsx').projectShape;
|
const projectShape = require('./projectshape.jsx').projectShape;
|
||||||
|
const Registration = require('../../components/registration/registration.jsx');
|
||||||
|
const ConnectedLogin = require('../../components/login/connected-login.jsx');
|
||||||
|
const CanceledDeletionModal = require('../../components/login/canceled-deletion-modal.jsx');
|
||||||
|
|
||||||
const sessionActions = require('../../redux/session.js');
|
const sessionActions = require('../../redux/session.js');
|
||||||
|
const navigationActions = require('../../redux/navigation.js');
|
||||||
const previewActions = require('../../redux/preview.js');
|
const previewActions = require('../../redux/preview.js');
|
||||||
|
|
||||||
const GUI = require('scratch-gui');
|
const GUI = require('scratch-gui');
|
||||||
|
@ -31,7 +35,6 @@ class Preview extends React.Component {
|
||||||
'handleFavoriteToggle',
|
'handleFavoriteToggle',
|
||||||
'handleLoadMore',
|
'handleLoadMore',
|
||||||
'handleLoveToggle',
|
'handleLoveToggle',
|
||||||
'handlePermissions',
|
|
||||||
'handlePopState',
|
'handlePopState',
|
||||||
'handleReportClick',
|
'handleReportClick',
|
||||||
'handleReportClose',
|
'handleReportClose',
|
||||||
|
@ -39,11 +42,11 @@ class Preview extends React.Component {
|
||||||
'handleAddToStudioClick',
|
'handleAddToStudioClick',
|
||||||
'handleAddToStudioClose',
|
'handleAddToStudioClose',
|
||||||
'handleSeeInside',
|
'handleSeeInside',
|
||||||
|
'handleUpdateProjectTitle',
|
||||||
'handleUpdate',
|
'handleUpdate',
|
||||||
'initCounts',
|
'initCounts',
|
||||||
'isShared',
|
|
||||||
'pushHistory',
|
'pushHistory',
|
||||||
'userOwnsProject'
|
'renderLogin'
|
||||||
]);
|
]);
|
||||||
const pathname = window.location.pathname.toLowerCase();
|
const pathname = window.location.pathname.toLowerCase();
|
||||||
const parts = pathname.split('/').filter(Boolean);
|
const parts = pathname.split('/').filter(Boolean);
|
||||||
|
@ -51,7 +54,6 @@ class Preview extends React.Component {
|
||||||
// parts[1]: either :id or 'editor'
|
// parts[1]: either :id or 'editor'
|
||||||
// parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen'
|
// parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen'
|
||||||
this.state = {
|
this.state = {
|
||||||
editable: false,
|
|
||||||
extensions: [],
|
extensions: [],
|
||||||
favoriteCount: 0,
|
favoriteCount: 0,
|
||||||
loveCount: 0,
|
loveCount: 0,
|
||||||
|
@ -86,7 +88,6 @@ class Preview extends React.Component {
|
||||||
if (this.props.projectInfo.id !== prevProps.projectInfo.id) {
|
if (this.props.projectInfo.id !== prevProps.projectInfo.id) {
|
||||||
this.getExtensions(this.state.projectId);
|
this.getExtensions(this.state.projectId);
|
||||||
this.initCounts(this.props.projectInfo.stats.favorites, this.props.projectInfo.stats.loves);
|
this.initCounts(this.props.projectInfo.stats.favorites, this.props.projectInfo.stats.loves);
|
||||||
this.handlePermissions();
|
|
||||||
if (this.props.projectInfo.remix.parent !== null) {
|
if (this.props.projectInfo.remix.parent !== null) {
|
||||||
this.props.getParentInfo(this.props.projectInfo.remix.parent);
|
this.props.getParentInfo(this.props.projectInfo.remix.parent);
|
||||||
}
|
}
|
||||||
|
@ -189,8 +190,8 @@ class Preview extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleToggleStudio (event) {
|
handleToggleStudio (id) {
|
||||||
const studioId = parseInt(event.currentTarget.dataset.id, 10);
|
const studioId = parseInt(id, 10);
|
||||||
if (isNaN(studioId)) { // sanity check in case event had no integer data-id
|
if (isNaN(studioId)) { // sanity check in case event had no integer data-id
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -242,15 +243,12 @@ class Preview extends React.Component {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handlePermissions () {
|
|
||||||
// TODO: handle admins and mods
|
|
||||||
if (this.props.projectInfo.author.username === this.props.user.username) {
|
|
||||||
this.setState({editable: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleSeeInside () {
|
handleSeeInside () {
|
||||||
this.props.setPlayer(false);
|
this.props.setPlayer(false);
|
||||||
}
|
}
|
||||||
|
handleShare () {
|
||||||
|
// This is just a placeholder, but enables the button in the editor
|
||||||
|
}
|
||||||
handleUpdate (jsonData) {
|
handleUpdate (jsonData) {
|
||||||
this.props.updateProject(
|
this.props.updateProject(
|
||||||
this.props.projectInfo.id,
|
this.props.projectInfo.id,
|
||||||
|
@ -259,32 +257,32 @@ class Preview extends React.Component {
|
||||||
this.props.user.token
|
this.props.user.token
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
handleUpdateProjectTitle (title) {
|
||||||
|
this.handleUpdate({
|
||||||
|
title: title
|
||||||
|
});
|
||||||
|
}
|
||||||
initCounts (favorites, loves) {
|
initCounts (favorites, loves) {
|
||||||
this.setState({
|
this.setState({
|
||||||
favoriteCount: favorites,
|
favoriteCount: favorites,
|
||||||
loveCount: loves
|
loveCount: loves
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
isShared () {
|
renderLogin ({onClose}) {
|
||||||
return (
|
return (
|
||||||
// if we don't have projectInfo assume shared until we know otherwise
|
<ConnectedLogin
|
||||||
Object.keys(this.props.projectInfo).length === 0 || (
|
key="login-dropdown-presentation"
|
||||||
this.props.projectInfo.history &&
|
/* eslint-disable react/jsx-no-bind */
|
||||||
this.props.projectInfo.history.shared.length > 0
|
onLogIn={(formData, callback) => {
|
||||||
)
|
this.props.handleLogIn(formData, result => {
|
||||||
);
|
if (result.success === true) {
|
||||||
|
onClose();
|
||||||
}
|
}
|
||||||
isLoggedIn () {
|
callback(result);
|
||||||
return (
|
});
|
||||||
this.props.sessionStatus === sessionActions.Status.FETCHED &&
|
}}
|
||||||
Object.keys(this.props.user).length > 0
|
/* eslint-ensable react/jsx-no-bind */
|
||||||
);
|
/>
|
||||||
}
|
|
||||||
userOwnsProject () {
|
|
||||||
return (
|
|
||||||
this.isLoggedIn() &&
|
|
||||||
Object.keys(this.props.projectInfo).length > 0 &&
|
|
||||||
this.props.user.id === this.props.projectInfo.author.id
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
|
@ -296,13 +294,13 @@ class Preview extends React.Component {
|
||||||
assetHost={this.props.assetHost}
|
assetHost={this.props.assetHost}
|
||||||
backpackOptions={this.props.backpackOptions}
|
backpackOptions={this.props.backpackOptions}
|
||||||
comments={this.props.comments}
|
comments={this.props.comments}
|
||||||
editable={this.state.editable}
|
editable={this.props.isEditable}
|
||||||
extensions={this.state.extensions}
|
extensions={this.state.extensions}
|
||||||
faved={this.props.faved}
|
faved={this.props.faved}
|
||||||
favoriteCount={this.state.favoriteCount}
|
favoriteCount={this.state.favoriteCount}
|
||||||
isFullScreen={this.state.isFullScreen}
|
isFullScreen={this.state.isFullScreen}
|
||||||
isLoggedIn={this.isLoggedIn()}
|
isLoggedIn={this.props.isLoggedIn}
|
||||||
isShared={this.isShared()}
|
isShared={this.props.isShared}
|
||||||
loveCount={this.state.loveCount}
|
loveCount={this.state.loveCount}
|
||||||
loved={this.props.loved}
|
loved={this.props.loved}
|
||||||
originalInfo={this.props.original}
|
originalInfo={this.props.original}
|
||||||
|
@ -315,8 +313,7 @@ class Preview extends React.Component {
|
||||||
replies={this.props.replies}
|
replies={this.props.replies}
|
||||||
reportOpen={this.state.reportOpen}
|
reportOpen={this.state.reportOpen}
|
||||||
studios={this.props.studios}
|
studios={this.props.studios}
|
||||||
user={this.props.user}
|
userOwnsProject={this.props.userOwnsProject}
|
||||||
userOwnsProject={this.userOwnsProject()}
|
|
||||||
onAddToStudioClicked={this.handleAddToStudioClick}
|
onAddToStudioClicked={this.handleAddToStudioClick}
|
||||||
onAddToStudioClosed={this.handleAddToStudioClose}
|
onAddToStudioClosed={this.handleAddToStudioClose}
|
||||||
onFavoriteClicked={this.handleFavoriteToggle}
|
onFavoriteClicked={this.handleFavoriteToggle}
|
||||||
|
@ -330,6 +327,7 @@ class Preview extends React.Component {
|
||||||
onUpdate={this.handleUpdate}
|
onUpdate={this.handleUpdate}
|
||||||
/>
|
/>
|
||||||
</Page> :
|
</Page> :
|
||||||
|
<React.Fragment>
|
||||||
<IntlGUI
|
<IntlGUI
|
||||||
enableCommunity
|
enableCommunity
|
||||||
hideIntro
|
hideIntro
|
||||||
|
@ -339,7 +337,18 @@ class Preview extends React.Component {
|
||||||
className="gui"
|
className="gui"
|
||||||
projectHost={this.props.projectHost}
|
projectHost={this.props.projectHost}
|
||||||
projectId={this.state.projectId}
|
projectId={this.state.projectId}
|
||||||
|
projectTitle={this.props.projectInfo.title}
|
||||||
|
renderLogin={this.renderLogin}
|
||||||
|
onLogOut={this.props.handleLogOut}
|
||||||
|
onOpenRegistration={this.props.handleOpenRegistration}
|
||||||
|
onShare={this.handleShare}
|
||||||
|
onToggleLoginOpen={this.props.handleToggleLoginOpen}
|
||||||
|
onUpdateProjectTitle={this.handleUpdateProjectTitle}
|
||||||
/>
|
/>
|
||||||
|
<Registration />
|
||||||
|
<CanceledDeletionModal />
|
||||||
|
</React.Fragment>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -362,6 +371,13 @@ Preview.propTypes = {
|
||||||
getProjectStudios: PropTypes.func.isRequired,
|
getProjectStudios: PropTypes.func.isRequired,
|
||||||
getRemixes: PropTypes.func.isRequired,
|
getRemixes: PropTypes.func.isRequired,
|
||||||
getTopLevelComments: PropTypes.func.isRequired,
|
getTopLevelComments: PropTypes.func.isRequired,
|
||||||
|
handleLogIn: PropTypes.func,
|
||||||
|
handleLogOut: PropTypes.func,
|
||||||
|
handleOpenRegistration: PropTypes.func,
|
||||||
|
handleToggleLoginOpen: PropTypes.func,
|
||||||
|
isEditable: PropTypes.bool,
|
||||||
|
isLoggedIn: PropTypes.bool,
|
||||||
|
isShared: PropTypes.bool,
|
||||||
loved: PropTypes.bool,
|
loved: PropTypes.bool,
|
||||||
original: projectShape,
|
original: projectShape,
|
||||||
parent: projectShape,
|
parent: projectShape,
|
||||||
|
@ -389,7 +405,8 @@ Preview.propTypes = {
|
||||||
dateJoined: PropTypes.string,
|
dateJoined: PropTypes.string,
|
||||||
email: PropTypes.string,
|
email: PropTypes.string,
|
||||||
classroomId: PropTypes.string
|
classroomId: PropTypes.string
|
||||||
})
|
}),
|
||||||
|
userOwnsProject: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
Preview.defaultProps = {
|
Preview.defaultProps = {
|
||||||
|
@ -437,26 +454,64 @@ const consolidateStudiosInfo = (curatedStudios, projectStudios, currentStudioIds
|
||||||
return consolidatedStudios;
|
return consolidatedStudios;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => {
|
||||||
projectInfo: state.preview.projectInfo,
|
const projectInfoPresent = Object.keys(state.preview.projectInfo).length > 0;
|
||||||
|
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,
|
comments: state.preview.comments,
|
||||||
faved: state.preview.faved,
|
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,
|
loved: state.preview.loved,
|
||||||
original: state.preview.original,
|
original: state.preview.original,
|
||||||
parent: state.preview.parent,
|
parent: state.preview.parent,
|
||||||
|
playerMode: state.scratchGui.mode.isPlayerOnly,
|
||||||
|
projectInfo: state.preview.projectInfo,
|
||||||
|
projectStudios: state.preview.projectStudios,
|
||||||
remixes: state.preview.remixes,
|
remixes: state.preview.remixes,
|
||||||
replies: state.preview.replies,
|
replies: state.preview.replies,
|
||||||
sessionStatus: state.session.status,
|
sessionStatus: state.session.status, // check if used
|
||||||
projectStudios: state.preview.projectStudios,
|
|
||||||
studios: consolidateStudiosInfo(state.preview.curatedStudios,
|
studios: consolidateStudiosInfo(state.preview.curatedStudios,
|
||||||
state.preview.projectStudios, state.preview.currentStudioIds,
|
state.preview.projectStudios, state.preview.currentStudioIds,
|
||||||
state.preview.status.studioRequests),
|
state.preview.status.studioRequests),
|
||||||
user: state.session.session.user,
|
user: state.session.session.user,
|
||||||
playerMode: state.scratchGui.mode.isPlayerOnly,
|
userOwnsProject: isLoggedIn && authorPresent &&
|
||||||
fullScreen: state.scratchGui.mode.isFullScreen
|
state.session.session.user.id === state.preview.projectInfo.author.id
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
handleOpenRegistration: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.setRegistrationOpen(true));
|
||||||
|
},
|
||||||
|
handleLogIn: (formData, callback) => {
|
||||||
|
dispatch(navigationActions.handleLogIn(formData, callback));
|
||||||
|
},
|
||||||
|
handleLogOut: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.handleLogOut());
|
||||||
|
},
|
||||||
|
handleToggleLoginOpen: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.toggleLoginOpen());
|
||||||
|
},
|
||||||
getOriginalInfo: id => {
|
getOriginalInfo: id => {
|
||||||
dispatch(previewActions.getOriginalInfo(id));
|
dispatch(previewActions.getOriginalInfo(id));
|
||||||
},
|
},
|
||||||
|
@ -497,9 +552,6 @@ const mapDispatchToProps = dispatch => ({
|
||||||
setLovedStatus: (loved, id, username, token) => {
|
setLovedStatus: (loved, id, username, token) => {
|
||||||
dispatch(previewActions.setLovedStatus(loved, id, username, token));
|
dispatch(previewActions.setLovedStatus(loved, id, username, token));
|
||||||
},
|
},
|
||||||
refreshSession: () => {
|
|
||||||
dispatch(sessionActions.refreshSession());
|
|
||||||
},
|
|
||||||
reportProject: (id, formData) => {
|
reportProject: (id, formData) => {
|
||||||
dispatch(previewActions.reportProject(id, formData));
|
dispatch(previewActions.reportProject(id, formData));
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,6 +6,12 @@ $player-width: 482px;
|
||||||
$player-height: 406px;
|
$player-height: 406px;
|
||||||
$stage-width: 480px;
|
$stage-width: 480px;
|
||||||
|
|
||||||
|
/* screen sizes */
|
||||||
|
$small: "screen and (max-width : #{$mobile}-1)";
|
||||||
|
$medium: "screen and (min-width : #{$mobile}) and (max-width : #{$tablet}-1)";
|
||||||
|
$big: "screen and (min-width : #{$tablet})";
|
||||||
|
$medium-and-small: "screen and (max-width : #{$tablet}-1)";
|
||||||
|
|
||||||
/* override view padding for share banner */
|
/* override view padding for share banner */
|
||||||
#view {
|
#view {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -29,16 +35,26 @@ $stage-width: 480px;
|
||||||
&.has-error {
|
&.has-error {
|
||||||
|
|
||||||
.validation-message {
|
.validation-message {
|
||||||
transform: translate(22rem, 0);
|
right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.no-edit {
|
||||||
|
/* titles of projects you don't own should not
|
||||||
|
show the full title if this is too long */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-header {
|
.project-header {
|
||||||
margin-right: 2rem;
|
margin-right: 2rem;
|
||||||
|
min-width: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
.inplace-input {
|
.inplace-input {
|
||||||
height: calc(3rem - 4px);
|
height: calc(3rem - 4px);
|
||||||
|
@ -63,18 +79,18 @@ $stage-width: 480px;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
|
min-width: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.validation-message {
|
.validation-message {
|
||||||
$arrow-border-width: 1rem;
|
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
z-index: 1;
|
||||||
left: 0;
|
$arrow-border-width: 1rem;
|
||||||
margin-left: $arrow-border-width;
|
margin-top: $arrow-border-width;
|
||||||
border: 1px solid $active-gray;
|
border: 1px solid $active-gray;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: $ui-orange;
|
background-color: $ui-orange;
|
||||||
|
@ -85,13 +101,18 @@ $stage-width: 480px;
|
||||||
color: $type-white;
|
color: $type-white;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
||||||
|
@media #{$medium-and-small} {
|
||||||
|
margin-top: calc($arrow-border-width / 2);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1rem;
|
top: -.5rem;
|
||||||
left: -$arrow-border-width / 2;
|
left: calc(50% - calc(#{$arrow-border-width} / 2));
|
||||||
|
|
||||||
transform: rotate(45deg);
|
transform: rotate(135deg);
|
||||||
|
|
||||||
border-bottom: 1px solid $active-gray;
|
border-bottom: 1px solid $active-gray;
|
||||||
border-left: 1px solid $active-gray;
|
border-left: 1px solid $active-gray;
|
||||||
|
@ -102,6 +123,10 @@ $stage-width: 480px;
|
||||||
height: $arrow-border-width;
|
height: $arrow-border-width;
|
||||||
|
|
||||||
content: "";
|
content: "";
|
||||||
|
|
||||||
|
@media #{$medium-and-small} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,12 +144,16 @@ $stage-width: 480px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-buttons {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comments-container {
|
.comments-container {
|
||||||
width: 60%;
|
width: 65%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remix-button,
|
.remix-button,
|
||||||
|
@ -174,12 +203,15 @@ $stage-width: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-notes {
|
.project-notes {
|
||||||
// not 1.5rem because of stage padding
|
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
height: $player-height;
|
height: $player-height;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
|
|
||||||
|
> .description-block:first-child {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-date {
|
.share-date {
|
||||||
|
@ -193,6 +225,7 @@ $stage-width: 480px;
|
||||||
.subactions {
|
.subactions {
|
||||||
margin-left: 1.5rem;
|
margin-left: 1.5rem;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
align-items: flex-start;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -368,6 +401,7 @@ $stage-width: 480px;
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-top: 0;
|
||||||
color: $type-white;
|
color: $type-white;
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@ -442,6 +476,7 @@ $stage-width: 480px;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-button {
|
.studio-button {
|
||||||
&:before {
|
&:before {
|
||||||
background-image: url("/svgs/project/studio-add-white.svg");
|
background-image: url("/svgs/project/studio-add-white.svg");
|
||||||
|
@ -480,14 +515,23 @@ $stage-width: 480px;
|
||||||
|
|
||||||
.extension-list {
|
.extension-list {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
@media #{$medium-and-small} {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.remix-list,
|
.remix-list,
|
||||||
.studio-list {
|
.studio-list {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.project {
|
.list-title {
|
||||||
margin-bottom: 1.5rem;
|
margin-left: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.creator-image img {
|
.creator-image img {
|
||||||
|
|
|
@ -19,6 +19,7 @@ const RemixCredit = props => {
|
||||||
{projectInfo.author.username}
|
{projectInfo.author.username}
|
||||||
</a> for the original project <a
|
</a> for the original project <a
|
||||||
href={`/preview/${projectInfo.id}`}
|
href={`/preview/${projectInfo.id}`}
|
||||||
|
title={projectInfo.title}
|
||||||
>
|
>
|
||||||
{projectInfo.title}
|
{projectInfo.title}
|
||||||
</a>.
|
</a>.
|
||||||
|
|
|
@ -9,7 +9,7 @@ const RemixList = props => {
|
||||||
if (remixes.length === 0) return null;
|
if (remixes.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<FlexRow className="remix-list">
|
<FlexRow className="remix-list">
|
||||||
<div className="project-title">
|
<div className="list-title">
|
||||||
Remixes
|
Remixes
|
||||||
</div>
|
</div>
|
||||||
{remixes.length === 0 ? (
|
{remixes.length === 0 ? (
|
||||||
|
|
|
@ -8,8 +8,8 @@ const StudioList = props => {
|
||||||
const studios = props.studios;
|
const studios = props.studios;
|
||||||
if (studios.length === 0) return null;
|
if (studios.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<FlexRow className="remix-list">
|
<FlexRow className="studio-list">
|
||||||
<div className="project-title">
|
<div className="list-title">
|
||||||
Studios
|
Studios
|
||||||
</div>
|
</div>
|
||||||
{studios.length === 0 ? (
|
{studios.length === 0 ? (
|
||||||
|
|
|
@ -57,12 +57,21 @@ class Search extends React.Component {
|
||||||
|
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const query = window.location.search;
|
const query = decodeURIComponent(window.location.search);
|
||||||
const q = query.lastIndexOf('q=');
|
let term = query;
|
||||||
let term = '';
|
|
||||||
if (q !== -1) {
|
const stripQueryValue = function (queryTerm) {
|
||||||
term = query.substring(q + 2, query.length).toLowerCase();
|
const queryIndex = query.indexOf('q=');
|
||||||
|
if (queryIndex !== -1) {
|
||||||
|
queryTerm = query.substring(queryIndex + 2, query.length).toLowerCase();
|
||||||
}
|
}
|
||||||
|
return queryTerm;
|
||||||
|
};
|
||||||
|
// Strip off the initial "?q="
|
||||||
|
term = stripQueryValue(term);
|
||||||
|
// Strip off user entered "?q="
|
||||||
|
term = stripQueryValue(term);
|
||||||
|
|
||||||
while (term.indexOf('/') > -1) {
|
while (term.indexOf('/') > -1) {
|
||||||
term = term.substring(0, term.indexOf('/'));
|
term = term.substring(0, term.indexOf('/'));
|
||||||
}
|
}
|
||||||
|
@ -232,7 +241,7 @@ Search.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
searchTerm: state.navigation
|
searchTerm: state.navigation.searchTerm
|
||||||
});
|
});
|
||||||
|
|
||||||
const WrappedSearch = injectIntl(Search);
|
const WrappedSearch = injectIntl(Search);
|
||||||
|
|
|
@ -495,7 +495,6 @@ class SplashPresentation extends React.Component { // eslint-disable-line react/
|
||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
]) : []
|
]) : []
|
||||||
}
|
}
|
||||||
|
|
||||||
{featured}
|
{featured}
|
||||||
|
|
||||||
{this.props.isAdmin ? [
|
{this.props.isAdmin ? [
|
||||||
|
@ -555,7 +554,7 @@ SplashPresentation.propTypes = {
|
||||||
isAdmin: PropTypes.bool.isRequired,
|
isAdmin: PropTypes.bool.isRequired,
|
||||||
isEducator: PropTypes.bool.isRequired,
|
isEducator: PropTypes.bool.isRequired,
|
||||||
lovedByFollowing: PropTypes.arrayOf(PropTypes.object),
|
lovedByFollowing: PropTypes.arrayOf(PropTypes.object),
|
||||||
news: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
news: PropTypes.arrayOf(PropTypes.object),
|
||||||
onDismiss: PropTypes.func.isRequired,
|
onDismiss: PropTypes.func.isRequired,
|
||||||
onHideEmailConfirmationModal: PropTypes.func.isRequired,
|
onHideEmailConfirmationModal: PropTypes.func.isRequired,
|
||||||
onRefreshHomepageCache: PropTypes.func.isRequired,
|
onRefreshHomepageCache: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -7,8 +7,8 @@ const React = require('react');
|
||||||
const api = require('../../lib/api');
|
const api = require('../../lib/api');
|
||||||
const injectIntl = require('../../lib/intl.jsx').injectIntl;
|
const injectIntl = require('../../lib/intl.jsx').injectIntl;
|
||||||
const intlShape = require('../../lib/intl.jsx').intlShape;
|
const intlShape = require('../../lib/intl.jsx').intlShape;
|
||||||
const log = require('../../lib/log.js');
|
|
||||||
const sessionStatus = require('../../redux/session').Status;
|
const sessionStatus = require('../../redux/session').Status;
|
||||||
|
const navigationActions = require('../../redux/navigation.js');
|
||||||
|
|
||||||
const Deck = require('../../components/deck/deck.jsx');
|
const Deck = require('../../components/deck/deck.jsx');
|
||||||
const Progression = require('../../components/progression/progression.jsx');
|
const Progression = require('../../components/progression/progression.jsx');
|
||||||
|
@ -24,7 +24,6 @@ class StudentCompleteRegistration extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'handleAdvanceStep',
|
'handleAdvanceStep',
|
||||||
'handleLogOut',
|
|
||||||
'handleRegister',
|
'handleRegister',
|
||||||
'handleGoToClass'
|
'handleGoToClass'
|
||||||
]);
|
]);
|
||||||
|
@ -61,18 +60,6 @@ class StudentCompleteRegistration extends React.Component {
|
||||||
formData: defaults({}, formData, this.state.formData)
|
formData: defaults({}, formData, this.state.formData)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleLogOut (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
api({
|
|
||||||
host: '',
|
|
||||||
method: 'post',
|
|
||||||
uri: '/accounts/logout/',
|
|
||||||
useCsrf: true
|
|
||||||
}, err => {
|
|
||||||
if (err) return log.error(err);
|
|
||||||
window.location = '/';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
handleRegister (formData) {
|
handleRegister (formData) {
|
||||||
this.setState({waiting: true});
|
this.setState({waiting: true});
|
||||||
|
|
||||||
|
@ -147,7 +134,7 @@ class StudentCompleteRegistration extends React.Component {
|
||||||
classroom={this.state.classroom}
|
classroom={this.state.classroom}
|
||||||
studentUsername={this.props.studentUsername}
|
studentUsername={this.props.studentUsername}
|
||||||
waiting={this.state.waiting}
|
waiting={this.state.waiting}
|
||||||
onHandleLogOut={this.handleLogOut}
|
onHandleLogOut={this.props.handleLogOut}
|
||||||
onNextStep={this.handleAdvanceStep}
|
onNextStep={this.handleAdvanceStep}
|
||||||
/>
|
/>
|
||||||
{this.props.must_reset_password ?
|
{this.props.must_reset_password ?
|
||||||
|
@ -178,6 +165,7 @@ class StudentCompleteRegistration extends React.Component {
|
||||||
|
|
||||||
StudentCompleteRegistration.propTypes = {
|
StudentCompleteRegistration.propTypes = {
|
||||||
classroomId: PropTypes.number.isRequired,
|
classroomId: PropTypes.number.isRequired,
|
||||||
|
handleLogOut: PropTypes.func,
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
must_reset_password: PropTypes.bool.isRequired,
|
must_reset_password: PropTypes.bool.isRequired,
|
||||||
newStudent: PropTypes.bool.isRequired,
|
newStudent: PropTypes.bool.isRequired,
|
||||||
|
@ -199,6 +187,16 @@ const mapStateToProps = state => ({
|
||||||
studentUsername: state.session.session.user && state.session.session.user.username
|
studentUsername: state.session.session.user && state.session.session.user.username
|
||||||
});
|
});
|
||||||
|
|
||||||
const ConnectedStudentCompleteRegistration = connect(mapStateToProps)(IntlStudentCompleteRegistration);
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
handleLogOut: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(navigationActions.handleLogOut());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ConnectedStudentCompleteRegistration = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(IntlStudentCompleteRegistration);
|
||||||
|
|
||||||
render(<ConnectedStudentCompleteRegistration />, document.getElementById('app'));
|
render(<ConnectedStudentCompleteRegistration />, document.getElementById('app'));
|
||||||
|
|
|
@ -25,5 +25,9 @@
|
||||||
"wedo2.updateLinkText": "Make sure you have installed the latest version of Scratch Link.",
|
"wedo2.updateLinkText": "Make sure you have installed the latest version of Scratch Link.",
|
||||||
"wedo2.legacyInfoTitle": "Using Scratch 2.0?",
|
"wedo2.legacyInfoTitle": "Using Scratch 2.0?",
|
||||||
"wedo2.legacyInfoText": "Visit our page about {wedoLegacyLink}.",
|
"wedo2.legacyInfoText": "Visit our page about {wedoLegacyLink}.",
|
||||||
"wedo2.legacyLinkText": "using LEGO WeDo with Scratch 2.0"
|
"wedo2.legacyLinkText": "using LEGO WeDo with Scratch 2.0",
|
||||||
|
"wedo2.imgAltWeDoIllustration": "An illustration of a WeDo2 featuring a tilt sensor and a motor.",
|
||||||
|
"wedo2.imgAltStarter1": "A Scratch project with a dog and a taco.",
|
||||||
|
"wedo2.imgAltStarter2": "A Scratch project with a toad playing instruments in space.",
|
||||||
|
"wedo2.imgAltStarter3": "A Scratch project with dinosaurs."
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,9 +28,15 @@ class Wedo2 extends ExtensionLanding {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className="extension-landing wedo2">
|
<div className="extension-landing wedo2">
|
||||||
<ExtensionHeader imageSrc="/images/wedo2/wedo2-illustration.png">
|
<ExtensionHeader
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'wedo2.imgAltWeDoIllustration'})}
|
||||||
|
imageSrc="/images/wedo2/wedo2-illustration.png"
|
||||||
|
>
|
||||||
<FlexRow className="column extension-copy">
|
<FlexRow className="column extension-copy">
|
||||||
<h2><img src="/images/wedo2/wedo2.svg" />LEGO WeDo 2.0</h2>
|
<h1><img
|
||||||
|
alt=""
|
||||||
|
src="/images/wedo2/wedo2.svg"
|
||||||
|
/>LEGO WeDo 2.0</h1>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="wedo2.headerText"
|
id="wedo2.headerText"
|
||||||
values={{
|
values={{
|
||||||
|
@ -48,19 +54,31 @@ class Wedo2 extends ExtensionLanding {
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
<ExtensionRequirements>
|
<ExtensionRequirements>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/windows.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/windows.svg"
|
||||||
|
/>
|
||||||
Windows 10+
|
Windows 10+
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/mac.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/mac.svg"
|
||||||
|
/>
|
||||||
macOS 10.13+
|
macOS 10.13+
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/bluetooth.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/bluetooth.svg"
|
||||||
|
/>
|
||||||
Bluetooth
|
Bluetooth
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<img src="/svgs/extensions/scratch-link.svg" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/svgs/extensions/scratch-link.svg"
|
||||||
|
/>
|
||||||
Scratch Link
|
Scratch Link
|
||||||
</span>
|
</span>
|
||||||
</ExtensionRequirements>
|
</ExtensionRequirements>
|
||||||
|
@ -80,6 +98,7 @@ class Wedo2 extends ExtensionLanding {
|
||||||
<Step number={1}>
|
<Step number={1}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
className="screenshot"
|
className="screenshot"
|
||||||
src="/images/wedo2/wedo2-connect-1.png"
|
src="/images/wedo2/wedo2-connect-1.png"
|
||||||
/>
|
/>
|
||||||
|
@ -104,6 +123,7 @@ class Wedo2 extends ExtensionLanding {
|
||||||
<Step number={2}>
|
<Step number={2}>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img
|
<img
|
||||||
|
alt={this.props.intl.formatMessage({id: 'extensionInstallation.addExtension'})}
|
||||||
className="screenshot"
|
className="screenshot"
|
||||||
src="/images/wedo2/wedo2-connect-2.png"
|
src="/images/wedo2/wedo2-connect-2.png"
|
||||||
/>
|
/>
|
||||||
|
@ -125,7 +145,10 @@ class Wedo2 extends ExtensionLanding {
|
||||||
<FormattedMessage id="wedo2.plugMotorIn" />
|
<FormattedMessage id="wedo2.plugMotorIn" />
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/wedo2/wedo2-motor.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/wedo2/wedo2-motor.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Step>
|
</Step>
|
||||||
<Step
|
<Step
|
||||||
|
@ -143,7 +166,10 @@ class Wedo2 extends ExtensionLanding {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="step-image">
|
<div className="step-image">
|
||||||
<img src="/images/wedo2/wedo2-motor-turn-block.png" />
|
<img
|
||||||
|
alt=""
|
||||||
|
src="/images/wedo2/wedo2-motor-turn-block.png"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
@ -153,18 +179,21 @@ class Wedo2 extends ExtensionLanding {
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://beta.scratch.mit.edu/#239284992"
|
cardUrl="https://beta.scratch.mit.edu/#239284992"
|
||||||
description={this.props.intl.formatMessage({id: 'wedo2.starter1Description'})}
|
description={this.props.intl.formatMessage({id: 'wedo2.starter1Description'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'wedo2.imgAltStarter1'})}
|
||||||
imageSrc="/images/wedo2/wedo2-starter1.png"
|
imageSrc="/images/wedo2/wedo2-starter1.png"
|
||||||
title={this.props.intl.formatMessage({id: 'wedo2.starter1Title'})}
|
title={this.props.intl.formatMessage({id: 'wedo2.starter1Title'})}
|
||||||
/>
|
/>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://beta.scratch.mit.edu/#239284997"
|
cardUrl="https://beta.scratch.mit.edu/#239284997"
|
||||||
description={this.props.intl.formatMessage({id: 'wedo2.starter2Description'})}
|
description={this.props.intl.formatMessage({id: 'wedo2.starter2Description'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'wedo2.imgAltStarter2'})}
|
||||||
imageSrc="/images/wedo2/wedo2-starter2.png"
|
imageSrc="/images/wedo2/wedo2-starter2.png"
|
||||||
title={this.props.intl.formatMessage({id: 'wedo2.starter2Title'})}
|
title={this.props.intl.formatMessage({id: 'wedo2.starter2Title'})}
|
||||||
/>
|
/>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
cardUrl="https://beta.scratch.mit.edu/#239285001"
|
cardUrl="https://beta.scratch.mit.edu/#239285001"
|
||||||
description={this.props.intl.formatMessage({id: 'wedo2.starter3Description'})}
|
description={this.props.intl.formatMessage({id: 'wedo2.starter3Description'})}
|
||||||
|
imageAlt={this.props.intl.formatMessage({id: 'wedo2.imgAltStarter3'})}
|
||||||
imageSrc="/images/wedo2/wedo2-starter3.png"
|
imageSrc="/images/wedo2/wedo2-starter3.png"
|
||||||
title={this.props.intl.formatMessage({id: 'wedo2.starter3Title'})}
|
title={this.props.intl.formatMessage({id: 'wedo2.starter3Title'})}
|
||||||
/>
|
/>
|
||||||
|
|
10
static/svgs/modal/spinner-blue.svg
Normal file
10
static/svgs/modal/spinner-blue.svg
Normal 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 |
10
static/svgs/modal/spinner-transparent-gray.svg
Normal file
10
static/svgs/modal/spinner-transparent-gray.svg
Normal 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 |
10
static/svgs/modal/spinner-white.svg
Normal file
10
static/svgs/modal/spinner-white.svg
Normal 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 |
|
@ -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 |
|
@ -52,7 +52,7 @@ test('Sign in to Scratch using scratchr2 navbar', t => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Sign in to Scratch & verify My Stuff structure (tabs, title)', t => {
|
test('Sign in to Scratch & verify My Stuff structure (tabs, title)', t => {
|
||||||
clickXpath('//a[@class="mystuff-icon"]')
|
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||||
.then(() => findByXpath('//div[@class="box-head"]/h2'))
|
.then(() => findByXpath('//div[@class="box-head"]/h2'))
|
||||||
.then((element) => element.getText('h2'))
|
.then((element) => element.getText('h2'))
|
||||||
.then((text) => t.equal('My Stuff', text, 'title should be My Stuff'))
|
.then((text) => t.equal('My Stuff', text, 'title should be My Stuff'))
|
||||||
|
@ -75,7 +75,7 @@ test('Sign in to Scratch & verify My Stuff structure (tabs, title)', t => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clicking See Inside should take you to the editor', t => {
|
test('clicking See Inside should take you to the editor', t => {
|
||||||
clickXpath('//a[@class="mystuff-icon"]')
|
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||||
.then(() => findByXpath('//a[@data-control="edit"]'))
|
.then(() => findByXpath('//a[@data-control="edit"]'))
|
||||||
.then((element) => element.getText('span'))
|
.then((element) => element.getText('span'))
|
||||||
.then((text) => t.equal(text, 'See inside', 'there should be a "See inside" button'))
|
.then((text) => t.equal(text, 'See inside', 'there should be a "See inside" button'))
|
||||||
|
@ -89,7 +89,7 @@ test('clicking See Inside should take you to the editor', t => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clicking a project title should take you to the project page', t => {
|
test('clicking a project title should take you to the project page', t => {
|
||||||
clickXpath('//a[@class="mystuff-icon"]')
|
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||||
.then(() => clickXpath('//a[@data-control="edit"]'))
|
.then(() => clickXpath('//a[@data-control="edit"]'))
|
||||||
.then(() => driver.getCurrentUrl())
|
.then(() => driver.getCurrentUrl())
|
||||||
.then(function (u) {
|
.then(function (u) {
|
||||||
|
@ -100,7 +100,7 @@ test('clicking a project title should take you to the project page', t => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Add To button should bring up a list of studios', t => {
|
test('Add To button should bring up a list of studios', t => {
|
||||||
clickXpath('//a[@class="mystuff-icon"]')
|
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||||
.then(() => findByXpath('//div[@data-control="add-to"]'))
|
.then(() => findByXpath('//div[@data-control="add-to"]'))
|
||||||
.then((element) => element.getText('span'))
|
.then((element) => element.getText('span'))
|
||||||
.then((text) => t.equal(text, 'Add to', 'there should be an "Add to" button'))
|
.then((text) => t.equal(text, 'Add to', 'there should be an "Add to" button'))
|
||||||
|
@ -115,7 +115,7 @@ test('Add To button should bring up a list of studios', t => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('+ New Studio button should take you to the studio page', t => {
|
test('+ New Studio button should take you to the studio page', t => {
|
||||||
clickXpath('//a[@class="mystuff-icon"]')
|
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||||
.then(() => clickXpath('//form[@id="new_studio"]/button[@type="submit"]'))
|
.then(() => clickXpath('//form[@id="new_studio"]/button[@type="submit"]'))
|
||||||
.then(() => findByXpath('//div[@id="show-add-project"]'))
|
.then(() => findByXpath('//div[@id="show-add-project"]'))
|
||||||
.then((element) => element.getText('span'))
|
.then((element) => element.getText('span'))
|
||||||
|
@ -130,7 +130,7 @@ test('+ New Studio button should take you to the studio page', t => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('+ New Project button should open the editor', t => {
|
test('+ New Project button should open the editor', t => {
|
||||||
clickXpath('//a[@class="mystuff-icon"]')
|
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||||
.then(() => clickText('+ New Project'))
|
.then(() => clickText('+ New Project'))
|
||||||
.then(() => driver.getCurrentUrl())
|
.then(() => driver.getCurrentUrl())
|
||||||
.then(function (u) {
|
.then(function (u) {
|
||||||
|
|
|
@ -79,7 +79,8 @@ tap.test('checkFeaturedStudiosRowWhenSignedOut', function (t) {
|
||||||
// checks that the link for a studio makes sense
|
// checks that the link for a studio makes sense
|
||||||
tap.test('checkFeaturedStudiosRowLinkWhenSignedOut', function (t) {
|
tap.test('checkFeaturedStudiosRowLinkWhenSignedOut', function (t) {
|
||||||
var xPathLink = '//div[contains(@class, "thumbnail") and contains(@class, "gallery") ' +
|
var xPathLink = '//div[contains(@class, "thumbnail") and contains(@class, "gallery") ' +
|
||||||
'and contains(@class, "slick-slide") and contains(@class, "slick-active")]/a[@class="thumbnail-image"]';
|
'and contains(@class, "slick-slide") ' +
|
||||||
|
'and contains(@class, "slick-active")]/a[@class="thumbnail-image"]';
|
||||||
driver.findElement(webdriver.By.xpath(xPathLink))
|
driver.findElement(webdriver.By.xpath(xPathLink))
|
||||||
.then(function (element) {
|
.then(function (element) {
|
||||||
element.getAttribute('href')
|
element.getAttribute('href')
|
||||||
|
|
|
@ -54,7 +54,7 @@ test('Sign in to Scratch using scratchr2 navbar', t => {
|
||||||
|
|
||||||
test('Sign out of Scratch using scratchr2 navbar', t => {
|
test('Sign out of Scratch using scratchr2 navbar', t => {
|
||||||
clickXpath('//span[contains(@class, "user-name")' +
|
clickXpath('//span[contains(@class, "user-name")' +
|
||||||
' and contains(@class, "dropdown-toggle")]/img[@class="user-icon"]')
|
' and contains(@class, "dropdown-toggle")]/img[contains(@class, "user-icon")]')
|
||||||
.then(() => clickXpath('//input[@value="Sign out"]'))
|
.then(() => clickXpath('//input[@value="Sign out"]'))
|
||||||
.then(() => findText('Sign in'))
|
.then(() => findText('Sign in'))
|
||||||
.then((element) => t.ok(element, 'Sign in reappeared on the page after signing out'))
|
.then((element) => t.ok(element, 'Sign in reappeared on the page after signing out'))
|
||||||
|
|
|
@ -43,7 +43,7 @@ test('Sign in to Scratch using scratch-www navbar', t => {
|
||||||
.then((element) => element.sendKeys(password))
|
.then((element) => element.sendKeys(password))
|
||||||
.then(() => clickXpath('//button[contains(@class, "button") and ' +
|
.then(() => clickXpath('//button[contains(@class, "button") and ' +
|
||||||
'contains(@class, "submit-button") and contains(@class, "white")]'))
|
'contains(@class, "submit-button") and contains(@class, "white")]'))
|
||||||
.then(() => findByXpath('//span[@class="profile-name"]'))
|
.then(() => findByXpath('//span[contains(@class, "profile-name")]'))
|
||||||
.then((element) => element.getText())
|
.then((element) => element.getText())
|
||||||
.then((text) => t.match(text.toLowerCase(), username.substring(0, 10).toLowerCase(),
|
.then((text) => t.match(text.toLowerCase(), username.substring(0, 10).toLowerCase(),
|
||||||
'first part of username should be displayed in navbar'))
|
'first part of username should be displayed in navbar'))
|
||||||
|
@ -51,7 +51,7 @@ test('Sign in to Scratch using scratch-www navbar', t => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Sign out of Scratch using scratch-www navbar', t => {
|
test('Sign out of Scratch using scratch-www navbar', t => {
|
||||||
clickXpath('//a[@class="user-info"]')
|
clickXpath('//a[contains(@class, "user-info")]')
|
||||||
.then(() => clickText('Sign out'))
|
.then(() => clickText('Sign out'))
|
||||||
.then(() => findText('Sign in'))
|
.then(() => findText('Sign in'))
|
||||||
.then((element) => t.ok(element, 'Sign in reappeared on the page after signing out'))
|
.then((element) => t.ok(element, 'Sign in reappeared on the page after signing out'))
|
||||||
|
|
Loading…
Reference in a new issue