Skip to content

Commit

Permalink
Feature(components): fetch user after page load
Browse files Browse the repository at this point in the history
This makes it easier to serve whole site statically in the future
Feature(redux): Move user state into entities
  • Loading branch information
Berkeley Martinez authored and Berkeley Martinez committed Jul 29, 2016
1 parent d9e9af0 commit 0c07e96
Show file tree
Hide file tree
Showing 12 changed files with 214 additions and 121 deletions.
2 changes: 1 addition & 1 deletion client/sagas/hard-go-to-saga.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function hardGoToSaga(action$, getState, { history }) {
return action$
.filter(({ type }) => type === hardGoTo)
.map(({ payload = '/settings' }) => {
history.push(history.state, null, payload);
history.pushState(history.state, null, payload);
return null;
});
}
48 changes: 22 additions & 26 deletions common/app/App.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { PropTypes } from 'react';
import { Button, Row } from 'react-bootstrap';
import { ToastMessage, ToastContainer } from 'react-toastr';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { contain } from 'redux-epic';
import { createSelector } from 'reselect';

import MapDrawer from './components/Map-Drawer.jsx';
Expand All @@ -19,26 +17,27 @@ import { submitChallenge } from './routes/challenges/redux/actions';

import Nav from './components/Nav';
import { randomCompliment } from './utils/get-words';
import { userSelector } from './redux/selectors';

const toastMessageFactory = React.createFactory(ToastMessage.animation);

