From 1f02e318946253a8ae9f853d033a486aa384473d Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 22 Jun 2016 14:55:35 -0700 Subject: [PATCH] feature(map): Add collapse to block level --- .../challenges/components/map/Block.jsx | 114 +++++++--------- .../challenges/components/map/Challenge.jsx | 110 +++++++++++++++ .../challenges/components/map/Full-Stack.jsx | 5 + .../challenges/components/map/Header.jsx | 11 +- .../routes/challenges/components/map/Map.jsx | 128 +++--------------- .../challenges/components/map/Super-Block.jsx | 74 ++++++---- common/app/routes/challenges/redux/reducer.js | 13 +- 7 files changed, 241 insertions(+), 214 deletions(-) create mode 100644 common/app/routes/challenges/components/map/Challenge.jsx diff --git a/common/app/routes/challenges/components/map/Block.jsx b/common/app/routes/challenges/components/map/Block.jsx index ab5dec96ae..b26ffdc64c 100644 --- a/common/app/routes/challenges/components/map/Block.jsx +++ b/common/app/routes/challenges/components/map/Block.jsx @@ -1,97 +1,76 @@ import React, { PropTypes } from 'react'; -import { Link } from 'react-router'; import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; import FA from 'react-fontawesome'; import PureComponent from 'react-pure-render/component'; import { Panel } from 'react-bootstrap'; -import classnames from 'classnames'; -import { updateCurrentChallenge } from '../../redux/actions'; +import Challenge from './Challenge.jsx'; +import { toggleThisPanel } from '../../redux/actions'; -const dispatchActions = { updateCurrentChallenge }; +const dispatchActions = { toggleThisPanel }; +const mapStateToProps = createSelector( + (_, props) => props.dashedName, + state => state.entities.block, + state => state.entities.challenge, + (state, props) => state.challengesApp.mapUi[props.dashedName], + (dashedName, blockMap, challengeMap, isOpen) => { + const block = blockMap[dashedName]; + return { + isOpen, + dashedName, + title: block.title, + time: block.time, + challenges: block.challenges + }; + } +); export class Block extends PureComponent { + constructor(...props) { + super(...props); + this.handleSelect = this.handleSelect.bind(this); + } static displayName = 'Block'; static propTypes = { title: PropTypes.string, dashedName: PropTypes.string, time: PropTypes.string, + isOpen: PropTypes.bool, challenges: PropTypes.array, - updateCurrentChallenge: PropTypes.func + toggleThisPanel: PropTypes.func }; - renderChallenges(blockName, challenges, updateCurrentChallenge) { + handleSelect(eventKey, e) { + e.preventDefault(); + this.props.toggleThisPanel(eventKey); + } + + renderChallenges(challenges) { if (!Array.isArray(challenges) || !challenges.length) { return
No Challenges Found
; } - return challenges.map(challenge => { - const { - title, - dashedName, - isLocked, - isRequired, - isCompleted - } = challenge; - const challengeClassName = classnames({ - 'text-primary': true, - 'padded-ionic-icon': true, - 'negative-15': true, - 'challenge-title': true, - 'ion-checkmark-circled faded': !isLocked && isCompleted, - 'ion-ios-circle-outline': !isLocked && !isCompleted, - 'ion-locked': isLocked, - disabled: isLocked - }); - if (isLocked) { - return ( -

- { title } - { - isRequired ? - * : - '' - } -

- ); - } - return ( -

- - updateCurrentChallenge(challenge) } - > - { title } - complete - { - isRequired ? - * : - '' - } - - -

- ); - }); + return challenges.map(dashedName => ( + + )); } render() { const { title, time, - challenges, - updateCurrentChallenge, - dashedName + dashedName, + isOpen, + challenges } = this.props; return (

{ title }

@@ -100,13 +79,12 @@ export class Block extends PureComponent { } id={ title } key={ title } + onSelect={ this.handleSelect } > - { - this.renderChallenges(dashedName, challenges, updateCurrentChallenge) - } + { this.renderChallenges(challenges) }
); } } -export default connect(null, dispatchActions)(Block); +export default connect(mapStateToProps, dispatchActions)(Block); diff --git a/common/app/routes/challenges/components/map/Challenge.jsx b/common/app/routes/challenges/components/map/Challenge.jsx new file mode 100644 index 0000000000..de45b172c6 --- /dev/null +++ b/common/app/routes/challenges/components/map/Challenge.jsx @@ -0,0 +1,110 @@ +import React, { PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { Link } from 'react-router'; +import PureComponent from 'react-pure-render/component'; +import classnames from 'classnames'; + +import { updateCurrentChallenge } from '../../redux/actions'; + +const bindableActions = { updateCurrentChallenge }; +const mapStateToProps = createSelector( + (_, props) => props.dashedName, + state => state.entities.challenge, + (dashedName, challengeMap) => { + const challenge = challengeMap[dashedName] || {}; + return { + dashedName, + challenge, + title: challenge.title, + block: challenge.block, + isLocked: challenge.isLocked, + isRequired: challenge.isRequired, + isCompleted: challenge.isCompleted + }; + } +); +export class Challenge extends PureComponent { + constructor(...args) { + super(...args); + } + static displayName = 'Challenge'; + static propTypes = { + title: PropTypes.string, + dashedName: PropTypes.string, + block: PropTypes.string, + isLocked: PropTypes.bool, + isRequired: PropTypes.bool, + isCompleted: PropTypes.bool, + challenge: PropTypes.object, + updateCurrentChallenge: PropTypes.func + }; + + renderCompleted(isCompleted, isLocked) { + if (isLocked || !isCompleted) { + return null; + } + return completed; + } + + renderRequired(isRequired) { + if (!isRequired) { + return ''; + } + return *; + } + + renderLocked(title, isRequired, className) { + return ( +

+ { title } + { this.renderRequired(isRequired) } +

+ ); + } + + render() { + const { + title, + dashedName, + block, + isLocked, + isRequired, + isCompleted, + challenge, + updateCurrentChallenge + } = this.props; + const challengeClassName = classnames({ + 'text-primary': true, + 'padded-ionic-icon': true, + 'negative-15': true, + 'challenge-title': true, + 'ion-checkmark-circled faded': !isLocked && isCompleted, + 'ion-ios-circle-outline': !isLocked && !isCompleted, + 'ion-locked': isLocked, + disabled: isLocked + }); + if (isLocked) { + return this.renderLocked(title, isRequired, challengeClassName); + } + return ( +

+ + updateCurrentChallenge(challenge) }> + { title } + { this.renderCompleted(isCompleted, isLocked) } + { this.renderRequired(isRequired) } + + +

+ ); + } +} + +export default connect(mapStateToProps, bindableActions)(Challenge); diff --git a/common/app/routes/challenges/components/map/Full-Stack.jsx b/common/app/routes/challenges/components/map/Full-Stack.jsx index 247c61ca95..f880b7de23 100644 --- a/common/app/routes/challenges/components/map/Full-Stack.jsx +++ b/common/app/routes/challenges/components/map/Full-Stack.jsx @@ -15,26 +15,31 @@ const nonprofitProjects = { challenges: [ { title: 'Greenfield Nonprofit Project #1', + dashedName: 'greenfield-1', isLocked: true, isRequired: true }, { title: 'Greenfield Nonprofit Project #2', + dashedName: 'greenfield-2', isLocked: true, isRequired: true }, { title: 'Legacy Code Nonprofit Project #1', + dashedName: 'legacy-1', isLocked: true, isRequired: true }, { title: 'Legacy Code Nonprofit Project #2', + dashedName: 'legacy-2', isLocked: true, isRequired: true }, { title: 'Claim your Full Stack Development Certification', + dashedName: 'claim-full-stack', isLocked: true } ] diff --git a/common/app/routes/challenges/components/map/Header.jsx b/common/app/routes/challenges/components/map/Header.jsx index 61543bb334..7f0c26e657 100644 --- a/common/app/routes/challenges/components/map/Header.jsx +++ b/common/app/routes/challenges/components/map/Header.jsx @@ -1,20 +1,24 @@ import React, { PropTypes } from 'react'; +import { connect } from 'react-redux'; import PureComponent from 'react-pure-render/component'; import { InputGroup, FormControl, Button, Row } from 'react-bootstrap'; import classnames from 'classnames'; +import { clearFilter, updateFilter } from '../../redux/actions'; +const ESC = 27; const clearIcon = ; const searchIcon = ; -const ESC = 27; -export default class Header extends PureComponent { +const bindableActions = { clearFilter, updateFilter }; +const mapStateToProps = state => ({ filter: state.challengesApp.filter }); +export class Header extends PureComponent { constructor(...props) { super(...props); this.handleKeyDown = this.handleKeyDown.bind(this); } static displayName = 'MapHeader'; static propTypes = { - filter: PropTypes.string, clearFilter: PropTypes.func, + filter: PropTypes.string, updateFilter: PropTypes.func }; @@ -79,3 +83,4 @@ export default class Header extends PureComponent { ); } } +export default connect(mapStateToProps, bindableActions)(Header); diff --git a/common/app/routes/challenges/components/map/Map.jsx b/common/app/routes/challenges/components/map/Map.jsx index 10a4571224..a7d2371a2d 100644 --- a/common/app/routes/challenges/components/map/Map.jsx +++ b/common/app/routes/challenges/components/map/Map.jsx @@ -2,141 +2,45 @@ import React, { PropTypes } from 'react'; import { compose } from 'redux'; import { contain } from 'redux-epic'; import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; import PureComponent from 'react-pure-render/component'; import MapHeader from './Header.jsx'; import SuperBlock from './Super-Block.jsx'; -import FullStack from './Full-Stack.jsx'; -import CodingPrep from './Coding-Prep.jsx'; -import { - clearFilter, - fetchChallenges, - toggleThisPanel, - updateFilter -} from '../../redux/actions'; - -const bindableActions = { - clearFilter, - fetchChallenges, - toggleThisPanel, - updateFilter -}; -const superBlocksSelector = createSelector( - state => state.challengesApp.superBlocks, - state => state.entities.superBlock, - state => state.entities.block, - state => state.entities.challenge, - (superBlocks, superBlockMap, blockMap, challengeMap) => { - if (!superBlockMap || !blockMap || !challengeMap) { - return { - superBlocks: [] - }; - } - return { - superBlocks: 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]) - })) - })) - }; - } -); - -const mapStateToProps = createSelector( - superBlocksSelector, - state => state.challengesApp.filter, - state => state.challengesApp.map, - ({ superBlocks }, filter, mapUi) => { - return { - superBlocks, - filter, - mapUi - }; - } -); +import { fetchChallenges } from '../../redux/actions'; +const bindableActions = { fetchChallenges }; +const mapStateToProps = state => ({ + superBlocks: state.challengesApp.superBlocks +}); const fetchOptions = { fetchAction: 'fetchChallenges', isPrimed({ superBlocks }) { return Array.isArray(superBlocks) && superBlocks.length > 1; } }; - export class ShowMap extends PureComponent { static displayName = 'Map'; - static propTypes = { - clearFilter: PropTypes.func, - filter: PropTypes.string, - superBlocks: PropTypes.array, - updateFilter: PropTypes.func, - mapUi: PropTypes.object - }; + static propTypes = { superBlocks: PropTypes.array }; - renderSuperBlocks( - superBlocks, - updateCurrentChallenge, - mapUi, - toggleThisPanel - ) { + renderSuperBlocks(superBlocks) { if (!Array.isArray(superBlocks) || !superBlocks.length) { return
No Super Blocks
; } - return superBlocks - .map((superBlock) => { - return ( - - ); - }); + return superBlocks.map(dashedName => ( + + )); } render() { - const { - updateCurrentChallenge, - superBlocks, - updateFilter, - clearFilter, - filter, - mapUi, - toggleThisPanel - } = this.props; + const { superBlocks } = this.props; return (
- +
- { - this.renderSuperBlocks( - superBlocks, - updateCurrentChallenge, - mapUi, - toggleThisPanel - ) - } - - + { this.renderSuperBlocks(superBlocks) }
diff --git a/common/app/routes/challenges/components/map/Super-Block.jsx b/common/app/routes/challenges/components/map/Super-Block.jsx index 220b00e060..2f6ac7cb8d 100644 --- a/common/app/routes/challenges/components/map/Super-Block.jsx +++ b/common/app/routes/challenges/components/map/Super-Block.jsx @@ -1,22 +1,36 @@ import React, { PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; import PureComponent from 'react-pure-render/component'; import FA from 'react-fontawesome'; import { Panel } from 'react-bootstrap'; import Block from './Block.jsx'; +import { toggleThisPanel } from '../../redux/actions'; -export default class SuperBlock extends PureComponent { - constructor(...props) { - super(...props); - this.handleSelect = this.handleSelect.bind(this); +const dispatchActions = { toggleThisPanel }; +const mapStateToProps = createSelector( + (_, props) => props.dashedName, + state => state.entities.superBlock, + (state, props) => state.challengesApp.mapUi[props.dashedName], + (dashedName, superBlockMap, isOpen) => ({ + isOpen, + title: superBlockMap[dashedName].title, + blocks: superBlockMap[dashedName].blocks + }) +); +export class SuperBlock extends PureComponent { + constructor(...props) { + super(...props); + this.handleSelect = this.handleSelect.bind(this); } static displayName = 'SuperBlock'; static propTypes = { title: PropTypes.string, dashedName: PropTypes.string, - message: PropTypes.string, blocks: PropTypes.array, - mapUi: PropTypes.object, + isOpen: PropTypes.bool, + message: PropTypes.string, toggleThisPanl: PropTypes.func }; @@ -29,36 +43,45 @@ export default class SuperBlock extends PureComponent { if (!Array.isArray(blocks) || !blocks.length) { return
No Blocks Found
; } - return blocks.map(block => { - return ( - - ); - }); + return blocks.map(dashedName => ( + + )); + } + + renderMessage(message) { + if (!message) { + return null; + } + return ( +
+ { message } +
+ ); } render() { - const { title, dashedName, blocks, message, mapUi = {} } = this.props; + const { + title, + dashedName, + blocks, + message, + isOpen + } = this.props; return ( { title } } id={ title } key={ dashedName || title } onSelect={ this.handleSelect } > - { - message ? -
- { message } -
: - '' - } + { this.renderMessage(message) }
@@ -68,3 +91,8 @@ export default class SuperBlock extends PureComponent { ); } } + +export default connect( + mapStateToProps, + dispatchActions +)(SuperBlock); diff --git a/common/app/routes/challenges/redux/reducer.js b/common/app/routes/challenges/redux/reducer.js index 61bbc9b062..b072489158 100644 --- a/common/app/routes/challenges/redux/reducer.js +++ b/common/app/routes/challenges/redux/reducer.js @@ -46,10 +46,7 @@ const initialState = { legacyKey: '', files: {}, // map - map: { - 'full-stack': true, - 'coding-prep': true - }, + mapUi: {}, filter: '', superBlocks: [], // misc @@ -251,7 +248,7 @@ const mapReducer = handleActions( [payload]: !state[payload] }) }, - initialState.map + initialState.mapUi ); export default function challengeReducers(state, action) { @@ -261,9 +258,9 @@ export default function challengeReducers(state, action) { return { ...newState, files }; } // map actions only effect this reducer; - const map = mapReducer(state && state.map || {}, action); - if (newState.map !== map) { - return { ...newState, map }; + const mapUi = mapReducer(state && state.mapUi || {}, action); + if (newState.mapUi !== mapUi) { + return { ...newState, mapUi }; } return newState; }