Add React Map
This commit is contained in:
parent
844afb6e2f
commit
c909cd032e
@ -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;
|
||||||
|
@ -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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
|
@ -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
|
||||||
|
127
common/app/routes/map/components/Map.jsx
Normal file
127
common/app/routes/map/components/Map.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
64
common/app/routes/map/components/Show.jsx
Normal file
64
common/app/routes/map/components/Show.jsx
Normal 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);
|
0
common/app/routes/map/components/Static-Blocks.jsx
Normal file
0
common/app/routes/map/components/Static-Blocks.jsx
Normal file
6
common/app/routes/map/index.js
Normal file
6
common/app/routes/map/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import ShowMap from './components/Show.jsx';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
path: 'map',
|
||||||
|
component: ShowMap
|
||||||
|
};
|
10
common/app/routes/map/redux/actions.js
Normal file
10
common/app/routes/map/redux/actions.js
Normal 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 })
|
||||||
|
);
|
26
common/app/routes/map/redux/fetch-challenges-saga.js
Normal file
26
common/app/routes/map/redux/fetch-challenges-saga.js
Normal 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);
|
||||||
|
};
|
||||||
|
};
|
6
common/app/routes/map/redux/index.js
Normal file
6
common/app/routes/map/redux/index.js
Normal 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 ];
|
17
common/app/routes/map/redux/reducer.js
Normal file
17
common/app/routes/map/redux/reducer.js
Normal 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
|
||||||
|
);
|
6
common/app/routes/map/redux/types.js
Normal file
6
common/app/routes/map/redux/types.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import createTypes from '../../../utils/create-types';
|
||||||
|
|
||||||
|
export default createTypes([
|
||||||
|
'fetchChallenges',
|
||||||
|
'fetchChallengesCompleted'
|
||||||
|
], 'map');
|
@ -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
|
||||||
];
|
];
|
||||||
|
@ -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": [],
|
||||||
|
@ -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)
|
||||||
|
@ -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');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user