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

View File

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

View File

@ -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({

View File

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

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

View File

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

View File

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

View File

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