move login/registration functions and view state to session reducer, pass to gui (#2078)

* move login/registration functions and view state to session reducer, pass to gui

* navigation reducer handles login; gui passed renderLogin function

* put back in join class to make smoke tests keep working
This commit is contained in:
Benjamin Wheeler 2018-09-24 11:04:30 -04:00 committed by GitHub
parent c047612d68
commit 935eb0b15f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 549 additions and 349 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

View file

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

View file

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

View file

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

View file

@ -23,10 +23,11 @@ const AccountNav = ({
}) => ( }) => (
<div className="account-nav"> <div className="account-nav">
<a <a
className={classNames({ className={classNames([
'user-info': true, 'ignore-react-onclickoutside',
'open': isOpen 'user-info',
})} {open: isOpen}
])}
href="#" href="#"
onClick={onClick} onClick={onClick}
> >

View file

@ -8,16 +8,14 @@ const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const messageCountActions = require('../../../redux/message-count.js'); const messageCountActions = require('../../../redux/message-count.js');
const navigationActions = require('../../../redux/navigation.js');
const sessionActions = require('../../../redux/session.js'); const sessionActions = require('../../../redux/session.js');
const api = require('../../../lib/api');
const Button = require('../../forms/button.jsx'); const Button = require('../../forms/button.jsx');
const Dropdown = require('../../dropdown/dropdown.jsx');
const Form = require('../../forms/form.jsx'); const Form = require('../../forms/form.jsx');
const Input = require('../../forms/input.jsx'); const Input = require('../../forms/input.jsx');
const log = require('../../../lib/log.js'); const LoginDropdown = require('../../login/login-dropdown.jsx');
const Login = require('../../login/login.jsx'); const CanceledDeletionModal = require('../../login/canceled-deletion-modal.jsx');
const Modal = require('../../modal/base/modal.jsx');
const NavigationBox = require('../base/navigation.jsx'); const NavigationBox = require('../base/navigation.jsx');
const Registration = require('../../registration/registration.jsx'); const Registration = require('../../registration/registration.jsx');
const AccountNav = require('./accountnav.jsx'); const AccountNav = require('./accountnav.jsx');
@ -29,32 +27,16 @@ class Navigation extends React.Component {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'getProfileUrl', 'getProfileUrl',
'handleJoinClick',
'handleLoginClick',
'handleCloseLogin',
'handleLogIn',
'handleLogOut',
'handleAccountNavClick',
'handleCloseAccountNav',
'showCanceledDeletion',
'handleCloseCanceledDeletion',
'handleCloseRegistration',
'handleCompleteRegistration',
'handleSearchSubmit' 'handleSearchSubmit'
]); ]);
this.state = { this.state = {
accountNavOpen: false,
canceledDeletionOpen: false,
loginOpen: false,
loginError: null,
registrationOpen: false,
messageCountIntervalId: -1 // javascript method interval id for getting messsage count. messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
}; };
} }
componentDidMount () { componentDidMount () {
if (this.props.session.session.user) { if (this.props.user) {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
this.props.getMessageCount(this.props.session.session.user.username); this.props.getMessageCount(this.props.user.username);
}, 120000); // check for new messages every 2 mins. }, 120000); // check for new messages every 2 mins.
this.setState({ // eslint-disable-line react/no-did-mount-set-state this.setState({ // eslint-disable-line react/no-did-mount-set-state
messageCountIntervalId: intervalId messageCountIntervalId: intervalId
@ -62,14 +44,11 @@ class Navigation extends React.Component {
} }
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.session.session.user !== this.props.session.session.user) { if (prevProps.user !== this.props.user) {
this.setState({ // eslint-disable-line react/no-did-update-set-state this.props.closeAccountMenus();
loginOpen: false, if (this.props.user) {
accountNavOpen: false
});
if (this.props.session.session.user) {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
this.props.getMessageCount(this.props.session.session.user.username); this.props.getMessageCount(this.props.user.username);
}, 120000); // check for new messages every 2 mins. }, 120000); // check for new messages every 2 mins.
this.setState({ // eslint-disable-line react/no-did-update-set-state this.setState({ // eslint-disable-line react/no-did-update-set-state
messageCountIntervalId: intervalId messageCountIntervalId: intervalId
@ -88,104 +67,25 @@ class Navigation extends React.Component {
// clear message interval if it exists // clear message interval if it exists
if (this.state.messageCountIntervalId !== -1) { if (this.state.messageCountIntervalId !== -1) {
clearInterval(this.state.messageCountIntervalId); clearInterval(this.state.messageCountIntervalId);
this.props.dispatch(messageCountActions.setCount(0)); this.props.setMessageCount(0);
this.setState({ this.setState({
messageCountIntervalId: -1 messageCountIntervalId: -1
}); });
} }
} }
getProfileUrl () { getProfileUrl () {
if (!this.props.session.session.user) return; if (!this.props.user) return;
return `/users/${this.props.session.session.user.username}/`; return `/users/${this.props.user.username}/`;
}
handleJoinClick (e) {
e.preventDefault();
this.setState({registrationOpen: true});
}
handleLoginClick (e) {
e.preventDefault();
this.setState({loginOpen: !this.state.loginOpen});
}
handleCloseLogin () {
this.setState({loginOpen: false});
}
// NOTE: TODO: continue here. Should move these two functions up to a redux level,
// maybe into session...
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.refreshSession();
} else {
if (body.redirect) {
window.location = body.redirect;
}
// Update login error message to a friendlier one if it exists
this.setState({loginError: body.msg});
}
}
// JS error already logged by api mixin
callback();
});
}
handleLogOut (e) {
e.preventDefault();
api({
host: '',
method: 'post',
uri: '/accounts/logout/',
useCsrf: true
}, err => {
if (err) log.error(err);
this.handleCloseLogin();
window.location = '/';
});
}
handleAccountNavClick (e) {
e.preventDefault();
this.setState({accountNavOpen: true});
}
handleCloseAccountNav () {
this.setState({accountNavOpen: false});
}
showCanceledDeletion () {
this.setState({canceledDeletionOpen: true});
}
handleCloseCanceledDeletion () {
this.setState({canceledDeletionOpen: false});
}
handleCloseRegistration () {
this.setState({registrationOpen: false});
}
handleCompleteRegistration () {
this.props.dispatch(sessionActions.refreshSession());
this.handleCloseRegistration();
} }
handleSearchSubmit (formData) { handleSearchSubmit (formData) {
window.location.href = `/search/projects?q=${encodeURIComponent(formData.q)}`; window.location.href = `/search/projects?q=${encodeURIComponent(formData.q)}`;
} }
render () { render () {
const createLink = this.props.session.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home'; const createLink = this.props.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
return ( return (
<NavigationBox <NavigationBox
className={classNames({ className={classNames({
'logged-in': this.props.session.session.user 'logged-in': this.props.user
})} })}
> >
<ul> <ul>
@ -233,7 +133,7 @@ class Navigation extends React.Component {
</Form> </Form>
</li> </li>
{this.props.session.status === sessionActions.Status.FETCHED ? ( {this.props.session.status === sessionActions.Status.FETCHED ? (
this.props.session.session.user ? [ this.props.user ? [
<li <li
className="link right messages" className="link right messages"
key="messages" key="messages"
@ -267,16 +167,16 @@ class Navigation extends React.Component {
key="account-nav" key="account-nav"
> >
<AccountNav <AccountNav
classroomId={this.props.session.session.user.classroomId} classroomId={this.props.user.classroomId}
isEducator={this.props.permissions.educator} isEducator={this.props.permissions.educator}
isOpen={this.state.accountNavOpen} isOpen={this.props.accountNavOpen}
isStudent={this.props.permissions.student} isStudent={this.props.permissions.student}
profileUrl={this.getProfileUrl()} profileUrl={this.getProfileUrl()}
thumbnailUrl={this.props.session.session.user.thumbnailUrl} thumbnailUrl={this.props.user.thumbnailUrl}
username={this.props.session.session.user.username} username={this.props.user.username}
onClick={this.handleAccountNavClick} onClick={this.props.handleToggleAccountNav}
onClickLogout={this.handleLogOut} onClickLogout={this.props.handleLogOut}
onClose={this.handleCloseAccountNav} onClose={this.props.handleCloseAccountNav}
/> />
</li> </li>
] : [ ] : [
@ -286,16 +186,13 @@ class Navigation extends React.Component {
> >
<a <a
href="#" href="#"
onClick={this.handleJoinClick} onClick={this.props.handleOpenRegistration}
> >
<FormattedMessage id="general.joinScratch" /> <FormattedMessage id="general.joinScratch" />
</a> </a>
</li>, </li>,
<Registration <Registration
isOpen={this.state.registrationOpen}
key="registration" key="registration"
onRegistrationDone={this.handleCompleteRegistration}
onRequestClose={this.handleCloseRegistration}
/>, />,
<li <li
className="link right login-item" className="link right login-item"
@ -305,54 +202,31 @@ class Navigation extends React.Component {
className="ignore-react-onclickoutside" className="ignore-react-onclickoutside"
href="#" href="#"
key="login-link" key="login-link"
onClick={this.handleLoginClick} onClick={this.props.handleToggleLoginOpen}
> >
<FormattedMessage id="general.signIn" /> <FormattedMessage id="general.signIn" />
</a> </a>
<Dropdown <LoginDropdown
className="login-dropdown with-arrow"
isOpen={this.state.loginOpen}
key="login-dropdown" key="login-dropdown"
onRequestClose={this.handleCloseLogin} />
>
<Login
error={this.state.loginError}
onLogIn={this.handleLogIn}
/>
</Dropdown>
</li> </li>
]) : []} ]) : []}
</ul> </ul>
<Modal <CanceledDeletionModal />
isOpen={this.state.canceledDeletionOpen}
style={{
content: {
padding: 15
}
}}
onRequestClose={this.handleCloseCanceledDeletion}
>
<h4>Your Account Will Not Be Deleted</h4>
<h4><FormattedMessage id="general.noDeletionTitle" /></h4>
<p>
<FormattedMessage
id="general.noDeletionDescription"
values={{
resetLink: <a href="/accounts/password_reset/">
{this.props.intl.formatMessage({id: 'general.noDeletionLink'})}
</a>
}}
/>
</p>
</Modal>
</NavigationBox> </NavigationBox>
); );
} }
} }
Navigation.propTypes = { Navigation.propTypes = {
dispatch: PropTypes.func, accountNavOpen: PropTypes.bool,
closeAccountMenus: PropTypes.func,
getMessageCount: PropTypes.func, getMessageCount: PropTypes.func,
handleCloseAccountNav: PropTypes.func,
handleLogOut: PropTypes.func,
handleOpenRegistration: PropTypes.func,
handleToggleAccountNav: PropTypes.func,
handleToggleLoginOpen: PropTypes.func,
intl: intlShape, intl: intlShape,
permissions: PropTypes.shape({ permissions: PropTypes.shape({
admin: PropTypes.bool, admin: PropTypes.bool,
@ -361,20 +235,17 @@ Navigation.propTypes = {
educator_invitee: PropTypes.bool, educator_invitee: PropTypes.bool,
student: PropTypes.bool student: PropTypes.bool
}), }),
refreshSession: PropTypes.func,
searchTerm: PropTypes.string, searchTerm: PropTypes.string,
session: PropTypes.shape({ session: PropTypes.shape({
session: PropTypes.shape({
user: PropTypes.shape({
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
thumbnailUrl: PropTypes.string,
username: PropTypes.string
})
}),
status: PropTypes.string status: PropTypes.string
}), }),
setMessageCount: PropTypes.func, setMessageCount: PropTypes.func,
unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
user: PropTypes.shape({
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
thumbnailUrl: PropTypes.string,
username: PropTypes.string
})
}; };
Navigation.defaultProps = { Navigation.defaultProps = {
@ -384,18 +255,39 @@ Navigation.defaultProps = {
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
accountNavOpen: state.navigation && state.navigation.accountNavOpen,
session: state.session, session: state.session,
permissions: state.permissions, permissions: state.permissions,
searchTerm: state.navigation.searchTerm,
unreadMessageCount: state.messageCount.messageCount, unreadMessageCount: state.messageCount.messageCount,
searchTerm: state.navigation user: state.session && state.session.session && state.session.session.user
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
closeAccountMenus: () => {
dispatch(navigationActions.closeAccountMenus());
},
getMessageCount: username => { getMessageCount: username => {
dispatch(messageCountActions.getCount(username)); dispatch(messageCountActions.getCount(username));
}, },
refreshSession: () => { handleToggleAccountNav: event => {
dispatch(sessionActions.refreshSession()); 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 => { setMessageCount: newCount => {
dispatch(messageCountActions.setCount(newCount)); dispatch(messageCountActions.setCount(newCount));

View file

@ -163,25 +163,6 @@
background-image: url("/images/mystuff.png"); background-image: url("/images/mystuff.png");
} }
} }
.login-dropdown {
width: 200px;
.button {
padding: .75em;
}
}
.dropdown {
.row {
margin-bottom: 1.25rem;
input {
margin: 0;
height: 2.25rem;
}
}
}
} }
//4 columns //4 columns

View file

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

View file

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

View file

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

View file

@ -8,7 +8,6 @@ const connect = require('react-redux').connect;
const injectIntl = require('react-intl').injectIntl; const injectIntl = require('react-intl').injectIntl;
const parser = require('scratch-parser'); const parser = require('scratch-parser');
const Page = require('../../components/page/www/page.jsx'); const Page = require('../../components/page/www/page.jsx');
const api = require('../../lib/api');
const render = require('../../lib/render.jsx'); const render = require('../../lib/render.jsx');
const storage = require('../../lib/storage.js').default; const storage = require('../../lib/storage.js').default;
const log = require('../../lib/log'); const log = require('../../lib/log');
@ -16,8 +15,12 @@ const EXTENSION_INFO = require('../../lib/extensions.js').default;
const PreviewPresentation = require('./presentation.jsx'); const PreviewPresentation = require('./presentation.jsx');
const projectShape = require('./projectshape.jsx').projectShape; const projectShape = require('./projectshape.jsx').projectShape;
const Registration = require('../../components/registration/registration.jsx');
const ConnectedLogin = require('../../components/login/connected-login.jsx');
const CanceledDeletionModal = require('../../components/login/canceled-deletion-modal.jsx');
const sessionActions = require('../../redux/session.js'); const sessionActions = require('../../redux/session.js');
const navigationActions = require('../../redux/navigation.js');
const previewActions = require('../../redux/preview.js'); const previewActions = require('../../redux/preview.js');
const GUI = require('scratch-gui'); const GUI = require('scratch-gui');
@ -31,10 +34,7 @@ class Preview extends React.Component {
'handleToggleStudio', 'handleToggleStudio',
'handleFavoriteToggle', 'handleFavoriteToggle',
'handleLoadMore', 'handleLoadMore',
// temporary, to pass to GUI. Remove when nav bar components are shared between www and gui.
'handleLogout',
'handleLoveToggle', 'handleLoveToggle',
'handlePermissions',
'handlePopState', 'handlePopState',
'handleReportClick', 'handleReportClick',
'handleReportClose', 'handleReportClose',
@ -45,9 +45,8 @@ class Preview extends React.Component {
'handleUpdateProjectTitle', 'handleUpdateProjectTitle',
'handleUpdate', 'handleUpdate',
'initCounts', 'initCounts',
'isShared',
'pushHistory', 'pushHistory',
'userOwnsProject' 'renderLogin'
]); ]);
const pathname = window.location.pathname.toLowerCase(); const pathname = window.location.pathname.toLowerCase();
const parts = pathname.split('/').filter(Boolean); const parts = pathname.split('/').filter(Boolean);
@ -55,7 +54,6 @@ class Preview extends React.Component {
// parts[1]: either :id or 'editor' // parts[1]: either :id or 'editor'
// parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen' // parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen'
this.state = { this.state = {
editable: false,
extensions: [], extensions: [],
favoriteCount: 0, favoriteCount: 0,
loveCount: 0, loveCount: 0,
@ -90,7 +88,6 @@ class Preview extends React.Component {
if (this.props.projectInfo.id !== prevProps.projectInfo.id) { if (this.props.projectInfo.id !== prevProps.projectInfo.id) {
this.getExtensions(this.state.projectId); this.getExtensions(this.state.projectId);
this.initCounts(this.props.projectInfo.stats.favorites, this.props.projectInfo.stats.loves); this.initCounts(this.props.projectInfo.stats.favorites, this.props.projectInfo.stats.loves);
this.handlePermissions();
if (this.props.projectInfo.remix.parent !== null) { if (this.props.projectInfo.remix.parent !== null) {
this.props.getParentInfo(this.props.projectInfo.remix.parent); this.props.getParentInfo(this.props.projectInfo.remix.parent);
} }
@ -145,21 +142,6 @@ class Preview extends React.Component {
}); });
}); });
} }
// Temporarily duplicated this function from navigation.jsx here.
// Should move handling of login/logout into session.js, and handle them
// from here as well as navigation.jsx.
handleLogout (e) {
e.preventDefault();
api({
host: '',
method: 'post',
uri: '/accounts/logout/',
useCsrf: true
}, err => {
if (err) log.error(err);
window.location = '/';
});
}
handleReportClick () { handleReportClick () {
this.setState({reportOpen: true}); this.setState({reportOpen: true});
} }
@ -261,12 +243,6 @@ class Preview extends React.Component {
})); }));
} }
} }
handlePermissions () {
// TODO: handle admins and mods
if (this.props.projectInfo.author.username === this.props.user.username) {
this.setState({editable: true});
}
}
handleSeeInside () { handleSeeInside () {
this.props.setPlayer(false); this.props.setPlayer(false);
} }
@ -289,26 +265,21 @@ class Preview extends React.Component {
loveCount: loves loveCount: loves
}); });
} }
isShared () { renderLogin ({onClose}) {
return ( return (
// if we don't have projectInfo assume shared until we know otherwise <ConnectedLogin
Object.keys(this.props.projectInfo).length === 0 || ( key="login-dropdown-presentation"
this.props.projectInfo.history && /* eslint-disable react/jsx-no-bind */
this.props.projectInfo.history.shared.length > 0 onLogIn={(formData, callback) => {
) this.props.handleLogIn(formData, result => {
); if (result.success === true) {
} onClose();
isLoggedIn () { }
return ( callback(result);
this.props.sessionStatus === sessionActions.Status.FETCHED && });
Object.keys(this.props.user).length > 0 }}
); /* eslint-ensable react/jsx-no-bind */
} />
userOwnsProject () {
return (
this.isLoggedIn() &&
Object.keys(this.props.projectInfo).length > 0 &&
this.props.user.id === this.props.projectInfo.author.id
); );
} }
render () { render () {
@ -320,13 +291,13 @@ class Preview extends React.Component {
assetHost={this.props.assetHost} assetHost={this.props.assetHost}
backpackOptions={this.props.backpackOptions} backpackOptions={this.props.backpackOptions}
comments={this.props.comments} comments={this.props.comments}
editable={this.state.editable} editable={this.props.isEditable}
extensions={this.state.extensions} extensions={this.state.extensions}
faved={this.props.faved} faved={this.props.faved}
favoriteCount={this.state.favoriteCount} favoriteCount={this.state.favoriteCount}
isFullScreen={this.state.isFullScreen} isFullScreen={this.state.isFullScreen}
isLoggedIn={this.isLoggedIn()} isLoggedIn={this.props.isLoggedIn}
isShared={this.isShared()} isShared={this.props.isShared}
loveCount={this.state.loveCount} loveCount={this.state.loveCount}
loved={this.props.loved} loved={this.props.loved}
originalInfo={this.props.original} originalInfo={this.props.original}
@ -339,7 +310,7 @@ class Preview extends React.Component {
replies={this.props.replies} replies={this.props.replies}
reportOpen={this.state.reportOpen} reportOpen={this.state.reportOpen}
studios={this.props.studios} studios={this.props.studios}
userOwnsProject={this.userOwnsProject()} userOwnsProject={this.props.userOwnsProject}
onAddToStudioClicked={this.handleAddToStudioClick} onAddToStudioClicked={this.handleAddToStudioClick}
onAddToStudioClosed={this.handleAddToStudioClose} onAddToStudioClosed={this.handleAddToStudioClose}
onFavoriteClicked={this.handleFavoriteToggle} onFavoriteClicked={this.handleFavoriteToggle}
@ -353,19 +324,27 @@ class Preview extends React.Component {
onUpdate={this.handleUpdate} onUpdate={this.handleUpdate}
/> />
</Page> : </Page> :
<IntlGUI <React.Fragment>
enableCommunity <IntlGUI
hideIntro enableCommunity
assetHost={this.props.assetHost} hideIntro
backpackOptions={this.props.backpackOptions} assetHost={this.props.assetHost}
basePath="/" backpackOptions={this.props.backpackOptions}
className="gui" basePath="/"
projectHost={this.props.projectHost} className="gui"
projectId={this.state.projectId} projectHost={this.props.projectHost}
projectTitle={this.props.projectInfo.title} projectId={this.state.projectId}
onClickLogout={this.handleLogout} projectTitle={this.props.projectInfo.title}
onUpdateProjectTitle={this.handleUpdateProjectTitle} renderLogin={this.renderLogin}
/> onLogOut={this.props.handleLogOut}
onOpenRegistration={this.props.handleOpenRegistration}
onToggleLoginOpen={this.props.handleToggleLoginOpen}
onUpdateProjectTitle={this.handleUpdateProjectTitle}
/>
<Registration />
<CanceledDeletionModal />
</React.Fragment>
); );
} }
} }
@ -388,6 +367,13 @@ Preview.propTypes = {
getProjectStudios: PropTypes.func.isRequired, getProjectStudios: PropTypes.func.isRequired,
getRemixes: PropTypes.func.isRequired, getRemixes: PropTypes.func.isRequired,
getTopLevelComments: PropTypes.func.isRequired, getTopLevelComments: PropTypes.func.isRequired,
handleLogIn: PropTypes.func,
handleLogOut: PropTypes.func,
handleOpenRegistration: PropTypes.func,
handleToggleLoginOpen: PropTypes.func,
isEditable: PropTypes.bool,
isLoggedIn: PropTypes.bool,
isShared: PropTypes.bool,
loved: PropTypes.bool, loved: PropTypes.bool,
original: projectShape, original: projectShape,
parent: projectShape, parent: projectShape,
@ -415,7 +401,8 @@ Preview.propTypes = {
dateJoined: PropTypes.string, dateJoined: PropTypes.string,
email: PropTypes.string, email: PropTypes.string,
classroomId: PropTypes.string classroomId: PropTypes.string
}) }),
userOwnsProject: PropTypes.bool
}; };
Preview.defaultProps = { Preview.defaultProps = {
@ -463,26 +450,64 @@ const consolidateStudiosInfo = (curatedStudios, projectStudios, currentStudioIds
return consolidatedStudios; return consolidatedStudios;
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => {
comments: state.preview.comments, const projectInfoPresent = Object.keys(state.preview.projectInfo).length > 0;
faved: state.preview.faved, const userPresent = state.session.session.user &&
loved: state.preview.loved, Object.keys(state.session.session.user).length > 0;
original: state.preview.original, const isLoggedIn = state.session.status === sessionActions.Status.FETCHED &&
parent: state.preview.parent, userPresent;
remixes: state.preview.remixes, const authorPresent = projectInfoPresent && state.preview.projectInfo.author &&
replies: state.preview.replies, Object.keys(state.preview.projectInfo.author).length > 0;
sessionStatus: state.session.status,
projectInfo: state.preview.projectInfo, return {
projectStudios: state.preview.projectStudios, comments: state.preview.comments,
studios: consolidateStudiosInfo(state.preview.curatedStudios, faved: state.preview.faved,
state.preview.projectStudios, state.preview.currentStudioIds, fullScreen: state.scratchGui.mode.isFullScreen,
state.preview.status.studioRequests), // project is editable iff logged in user is the author of the project, or
user: state.session.session.user, // logged in user is an admin.
playerMode: state.scratchGui.mode.isPlayerOnly, isEditable: isLoggedIn &&
fullScreen: state.scratchGui.mode.isFullScreen ((authorPresent && state.preview.projectInfo.author.username === state.session.session.user.username) ||
}); state.permissions.admin === true),
isLoggedIn: isLoggedIn,
// if we don't have projectInfo, assume it's shared until we know otherwise
isShared: !projectInfoPresent || (
state.preview.projectInfo.history &&
state.preview.projectInfo.history.shared &&
state.preview.projectInfo.history.shared.length > 0),
loved: state.preview.loved,
original: state.preview.original,
parent: state.preview.parent,
playerMode: state.scratchGui.mode.isPlayerOnly,
projectInfo: state.preview.projectInfo,
projectStudios: state.preview.projectStudios,
remixes: state.preview.remixes,
replies: state.preview.replies,
sessionStatus: state.session.status, // check if used
studios: consolidateStudiosInfo(state.preview.curatedStudios,
state.preview.projectStudios, state.preview.currentStudioIds,
state.preview.status.studioRequests),
user: state.session.session.user,
userOwnsProject: isLoggedIn && authorPresent &&
state.session.session.user.id === state.preview.projectInfo.author.id
};
};
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
handleOpenRegistration: event => {
event.preventDefault();
dispatch(navigationActions.setRegistrationOpen(true));
},
handleLogIn: (formData, callback) => {
dispatch(navigationActions.handleLogIn(formData, callback));
},
handleLogOut: event => {
event.preventDefault();
dispatch(navigationActions.handleLogOut());
},
handleToggleLoginOpen: event => {
event.preventDefault();
dispatch(navigationActions.toggleLoginOpen());
},
getOriginalInfo: id => { getOriginalInfo: id => {
dispatch(previewActions.getOriginalInfo(id)); dispatch(previewActions.getOriginalInfo(id));
}, },
@ -523,9 +548,6 @@ const mapDispatchToProps = dispatch => ({
setLovedStatus: (loved, id, username, token) => { setLovedStatus: (loved, id, username, token) => {
dispatch(previewActions.setLovedStatus(loved, id, username, token)); dispatch(previewActions.setLovedStatus(loved, id, username, token));
}, },
refreshSession: () => {
dispatch(sessionActions.refreshSession());
},
reportProject: (id, formData) => { reportProject: (id, formData) => {
dispatch(previewActions.reportProject(id, formData)); dispatch(previewActions.reportProject(id, formData));
}, },

View file

@ -232,7 +232,7 @@ Search.propTypes = {
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
searchTerm: state.navigation searchTerm: state.navigation.searchTerm
}); });
const WrappedSearch = injectIntl(Search); const WrappedSearch = injectIntl(Search);

View file

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