Feature(map): Add top level collapse

This commit is contained in:
Berkeley Martinez
2016-06-21 16:28:13 -07:00
parent a50a56a1b6
commit b8434edd51
9 changed files with 163 additions and 47 deletions

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { PropTypes } from 'react';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import dedent from 'dedent'; import dedent from 'dedent';
import SuperBlock from './Super-Block.jsx'; import SuperBlock from './Super-Block.jsx';
@ -44,19 +44,25 @@ const codingPrep = [{
} }
] ]
}]; }];
const title = 'Coding Interview Prep';
const dashedName = 'coding-prep';
export default class CodingPrep extends PureComponent { export default class CodingPrep extends PureComponent {
static displayName = 'CodingPrep;' static displayName = 'CodingPrep;'
static propTypes = {
mapUi: PropTypes.object,
toggleThisPanel: PropTypes.func
};
render() { render() {
const { mapUi, toggleThisPanel } = this.props;
return ( return (
<div> <SuperBlock
<SuperBlock blocks={ codingPrep }
blocks={ codingPrep } dashedName={ dashedName }
message={ lockMessage } mapUi={ mapUi }
title='Coding Interview Prep ' message={ lockMessage }
/> title={ title }
</div> toggleThisPanel={ toggleThisPanel }
/>
); );
} }
} }

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { PropTypes } from 'react';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import dedent from 'dedent'; import dedent from 'dedent';
import SuperBlock from './Super-Block.jsx'; import SuperBlock from './Super-Block.jsx';
@ -40,15 +40,24 @@ const nonprofitProjects = {
] ]
}; };
const title = 'Full Stack Development Certification';
const dashedName = 'full-stack';
export default class FullStack extends PureComponent { export default class FullStack extends PureComponent {
static displayName = 'FullStack'; static displayName = 'FullStack';
static propTypes = {
mapUi: PropTypes.object,
toggleThisPanel: PropTypes.func
};
render() { render() {
const title = 'Full Stack Development Certification'; const { mapUi, toggleThisPanel } = this.props;
return ( return (
<SuperBlock <SuperBlock
blocks={ [ nonprofitProjects ] } blocks={ [ nonprofitProjects ] }
dashedName={ dashedName }
mapUi={ mapUi }
message={ lockMessage } message={ lockMessage }
title={ title } title={ title }
toggleThisPanel={ toggleThisPanel }
/> />
); );
} }

View File

@ -1,11 +1,12 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import PureComponent from 'react-pure-render/component';
import { InputGroup, FormControl, Button, Row } from 'react-bootstrap'; import { InputGroup, FormControl, Button, Row } from 'react-bootstrap';
import classnames from 'classnames'; import classnames from 'classnames';
const clearIcon = <i className='fa fa-times' />; const clearIcon = <i className='fa fa-times' />;
const searchIcon = <i className='fa fa-search' />; const searchIcon = <i className='fa fa-search' />;
const ESC = 27; const ESC = 27;
export default class Header extends React.Component { export default class Header extends PureComponent {
constructor(...props) { constructor(...props) {
super(...props); super(...props);
this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this);

View File

@ -11,13 +11,15 @@ import FullStack from './Full-Stack.jsx';
import CodingPrep from './Coding-Prep.jsx'; import CodingPrep from './Coding-Prep.jsx';
import { import {
clearFilter, clearFilter,
updateFilter, fetchChallenges,
fetchChallenges toggleThisPanel,
updateFilter
} from '../../redux/actions'; } from '../../redux/actions';
const bindableActions = { const bindableActions = {
clearFilter, clearFilter,
fetchChallenges, fetchChallenges,
toggleThisPanel,
updateFilter updateFilter
}; };
const superBlocksSelector = createSelector( const superBlocksSelector = createSelector(
@ -51,10 +53,12 @@ const superBlocksSelector = createSelector(
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
superBlocksSelector, superBlocksSelector,
state => state.challengesApp.filter, state => state.challengesApp.filter,
({ superBlocks }, filter) => { state => state.challengesApp.map,
({ superBlocks }, filter, mapUi) => {
return { return {
superBlocks, superBlocks,
filter filter,
mapUi
}; };
} }
); );
@ -72,22 +76,31 @@ export class ShowMap extends PureComponent {
clearFilter: PropTypes.func, clearFilter: PropTypes.func,
filter: PropTypes.string, filter: PropTypes.string,
superBlocks: PropTypes.array, superBlocks: PropTypes.array,
updateFilter: PropTypes.func updateFilter: PropTypes.func,
mapUi: PropTypes.object
}; };
renderSuperBlocks(superBlocks, updateCurrentChallenge) { renderSuperBlocks(
superBlocks,
updateCurrentChallenge,
mapUi,
toggleThisPanel
) {
if (!Array.isArray(superBlocks) || !superBlocks.length) { if (!Array.isArray(superBlocks) || !superBlocks.length) {
return <div>No Super Blocks</div>; return <div>No Super Blocks</div>;
} }
return superBlocks.map((superBlock) => { return superBlocks
return ( .map((superBlock) => {
<SuperBlock return (
key={ superBlock.title } <SuperBlock
updateCurrentChallenge={ updateCurrentChallenge } key={ superBlock.title }
{ ...superBlock } mapUi={ mapUi }
/> toggleThisPanel={ toggleThisPanel }
); updateCurrentChallenge={ updateCurrentChallenge }
}); { ...superBlock }
/>
);
});
} }
render() { render() {
@ -96,7 +109,9 @@ export class ShowMap extends PureComponent {
superBlocks, superBlocks,
updateFilter, updateFilter,
clearFilter, clearFilter,
filter filter,
mapUi,
toggleThisPanel
} = this.props; } = this.props;
return ( return (
<div> <div>
@ -105,12 +120,24 @@ export class ShowMap extends PureComponent {
filter={ filter } filter={ filter }
updateFilter={ updateFilter } updateFilter={ updateFilter }
/> />
<div <div className='map-accordion'>
className='map-accordion' {
> this.renderSuperBlocks(
{ this.renderSuperBlocks(superBlocks, updateCurrentChallenge) } superBlocks,
<FullStack /> updateCurrentChallenge,
<CodingPrep /> mapUi,
toggleThisPanel
)
}
<FullStack
mapUi={ mapUi }
toggleThisPanel={ toggleThisPanel }
/>
<CodingPrep
mapUi={ mapUi }
toggleThisPanel={ toggleThisPanel }
/>
<div className='spacer' />
</div> </div>
</div> </div>
); );

View File

@ -6,13 +6,25 @@ import { Panel } from 'react-bootstrap';
import Block from './Block.jsx'; import Block from './Block.jsx';
export default class SuperBlock extends PureComponent { export default class SuperBlock extends PureComponent {
constructor(...props) {
super(...props);
this.handleSelect = this.handleSelect.bind(this);
}
static displayName = 'SuperBlock'; static displayName = 'SuperBlock';
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
dashedName: PropTypes.string,
message: PropTypes.string, message: PropTypes.string,
blocks: PropTypes.array blocks: PropTypes.array,
mapUi: PropTypes.object,
toggleThisPanl: PropTypes.func
}; };
handleSelect(eventKey, e) {
e.preventDefault();
this.props.toggleThisPanel(eventKey);
}
renderBlocks(blocks) { renderBlocks(blocks) {
if (!Array.isArray(blocks) || !blocks.length) { if (!Array.isArray(blocks) || !blocks.length) {
return <div>No Blocks Found</div>; return <div>No Blocks Found</div>;
@ -28,15 +40,17 @@ export default class SuperBlock extends PureComponent {
} }
render() { render() {
const { title, blocks, message } = this.props; const { title, dashedName, blocks, message, mapUi = {} } = this.props;
return ( return (
<Panel <Panel
bsClass='map-accordion-panel' bsClass='map-accordion-panel'
collapsible={ true } collapsible={ true }
expanded={ true } eventKey={ dashedName || title }
expanded={ dashedName ? mapUi[dashedName] : true }
header={ <h2><FA name='caret-right' />{ title }</h2> } header={ <h2><FA name='caret-right' />{ title }</h2> }
id={ title } id={ title }
key={ title } key={ dashedName || title }
onSelect={ this.handleSelect }
> >
{ {
message ? message ?

View File

@ -42,6 +42,29 @@ export const updateFilter = createAction(
e => e.target.value e => e.target.value
); );
function createMapKey(map, key) {
map[key] = true;
return map;
}
export const initMap = createAction(
types.initMap,
(
{ superBlock: superBlockMap },
superBlocks
) => {
if (!superBlocks || !superBlockMap) {
return {};
}
const blocks = superBlocks
.map(superBlock => superBlockMap[superBlock].blocks)
.reduce((blocks, block) => blocks.concat(block));
return superBlocks
.concat(blocks)
.reduce(createMapKey, {});
}
);
export const toggleThisPanel = createAction(types.toggleThisPanel);
export const clearFilter = createAction(types.clearFilter); export const clearFilter = createAction(types.clearFilter);
// files // files

View File

@ -5,15 +5,16 @@ import {
fetchChallenges, fetchChallenges,
replaceChallenge replaceChallenge
} from './types'; } from './types';
import {
fetchChallengeCompleted,
fetchChallengesCompleted,
updateCurrentChallenge,
initMap
} from './actions';
import { import {
delayedRedirect, delayedRedirect,
createErrorObserable createErrorObserable
} from '../../../redux/actions'; } from '../../../redux/actions';
import {
fetchChallengeCompleted,
fetchChallengesCompleted,
updateCurrentChallenge
} from './actions';
function createNameIdMap(entities) { function createNameIdMap(entities) {
const { challenge } = entities; const { challenge } = entities;
@ -68,11 +69,12 @@ export default function fetchChallengesSaga(action$, getState, { services }) {
redirect ? delayedRedirect(redirect) : null redirect ? delayedRedirect(redirect) : null
); );
} }
return Observable.just( return Observable.of(
fetchChallengesCompleted( fetchChallengesCompleted(
createNameIdMap(entities), createNameIdMap(entities),
result result
) ),
initMap(entities, result),
); );
}) })
.catch(createErrorObserable); .catch(createErrorObserable);

View File

@ -46,6 +46,10 @@ const initialState = {
legacyKey: '', legacyKey: '',
files: {}, files: {},
// map // map
map: {
'full-stack': true,
'coding-prep': true
},
filter: '', filter: '',
superBlocks: [], superBlocks: [],
// misc // misc
@ -230,8 +234,36 @@ const filesReducer = handleActions(
{} {}
); );
// {
// // show
// [(super)BlockName]: { open: Boolean }
// // do not show
// [(super)BlockName]: null
// }
const mapReducer = handleActions(
{
[types.initMap]: (state, { payload }) => ({
...state,
...payload
}),
[types.toggleThisPanel]: (state, { payload }) => ({
...state,
[payload]: !state[payload]
})
},
initialState.map
);
export default function challengeReducers(state, action) { export default function challengeReducers(state, action) {
const newState = mainReducer(state, action); const newState = mainReducer(state, action);
const files = filesReducer(state && state.files || {}, action); const files = filesReducer(state && state.files || {}, action);
return newState.files !== files ? { ...newState, files } : newState; if (newState.files !== files) {
return { ...newState, files };
}
// map actions only effect this reducer;
const map = mapReducer(state && state.map || {}, action);
if (newState.map !== map) {
return { ...newState, map };
}
return newState;
} }

View File

@ -20,6 +20,8 @@ export default createTypes([
// map // map
'updateFilter', 'updateFilter',
'clearFilter', 'clearFilter',
'initMap',
'toggleThisPanel',
// files // files
'updateFile', 'updateFile',