const mapStateToProps = createSelector(
state => state.app.username,
state => state.app.points,
state => state.app.picture,
userSelector,
state => state.app.shouldShowSignIn,
state => state.app.toast,
state => state.app.isMapDrawerOpen,
state => state.app.isMapAlreadyLoaded,
state => state.challengesApp.toast,
(
username,
points,
picture,
{ user: { username, points, picture } },
shouldShowSignIn,
toast,
isMapDrawerOpen,
isMapAlreadyLoaded,
showChallengeComplete
) => ({
shouldShowSignIn,
isSignedIn: !!username,
username,
points,
picture,
Expand All @@ -58,20 +57,14 @@ const bindableActions = {
toggleMainChat
};

const fetchContainerOptions = {
fetchAction: 'fetchUser',
isPrimed({ username }) {
return !!username;
}
};

// export plain class for testing
export class FreeCodeCamp extends React.Component {
static displayName = 'FreeCodeCamp';

static propTypes = {
children: PropTypes.node,
username: PropTypes.string,
isSignedIn: PropTypes.bool,
points: PropTypes.number,
picture: PropTypes.string,
toast: PropTypes.object,
Expand All @@ -82,7 +75,9 @@ export class FreeCodeCamp extends React.Component {
isMapDrawerOpen: PropTypes.bool,
isMapAlreadyLoaded: PropTypes.bool,
toggleMapDrawer: PropTypes.func,
toggleMainChat: PropTypes.func
toggleMainChat: PropTypes.func,
fetchUser: PropTypes.func,
shouldShowSignIn: PropTypes.bool
};

componentWillReceiveProps({
Expand Down Expand Up @@ -119,6 +114,9 @@ export class FreeCodeCamp extends React.Component {

componentDidMount() {
this.props.initWindowHeight();
if (!this.props.isSignedIn) {
this.props.fetchUser();
}
}

renderChallengeComplete() {
Expand All @@ -145,15 +143,17 @@ export class FreeCodeCamp extends React.Component {
isMapDrawerOpen,
isMapAlreadyLoaded,
toggleMapDrawer,
toggleMainChat
toggleMainChat,
shouldShowSignIn
} = this.props;
const navProps = {
username,
points,
picture,
updateNavHeight,
toggleMapDrawer,
toggleMainChat
toggleMainChat,
shouldShowSignIn
};

return (
Expand All @@ -177,11 +177,7 @@ export class FreeCodeCamp extends React.Component {
}
}

const wrapComponent = compose(
// connect Component to Redux Store
connect(mapStateToProps, bindableActions),
// handles prefetching data
contain(fetchContainerOptions)
);

export default wrapComponent(FreeCodeCamp);
export default connect(
mapStateToProps,
bindableActions
)(FreeCodeCamp);
61 changes: 40 additions & 21 deletions common/app/components/Nav/Nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const logoElement = (
<img
alt='learn to code javascript at Free Code Camp logo'
className='img-responsive nav-logo'
src={ fCClogo } />
src={ fCClogo }
/>
</a>
);

Expand All @@ -41,7 +42,8 @@ export default class extends React.Component {
username: PropTypes.string,
updateNavHeight: PropTypes.func,
toggleMapDrawer: PropTypes.func,
toggleMainChat: PropTypes.func
toggleMainChat: PropTypes.func,
shouldShowSignIn: PropTypes.bool
};

componentDidMount() {
Expand All @@ -56,7 +58,7 @@ export default class extends React.Component {
<a
href='#'
onClick={ e => e.preventDefault()}
>
>
Map
</a>
</li>
Expand All @@ -65,7 +67,8 @@ export default class extends React.Component {
return (
<LinkContainer
eventKey={ 1 }
to='/map'>
to='/map'
>
<NavItem
onClick={ e => {
if (!(e.ctrlKey || e.metaKey)) {
Expand All @@ -74,7 +77,7 @@ export default class extends React.Component {
}
}}
target='/map'
>
>
Map
</NavItem>
</LinkContainer>
Expand All @@ -93,7 +96,7 @@ export default class extends React.Component {
}
}}
target='_blank'
>
>
Chat
</NavItem>
);
Expand All @@ -106,9 +109,11 @@ export default class extends React.Component {
<LinkContainer
eventKey={ index + 2 }
key={ content }
to={ link }>
to={ link }
>
<NavItem
target={ target || null }>
target={ target || null }
>
{ content }
</NavItem>
</LinkContainer>
Expand All @@ -119,44 +124,55 @@ export default class extends React.Component {
eventKey={ index + 1 }
href={ link }
key={ content }
target={ target || null }>
target={ target || null }
>
{ content }
</NavItem>
);
});
}

renderPoints(username, points) {
if (!username) {
renderPoints(username, points, shouldShowSignIn) {
if (!username || !shouldShowSignIn) {
return null;
}
return (
<FCCNavItem
className='brownie-points-nav'
href={ '/' + username }>
href={ '/' + username }
key='points'
>
[ { points } ]
</FCCNavItem>
);
}

renderSignin(username, picture) {
renderSignIn(username, picture, shouldShowSignIn) {
if (!shouldShowSignIn) {
return null;
}
if (username) {
return (
<li
className='hidden-xs hidden-sm avatar'
eventKey={ 2 }>
eventKey={ 2 }
key='user'
>
<a href={ '/' + username }>
<img
className='profile-picture float-right'
src={ picture } />
src={ picture }
/>
</a>
</li>
);
} else {
return (
<NavItem
eventKey={ 2 }
href='/signin'>
href='/signin'
key='signin'
>
Sign In
</NavItem>
);
Expand All @@ -169,27 +185,30 @@ export default class extends React.Component {
points,
picture,
toggleMapDrawer,
toggleMainChat
toggleMainChat,
shouldShowSignIn
} = this.props;
const { router } = this.context;
const isOnMap = router.isActive('/map');

return (
<Navbar
className='nav-height'
fixedTop={ true }>
fixedTop={ true }
>
<NavbarBrand>{ logoElement }</NavbarBrand>
<Navbar.Toggle children={ toggleButtonChild } />
<Navbar.Collapse eventKey={ 0 }>
<Nav
className='hamburger-dropdown'
navbar={ true }
pullRight={ true }>
pullRight={ true }
>
{ this.renderMapLink(isOnMap, toggleMapDrawer) }
{ this.renderChat(toggleMainChat) }
{ this.renderLinks() }
{ this.renderPoints(username, points) }
{ this.renderSignin(username, picture) }
{ this.renderPoints(username, points, shouldShowSignIn) }
{ this.renderSignIn(username, picture, shouldShowSignIn) }
</Nav>
</Navbar.Collapse>
</Navbar>
Expand Down
18 changes: 14 additions & 4 deletions common/app/redux/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,21 @@ export const makeToast = createAction(
// used in combination with fetch-user-saga
export const fetchUser = createAction(types.fetchUser);

// setUser(userInfo: Object) => Action
export const setUser = createAction(types.setUser);
// setUser(
// entities: { [userId]: User }
// ) => Action
export const addUser = createAction(
types.addUser,
() => {},
entities => ({ entities })
);
export const updateThisUser = createAction(types.updateThisUser);

// updatePoints(points: Number) => Action
export const updatePoints = createAction(types.updatePoints);
// updateUserPoints(username: String, points: Number) => Action
export const updateUserPoints = createAction(
types.updateUserPoints,
(username, points) => ({ username, points })
);
// used when server needs client to redirect
export const delayedRedirect = createAction(types.delayedRedirect);

Expand Down
18 changes: 16 additions & 2 deletions common/app/redux/entities-reducer.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { updateUserPoints } from './types';

const initialState = {
hike: {},
superBlock: {},
block: {},
challenge: {},
job: {}
user: {}
};

export default function entities(state = initialState, action) {
const { type, payload: { username, points } = {} } = action;
if (type === updateUserPoints) {
return {
...state,
user: {
...state.user,
[username]: {
...state.user[username],
points
}
}
};
}
if (action.meta && action.meta.entities) {
return {
...state,
Expand Down
23 changes: 16 additions & 7 deletions common/app/redux/fetch-user-saga.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { setUser, fetchUser } from './types';
import { createErrorObservable } from './actions';
import { Observable } from 'rx';
import { fetchUser } from './types';
import {
addUser,
updateThisUser,
createErrorObservable,
showSignIn
} from './actions';

export default function getUserSaga(action$, getState, { services }) {
return action$
.filter(action => action.type === fetchUser)
.flatMap(() => {
return services.readService$({ service: 'user' })
.map(user => {
return {
type: setUser,
payload: user
};
.flatMap(({ entities, result })=> {
if (!entities || !result) {
return Observable.just(showSignIn());
}
return Observable.of(
addUser(entities),
updateThisUser(result)
);
})
.catch(createErrorObservable);
});
Expand Down
Loading

0 comments on commit 0c07e96

Please sign in to comment.