Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions src/client/components/app/components/ProtectedRoute.jsx
Original file line number Diff line number Diff line change
@@ -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 ? <Route {...props} /> : <Redirect to="/login" />
}

DumbProtectedRoute.propTypes = {
hasAuth: PropTypes.bool.isRequired
}

export default connect(mapStateToProps)(DumbProtectedRoute)
18 changes: 18 additions & 0 deletions src/client/components/app/components/ProtectedRoute.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'
import { shallow } from 'enzyme'
import { shallowToJson } from 'enzyme-to-json'
import { DumbProtectedRoute } from './ProtectedRoute'

describe('<ProtectedRoute />', () => {
it('renders a redirect when hasAuth is false', () => {
const wrapper = shallow(
<DumbProtectedRoute hasAuth={false} exact path="/" render={() => null} />
)
expect(shallowToJson(wrapper)).toMatchSnapshot()
})

it('renders the route when hasAuth is true', () => {
const wrapper = shallow(<DumbProtectedRoute hasAuth exact path="/" render={() => null} />)
expect(shallowToJson(wrapper)).toMatchSnapshot()
})
})
44 changes: 22 additions & 22 deletions src/client/components/app/components/Routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
<Header />
<Switch>
<Route exact path="/" component={Home} />
const Routes = () => (
<div>
<Header />
<Switch>
{/* Login */}
<Route exact path="/login" component={Login} />
<Route path="/login/forgot-password" component={ForgotPassword} />
<Route path="/login/reset-password" component={ResetPassword} />

{/* Login */}
<Route exact path="/login" component={Login} />
<Route exact path="/login/forgot-password" component={ForgotPassword} />
<Route path="/login/reset-password" component={ResetPassword} />
{/* Signup */}
<Route exact path="/signup" component={Signup} />
<Route path="/signup/confirm" component={SignupConfirmContainer} />

{/* Signup */}
<Route exact path="/signup" component={Signup} />
<Route path="/signup/confirm" component={SignupConfirmContainer} />
{/* Home */}
<ProtectedRoute exact path="/" component={Home} />

{/* Profile */}
<Route path="/profile" component={Profile} />
{/* Profile */}
<ProtectedRoute path="/profile" component={Profile} />

{/* 404 */}
<Route component={Missing} />
</Switch>
<NotificationsContainer />
</div>
)
}
{/* 404 */}
<Route component={Missing} />
</Switch>
<NotificationsContainer />
</div>
)

export default Routes
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<ProtectedRoute /> renders a redirect when hasAuth is false 1`] = `
<Redirect
push={false}
to="/login"
/>
`;

exports[`<ProtectedRoute /> renders the route when hasAuth is true 1`] = `
<Route
exact={true}
path="/"
render={[Function]}
/>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,15 @@

exports[`<Routes /> renders correctly 1`] = `
<div>
<Header />
<Connect(withRouter()) />
<Switch>
<Route
component={[Function]}
exact={true}
path="/"
/>
<Route
component={[Function]}
exact={true}
path="/login"
/>
<Route
component={[Function]}
exact={true}
path="/login/forgot-password"
/>
<Route
Expand All @@ -32,7 +26,12 @@ exports[`<Routes /> renders correctly 1`] = `
component={[Function]}
path="/signup/confirm"
/>
<Route
<Connect(DumbProtectedRoute)
component={[Function]}
exact={true}
path="/"
/>
<Connect(DumbProtectedRoute)
component={[Function]}
path="/profile"
/>
Expand Down
41 changes: 34 additions & 7 deletions src/client/components/header/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Wrapper>
<Logo />
</Wrapper>
)
const mapStateToProps = state => ({
hasAuth: selectors.hasToken(state)
})

export const DumbHeader = ({ hasAuth, history }) => (
<Wrapper>
<Logo />
{hasAuth
? <ProfileWidget />
: <RaisedButton
label="Inloggen"
labelColor={colors.white}
backgroundColor={colors.green}
style={{ backgroundColor: 'none' }}
onTouchTap={() => {
history.push('/login')
}}
/>}
</Wrapper>
)

DumbHeader.propTypes = {
hasAuth: PropTypes.bool.isRequired,
history: PropTypes.object.isRequired
}

export default Header
export default connect(mapStateToProps)(withRouter(DumbHeader))
21 changes: 17 additions & 4 deletions src/client/components/header/components/Header.test.jsx
Original file line number Diff line number Diff line change
@@ -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('<Header />', () => {
it('renders correctly', () => {
const wrapper = shallow(<Header />)
describe('<DumbHeader />', () => {
it('renders with login button when we are not authenticated', () => {
const wrapper = shallow(<DumbHeader hasAuth={false} history={{}} />)
expect(shallowToJson(wrapper)).toMatchSnapshot()
})

it('renders with a profile widget when we are authenticated', () => {
const wrapper = shallow(<DumbHeader hasAuth history={{}} />)
expect(shallowToJson(wrapper)).toMatchSnapshot()
})

it('navigates to the login page when clicking the login button', () => {
const push = jest.fn()
const wrapper = shallow(<DumbHeader hasAuth={false} history={{ push }} />)
wrapper.find(RaisedButton).simulate('click')
expect(push).toBeCalledWith('/login')
})
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Header /> renders correctly 1`] = `
exports[`<DumbHeader /> renders with a profile widget when we are authenticated 1`] = `
<styled.header>
<Logo />
<Connect(withRouter(DumbProfileWidget)) />
</styled.header>
`;

exports[`<DumbHeader /> renders with login button when we are not authenticated 1`] = `
<styled.header>
<Logo />
<RaisedButton
backgroundColor="#27AE60"
disabled={false}
fullWidth={false}
label="Inloggen"
labelColor="#FFFFFF"
labelPosition="after"
onTouchTap={[Function]}
primary={false}
secondary={false}
style={
Object {
"backgroundColor": "none",
}
}
/>
</styled.header>
`;
87 changes: 87 additions & 0 deletions src/client/components/profile/components/ProfileWidget.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<IconMenu
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
targetOrigin={{ horizontal: 'right', vertical: 'top' }}
iconButtonElement={
<div>
{/* Note: must be wrapped in a div, otherwise click events are not properly attached */}
<ProfileWidgetContent>
<Avatar size={35} src="http://i.pravatar.cc/150?u=a042581f4e290267014f" />
<ProfileName>{profile.name}</ProfileName>
<HardwareKeyboardArrowDown
color={colors.white}
style={{ marginLeft: '2px', marginTop: '2px' }}
/>
</ProfileWidgetContent>
</div>
}
>
<MenuItem
leftIcon={<ActionSettings />}
primaryText="Instellingen"
onTouchTap={() => {
this.props.history.push('/settings')
}}
/>
<MenuItem
leftIcon={<ActionExitToApp />}
primaryText="Uitloggen"
onTouchTap={() => {
this.props.logout()
this.props.history.push('/login')
}}
/>
</IconMenu>
)
}
}

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))
20 changes: 20 additions & 0 deletions src/client/components/profile/components/ProfileWidget.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react'
import { shallow } from 'enzyme'
import { shallowToJson } from 'enzyme-to-json'
import { DumbProfileWidget } from './ProfileWidget'

describe('<DumbProfileWidget />', () => {
it('renders correctly', () => {
const wrapper = shallow(
<DumbProfileWidget
history={{}}
profile={{
name: 'John Doe'
}}
fetchProfile={() => {}}
logout={() => {}}
/>
)
expect(shallowToJson(wrapper)).toMatchSnapshot()
})
})
Loading