diff --git a/client/sagas/hard-go-to-saga.js b/client/sagas/hard-go-to-saga.js
index 46bfd15289..b3c9a8a3a7 100644
--- a/client/sagas/hard-go-to-saga.js
+++ b/client/sagas/hard-go-to-saga.js
@@ -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;
});
}
diff --git a/common/app/App.jsx b/common/app/App.jsx
index f8494c14c8..e6554b7711 100644
--- a/common/app/App.jsx
+++ b/common/app/App.jsx
@@ -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';
@@ -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,
@@ -58,13 +57,6 @@ 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';
@@ -72,6 +64,7 @@ export class FreeCodeCamp extends React.Component {
static propTypes = {
children: PropTypes.node,
username: PropTypes.string,
+ isSignedIn: PropTypes.bool,
points: PropTypes.number,
picture: PropTypes.string,
toast: PropTypes.object,
@@ -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({
@@ -119,6 +114,9 @@ export class FreeCodeCamp extends React.Component {
componentDidMount() {
this.props.initWindowHeight();
+ if (!this.props.isSignedIn) {
+ this.props.fetchUser();
+ }
}
renderChallengeComplete() {
@@ -145,7 +143,8 @@ export class FreeCodeCamp extends React.Component {
isMapDrawerOpen,
isMapAlreadyLoaded,
toggleMapDrawer,
- toggleMainChat
+ toggleMainChat,
+ shouldShowSignIn
} = this.props;
const navProps = {
username,
@@ -153,7 +152,8 @@ export class FreeCodeCamp extends React.Component {
picture,
updateNavHeight,
toggleMapDrawer,
- toggleMainChat
+ toggleMainChat,
+ shouldShowSignIn
};
return (
@@ -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);
diff --git a/common/app/components/Nav/Nav.jsx b/common/app/components/Nav/Nav.jsx
index 1989a7a1f3..bb13d5a810 100644
--- a/common/app/components/Nav/Nav.jsx
+++ b/common/app/components/Nav/Nav.jsx
@@ -19,7 +19,8 @@ const logoElement = (
+ src={ fCClogo }
+ />
);
@@ -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() {
@@ -56,7 +58,7 @@ export default class extends React.Component {
e.preventDefault()}
- >
+ >
Map
@@ -65,7 +67,8 @@ export default class extends React.Component {
return (
+ to='/map'
+ >
{
if (!(e.ctrlKey || e.metaKey)) {
@@ -74,7 +77,7 @@ export default class extends React.Component {
}
}}
target='/map'
- >
+ >
Map
@@ -93,7 +96,7 @@ export default class extends React.Component {
}
}}
target='_blank'
- >
+ >
Chat
);
@@ -106,9 +109,11 @@ export default class extends React.Component {
+ to={ link }
+ >
+ target={ target || null }
+ >
{ content }
@@ -119,36 +124,45 @@ export default class extends React.Component {
eventKey={ index + 1 }
href={ link }
key={ content }
- target={ target || null }>
+ target={ target || null }
+ >
{ content }
);
});
}
- renderPoints(username, points) {
- if (!username) {
+ renderPoints(username, points, shouldShowSignIn) {
+ if (!username || !shouldShowSignIn) {
return null;
}
return (
+ href={ '/' + username }
+ key='points'
+ >
[ { points } ]
);
}
- renderSignin(username, picture) {
+ renderSignIn(username, picture, shouldShowSignIn) {
+ if (!shouldShowSignIn) {
+ return null;
+ }
if (username) {
return (
+ eventKey={ 2 }
+ key='user'
+ >
+ src={ picture }
+ />
);
@@ -156,7 +170,9 @@ export default class extends React.Component {
return (
+ href='/signin'
+ key='signin'
+ >
Sign In
);
@@ -169,7 +185,8 @@ export default class extends React.Component {
points,
picture,
toggleMapDrawer,
- toggleMainChat
+ toggleMainChat,
+ shouldShowSignIn
} = this.props;
const { router } = this.context;
const isOnMap = router.isActive('/map');
@@ -177,19 +194,21 @@ export default class extends React.Component {
return (
+ fixedTop={ true }
+ >
{ logoElement }
diff --git a/common/app/redux/actions.js b/common/app/redux/actions.js
index 281c937080..5407e521f7 100644
--- a/common/app/redux/actions.js
+++ b/common/app/redux/actions.js
@@ -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);
diff --git a/common/app/redux/entities-reducer.js b/common/app/redux/entities-reducer.js
index e7487b0534..8ba8dc5ab0 100644
--- a/common/app/redux/entities-reducer.js
+++ b/common/app/redux/entities-reducer.js
@@ -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,
diff --git a/common/app/redux/fetch-user-saga.js b/common/app/redux/fetch-user-saga.js
index 874ee4ff78..db3bd88a29 100644
--- a/common/app/redux/fetch-user-saga.js
+++ b/common/app/redux/fetch-user-saga.js
@@ -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);
});
diff --git a/common/app/redux/reducer.js b/common/app/redux/reducer.js
index d3d28812e3..066bac4c13 100644
--- a/common/app/redux/reducer.js
+++ b/common/app/redux/reducer.js
@@ -3,10 +3,8 @@ import types from './types';
const initialState = {
title: 'Learn To Code | Free Code Camp',
- username: null,
- picture: null,
- points: 0,
- isSignedIn: false,
+ shouldShowSignIn: false,
+ user: '',
csrfToken: '',
windowHeight: 0,
navHeight: 0,
@@ -25,20 +23,20 @@ export default handleActions(
toast
}),
- [types.setUser]: (state, { payload: user }) => ({
+ [types.updateThisUser]: (state, { payload: user }) => ({
...state,
- ...user,
- isSignedIn: true
+ user,
+ shouldShowSignIn: true
+ }),
+ [types.showSignIn]: state => ({
+ ...state,
+ shouldShowSignIn: true
}),
[types.challengeSaved]: (state, { payload: { points = 0 } }) => ({
...state,
points
}),
- [types.updatePoints]: (state, { payload: points }) => ({
- ...state,
- points
- }),
[types.updateWindowHeight]: (state, { payload: windowHeight }) => ({
...state,
windowHeight
diff --git a/common/app/redux/selectors.js b/common/app/redux/selectors.js
new file mode 100644
index 0000000000..0af91b151d
--- /dev/null
+++ b/common/app/redux/selectors.js
@@ -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] || {}
+ })
+);
diff --git a/common/app/redux/types.js b/common/app/redux/types.js
index 8338e9ff69..5a5dd84883 100644
--- a/common/app/redux/types.js
+++ b/common/app/redux/types.js
@@ -4,10 +4,12 @@ export default createTypes([
'updateTitle',
'fetchUser',
- 'setUser',
+ 'addUser',
+ 'updateThisUser',
+ 'updateUserPoints',
+ 'showSignIn',
'makeToast',
- 'updatePoints',
'handleError',
'toggleNightMode',
// used to hit the server
diff --git a/common/app/routes/challenges/redux/completion-saga.js b/common/app/routes/challenges/redux/completion-saga.js
index 870698b66a..5cff0717cb 100644
--- a/common/app/routes/challenges/redux/completion-saga.js
+++ b/common/app/routes/challenges/redux/completion-saga.js
@@ -4,7 +4,7 @@ import { showChallengeComplete, moveToNextChallenge } from './actions';
import {
createErrorObservable,
makeToast,
- updatePoints
+ updateUserPoints
} from '../../../redux/actions';
import { challengeSelector } from './selectors';
@@ -16,25 +16,16 @@ import { postJSON$ } from '../../../../utils/ajax-stream';
// lots of repeat code
function completedChallenge(state) {
- let body;
- let isSignedIn = false;
- try {
- const {
- challenge: { id }
- } = challengeSelector(state);
- const {
- app: { isSignedIn: _isSignedId, csrfToken },
- challengesApp: { files }
- } = state;
- isSignedIn = _isSignedId;
- body = {
- id,
- _csrf: csrfToken,
- files
- };
- } catch (err) {
- return createErrorObservable(err);
- }
+ const { challenge: { id } } = challengeSelector(state);
+ const {
+ app: { user, csrfToken },
+ challengesApp: { files }
+ } = state;
+ const body = {
+ id,
+ _csrf: csrfToken,
+ files
+ };
const saveChallenge$ = postJSON$('/modern-challenge-completed', body)
.retry(3)
.flatMap(({ alreadyCompleted, points }) => {
@@ -46,7 +37,7 @@ function completedChallenge(state) {
title: 'Saved',
type: 'info'
}),
- updatePoints(points)
+ updateUserPoints(user, points)
);
})
.catch(createErrorObservable);
@@ -55,7 +46,7 @@ function completedChallenge(state) {
moveToNextChallenge(),
makeToast({
title: 'Congratulations!',
- message: isSignedIn ? ' Saving...' : 'Moving on to next challenge.',
+ message: user ? ' Saving...' : 'Moving on to next challenge.',
type: 'success'
})
);
@@ -87,7 +78,7 @@ function submitProject(type, state, { solution, githubLink }) {
challenge: { id, challengeType }
} = challengeSelector(state);
const {
- app: { isSignedIn, csrfToken }
+ app: { user, csrfToken }
} = state;
const body = {
id,
@@ -109,7 +100,7 @@ function submitProject(type, state, { solution, githubLink }) {
title: 'Saved',
type: 'info'
}),
- updatePoints(points)
+ updateUserPoints(user, points)
);
})
.catch(createErrorObservable);
@@ -117,7 +108,7 @@ function submitProject(type, state, { solution, githubLink }) {
const challengeCompleted$ = Observable.of(
makeToast({
title: randomCompliment(),
- message: isSignedIn ? ' Saving...' : 'Moving on to next challenge.',
+ message: user ? ' Saving...' : 'Moving on to next challenge.',
type: 'success'
})
// moveToNextChallenge()
@@ -130,7 +121,7 @@ function submitSimpleChallenge(type, state) {
challenge: { id }
} = challengeSelector(state);
const {
- app: { isSignedIn, csrfToken }
+ app: { user, csrfToken }
} = state;
const body = {
id,
@@ -147,7 +138,7 @@ function submitSimpleChallenge(type, state) {
title: 'Saved',
type: 'info'
}),
- updatePoints(points)
+ updateUserPoints(user, points)
);
})
.catch(createErrorObservable);
@@ -155,7 +146,7 @@ function submitSimpleChallenge(type, state) {
const challengeCompleted$ = Observable.of(
makeToast({
title: randomCompliment(),
- message: isSignedIn ? ' Saving...' : 'Moving on to next challenge.',
+ message: user ? ' Saving...' : 'Moving on to next challenge.',
type: 'success'
}),
moveToNextChallenge()
diff --git a/common/app/routes/challenges/redux/fetch-challenges-saga.js b/common/app/routes/challenges/redux/fetch-challenges-saga.js
index 8fccade58c..72cc63c904 100644
--- a/common/app/routes/challenges/redux/fetch-challenges-saga.js
+++ b/common/app/routes/challenges/redux/fetch-challenges-saga.js
@@ -15,6 +15,18 @@ import {
updateCurrentChallenge
} 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 }) {
return action$
.filter(({ type }) => (
@@ -48,12 +60,20 @@ export default function fetchChallengesSaga(action$, getState, { services }) {
.flatMap(({ entities, result, redirect } = {}) => {
if (type === fetchChallenge) {
return Observable.of(
- fetchChallengeCompleted(entities, result),
+ fetchChallengeCompleted(
+ createNameIdMap(entities),
+ result
+ ),
updateCurrentChallenge(entities.challenge[result.challenge]),
redirect ? delayedRedirect(redirect) : null
);
}
- return Observable.just(fetchChallengesCompleted(entities, result));
+ return Observable.just(
+ fetchChallengesCompleted(
+ createNameIdMap(entities),
+ result
+ )
+ );
})
.catch(createErrorObserable);
});
diff --git a/server/services/user.js b/server/services/user.js
index 51871ae503..378bc6e00f 100644
--- a/server/services/user.js
+++ b/server/services/user.js
@@ -1,11 +1,29 @@
-import debugFactory from 'debug';
+import debug from 'debug';
+import _ from 'lodash';
-const censor = '**********************:P********';
-const debug = debugFactory('fcc:services:user');
-const protectedUserFields = {
- password: censor,
- profiles: censor
-};
+const publicUserProps = [
+ 'id',
+ 'name',
+ 'username',
+ 'bio',
+ 'theme',
+ 'picture',
+ 'points',
+ 'languageTag',
+
+ 'isCheater',
+ 'isGithubCool',
+
+ 'isFrontEndCert',
+ 'isBackEndCert',
+ 'isDataVisCert',
+ 'isFullStackCert',
+
+ 'githubURL',
+ 'currentChallenge',
+ 'challengeMap'
+];
+const log = debug('fcc:services:user');
export default function userServices() {
return {
@@ -13,19 +31,26 @@ export default function userServices() {
read: (req, resource, params, config, cb) => {
let { user } = req;
if (user) {
- debug('user is signed in');
- // Zalgo!!!
- return process.nextTick(() => {
- cb(
- null,
- {
- ...user.toJSON(),
- ...protectedUserFields
- }
+ log('user is signed in');
+ return user.getChallengeMap$()
+ .map(challengeMap => ({ ...user.toJSON(), challengeMap }))
+ .subscribe(
+ user => cb(
+ null,
+ {
+ entities: {
+ user: {
+ [user.username]: _.pick(user, publicUserProps)
+ }
+ },
+ result: user.username
+ }
+ ),
+ cb
);
- });
}
debug('user is not signed in');
+ // Zalgo!!!
return process.nextTick(() => {
cb(null, {});
});