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