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');