From c909cd032e47eab2c2932dee6743ea004067b4ee Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 21 Mar 2016 15:39:45 -0700 Subject: [PATCH] Add React Map --- client/less/map.less | 37 +++-- common/app/create-reducer.js | 6 +- common/app/redux/fetch-user-saga.js | 19 +-- common/app/routes/index.js | 6 +- common/app/routes/map/components/Map.jsx | 127 ++++++++++++++++++ common/app/routes/map/components/Show.jsx | 64 +++++++++ .../routes/map/components/Static-Blocks.jsx | 0 common/app/routes/map/index.js | 6 + common/app/routes/map/redux/actions.js | 10 ++ .../routes/map/redux/fetch-challenges-saga.js | 26 ++++ common/app/routes/map/redux/index.js | 6 + common/app/routes/map/redux/reducer.js | 17 +++ common/app/routes/map/redux/types.js | 6 + common/app/sagas.js | 4 +- common/models/block.json | 4 + seed/index.js | 4 +- server/services/user.js | 10 +- 17 files changed, 313 insertions(+), 39 deletions(-) create mode 100644 common/app/routes/map/components/Map.jsx create mode 100644 common/app/routes/map/components/Show.jsx create mode 100644 common/app/routes/map/components/Static-Blocks.jsx create mode 100644 common/app/routes/map/index.js create mode 100644 common/app/routes/map/redux/actions.js create mode 100644 common/app/routes/map/redux/fetch-challenges-saga.js create mode 100644 common/app/routes/map/redux/index.js create mode 100644 common/app/routes/map/redux/reducer.js create mode 100644 common/app/routes/map/redux/types.js diff --git a/client/less/map.less b/client/less/map.less index bfaeedfcac..3ea364e70e 100644 --- a/client/less/map.less +++ b/client/less/map.less @@ -125,10 +125,11 @@ } } -#map-filter { +.map-filter { background:#fff; border-color: darkgreen; } + .input-group-addon { width:40px; color: darkgreen; @@ -149,22 +150,29 @@ } } -.mapWrapper { +.map-wrapper { + position: absolute; display: block; height: 100%; width: 100%; } .map-accordion { - width:700px; - margin:155px auto 0; - position:relative; - #nested { - margin:0 10px; + width: 700px; + margin: 180px auto 0; + position: relative; + + .map-accordion-panel-nested { + margin: 0 20px; @media (max-width: 400px) { margin:0; } } + + .map-accordion-panel-title { + padding-bottom: 0px + } + a:focus { text-decoration: none; color:darkgreen; @@ -182,13 +190,15 @@ padding-right:20px; } - h3 { + a { margin:15px 0; padding:0; + &:first-child { margin-top:25px } - > a { + + > h3 { padding-left: 40px; padding-bottom: 10px; display:block; @@ -196,10 +206,11 @@ } } - div.chapterBlock { + .map-accordion-block { :before { margin-right: 15px; } + p { text-indent: -15px; margin-left: 60px; @@ -210,7 +221,7 @@ } } - .challengeBlockDescription { + .challenge-block-description { margin:0; margin-top:-10px; padding:0 15px 23px 30px; @@ -223,9 +234,9 @@ } div > div:last-child { margin-bottom:30px - } + } } -.challengeBlockTime { +.challenge-block-time { font-size: 18px; color: #BBBBBB; display:block; diff --git a/common/app/create-reducer.js b/common/app/create-reducer.js index e18719e851..a42d4f8aac 100644 --- a/common/app/create-reducer.js +++ b/common/app/create-reducer.js @@ -2,22 +2,24 @@ import { combineReducers } from 'redux'; import { reducer as formReducer } from 'redux-form'; import { reducer as app } from './redux'; -import entitieReducer from './redux/entities-reducer'; +import entitiesReducer from './redux/entities-reducer'; import { reducer as hikesApp } from './routes/Hikes/redux'; import { reducer as challengesApp } from './routes/challenges/redux'; import { reducer as jobsApp, formNormalizer as jobsNormalizer } from './routes/Jobs/redux'; +import { reducer as map } from './routes/map/redux'; export default function createReducer(sideReducers = {}) { return combineReducers({ ...sideReducers, - entities: entitieReducer, + entities: entitiesReducer, app, hikesApp, jobsApp, challengesApp, + map, form: formReducer.normalize(jobsNormalizer) }); } diff --git a/common/app/redux/fetch-user-saga.js b/common/app/redux/fetch-user-saga.js index 83b1fdf0ba..63060eb844 100644 --- a/common/app/redux/fetch-user-saga.js +++ b/common/app/redux/fetch-user-saga.js @@ -8,25 +8,10 @@ export default ({ services }) => ({ dispatch }) => next => { } return services.readService$({ service: 'user' }) - .map(({ - username, - picture, - points, - isFrontEndCert, - isBackEndCert, - isFullStackCert - }) => { + .map((user) => { return { type: setUser, - payload: { - username, - picture, - points, - isFrontEndCert, - isBackEndCert, - isFullStackCert, - isSignedIn: true - } + payload: user }; }) .catch(error => Observable.just({ diff --git a/common/app/routes/index.js b/common/app/routes/index.js index d975f7e78b..875ea4d14b 100644 --- a/common/app/routes/index.js +++ b/common/app/routes/index.js @@ -1,6 +1,7 @@ import Jobs from './Jobs'; import Hikes from './Hikes'; -import Challenges from './challenges'; +import challenges from './challenges'; +import map from './map'; import NotFound from '../components/NotFound/index.jsx'; export default { @@ -8,7 +9,8 @@ export default { childRoutes: [ Jobs, Hikes, - Challenges, + challenges, + map, { path: '*', component: NotFound diff --git a/common/app/routes/map/components/Map.jsx b/common/app/routes/map/components/Map.jsx new file mode 100644 index 0000000000..aea717bba7 --- /dev/null +++ b/common/app/routes/map/components/Map.jsx @@ -0,0 +1,127 @@ +import React, { PropTypes } from 'react'; +import FA from 'react-fontawesome'; +import PureComponent from 'react-pure-render/component'; +import { + Input, + Button, + Row, + Panel +} from 'react-bootstrap'; + +const challengeClassName = ` + text-primary + padded-ionic-icon + negative-15 + challenge-title + ion-checkmark-circled +`.replace(/[\n]/g, ''); + +export default class ShowMap extends PureComponent { + static displayName = 'Map'; + static propTypes = { + superBlocks: PropTypes.array + }; + + renderChallenges(challenges) { + if (!Array.isArray(challenges) || !challenges.length) { + return
No Challenges Found
; + } + return challenges.map(challenge => { + const { title, dashedName } = challenge; + return ( +

+ + { title } + complete + +

+ ); + }); + } + + renderBlocks(blocks) { + if (!Array.isArray(blocks) || !blocks.length) { + return
No Blocks Found
; + } + return blocks.map(block => { + const { title, time, challenges } = block; + return ( + +

{ title }

+ ({ time }) + + } + id={ title } + key={ title }> + { this.renderChallenges(challenges) } +
+ ); + }); + } + + renderSuperBlocks(superBlocks) { + if (!Array.isArray(superBlocks) || !superBlocks.length) { + return
No Super Blocks
; + } + return superBlocks.map((superBlock) => { + const { title, blocks } = superBlock; + return ( + { title } } + id={ title } + key={ title }> +
+ { this.renderBlocks(blocks) } +
+
+ ); + }); + } + + render() { + const { superBlocks } = this.props; + return ( +
+
+
+

Challenges required for certifications are marked with a *

+ + + + + } + autocompleted='off' + className='map-filter' + placeholder='Type a challenge name' + type='text' /> + +
+
+
+
+ { this.renderSuperBlocks(superBlocks) } +
+
+ ); + } +} diff --git a/common/app/routes/map/components/Show.jsx b/common/app/routes/map/components/Show.jsx new file mode 100644 index 0000000000..aa54705d95 --- /dev/null +++ b/common/app/routes/map/components/Show.jsx @@ -0,0 +1,64 @@ +import React, { PropTypes } from 'react'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import PureComponent from 'react-pure-render/component'; +import { createSelector } from 'reselect'; + +import Map from './Map.jsx'; +import contain from '../../../utils/professor-x'; +import { fetchChallenges } from '../redux/actions'; + +const bindableActions = { fetchChallenges }; +const mapStateToProps = createSelector( + state => state.map.superBlocks, + state => state.entities.superBlock, + state => state.entities.block, + state => state.entities.challenge, + (superBlocks, superBlockMap, blockMap, challengeMap) => { + if (!superBlockMap || !blockMap || !challengeMap) { + return { + superBlocks: [] + }; + } + const finalBlocks = superBlocks + .map(superBlockName => superBlockMap[superBlockName]) + .map(superBlock => ({ + ...superBlock, + blocks: superBlock.blocks + .map(blockName => blockMap[blockName]) + .map(block => ({ + ...block, + challenges: block.challenges + .map(dashedName => challengeMap[dashedName]) + })) + })); + return { + superBlocks: finalBlocks + }; + } +); +const fetchOptions = { + fetchAction: 'fetchChallenges', + isPrimed({ superBlocks }) { + return Array.isArray(superBlocks) && superBlocks.length > 0; + } +}; + +export class ShowMap extends PureComponent { + static displayName = 'ShowMap'; + static propTypes = { + superBlocks: PropTypes.array + }; + + render() { + const { superBlocks } = this.props; + return ( + + ); + } +} + +export default compose( + connect(mapStateToProps, bindableActions), + contain(fetchOptions) +)(ShowMap); diff --git a/common/app/routes/map/components/Static-Blocks.jsx b/common/app/routes/map/components/Static-Blocks.jsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/app/routes/map/index.js b/common/app/routes/map/index.js new file mode 100644 index 0000000000..f1fbc4bf70 --- /dev/null +++ b/common/app/routes/map/index.js @@ -0,0 +1,6 @@ +import ShowMap from './components/Show.jsx'; + +export default { + path: 'map', + component: ShowMap +}; diff --git a/common/app/routes/map/redux/actions.js b/common/app/routes/map/redux/actions.js new file mode 100644 index 0000000000..fd8b86fec0 --- /dev/null +++ b/common/app/routes/map/redux/actions.js @@ -0,0 +1,10 @@ +import { createAction } from 'redux-actions'; + +import types from './types'; + +export const fetchChallenges = createAction(types.fetchChallenges); +export const fetchChallengesCompleted = createAction( + types.fetchChallengesCompleted, + (_, superBlocks) => superBlocks, + entities => ({ entities }) +); diff --git a/common/app/routes/map/redux/fetch-challenges-saga.js b/common/app/routes/map/redux/fetch-challenges-saga.js new file mode 100644 index 0000000000..067f788b0b --- /dev/null +++ b/common/app/routes/map/redux/fetch-challenges-saga.js @@ -0,0 +1,26 @@ +import { Observable } from 'rx'; +import { fetchChallenges } from './types'; +import { fetchChallengesCompleted } from './actions'; + +import { handleError } from '../../../redux/types'; + +export default ({ services }) => ({ dispatch }) => next => { + return function fetchChallengesSaga(action) { + const result = next(action); + if (action.type !== fetchChallenges) { + return result; + } + + return services.readService$({ service: 'map' }) + .map(({ entities, result } = {}) => { + return fetchChallengesCompleted(entities, result); + }) + .catch(error => { + return Observable.just({ + type: handleError, + error + }); + }) + .doOnNext(dispatch); + }; +}; diff --git a/common/app/routes/map/redux/index.js b/common/app/routes/map/redux/index.js new file mode 100644 index 0000000000..79b8c842e4 --- /dev/null +++ b/common/app/routes/map/redux/index.js @@ -0,0 +1,6 @@ +export actions from './actions'; +export reducer from './reducer'; +export types from './types'; + +import fetchChallengesSaga from './fetch-challenges-saga'; +export const sagas = [ fetchChallengesSaga ]; diff --git a/common/app/routes/map/redux/reducer.js b/common/app/routes/map/redux/reducer.js new file mode 100644 index 0000000000..575171aade --- /dev/null +++ b/common/app/routes/map/redux/reducer.js @@ -0,0 +1,17 @@ +import { handleActions } from 'redux-actions'; + +import types from './types'; + +const initialState = { + superBlocks: [] +}; + +export default handleActions( + { + [types.fetchChallengesCompleted]: (state, { payload = [] }) => ({ + ...state, + superBlocks: payload + }) + }, + initialState +); diff --git a/common/app/routes/map/redux/types.js b/common/app/routes/map/redux/types.js new file mode 100644 index 0000000000..51d8fbd687 --- /dev/null +++ b/common/app/routes/map/redux/types.js @@ -0,0 +1,6 @@ +import createTypes from '../../../utils/create-types'; + +export default createTypes([ + 'fetchChallenges', + 'fetchChallengesCompleted' +], 'map'); diff --git a/common/app/sagas.js b/common/app/sagas.js index cfd486242b..f1964d402a 100644 --- a/common/app/sagas.js +++ b/common/app/sagas.js @@ -1,9 +1,11 @@ import { sagas as appSagas } from './redux'; import { sagas as hikesSagas} from './routes/Hikes/redux'; import { sagas as jobsSagas } from './routes/Jobs/redux'; +import { sagas as mapSagas } from './routes/map/redux'; export default [ ...appSagas, ...hikesSagas, - ...jobsSagas + ...jobsSagas, + ...mapSagas ]; diff --git a/common/models/block.json b/common/models/block.json index 222fe0c8a6..ff96ef5b12 100644 --- a/common/models/block.json +++ b/common/models/block.json @@ -34,6 +34,10 @@ "type": "string", "required": true, "description": "The title of this block, suitable for display" + }, + "time": { + "type": "string", + "required": true } }, "validations": [], diff --git a/seed/index.js b/seed/index.js index b5f4e11dab..f7fe8d2b98 100644 --- a/seed/index.js +++ b/seed/index.js @@ -39,6 +39,7 @@ Observable.combineLatest( var isComingSoon = !!challengeSpec.isComingSoon; var fileName = challengeSpec.fileName; var helpRoom = challengeSpec.helpRoom || 'Help'; + var time = challengeSpec.time || 'N/A'; console.log('parsed %s successfully', blockName); @@ -53,7 +54,8 @@ Observable.combineLatest( dashedName: dasherize(blockName), superOrder: superOrder, superBlock: superBlock, - order: order + order: order, + time: time }; return createBlocks(block) diff --git a/server/services/user.js b/server/services/user.js index a2816e818c..51871ae503 100644 --- a/server/services/user.js +++ b/server/services/user.js @@ -1,10 +1,8 @@ import debugFactory from 'debug'; -import assign from 'object.assign'; const censor = '**********************:P********'; const debug = debugFactory('fcc:services:user'); const protectedUserFields = { - id: censor, password: censor, profiles: censor }; @@ -18,7 +16,13 @@ export default function userServices() { debug('user is signed in'); // Zalgo!!! return process.nextTick(() => { - cb(null, assign({}, user.toJSON(), protectedUserFields)); + cb( + null, + { + ...user.toJSON(), + ...protectedUserFields + } + ); }); } debug('user is not signed in');