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'