diff --git a/api-server/server/boot/user.js b/api-server/server/boot/user.js index d34110e2cc..c4ef2903dc 100644 --- a/api-server/server/boot/user.js +++ b/api-server/server/boot/user.js @@ -15,57 +15,73 @@ import { ifNoUser401, ifNoUserRedirectTo } from '../utils/middleware'; const log = debugFactory('fcc:boot:user'); const sendNonUserToHome = ifNoUserRedirectTo(homeLocation); -module.exports = function bootUser(app) { +function bootUser(app) { const api = app.loopback.Router(); + const getSessionUser = createReadSessionUser(app); + const postReportUserProfile = createPostReportUserProfile(app); + const postDeleteAccount = createPostDeleteAccount(app); + api.get('/account', sendNonUserToHome, getAccount); api.get('/account/unlink/:social', sendNonUserToHome, getUnlinkSocial); - api.get('/user/get-session-user', readSessionUser); + api.get('/user/get-session-user', getSessionUser); - api.post('/account/delete', ifNoUser401, createPostDeleteAccount(app)); + api.post('/account/delete', ifNoUser401, postDeleteAccount); api.post('/account/reset-progress', ifNoUser401, postResetProgress); - api.post('/user/report-user/', ifNoUser401, createPostReportUserProfile(app)); + api.post('/user/report-user/', ifNoUser401, postReportUserProfile); app.use('/internal', api); -}; +} -function readSessionUser(req, res, next) { - const queryUser = req.user; +function createReadSessionUser(app) { + const { Donation } = app.models; - const source = - queryUser && - Observable.forkJoin( - queryUser.getCompletedChallenges$(), - queryUser.getPoints$(), - (completedChallenges, progressTimestamps) => ({ - completedChallenges, - progress: getProgress(progressTimestamps, queryUser.timezone) - }) - ); - Observable.if( - () => !queryUser, - Observable.of({ user: {}, result: '' }), - Observable.defer(() => source) - .map(({ completedChallenges, progress }) => ({ - ...queryUser.toJSON(), - ...progress, - completedChallenges: completedChallenges.map(fixCompletedChallengeItem) - })) - .map(user => ({ - user: { - [user.username]: { - ...pick(user, userPropsForSession), - isEmailVerified: !!user.emailVerified, - isGithub: !!user.githubProfile, - isLinkedIn: !!user.linkedin, - isTwitter: !!user.twitter, - isWebsite: !!user.website, - ...normaliseUserFields(user) - } - }, - result: user.username - })) - ).subscribe(user => res.json(user), next); + return function getSessionUser(req, res, next) { + const queryUser = req.user; + + const source = + queryUser && + Observable.forkJoin( + queryUser.getCompletedChallenges$(), + queryUser.getPoints$(), + Donation.getCurrentActiveDonationCount$(), + (completedChallenges, progressTimestamps, activeDonations) => ({ + activeDonations, + completedChallenges, + progress: getProgress(progressTimestamps, queryUser.timezone) + }) + ); + Observable.if( + () => !queryUser, + Observable.of({ user: {}, result: '' }), + Observable.defer(() => source) + .map(({ activeDonations, completedChallenges, progress }) => ({ + user: { + ...queryUser.toJSON(), + ...progress, + completedChallenges: completedChallenges.map( + fixCompletedChallengeItem + ) + }, + sessionMeta: { activeDonations } + })) + .map(({ user, sessionMeta }) => ({ + user: { + [user.username]: { + ...pick(user, userPropsForSession), + isEmailVerified: !!user.emailVerified, + isGithub: !!user.githubProfile, + isLinkedIn: !!user.linkedin, + isTwitter: !!user.twitter, + isWebsite: !!user.website, + ...normaliseUserFields(user) + } + }, + sessionMeta, + result: user.username + })) + ).subscribe(user => res.json(user), next); + }; } function getAccount(req, res) { @@ -241,3 +257,4 @@ function createPostReportUserProfile(app) { ); }; } +export default bootUser; diff --git a/api-server/server/models/donation.js b/api-server/server/models/donation.js index 6ccf65a886..e605e43da6 100644 --- a/api-server/server/models/donation.js +++ b/api-server/server/models/donation.js @@ -2,14 +2,18 @@ import { Observable } from 'rx'; export default function(Donation) { Donation.on('dataSourceAttached', () => { + Donation.find$ = Observable.fromNodeCallback(Donation.find.bind(Donation)); Donation.findOne$ = Observable.fromNodeCallback( Donation.findOne.bind(Donation) ); - Donation.prototype.validate$ = Observable.fromNodeCallback( - Donation.prototype.validate - ); - Donation.prototype.destroy$ = Observable.fromNodeCallback( - Donation.prototype.destroy - ); }); + + function getCurrentActiveDonationCount$() { + // eslint-disable-next-line no-undefined + return Donation.find$({ where: { endDate: undefined } }).map( + instances => instances.length + ); + } + + Donation.getCurrentActiveDonationCount$ = getCurrentActiveDonationCount$; } diff --git a/client/src/components/Supporters.js b/client/src/components/Supporters.js index 46ab966c5d..3ed5de7cd5 100644 --- a/client/src/components/Supporters.js +++ b/client/src/components/Supporters.js @@ -6,9 +6,12 @@ import FullWidthRow from '../components/helpers/FullWidthRow'; import './supporters.css'; -const propTypes = { isDonating: PropTypes.bool.isRequired }; +const propTypes = { + activeDonations: PropTypes.number.isRequired, + isDonating: PropTypes.bool.isRequired +}; -function Supporters({ isDonating }) { +function Supporters({ isDonating, activeDonations }) { return ( @@ -16,10 +19,10 @@ function Supporters({ isDonating }) {
- +
- 4000 supporters out of 10,000 goal + {activeDonations} supporters out of 10,000 goal
diff --git a/client/src/pages/welcome.js b/client/src/pages/welcome.js index ff4e190b57..29eb53d5d3 100644 --- a/client/src/pages/welcome.js +++ b/client/src/pages/welcome.js @@ -14,13 +14,15 @@ import Supporters from '../components/Supporters'; import { userSelector, userFetchStateSelector, - isSignedInSelector + isSignedInSelector, + activeDonationsSelector } from '../redux'; import { randomQuote } from '../utils/get-words'; import './welcome.css'; const propTypes = { + activedonations: PropTypes.number, fetchState: PropTypes.shape({ pending: PropTypes.bool, complete: PropTypes.bool, @@ -42,7 +44,13 @@ const mapStateToProps = createSelector( userFetchStateSelector, isSignedInSelector, userSelector, - (fetchState, isSignedIn, user) => ({ fetchState, isSignedIn, user }) + activeDonationsSelector, + (fetchState, isSignedIn, user, activeDonations) => ({ + activeDonations, + fetchState, + isSignedIn, + user + }) ); const mapDispatchToProps = dispatch => bindActionCreators({}, dispatch); @@ -57,7 +65,8 @@ function Welcome({ completedCertCount = 0, completedLegacyCertCount: completedLegacyCerts = 0, isDonating - } + }, + activeDonations }) { if (pending && !complete) { return ( @@ -94,7 +103,10 @@ function Welcome({ - + diff --git a/client/src/redux/fetch-user-saga.js b/client/src/redux/fetch-user-saga.js index 86babd45d0..4f6dc681c4 100644 --- a/client/src/redux/fetch-user-saga.js +++ b/client/src/redux/fetch-user-saga.js @@ -16,10 +16,12 @@ function* fetchSessionUser() { } try { const { - data: { user = {}, result = '' } + data: { user = {}, result = '', sessionMeta = {} } } = yield call(getSessionUser); const appUser = user[result] || {}; - yield put(fetchUserComplete({ user: appUser, username: result })); + yield put( + fetchUserComplete({ user: appUser, username: result, sessionMeta }) + ); } catch (e) { yield put(fetchUserError(e)); } diff --git a/client/src/redux/index.js b/client/src/redux/index.js index 142c090a6d..cd4483b61b 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -39,6 +39,7 @@ const initialState = { userProfileFetchState: { ...defaultFetchState }, + sessionMeta: {}, showDonationModal: false, isOnline: true }; @@ -158,6 +159,13 @@ export const userSelector = state => { return state[ns].user[username] || {}; }; +export const sessionMetaSelector = state => state[ns].sessionMeta; +export const activeDonationsSelector = state => +// this default is mostly for development where there are likely no donators +// in the local db +// If we see this in production then things are getting weird + sessionMetaSelector(state).activeDonations || 4040; + function spreadThePayloadOnUser(state, payload) { return { ...state, @@ -181,7 +189,10 @@ export const reducer = handleActions( ...state, userProfileFetchState: { ...defaultFetchState } }), - [types.fetchUserComplete]: (state, { payload: { user, username } }) => ({ + [types.fetchUserComplete]: ( + state, + { payload: { user, username, sessionMeta } } + ) => ({ ...state, user: { ...state.user, @@ -193,6 +204,10 @@ export const reducer = handleActions( complete: true, errored: false, error: null + }, + sessionMeta: { + ...state.sessionMeta, + ...sessionMeta } }), [types.fetchUserError]: (state, { payload }) => ({