Add React Map

This commit is contained in:
Berkeley Martinez 2016-03-21 15:39:45 -07:00
parent 844afb6e2f
commit c909cd032e
17 changed files with 313 additions and 39 deletions

View File

@ -125,10 +125,11 @@
} }
} }
#map-filter { .map-filter {
background:#fff; background:#fff;
border-color: darkgreen; border-color: darkgreen;
} }
.input-group-addon { .input-group-addon {
width:40px; width:40px;
color: darkgreen; color: darkgreen;
@ -149,22 +150,29 @@
} }
} }
.mapWrapper { .map-wrapper {
position: absolute;
display: block; display: block;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.map-accordion { .map-accordion {
width:700px; width: 700px;
margin:155px auto 0; margin: 180px auto 0;
position:relative; position: relative;
#nested {
margin:0 10px; .map-accordion-panel-nested {
margin: 0 20px;
@media (max-width: 400px) { @media (max-width: 400px) {
margin:0; margin:0;
} }
} }
.map-accordion-panel-title {
padding-bottom: 0px
}
a:focus { a:focus {
text-decoration: none; text-decoration: none;
color:darkgreen; color:darkgreen;
@ -182,13 +190,15 @@
padding-right:20px; padding-right:20px;
} }
h3 { a {
margin:15px 0; margin:15px 0;
padding:0; padding:0;
&:first-child { &:first-child {
margin-top:25px margin-top:25px
} }
> a {
> h3 {
padding-left: 40px; padding-left: 40px;
padding-bottom: 10px; padding-bottom: 10px;
display:block; display:block;
@ -196,10 +206,11 @@
} }
} }
div.chapterBlock { .map-accordion-block {
:before { :before {
margin-right: 15px; margin-right: 15px;
} }
p { p {
text-indent: -15px; text-indent: -15px;
margin-left: 60px; margin-left: 60px;
@ -210,7 +221,7 @@
} }
} }
.challengeBlockDescription { .challenge-block-description {
margin:0; margin:0;
margin-top:-10px; margin-top:-10px;
padding:0 15px 23px 30px; padding:0 15px 23px 30px;
@ -223,9 +234,9 @@
} }
div > div:last-child { div > div:last-child {
margin-bottom:30px margin-bottom:30px
} }
} }
.challengeBlockTime { .challenge-block-time {
font-size: 18px; font-size: 18px;
color: #BBBBBB; color: #BBBBBB;
display:block; display:block;

View File

@ -2,22 +2,24 @@ import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form'; import { reducer as formReducer } from 'redux-form';
import { reducer as app } from './redux'; 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 hikesApp } from './routes/Hikes/redux';
import { reducer as challengesApp } from './routes/challenges/redux'; import { reducer as challengesApp } from './routes/challenges/redux';
import { import {
reducer as jobsApp, reducer as jobsApp,
formNormalizer as jobsNormalizer formNormalizer as jobsNormalizer
} from './routes/Jobs/redux'; } from './routes/Jobs/redux';
import { reducer as map } from './routes/map/redux';
export default function createReducer(sideReducers = {}) { export default function createReducer(sideReducers = {}) {
return combineReducers({ return combineReducers({
...sideReducers, ...sideReducers,
entities: entitieReducer, entities: entitiesReducer,
app, app,
hikesApp, hikesApp,
jobsApp, jobsApp,
challengesApp, challengesApp,
map,
form: formReducer.normalize(jobsNormalizer) form: formReducer.normalize(jobsNormalizer)
}); });
} }

View File

@ -8,25 +8,10 @@ export default ({ services }) => ({ dispatch }) => next => {
} }
return services.readService$({ service: 'user' }) return services.readService$({ service: 'user' })
.map(({ .map((user) => {
username,
picture,
points,
isFrontEndCert,
isBackEndCert,
isFullStackCert
}) => {
return { return {
type: setUser, type: setUser,
payload: { payload: user
username,
picture,
points,
isFrontEndCert,
isBackEndCert,
isFullStackCert,
isSignedIn: true
}
}; };
}) })
.catch(error => Observable.just({ .catch(error => Observable.just({

View File

@ -1,6 +1,7 @@
import Jobs from './Jobs'; import Jobs from './Jobs';
import Hikes from './Hikes'; import Hikes from './Hikes';
import Challenges from './challenges'; import challenges from './challenges';
import map from './map';
import NotFound from '../components/NotFound/index.jsx'; import NotFound from '../components/NotFound/index.jsx';
export default { export default {
@ -8,7 +9,8 @@ export default {
childRoutes: [ childRoutes: [
Jobs, Jobs,
Hikes, Hikes,
Challenges, challenges,
map,
{ {
path: '*', path: '*',
component: NotFound component: NotFound

View File

@ -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 <div>No Challenges Found</div>;
}
return challenges.map(challenge => {
const { title, dashedName } = challenge;
return (
<p
className={ challengeClassName }
key={ title }>
<a href={ `/challenges/${dashedName}` }>
{ title }
<span className='sr-only'>complete</span>
</a>
</p>
);
});
}
renderBlocks(blocks) {
if (!Array.isArray(blocks) || !blocks.length) {
return <div>No Blocks Found</div>;
}
return blocks.map(block => {
const { title, time, challenges } = block;
return (
<Panel
bsClass='map-accordion-panel-nested'
collapsible={ true }
expanded={ true }
header={
<div>
<h3><FA name='caret-right' />{ title }</h3>
<span className='challenge-block-time'>({ time })</span>
</div>
}
id={ title }
key={ title }>
{ this.renderChallenges(challenges) }
</Panel>
);
});
}
renderSuperBlocks(superBlocks) {
if (!Array.isArray(superBlocks) || !superBlocks.length) {
return <div>No Super Blocks</div>;
}
return superBlocks.map((superBlock) => {
const { title, blocks } = superBlock;
return (
<Panel
bsClass='map-accordion-panel'
collapsible={ true }
expanded={ true }
header={ <h2><FA name='caret-right' />{ title }</h2> }
id={ title }
key={ title }>
<div
className='map-accordion-block'>
{ this.renderBlocks(blocks) }
</div>
</Panel>
);
});
}
render() {
const { superBlocks } = this.props;
return (
<div>
<div className='map-wrapper'>
<div
className='text-center map-fixed-header'
style={{ top: '50px' }}>
<p>Challenges required for certifications are marked with a *</p>
<Row className='map-buttons'>
<Button
block={ true }
bsStyle='primary'
className='active center-block'>
Collapse all challenges
</Button>
</Row>
<Row className='map-buttons'>
<Input
addonAfter={ <span><i className='fa fa-search' /></span> }
autocompleted='off'
className='map-filter'
placeholder='Type a challenge name'
type='text' />
</Row>
<hr />
</div>
</div>
<div
className='map-accordion'>
{ this.renderSuperBlocks(superBlocks) }
</div>
</div>
);
}
}

View File

@ -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 (
<Map superBlocks={ superBlocks } />
);
}
}
export default compose(
connect(mapStateToProps, bindableActions),
contain(fetchOptions)
)(ShowMap);

View File

@ -0,0 +1,6 @@
import ShowMap from './components/Show.jsx';
export default {
path: 'map',
component: ShowMap
};

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
import createTypes from '../../../utils/create-types';
export default createTypes([
'fetchChallenges',
'fetchChallengesCompleted'
], 'map');

View File

@ -1,9 +1,11 @@
import { sagas as appSagas } from './redux'; import { sagas as appSagas } from './redux';
import { sagas as hikesSagas} from './routes/Hikes/redux'; import { sagas as hikesSagas} from './routes/Hikes/redux';
import { sagas as jobsSagas } from './routes/Jobs/redux'; import { sagas as jobsSagas } from './routes/Jobs/redux';
import { sagas as mapSagas } from './routes/map/redux';
export default [ export default [
...appSagas, ...appSagas,
...hikesSagas, ...hikesSagas,
...jobsSagas ...jobsSagas,
...mapSagas
]; ];

View File

@ -34,6 +34,10 @@
"type": "string", "type": "string",
"required": true, "required": true,
"description": "The title of this block, suitable for display" "description": "The title of this block, suitable for display"
},
"time": {
"type": "string",
"required": true
} }
}, },
"validations": [], "validations": [],

View File

@ -39,6 +39,7 @@ Observable.combineLatest(
var isComingSoon = !!challengeSpec.isComingSoon; var isComingSoon = !!challengeSpec.isComingSoon;
var fileName = challengeSpec.fileName; var fileName = challengeSpec.fileName;
var helpRoom = challengeSpec.helpRoom || 'Help'; var helpRoom = challengeSpec.helpRoom || 'Help';
var time = challengeSpec.time || 'N/A';
console.log('parsed %s successfully', blockName); console.log('parsed %s successfully', blockName);
@ -53,7 +54,8 @@ Observable.combineLatest(
dashedName: dasherize(blockName), dashedName: dasherize(blockName),
superOrder: superOrder, superOrder: superOrder,
superBlock: superBlock, superBlock: superBlock,
order: order order: order,
time: time
}; };
return createBlocks(block) return createBlocks(block)

View File

@ -1,10 +1,8 @@
import debugFactory from 'debug'; import debugFactory from 'debug';
import assign from 'object.assign';
const censor = '**********************:P********'; const censor = '**********************:P********';
const debug = debugFactory('fcc:services:user'); const debug = debugFactory('fcc:services:user');
const protectedUserFields = { const protectedUserFields = {
id: censor,
password: censor, password: censor,
profiles: censor profiles: censor
}; };
@ -18,7 +16,13 @@ export default function userServices() {
debug('user is signed in'); debug('user is signed in');
// Zalgo!!! // Zalgo!!!
return process.nextTick(() => { return process.nextTick(() => {
cb(null, assign({}, user.toJSON(), protectedUserFields)); cb(
null,
{
...user.toJSON(),
...protectedUserFields
}
);
}); });
} }
debug('user is not signed in'); debug('user is not signed in');