feat: Create and use dynamic active donation counts

This commit is contained in:
Bouncey
2018-11-29 14:24:17 +00:00
committed by mrugesh mohapatra
parent 331ea3ebf9
commit 8fab33ba99
6 changed files with 111 additions and 58 deletions

View File

@ -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;

View File

@ -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$;
}

View File

@ -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 (
<Fragment>
<FullWidthRow>
@ -16,10 +19,10 @@ function Supporters({ isDonating }) {
</FullWidthRow>
<FullWidthRow>
<div id='supporter-progress-wrapper'>
<ProgressBar max={10000} now={400} />
<ProgressBar max={10000} now={activeDonations} />
<div id='progress-label-wrapper'>
<span className='progress-label'>
4000 supporters out of 10,000 goal
{activeDonations} supporters out of 10,000 goal
</span>
</div>
</div>

View File

@ -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({
</Col>
</Row>
<Spacer />
<Supporters isDonating={isDonating} />
<Supporters
activeDonations={activeDonations}
isDonating={isDonating}
/>
<Spacer size={2} />
<Row>
<Col sm={8} smOffset={2} xs={12}>

View File

@ -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));
}

View File

@ -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 }) => ({