Feature(challenges): save users current challenge to db

This allows us to automatically load their current challenge
This commit is contained in:
Berkeley Martinez
2016-08-03 15:26:05 -07:00
parent 42de7c57ef
commit 2b32fb3633
6 changed files with 83 additions and 3 deletions

View File

@ -61,6 +61,10 @@ export const addUser = createAction(
export const updateThisUser = createAction(types.updateThisUser); export const updateThisUser = createAction(types.updateThisUser);
export const showSignIn = createAction(types.showSignIn); export const showSignIn = createAction(types.showSignIn);
export const loadCurrentChallenge = createAction(types.loadCurrentChallenge); export const loadCurrentChallenge = createAction(types.loadCurrentChallenge);
export const updateMyCurrentChallenge = createAction(
types.updateMyCurrentChallenge,
(username, currentChallengeId) => ({ username, currentChallengeId })
);
// updateUserPoints(username: String, points: Number) => Action // updateUserPoints(username: String, points: Number) => Action
export const updateUserPoints = createAction( export const updateUserPoints = createAction(

View File

@ -85,5 +85,18 @@ export default function entities(state = initialState, action) {
} }
}; };
} }
if (action.type === types.updateMyCurrentChallenge) {
return {
...state,
user: {
...state.user,
[username]: {
...state.user[username],
currentChallengeId: action.payload.currentChallengeId
}
}
};
}
return state; return state;
} }

View File

@ -1,15 +1,51 @@
import { Observable } from 'rx'; import { Observable } from 'rx';
import debug from 'debug';
import { push } from 'react-router-redux'; import { push } from 'react-router-redux';
import types from './types'; import types from './types';
import {
updateMyCurrentChallenge,
createErrorObservable
} from './actions';
import { import {
userSelector, userSelector,
firstChallengeSelector firstChallengeSelector
} from './selectors'; } from './selectors';
import getActionsOfType from '../../utils/get-actions-of-type';
import { updateCurrentChallenge } from '../routes/challenges/redux/actions'; import { updateCurrentChallenge } from '../routes/challenges/redux/actions';
import getActionsOfType from '../../utils/get-actions-of-type';
import combineSagas from '../utils/combine-sagas';
import { postJSON$ } from '../../utils/ajax-stream';
export default function loadCurrentChallengeSaga(actions, getState) { const log = debug('fcc:app/redux/load-current-challenge-saga');
export function updateMyCurrentChallengeSaga(actions, getState) {
const updateChallenge$ = getActionsOfType(
actions,
updateCurrentChallenge.toString()
)
.map(({ payload: { id } }) => id)
.filter(() => {
const { app: { user: username } } = getState();
return !!username;
});
const optimistic = updateChallenge$.map(id => {
const { app: { user: username } } = getState();
return updateMyCurrentChallenge(username, id);
});
const ajaxUpdate = updateChallenge$
.debounce(250)
.flatMapLatest(currentChallengeId => {
const { app: { csrfToken: _csrf } } = getState();
return postJSON$(
'/update-my-current-challenge',
{ _csrf, currentChallengeId }
)
.map(({ message }) => log(message))
.catch(createErrorObservable);
});
return Observable.merge(optimistic, ajaxUpdate);
}
export function loadCurrentChallengeSaga(actions, getState) {
return getActionsOfType(actions, types.loadCurrentChallenge) return getActionsOfType(actions, types.loadCurrentChallenge)
.flatMap(() => { .flatMap(() => {
let finalChallenge; let finalChallenge;
@ -40,3 +76,8 @@ export default function loadCurrentChallengeSaga(actions, getState) {
); );
}); });
} }
export default combineSagas(
updateMyCurrentChallengeSaga,
loadCurrentChallengeSaga
);

View File

@ -15,6 +15,7 @@ export default createTypes([
'updateCompletedChallenges', 'updateCompletedChallenges',
'showSignIn', 'showSignIn',
'loadCurrentChallenge', 'loadCurrentChallenge',
'updateMyCurrentChallenge',
'handleError', 'handleError',
'toggleNightMode', 'toggleNightMode',

View File

@ -1,4 +1,5 @@
import { ifNoUser401 } from '../utils/middleware'; import { ifNoUser401 } from '../utils/middleware';
import { isMongoId } from 'validator';
import supportedLanguages from '../../common/utils/supported-languages.js'; import supportedLanguages from '../../common/utils/supported-languages.js';
export default function settingsController(app) { export default function settingsController(app) {
@ -49,6 +50,20 @@ export default function settingsController(app) {
); );
} }
function updateMyCurrentChallenge(req, res, next) {
const { user, body: { currentChallengeId } } = req;
if (!isMongoId('' + currentChallengeId)) {
return next(new Error(`${currentChallengeId} is not a valid ObjectId`));
}
return user.update$({ currentChallengeId }).subscribe(
() => res.json({
message:
`your current challenge has been updated to ${currentChallengeId}`
}),
next
);
}
api.post( api.post(
'/toggle-lockdown', '/toggle-lockdown',
toggleUserFlag('isLocked') toggleUserFlag('isLocked')
@ -78,5 +93,11 @@ export default function settingsController(app) {
ifNoUser401, ifNoUser401,
updateMyLang updateMyLang
); );
api.post(
'/update-my-current-challenge',
ifNoUser401,
updateMyCurrentChallenge
);
app.use(api); app.use(api);
} }

View File

@ -26,7 +26,7 @@ const publicUserProps = [
'sendNotificationEmail', 'sendNotificationEmail',
'sendQuincyEmail', 'sendQuincyEmail',
'currentChallenge', 'currentChallengeId',
'challengeMap' 'challengeMap'
]; ];
const log = debug('fcc:services:user'); const log = debug('fcc:services:user');