diff --git a/.eslintrc b/.eslintrc index 17145bb..4192336 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,7 @@ ], "rules": { "arrow-parens": 0, + "no-confusing-arrow": 0, "comma-dangle": 0, "func-names": ["error", "as-needed"], "import/no-extraneous-dependencies": 0, diff --git a/package-lock.json b/package-lock.json index 585e202..6bb3426 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5876,6 +5876,23 @@ "resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.4.5.tgz", "integrity": "sha1-4+iVoJcM8U7o+JAROvaBl6vz0LE=" }, + "react-icon-base": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/react-icon-base/-/react-icon-base-2.0.7.tgz", + "integrity": "sha1-C9GHNr1s55ym1pzoOHoH+41M7/4=", + "dependencies": { + "prop-types": { + "version": "15.5.8", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.8.tgz", + "integrity": "sha1-a3suFBCDvjjIWVqlH8VXdccZk5Q=" + } + } + }, + "react-icons": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-2.2.5.tgz", + "integrity": "sha1-+UJQHCGkzARWziu+5QMsk/YFHc8=" + }, "react-notification-system": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/react-notification-system/-/react-notification-system-0.2.14.tgz", diff --git a/src/client/components/app/components/ProtectedRoute.jsx b/src/client/components/app/components/ProtectedRoute.jsx new file mode 100644 index 0000000..035a517 --- /dev/null +++ b/src/client/components/app/components/ProtectedRoute.jsx @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import React from 'react' +import { Route, Redirect } from 'react-router-dom' +import { selectors } from '../../../data/session' + +const mapStateToProps = state => ({ + hasAuth: selectors.hasToken(state) +}) + +export function DumbProtectedRoute({ hasAuth, ...props }) { + return hasAuth ? : +} + +DumbProtectedRoute.propTypes = { + hasAuth: PropTypes.bool.isRequired +} + +export default connect(mapStateToProps)(DumbProtectedRoute) diff --git a/src/client/components/app/components/ProtectedRoute.test.jsx b/src/client/components/app/components/ProtectedRoute.test.jsx new file mode 100644 index 0000000..bdcb6ae --- /dev/null +++ b/src/client/components/app/components/ProtectedRoute.test.jsx @@ -0,0 +1,18 @@ +import React from 'react' +import { shallow } from 'enzyme' +import { shallowToJson } from 'enzyme-to-json' +import { DumbProtectedRoute } from './ProtectedRoute' + +describe('', () => { + it('renders a redirect when hasAuth is false', () => { + const wrapper = shallow( + null} /> + ) + expect(shallowToJson(wrapper)).toMatchSnapshot() + }) + + it('renders the route when hasAuth is true', () => { + const wrapper = shallow( null} />) + expect(shallowToJson(wrapper)).toMatchSnapshot() + }) +}) diff --git a/src/client/components/app/components/Routes.jsx b/src/client/components/app/components/Routes.jsx index 746104d..0b226a4 100644 --- a/src/client/components/app/components/Routes.jsx +++ b/src/client/components/app/components/Routes.jsx @@ -10,32 +10,32 @@ import Signup from '../../../scenes/Signup' import SignupConfirmContainer from '../../../scenes/SignupConfirm' import ResetPassword from '../../../scenes/ResetPassword' import Missing from '../../../scenes/Missing' +import ProtectedRoute from './ProtectedRoute' -function Routes() { - return ( -
-
- - +const Routes = () => ( +
+
+ + {/* Login */} + + + - {/* Login */} - - - + {/* Signup */} + + - {/* Signup */} - - + {/* Home */} + - {/* Profile */} - + {/* Profile */} + - {/* 404 */} - - - -
- ) -} + {/* 404 */} + +
+ +
+) export default Routes diff --git a/src/client/components/app/components/__snapshots__/ProtectedRoute.test.jsx.snap b/src/client/components/app/components/__snapshots__/ProtectedRoute.test.jsx.snap new file mode 100644 index 0000000..2b386ee --- /dev/null +++ b/src/client/components/app/components/__snapshots__/ProtectedRoute.test.jsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders a redirect when hasAuth is false 1`] = ` + +`; + +exports[` renders the route when hasAuth is true 1`] = ` + +`; diff --git a/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap b/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap index 84afe11..944f51e 100644 --- a/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap +++ b/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap @@ -2,13 +2,8 @@ exports[` renders correctly 1`] = `
-
+ - renders correctly 1`] = ` /> renders correctly 1`] = ` component={[Function]} path="/signup/confirm" /> - + diff --git a/src/client/components/header/components/Header.jsx b/src/client/components/header/components/Header.jsx index 44f97a0..5faa560 100644 --- a/src/client/components/header/components/Header.jsx +++ b/src/client/components/header/components/Header.jsx @@ -1,13 +1,40 @@ +/* eslint-disable react/jsx-indent-props, react/jsx-closing-bracket-location */ + +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { withRouter } from 'react-router' import React from 'react' +import { RaisedButton } from 'material-ui' +import { colors } from '../../../styles' +import { selectors } from '../../../data/session' +import { ProfileWidget } from '../../profile' import Wrapper from './Wrapper' import Logo from './Logo' -function Header() { - return ( - - - - ) +const mapStateToProps = state => ({ + hasAuth: selectors.hasToken(state) +}) + +export const DumbHeader = ({ hasAuth, history }) => ( + + + {hasAuth + ? + : { + history.push('/login') + }} + />} + +) + +DumbHeader.propTypes = { + hasAuth: PropTypes.bool.isRequired, + history: PropTypes.object.isRequired } -export default Header +export default connect(mapStateToProps)(withRouter(DumbHeader)) diff --git a/src/client/components/header/components/Header.test.jsx b/src/client/components/header/components/Header.test.jsx index 598aa73..7802e86 100644 --- a/src/client/components/header/components/Header.test.jsx +++ b/src/client/components/header/components/Header.test.jsx @@ -1,11 +1,24 @@ import React from 'react' import { shallow } from 'enzyme' import { shallowToJson } from 'enzyme-to-json' -import Header from './Header' +import { RaisedButton } from 'material-ui' +import { DumbHeader } from './Header' -describe('
', () => { - it('renders correctly', () => { - const wrapper = shallow(
) +describe('', () => { + it('renders with login button when we are not authenticated', () => { + const wrapper = shallow() expect(shallowToJson(wrapper)).toMatchSnapshot() }) + + it('renders with a profile widget when we are authenticated', () => { + const wrapper = shallow() + expect(shallowToJson(wrapper)).toMatchSnapshot() + }) + + it('navigates to the login page when clicking the login button', () => { + const push = jest.fn() + const wrapper = shallow() + wrapper.find(RaisedButton).simulate('click') + expect(push).toBeCalledWith('/login') + }) }) diff --git a/src/client/components/header/components/__snapshots__/Header.test.jsx.snap b/src/client/components/header/components/__snapshots__/Header.test.jsx.snap index 91a32ab..50e8242 100644 --- a/src/client/components/header/components/__snapshots__/Header.test.jsx.snap +++ b/src/client/components/header/components/__snapshots__/Header.test.jsx.snap @@ -1,7 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`
renders correctly 1`] = ` +exports[` renders with a profile widget when we are authenticated 1`] = ` + + +`; + +exports[` renders with login button when we are not authenticated 1`] = ` + + + `; diff --git a/src/client/components/profile/components/ProfileWidget.jsx b/src/client/components/profile/components/ProfileWidget.jsx new file mode 100644 index 0000000..054f26e --- /dev/null +++ b/src/client/components/profile/components/ProfileWidget.jsx @@ -0,0 +1,87 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { connect } from 'react-redux' +import { withRouter } from 'react-router' +import { IconMenu, MenuItem, Avatar } from 'material-ui' +import { HardwareKeyboardArrowDown, ActionSettings, ActionExitToApp } from 'material-ui/svg-icons' +import styled from 'styled-components' +import { colors } from '../../../styles' +import { fetchProfile } from '../actions' +import { logout } from '../../../data/session/actions' +import { getProfile } from '../selectors' + +const mapStateToProps = state => ({ profile: getProfile(state) }) +const mapStateToDispatch = dispatch => ({ + fetchProfile: () => dispatch(fetchProfile()), + logout: () => dispatch(logout()) +}) + +const ProfileName = styled.span` + color: ${colors.white}; + font-size: 0.8rem; + font-weight: 200; + margin-left: 15px; +` + +const ProfileWidgetContent = styled.div` + & * { + vertical-align: middle; + } + &:hover { + cursor: pointer; + } +` + +export class DumbProfileWidget extends React.Component { + componentWillMount() { + this.props.fetchProfile() + } + + render() { + const { profile } = this.props + return ( + + {/* Note: must be wrapped in a div, otherwise click events are not properly attached */} + + + {profile.name} + + +
+ } + > + } + primaryText="Instellingen" + onTouchTap={() => { + this.props.history.push('/settings') + }} + /> + } + primaryText="Uitloggen" + onTouchTap={() => { + this.props.logout() + this.props.history.push('/login') + }} + /> + + ) + } +} + +DumbProfileWidget.propTypes = { + history: PropTypes.object.isRequired, + profile: PropTypes.object.isRequired, + fetchProfile: PropTypes.func.isRequired, + logout: PropTypes.func.isRequired +} + +export default connect(mapStateToProps, mapStateToDispatch)(withRouter(DumbProfileWidget)) diff --git a/src/client/components/profile/components/ProfileWidget.test.jsx b/src/client/components/profile/components/ProfileWidget.test.jsx new file mode 100644 index 0000000..2e7593a --- /dev/null +++ b/src/client/components/profile/components/ProfileWidget.test.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import { shallow } from 'enzyme' +import { shallowToJson } from 'enzyme-to-json' +import { DumbProfileWidget } from './ProfileWidget' + +describe('', () => { + it('renders correctly', () => { + const wrapper = shallow( + {}} + logout={() => {}} + /> + ) + expect(shallowToJson(wrapper)).toMatchSnapshot() + }) +}) diff --git a/src/client/components/profile/components/__snapshots__/ProfileWidget.test.jsx.snap b/src/client/components/profile/components/__snapshots__/ProfileWidget.test.jsx.snap new file mode 100644 index 0000000..5ccd36c --- /dev/null +++ b/src/client/components/profile/components/__snapshots__/ProfileWidget.test.jsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` + + + + + John Doe + + + + + } + multiple={false} + onItemTouchTap={[Function]} + onKeyboardFocus={[Function]} + onMouseDown={[Function]} + onMouseEnter={[Function]} + onMouseLeave={[Function]} + onMouseUp={[Function]} + onRequestChange={[Function]} + onTouchTap={[Function]} + open={null} + targetOrigin={ + Object { + "horizontal": "right", + "vertical": "top", + } + } + touchTapCloseDelay={200} + useLayerForClickAway={false} +> + } + onTouchTap={[Function]} + primaryText="Instellingen" + targetOrigin={ + Object { + "horizontal": "left", + "vertical": "top", + } + } + /> + } + onTouchTap={[Function]} + primaryText="Uitloggen" + targetOrigin={ + Object { + "horizontal": "left", + "vertical": "top", + } + } + /> + +`; diff --git a/src/client/components/profile/index.js b/src/client/components/profile/index.js index 36b5837..b315997 100644 --- a/src/client/components/profile/index.js +++ b/src/client/components/profile/index.js @@ -4,4 +4,5 @@ import * as actionTypes from './actionTypes' export { actionTypes, sagas } export { default as ProfileForm } from './components/ProfileForm' export { default as PasswordForm } from './components/PasswordForm' +export { default as ProfileWidget } from './components/ProfileWidget' export { default as reducer } from './reducer'