Feature(components): fetch user after page load
This makes it easier to serve whole site statically in the future Feature(redux): Move user state into entities
This commit is contained in:
@ -4,7 +4,7 @@ export default function hardGoToSaga(action$, getState, { history }) {
|
|||||||
return action$
|
return action$
|
||||||
.filter(({ type }) => type === hardGoTo)
|
.filter(({ type }) => type === hardGoTo)
|
||||||
.map(({ payload = '/settings' }) => {
|
.map(({ payload = '/settings' }) => {
|
||||||
history.push(history.state, null, payload);
|
history.pushState(history.state, null, payload);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Button, Row } from 'react-bootstrap';
|
import { Button, Row } from 'react-bootstrap';
|
||||||
import { ToastMessage, ToastContainer } from 'react-toastr';
|
import { ToastMessage, ToastContainer } from 'react-toastr';
|
||||||
import { compose } from 'redux';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { contain } from 'redux-epic';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import MapDrawer from './components/Map-Drawer.jsx';
|
import MapDrawer from './components/Map-Drawer.jsx';
|
||||||
@ -19,26 +17,27 @@ import { submitChallenge } from './routes/challenges/redux/actions';
|
|||||||
|
|
||||||
import Nav from './components/Nav';
|
import Nav from './components/Nav';
|
||||||
import { randomCompliment } from './utils/get-words';
|
import { randomCompliment } from './utils/get-words';
|
||||||
|
import { userSelector } from './redux/selectors';
|
||||||
|
|
||||||
const toastMessageFactory = React.createFactory(ToastMessage.animation);
|
const toastMessageFactory = React.createFactory(ToastMessage.animation);
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
state => state.app.username,
|
userSelector,
|
||||||
state => state.app.points,
|
state => state.app.shouldShowSignIn,
|
||||||
state => state.app.picture,
|
|
||||||
state => state.app.toast,
|
state => state.app.toast,
|
||||||
state => state.app.isMapDrawerOpen,
|
state => state.app.isMapDrawerOpen,
|
||||||
state => state.app.isMapAlreadyLoaded,
|
state => state.app.isMapAlreadyLoaded,
|
||||||
state => state.challengesApp.toast,
|
state => state.challengesApp.toast,
|
||||||
(
|
(
|
||||||
username,
|
{ user: { username, points, picture } },
|
||||||
points,
|
shouldShowSignIn,
|
||||||
picture,
|
|
||||||
toast,
|
toast,
|
||||||
isMapDrawerOpen,
|
isMapDrawerOpen,
|
||||||
isMapAlreadyLoaded,
|
isMapAlreadyLoaded,
|
||||||
showChallengeComplete
|
showChallengeComplete
|
||||||
) => ({
|
) => ({
|
||||||
|
shouldShowSignIn,
|
||||||
|
isSignedIn: !!username,
|
||||||
username,
|
username,
|
||||||
points,
|
points,
|
||||||
picture,
|
picture,
|
||||||
@ -58,13 +57,6 @@ const bindableActions = {
|
|||||||
toggleMainChat
|
toggleMainChat
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchContainerOptions = {
|
|
||||||
fetchAction: 'fetchUser',
|
|
||||||
isPrimed({ username }) {
|
|
||||||
return !!username;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// export plain class for testing
|
// export plain class for testing
|
||||||
export class FreeCodeCamp extends React.Component {
|
export class FreeCodeCamp extends React.Component {
|
||||||
static displayName = 'FreeCodeCamp';
|
static displayName = 'FreeCodeCamp';
|
||||||
@ -72,6 +64,7 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
|
isSignedIn: PropTypes.bool,
|
||||||
points: PropTypes.number,
|
points: PropTypes.number,
|
||||||
picture: PropTypes.string,
|
picture: PropTypes.string,
|
||||||
toast: PropTypes.object,
|
toast: PropTypes.object,
|
||||||
@ -82,7 +75,9 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
isMapDrawerOpen: PropTypes.bool,
|
isMapDrawerOpen: PropTypes.bool,
|
||||||
isMapAlreadyLoaded: PropTypes.bool,
|
isMapAlreadyLoaded: PropTypes.bool,
|
||||||
toggleMapDrawer: PropTypes.func,
|
toggleMapDrawer: PropTypes.func,
|
||||||
toggleMainChat: PropTypes.func
|
toggleMainChat: PropTypes.func,
|
||||||
|
fetchUser: PropTypes.func,
|
||||||
|
shouldShowSignIn: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillReceiveProps({
|
componentWillReceiveProps({
|
||||||
@ -119,6 +114,9 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.initWindowHeight();
|
this.props.initWindowHeight();
|
||||||
|
if (!this.props.isSignedIn) {
|
||||||
|
this.props.fetchUser();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderChallengeComplete() {
|
renderChallengeComplete() {
|
||||||
@ -145,7 +143,8 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
isMapDrawerOpen,
|
isMapDrawerOpen,
|
||||||
isMapAlreadyLoaded,
|
isMapAlreadyLoaded,
|
||||||
toggleMapDrawer,
|
toggleMapDrawer,
|
||||||
toggleMainChat
|
toggleMainChat,
|
||||||
|
shouldShowSignIn
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const navProps = {
|
const navProps = {
|
||||||
username,
|
username,
|
||||||
@ -153,7 +152,8 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
picture,
|
picture,
|
||||||
updateNavHeight,
|
updateNavHeight,
|
||||||
toggleMapDrawer,
|
toggleMapDrawer,
|
||||||
toggleMainChat
|
toggleMainChat,
|
||||||
|
shouldShowSignIn
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -177,11 +177,7 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapComponent = compose(
|
export default connect(
|
||||||
// connect Component to Redux Store
|
mapStateToProps,
|
||||||
connect(mapStateToProps, bindableActions),
|
bindableActions
|
||||||
// handles prefetching data
|
)(FreeCodeCamp);
|
||||||
contain(fetchContainerOptions)
|
|
||||||
);
|
|
||||||
|
|
||||||
export default wrapComponent(FreeCodeCamp);
|
|
||||||
|
@ -19,7 +19,8 @@ const logoElement = (
|
|||||||
<img
|
<img
|
||||||
alt='learn to code javascript at Free Code Camp logo'
|
alt='learn to code javascript at Free Code Camp logo'
|
||||||
className='img-responsive nav-logo'
|
className='img-responsive nav-logo'
|
||||||
src={ fCClogo } />
|
src={ fCClogo }
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -41,7 +42,8 @@ export default class extends React.Component {
|
|||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
updateNavHeight: PropTypes.func,
|
updateNavHeight: PropTypes.func,
|
||||||
toggleMapDrawer: PropTypes.func,
|
toggleMapDrawer: PropTypes.func,
|
||||||
toggleMainChat: PropTypes.func
|
toggleMainChat: PropTypes.func,
|
||||||
|
shouldShowSignIn: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -65,7 +67,8 @@ export default class extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<LinkContainer
|
<LinkContainer
|
||||||
eventKey={ 1 }
|
eventKey={ 1 }
|
||||||
to='/map'>
|
to='/map'
|
||||||
|
>
|
||||||
<NavItem
|
<NavItem
|
||||||
onClick={ e => {
|
onClick={ e => {
|
||||||
if (!(e.ctrlKey || e.metaKey)) {
|
if (!(e.ctrlKey || e.metaKey)) {
|
||||||
@ -106,9 +109,11 @@ export default class extends React.Component {
|
|||||||
<LinkContainer
|
<LinkContainer
|
||||||
eventKey={ index + 2 }
|
eventKey={ index + 2 }
|
||||||
key={ content }
|
key={ content }
|
||||||
to={ link }>
|
to={ link }
|
||||||
|
>
|
||||||
<NavItem
|
<NavItem
|
||||||
target={ target || null }>
|
target={ target || null }
|
||||||
|
>
|
||||||
{ content }
|
{ content }
|
||||||
</NavItem>
|
</NavItem>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
@ -119,36 +124,45 @@ export default class extends React.Component {
|
|||||||
eventKey={ index + 1 }
|
eventKey={ index + 1 }
|
||||||
href={ link }
|
href={ link }
|
||||||
key={ content }
|
key={ content }
|
||||||
target={ target || null }>
|
target={ target || null }
|
||||||
|
>
|
||||||
{ content }
|
{ content }
|
||||||
</NavItem>
|
</NavItem>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPoints(username, points) {
|
renderPoints(username, points, shouldShowSignIn) {
|
||||||
if (!username) {
|
if (!username || !shouldShowSignIn) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<FCCNavItem
|
<FCCNavItem
|
||||||
className='brownie-points-nav'
|
className='brownie-points-nav'
|
||||||
href={ '/' + username }>
|
href={ '/' + username }
|
||||||
|
key='points'
|
||||||
|
>
|
||||||
[ { points } ]
|
[ { points } ]
|
||||||
</FCCNavItem>
|
</FCCNavItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSignin(username, picture) {
|
renderSignIn(username, picture, shouldShowSignIn) {
|
||||||
|
if (!shouldShowSignIn) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (username) {
|
if (username) {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className='hidden-xs hidden-sm avatar'
|
className='hidden-xs hidden-sm avatar'
|
||||||
eventKey={ 2 }>
|
eventKey={ 2 }
|
||||||
|
key='user'
|
||||||
|
>
|
||||||
<a href={ '/' + username }>
|
<a href={ '/' + username }>
|
||||||
<img
|
<img
|
||||||
className='profile-picture float-right'
|
className='profile-picture float-right'
|
||||||
src={ picture } />
|
src={ picture }
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@ -156,7 +170,9 @@ export default class extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<NavItem
|
<NavItem
|
||||||
eventKey={ 2 }
|
eventKey={ 2 }
|
||||||
href='/signin'>
|
href='/signin'
|
||||||
|
key='signin'
|
||||||
|
>
|
||||||
Sign In
|
Sign In
|
||||||
</NavItem>
|
</NavItem>
|
||||||
);
|
);
|
||||||
@ -169,7 +185,8 @@ export default class extends React.Component {
|
|||||||
points,
|
points,
|
||||||
picture,
|
picture,
|
||||||
toggleMapDrawer,
|
toggleMapDrawer,
|
||||||
toggleMainChat
|
toggleMainChat,
|
||||||
|
shouldShowSignIn
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { router } = this.context;
|
const { router } = this.context;
|
||||||
const isOnMap = router.isActive('/map');
|
const isOnMap = router.isActive('/map');
|
||||||
@ -177,19 +194,21 @@ export default class extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<Navbar
|
<Navbar
|
||||||
className='nav-height'
|
className='nav-height'
|
||||||
fixedTop={ true }>
|
fixedTop={ true }
|
||||||
|
>
|
||||||
<NavbarBrand>{ logoElement }</NavbarBrand>
|
<NavbarBrand>{ logoElement }</NavbarBrand>
|
||||||
<Navbar.Toggle children={ toggleButtonChild } />
|
<Navbar.Toggle children={ toggleButtonChild } />
|
||||||
<Navbar.Collapse eventKey={ 0 }>
|
<Navbar.Collapse eventKey={ 0 }>
|
||||||
<Nav
|
<Nav
|
||||||
className='hamburger-dropdown'
|
className='hamburger-dropdown'
|
||||||
navbar={ true }
|
navbar={ true }
|
||||||
pullRight={ true }>
|
pullRight={ true }
|
||||||
|
>
|
||||||
{ this.renderMapLink(isOnMap, toggleMapDrawer) }
|
{ this.renderMapLink(isOnMap, toggleMapDrawer) }
|
||||||
{ this.renderChat(toggleMainChat) }
|
{ this.renderChat(toggleMainChat) }
|
||||||
{ this.renderLinks() }
|
{ this.renderLinks() }
|
||||||
{ this.renderPoints(username, points) }
|
{ this.renderPoints(username, points, shouldShowSignIn) }
|
||||||
{ this.renderSignin(username, picture) }
|
{ this.renderSignIn(username, picture, shouldShowSignIn) }
|
||||||
</Nav>
|
</Nav>
|
||||||
</Navbar.Collapse>
|
</Navbar.Collapse>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
@ -23,11 +23,21 @@ export const makeToast = createAction(
|
|||||||
// used in combination with fetch-user-saga
|
// used in combination with fetch-user-saga
|
||||||
export const fetchUser = createAction(types.fetchUser);
|
export const fetchUser = createAction(types.fetchUser);
|
||||||
|
|
||||||
// setUser(userInfo: Object) => Action
|
// setUser(
|
||||||
export const setUser = createAction(types.setUser);
|
// entities: { [userId]: User }
|
||||||
|
// ) => Action
|
||||||
|
export const addUser = createAction(
|
||||||
|
types.addUser,
|
||||||
|
() => {},
|
||||||
|
entities => ({ entities })
|
||||||
|
);
|
||||||
|
export const updateThisUser = createAction(types.updateThisUser);
|
||||||
|
|
||||||
// updatePoints(points: Number) => Action
|
// updateUserPoints(username: String, points: Number) => Action
|
||||||
export const updatePoints = createAction(types.updatePoints);
|
export const updateUserPoints = createAction(
|
||||||
|
types.updateUserPoints,
|
||||||
|
(username, points) => ({ username, points })
|
||||||
|
);
|
||||||
// used when server needs client to redirect
|
// used when server needs client to redirect
|
||||||
export const delayedRedirect = createAction(types.delayedRedirect);
|
export const delayedRedirect = createAction(types.delayedRedirect);
|
||||||
|
|
||||||
|
@ -1,12 +1,26 @@
|
|||||||
|
import { updateUserPoints } from './types';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
hike: {},
|
|
||||||
superBlock: {},
|
superBlock: {},
|
||||||
block: {},
|
block: {},
|
||||||
challenge: {},
|
challenge: {},
|
||||||
job: {}
|
user: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function entities(state = initialState, action) {
|
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) {
|
if (action.meta && action.meta.entities) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -1,16 +1,25 @@
|
|||||||
import { setUser, fetchUser } from './types';
|
import { Observable } from 'rx';
|
||||||
import { createErrorObservable } from './actions';
|
import { fetchUser } from './types';
|
||||||
|
import {
|
||||||
|
addUser,
|
||||||
|
updateThisUser,
|
||||||
|
createErrorObservable,
|
||||||
|
showSignIn
|
||||||
|
} from './actions';
|
||||||
|
|
||||||
export default function getUserSaga(action$, getState, { services }) {
|
export default function getUserSaga(action$, getState, { services }) {
|
||||||
return action$
|
return action$
|
||||||
.filter(action => action.type === fetchUser)
|
.filter(action => action.type === fetchUser)
|
||||||
.flatMap(() => {
|
.flatMap(() => {
|
||||||
return services.readService$({ service: 'user' })
|
return services.readService$({ service: 'user' })
|
||||||
.map(user => {
|
.flatMap(({ entities, result })=> {
|
||||||
return {
|
if (!entities || !result) {
|
||||||
type: setUser,
|
return Observable.just(showSignIn());
|
||||||
payload: user
|
}
|
||||||
};
|
return Observable.of(
|
||||||
|
addUser(entities),
|
||||||
|
updateThisUser(result)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(createErrorObservable);
|
.catch(createErrorObservable);
|
||||||
});
|
});
|
||||||
|
@ -3,10 +3,8 @@ import types from './types';
|
|||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
title: 'Learn To Code | Free Code Camp',
|
title: 'Learn To Code | Free Code Camp',
|
||||||
username: null,
|
shouldShowSignIn: false,
|
||||||
picture: null,
|
user: '',
|
||||||
points: 0,
|
|
||||||
isSignedIn: false,
|
|
||||||
csrfToken: '',
|
csrfToken: '',
|
||||||
windowHeight: 0,
|
windowHeight: 0,
|
||||||
navHeight: 0,
|
navHeight: 0,
|
||||||
@ -25,20 +23,20 @@ export default handleActions(
|
|||||||
toast
|
toast
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[types.setUser]: (state, { payload: user }) => ({
|
[types.updateThisUser]: (state, { payload: user }) => ({
|
||||||
...state,
|
...state,
|
||||||
...user,
|
user,
|
||||||
isSignedIn: true
|
shouldShowSignIn: true
|
||||||
|
}),
|
||||||
|
[types.showSignIn]: state => ({
|
||||||
|
...state,
|
||||||
|
shouldShowSignIn: true
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[types.challengeSaved]: (state, { payload: { points = 0 } }) => ({
|
[types.challengeSaved]: (state, { payload: { points = 0 } }) => ({
|
||||||
...state,
|
...state,
|
||||||
points
|
points
|
||||||
}),
|
}),
|
||||||
[types.updatePoints]: (state, { payload: points }) => ({
|
|
||||||
...state,
|
|
||||||
points
|
|
||||||
}),
|
|
||||||
[types.updateWindowHeight]: (state, { payload: windowHeight }) => ({
|
[types.updateWindowHeight]: (state, { payload: windowHeight }) => ({
|
||||||
...state,
|
...state,
|
||||||
windowHeight
|
windowHeight
|
||||||
|
9
common/app/redux/selectors.js
Normal file
9
common/app/redux/selectors.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
export const userSelector = createSelector(
|
||||||
|
state => state.app.user,
|
||||||
|
state => state.entities.user,
|
||||||
|
(username, userMap) => ({
|
||||||
|
user: userMap[username] || {}
|
||||||
|
})
|
||||||
|
);
|
@ -4,10 +4,12 @@ export default createTypes([
|
|||||||
'updateTitle',
|
'updateTitle',
|
||||||
|
|
||||||
'fetchUser',
|
'fetchUser',
|
||||||
'setUser',
|
'addUser',
|
||||||
|
'updateThisUser',
|
||||||
|
'updateUserPoints',
|
||||||
|
'showSignIn',
|
||||||
|
|
||||||
'makeToast',
|
'makeToast',
|
||||||
'updatePoints',
|
|
||||||
'handleError',
|
'handleError',
|
||||||
'toggleNightMode',
|
'toggleNightMode',
|
||||||
// used to hit the server
|
// used to hit the server
|
||||||
|
@ -4,7 +4,7 @@ import { showChallengeComplete, moveToNextChallenge } from './actions';
|
|||||||
import {
|
import {
|
||||||
createErrorObservable,
|
createErrorObservable,
|
||||||
makeToast,
|
makeToast,
|
||||||
updatePoints
|
updateUserPoints
|
||||||
} from '../../../redux/actions';
|
} from '../../../redux/actions';
|
||||||
|
|
||||||
import { challengeSelector } from './selectors';
|
import { challengeSelector } from './selectors';
|
||||||
@ -16,25 +16,16 @@ import { postJSON$ } from '../../../../utils/ajax-stream';
|
|||||||
// lots of repeat code
|
// lots of repeat code
|
||||||
|
|
||||||
function completedChallenge(state) {
|
function completedChallenge(state) {
|
||||||
let body;
|
const { challenge: { id } } = challengeSelector(state);
|
||||||
let isSignedIn = false;
|
|
||||||
try {
|
|
||||||
const {
|
const {
|
||||||
challenge: { id }
|
app: { user, csrfToken },
|
||||||
} = challengeSelector(state);
|
|
||||||
const {
|
|
||||||
app: { isSignedIn: _isSignedId, csrfToken },
|
|
||||||
challengesApp: { files }
|
challengesApp: { files }
|
||||||
} = state;
|
} = state;
|
||||||
isSignedIn = _isSignedId;
|
const body = {
|
||||||
body = {
|
|
||||||
id,
|
id,
|
||||||
_csrf: csrfToken,
|
_csrf: csrfToken,
|
||||||
files
|
files
|
||||||
};
|
};
|
||||||
} catch (err) {
|
|
||||||
return createErrorObservable(err);
|
|
||||||
}
|
|
||||||
const saveChallenge$ = postJSON$('/modern-challenge-completed', body)
|
const saveChallenge$ = postJSON$('/modern-challenge-completed', body)
|
||||||
.retry(3)
|
.retry(3)
|
||||||
.flatMap(({ alreadyCompleted, points }) => {
|
.flatMap(({ alreadyCompleted, points }) => {
|
||||||
@ -46,7 +37,7 @@ function completedChallenge(state) {
|
|||||||
title: 'Saved',
|
title: 'Saved',
|
||||||
type: 'info'
|
type: 'info'
|
||||||
}),
|
}),
|
||||||
updatePoints(points)
|
updateUserPoints(user, points)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(createErrorObservable);
|
.catch(createErrorObservable);
|
||||||
@ -55,7 +46,7 @@ function completedChallenge(state) {
|
|||||||
moveToNextChallenge(),
|
moveToNextChallenge(),
|
||||||
makeToast({
|
makeToast({
|
||||||
title: 'Congratulations!',
|
title: 'Congratulations!',
|
||||||
message: isSignedIn ? ' Saving...' : 'Moving on to next challenge.',
|
message: user ? ' Saving...' : 'Moving on to next challenge.',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -87,7 +78,7 @@ function submitProject(type, state, { solution, githubLink }) {
|
|||||||
challenge: { id, challengeType }
|
challenge: { id, challengeType }
|
||||||
} = challengeSelector(state);
|
} = challengeSelector(state);
|
||||||
const {
|
const {
|
||||||
app: { isSignedIn, csrfToken }
|
app: { user, csrfToken }
|
||||||
} = state;
|
} = state;
|
||||||
const body = {
|
const body = {
|
||||||
id,
|
id,
|
||||||
@ -109,7 +100,7 @@ function submitProject(type, state, { solution, githubLink }) {
|
|||||||
title: 'Saved',
|
title: 'Saved',
|
||||||
type: 'info'
|
type: 'info'
|
||||||
}),
|
}),
|
||||||
updatePoints(points)
|
updateUserPoints(user, points)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(createErrorObservable);
|
.catch(createErrorObservable);
|
||||||
@ -117,7 +108,7 @@ function submitProject(type, state, { solution, githubLink }) {
|
|||||||
const challengeCompleted$ = Observable.of(
|
const challengeCompleted$ = Observable.of(
|
||||||
makeToast({
|
makeToast({
|
||||||
title: randomCompliment(),
|
title: randomCompliment(),
|
||||||
message: isSignedIn ? ' Saving...' : 'Moving on to next challenge.',
|
message: user ? ' Saving...' : 'Moving on to next challenge.',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
})
|
})
|
||||||
// moveToNextChallenge()
|
// moveToNextChallenge()
|
||||||
@ -130,7 +121,7 @@ function submitSimpleChallenge(type, state) {
|
|||||||
challenge: { id }
|
challenge: { id }
|
||||||
} = challengeSelector(state);
|
} = challengeSelector(state);
|
||||||
const {
|
const {
|
||||||
app: { isSignedIn, csrfToken }
|
app: { user, csrfToken }
|
||||||
} = state;
|
} = state;
|
||||||
const body = {
|
const body = {
|
||||||
id,
|
id,
|
||||||
@ -147,7 +138,7 @@ function submitSimpleChallenge(type, state) {
|
|||||||
title: 'Saved',
|
title: 'Saved',
|
||||||
type: 'info'
|
type: 'info'
|
||||||
}),
|
}),
|
||||||
updatePoints(points)
|
updateUserPoints(user, points)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(createErrorObservable);
|
.catch(createErrorObservable);
|
||||||
@ -155,7 +146,7 @@ function submitSimpleChallenge(type, state) {
|
|||||||
const challengeCompleted$ = Observable.of(
|
const challengeCompleted$ = Observable.of(
|
||||||
makeToast({
|
makeToast({
|
||||||
title: randomCompliment(),
|
title: randomCompliment(),
|
||||||
message: isSignedIn ? ' Saving...' : 'Moving on to next challenge.',
|
message: user ? ' Saving...' : 'Moving on to next challenge.',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
}),
|
}),
|
||||||
moveToNextChallenge()
|
moveToNextChallenge()
|
||||||
|
@ -15,6 +15,18 @@ import {
|
|||||||
updateCurrentChallenge
|
updateCurrentChallenge
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
|
function createNameIdMap(entities) {
|
||||||
|
const { challenge } = entities;
|
||||||
|
return {
|
||||||
|
...entities,
|
||||||
|
challengeIdToName: Object.keys(challenge)
|
||||||
|
.reduce((map, challengeName) => {
|
||||||
|
map[challenge[challengeName].id] = challenge[challengeName].dashedName;
|
||||||
|
return map;
|
||||||
|
}, {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function fetchChallengesSaga(action$, getState, { services }) {
|
export default function fetchChallengesSaga(action$, getState, { services }) {
|
||||||
return action$
|
return action$
|
||||||
.filter(({ type }) => (
|
.filter(({ type }) => (
|
||||||
@ -48,12 +60,20 @@ export default function fetchChallengesSaga(action$, getState, { services }) {
|
|||||||
.flatMap(({ entities, result, redirect } = {}) => {
|
.flatMap(({ entities, result, redirect } = {}) => {
|
||||||
if (type === fetchChallenge) {
|
if (type === fetchChallenge) {
|
||||||
return Observable.of(
|
return Observable.of(
|
||||||
fetchChallengeCompleted(entities, result),
|
fetchChallengeCompleted(
|
||||||
|
createNameIdMap(entities),
|
||||||
|
result
|
||||||
|
),
|
||||||
updateCurrentChallenge(entities.challenge[result.challenge]),
|
updateCurrentChallenge(entities.challenge[result.challenge]),
|
||||||
redirect ? delayedRedirect(redirect) : null
|
redirect ? delayedRedirect(redirect) : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Observable.just(fetchChallengesCompleted(entities, result));
|
return Observable.just(
|
||||||
|
fetchChallengesCompleted(
|
||||||
|
createNameIdMap(entities),
|
||||||
|
result
|
||||||
|
)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(createErrorObserable);
|
.catch(createErrorObserable);
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,29 @@
|
|||||||
import debugFactory from 'debug';
|
import debug from 'debug';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
const censor = '**********************:P********';
|
const publicUserProps = [
|
||||||
const debug = debugFactory('fcc:services:user');
|
'id',
|
||||||
const protectedUserFields = {
|
'name',
|
||||||
password: censor,
|
'username',
|
||||||
profiles: censor
|
'bio',
|
||||||
};
|
'theme',
|
||||||
|
'picture',
|
||||||
|
'points',
|
||||||
|
'languageTag',
|
||||||
|
|
||||||
|
'isCheater',
|
||||||
|
'isGithubCool',
|
||||||
|
|
||||||
|
'isFrontEndCert',
|
||||||
|
'isBackEndCert',
|
||||||
|
'isDataVisCert',
|
||||||
|
'isFullStackCert',
|
||||||
|
|
||||||
|
'githubURL',
|
||||||
|
'currentChallenge',
|
||||||
|
'challengeMap'
|
||||||
|
];
|
||||||
|
const log = debug('fcc:services:user');
|
||||||
|
|
||||||
export default function userServices() {
|
export default function userServices() {
|
||||||
return {
|
return {
|
||||||
@ -13,19 +31,26 @@ export default function userServices() {
|
|||||||
read: (req, resource, params, config, cb) => {
|
read: (req, resource, params, config, cb) => {
|
||||||
let { user } = req;
|
let { user } = req;
|
||||||
if (user) {
|
if (user) {
|
||||||
debug('user is signed in');
|
log('user is signed in');
|
||||||
// Zalgo!!!
|
return user.getChallengeMap$()
|
||||||
return process.nextTick(() => {
|
.map(challengeMap => ({ ...user.toJSON(), challengeMap }))
|
||||||
cb(
|
.subscribe(
|
||||||
|
user => cb(
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
...user.toJSON(),
|
entities: {
|
||||||
...protectedUserFields
|
user: {
|
||||||
|
[user.username]: _.pick(user, publicUserProps)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
result: user.username
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cb
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
debug('user is not signed in');
|
debug('user is not signed in');
|
||||||
|
// Zalgo!!!
|
||||||
return process.nextTick(() => {
|
return process.nextTick(() => {
|
||||||
cb(null, {});
|
cb(null, {});
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user