diff options
author | Noah Loomans <noahloomans@gmail.com> | 2018-01-29 16:31:05 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-01-29 16:31:05 +0100 |
commit | 694580bc532239a32c2fbf61d7f09e793fd1cb11 (patch) | |
tree | acd21e2654d6c5e70dc41c675972794ce95b4062 /src/client/react/components/container | |
parent | f18692872cdc28d29917247ef4f8ef7553a8b023 (diff) | |
parent | 9a9edd1865d619caada787231c8bb34be25af3af (diff) |
Merge pull request #15 from nloomans/react
Move project over to react
Diffstat (limited to 'src/client/react/components/container')
-rw-r--r-- | src/client/react/components/container/HelpBox.js | 30 | ||||
-rw-r--r-- | src/client/react/components/container/Results.js | 38 | ||||
-rw-r--r-- | src/client/react/components/container/Search.js | 142 | ||||
-rw-r--r-- | src/client/react/components/container/View.js | 47 | ||||
-rw-r--r-- | src/client/react/components/container/WeekSelector.js | 39 |
5 files changed, 296 insertions, 0 deletions
diff --git a/src/client/react/components/container/HelpBox.js b/src/client/react/components/container/HelpBox.js new file mode 100644 index 0000000..a74b43c --- /dev/null +++ b/src/client/react/components/container/HelpBox.js @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +const HelpBox = ({ results, searchText }) => { + if (results.length > 0 || searchText !== '') { + return <div />; + } + + return ( + <div className="help-box"> + <div className="arrow" /> + <div className="bubble"> + Voer hier een docentafkorting, klas, leerlingnummer of lokaalnummer in. + </div> + </div> + ); +}; + +HelpBox.propTypes = { + results: PropTypes.arrayOf(PropTypes.string).isRequired, + searchText: PropTypes.string.isRequired, +}; + +const mapStateToProps = state => ({ + results: state.search.results, + searchText: state.search.searchText, +}); + +export default connect(mapStateToProps)(HelpBox); diff --git a/src/client/react/components/container/Results.js b/src/client/react/components/container/Results.js new file mode 100644 index 0000000..1fb5f44 --- /dev/null +++ b/src/client/react/components/container/Results.js @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import classnames from 'classnames'; +import Result from '../presentational/Result'; + +const Results = (({ results, isExactMatch, selectedResult }) => ( + <div + className={classnames('search__results', { + 'search__results--has-results': !isExactMatch && results.length > 0, + })} + style={{ + minHeight: isExactMatch ? 0 : results.length * 54, + }} + > + {!isExactMatch && results.map(userId => ( + <Result key={userId} userId={userId} isSelected={userId === selectedResult} /> + ))} + </div> +)); + +Results.propTypes = { + results: PropTypes.arrayOf(PropTypes.string).isRequired, + isExactMatch: PropTypes.bool.isRequired, + selectedResult: PropTypes.string, +}; + +Results.defaultProps = { + selectedResult: null, +}; + +const mapStateToProps = state => ({ + results: state.search.results, + isExactMatch: state.search.isExactMatch, + selectedResult: state.search.selectedResult, +}); + +export default connect(mapStateToProps)(Results); diff --git a/src/client/react/components/container/Search.js b/src/client/react/components/container/Search.js new file mode 100644 index 0000000..8acbe99 --- /dev/null +++ b/src/client/react/components/container/Search.js @@ -0,0 +1,142 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import classnames from 'classnames'; +import { withRouter } from 'react-router-dom'; + +import SearchIcon from 'react-icons/lib/md/search'; + +import { setUser, inputChange, changeSelectedResult } from '../../actions/search'; + +import users from '../../users'; +import Results from './Results'; +import IconFromUserType from '../presentational/IconFromUserType'; + +class Search extends React.Component { + constructor(props) { + super(props); + + this.state = { + hasFocus: false, + }; + + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + } + + componentDidMount() { + this.props.dispatch(setUser(this.props.urlUser)); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.urlUser !== this.props.urlUser) { + this.props.dispatch(setUser(nextProps.urlUser)); + } + } + + onFocus() { + this.setState({ + hasFocus: true, + }); + } + + onBlur() { + this.setState({ + hasFocus: false, + }); + } + + onKeyDown(event) { + if (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'Enter') { + event.preventDefault(); + switch (event.key) { + case 'ArrowUp': + this.props.dispatch(changeSelectedResult(-1)); + break; + case 'ArrowDown': + this.props.dispatch(changeSelectedResult(+1)); + break; + case 'Enter': { + const result = this.props.selectedResult || this.props.results[0]; + + if (result === this.props.urlUser) { + // EDGE CASE: The user is set if the user changes, but it doesn't + // change if the result is already the one we are viewing. + // Therefor, we need to dispatch the SET_USER command manually. + this.props.dispatch(setUser(this.props.urlUser)); + } else if (result) { + this.props.history.push(`/${result}`); + } + } + break; + default: + throw new Error('This should never happen... pls?'); + } + } + } + + render() { + const { + selectedResult, + isExactMatch, + searchText, + dispatch, + } = this.props; + + const { + hasFocus, + } = this.state; + + return ( + <div className="search"> + <div className={classnames('search-overflow', { 'search--has-focus': hasFocus })}> + <div className="search__input-wrapper"> + <div className="search__icon-wrapper"> + <IconFromUserType + userType={isExactMatch ? users.byId[selectedResult].type : null} + defaultIcon={<SearchIcon />} + /> + </div> + <input + id="search__input" + onChange={event => dispatch(inputChange(event.target.value))} + onKeyDown={this.onKeyDown} + value={searchText} + placeholder="Zoeken" + onFocus={this.onFocus} + onBlur={this.onBlur} + /> + </div> + <Results /> + </div> + </div> + ); + } +} + +Search.propTypes = { + results: PropTypes.arrayOf(PropTypes.string).isRequired, + selectedResult: PropTypes.string, + urlUser: PropTypes.string, + isExactMatch: PropTypes.bool.isRequired, + searchText: PropTypes.string.isRequired, + dispatch: PropTypes.func.isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, +}; + +Search.defaultProps = { + selectedResult: null, + urlUser: null, +}; + +const mapStateToProps = state => ({ + results: state.search.results, + searchText: state.search.searchText, + selectedResult: state.search.selectedResult, + isExactMatch: state.search.isExactMatch, +}); + +export default withRouter(connect(mapStateToProps)(Search)); diff --git a/src/client/react/components/container/View.js b/src/client/react/components/container/View.js new file mode 100644 index 0000000..4f16100 --- /dev/null +++ b/src/client/react/components/container/View.js @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import { fetchSchedule } from '../../actions/view'; +import extractSchedule from '../../lib/extractSchedule'; + +import Schedule from '../presentational/Schedule'; +import Loading from '../presentational/Loading'; + +const View = ({ + schedules, + user, + week, + dispatch, +}) => { + const schedule = extractSchedule(schedules, user, week); + + switch (schedule.state) { + case 'NOT_REQUESTED': + dispatch(fetchSchedule(user, week)); + return <Loading />; + case 'FETCHING': + return <Loading />; + case 'FINISHED': + return <Schedule htmlStr={schedule.htmlStr} />; + default: + throw new Error(`${schedule.state} is not a valid schedule state.`); + } +}; + +View.propTypes = { + schedules: PropTypes.objectOf(PropTypes.objectOf(PropTypes.shape({ + state: PropTypes.string.isRequired, + htmlStr: PropTypes.string, + }))).isRequired, + user: PropTypes.string.isRequired, + week: PropTypes.number.isRequired, + dispatch: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => ({ + schedules: state.view.schedules, +}); + +export default withRouter(connect(mapStateToProps)(View)); diff --git a/src/client/react/components/container/WeekSelector.js b/src/client/react/components/container/WeekSelector.js new file mode 100644 index 0000000..eef8d8d --- /dev/null +++ b/src/client/react/components/container/WeekSelector.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import queryString from 'query-string'; +import { withRouter } from 'react-router-dom'; + +import purifyWeek from '../../lib/purifyWeek'; + +const WeekSelector = ({ urlWeek, location, history }) => { + const updateWeek = (change) => { + const newWeek = purifyWeek(urlWeek + change); + const isCurrentWeek = moment().week() === newWeek; + + const query = queryString.stringify({ + week: isCurrentWeek ? undefined : newWeek, + }); + history.push(`${location.pathname}?${query}`); + }; + + return ( + <div> + <button onClick={() => updateWeek(-1)}>Prev</button> + Week {urlWeek} + <button onClick={() => updateWeek(+1)}>Next</button> + </div> + ); +}; + +WeekSelector.propTypes = { + urlWeek: PropTypes.number.isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, + location: PropTypes.shape({ + pathname: PropTypes.string.isRequired, + }).isRequired, +}; + +export default withRouter(WeekSelector); |