From 6926de1108b1a084e133d5f8363f080d7c20a99f Mon Sep 17 00:00:00 2001 From: Noah Loomans Date: Fri, 9 Feb 2018 17:04:12 +0100 Subject: Use classes instead of stateless functions for Components --- .eslintrc.js | 3 + .vscode/settings.json | 2 +- package.json | 1 + src/client/react/components/container/HelpBox.js | 33 ++++--- src/client/react/components/container/Results.js | 103 ++++++++++----------- src/client/react/components/container/Search.js | 34 +++---- src/client/react/components/container/View.js | 29 +++--- .../react/components/container/WeekSelector.js | 66 ++++++------- src/client/react/components/page/Index.js | 22 +++-- src/client/react/components/page/User.js | 50 +++++----- .../components/presentational/IconFromUserType.js | 52 ++++++----- .../react/components/presentational/Loading.js | 6 +- .../react/components/presentational/Result.js | 42 +++++---- .../react/components/presentational/Schedule.js | 8 +- yarn.lock | 81 +++++++++++++++- 15 files changed, 318 insertions(+), 214 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 389b34b..da38fd5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,15 +1,18 @@ module.exports = { "extends": "airbnb", + "parser": "babel-eslint", "env": { "browser": true, "node": true, "jest": true, }, "rules": { + "strict": "off", "react/jsx-filename-extension": ["error", { "extensions": [".js"] }], "no-underscore-dangle": ["error", { "allow": ["_test"] }], "class-methods-use-this": "off", "no-prototype-builtins": "off", "react/forbid-prop-types": "off", + "react/prefer-stateless-function": "off", } }; diff --git a/.vscode/settings.json b/.vscode/settings.json index bc66eb0..57d62d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "eslint.enable": true, + "eslint.enable": false, "cSpell.language": "en,nl" } \ No newline at end of file diff --git a/package.json b/package.json index f98c220..b45db7b 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ }, "devDependencies": { "babel-core": "^6.26.0", + "babel-eslint": "^8.2.1", "babel-jest": "^21.2.0", "babel-preset-es2015": "^6.24.1", "babelify": "^7.3.0", diff --git a/src/client/react/components/container/HelpBox.js b/src/client/react/components/container/HelpBox.js index a74b43c..effe4f5 100644 --- a/src/client/react/components/container/HelpBox.js +++ b/src/client/react/components/container/HelpBox.js @@ -2,26 +2,37 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -const HelpBox = ({ results, searchText }) => { - if (results.length > 0 || searchText !== '') { - return
; +class HelpBox extends React.Component { + static propTypes = { + // redux + results: PropTypes.arrayOf(PropTypes.string).isRequired, + searchText: PropTypes.string.isRequired, } - return ( -
-
-
- Voer hier een docentafkorting, klas, leerlingnummer of lokaalnummer in. + render() { + if (this.props.results.length > 0 || this.props.searchText !== '') { + return
; + } + + return ( +
+
+
+ Voer hier een docentafkorting, klas, leerlingnummer of lokaalnummer in. +
-
- ); -}; + ); + } +} HelpBox.propTypes = { + // redux results: PropTypes.arrayOf(PropTypes.string).isRequired, searchText: PropTypes.string.isRequired, }; +HelpBox.propTypes + const mapStateToProps = state => ({ results: state.search.results, searchText: state.search.searchText, diff --git a/src/client/react/components/container/Results.js b/src/client/react/components/container/Results.js index f65c0c8..41030e7 100644 --- a/src/client/react/components/container/Results.js +++ b/src/client/react/components/container/Results.js @@ -9,66 +9,61 @@ import { setUser } from '../../actions/search'; import { userFromMatch } from '../../lib/url'; import Result from '../presentational/Result'; -const Results = ({ - results, - searchText, - selectedResult, - match, - history, - dispatch, -}) => { - const user = userFromMatch(match); +class Results extends React.Component { + static propTypes = { + results: PropTypes.arrayOf(PropTypes.string).isRequired, + searchText: PropTypes.string.isRequired, + selectedResult: PropTypes.string, - const isExactMatch = - user != null && - searchText === users.byId[user].value; + // react-router + match: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, - return ( -
0, - })} - style={{ - minHeight: isExactMatch ? 0 : results.length * 54, - }} - > - {!isExactMatch && results.map(userId => ( - { - if (userId === user) { - // 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. - dispatch(setUser(user)); - } else { - history.push(`/${userId}`); - } - }} - /> - ))} -
- ); -}; + // redux + dispatch: PropTypes.func.isRequired, + }; -Results.propTypes = { - results: PropTypes.arrayOf(PropTypes.string).isRequired, - searchText: PropTypes.string.isRequired, - selectedResult: PropTypes.string, + static defaultProps = { + selectedResult: null, + }; - // react-router - match: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, + render() { + const user = userFromMatch(this.props.match); - // redux - dispatch: PropTypes.func.isRequired, -}; + const isExactMatch = + user != null && + this.props.searchText === users.byId[user].value; -Results.defaultProps = { - selectedResult: null, -}; + return ( +
0, + })} + style={{ + minHeight: isExactMatch ? 0 : this.props.results.length * 54, + }} + > + {!isExactMatch && this.props.results.map(userId => ( + { + if (userId === user) { + // 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(user)); + } else { + this.props.history.push(`/${userId}`); + } + }} + /> + ))} +
+ ); + } +} const mapStateToProps = state => ({ results: state.search.results, diff --git a/src/client/react/components/container/Search.js b/src/client/react/components/container/Search.js index 8ca386c..44f2ba1 100644 --- a/src/client/react/components/container/Search.js +++ b/src/client/react/components/container/Search.js @@ -14,6 +14,23 @@ import Results from './Results'; import IconFromUserType from '../presentational/IconFromUserType'; class Search extends React.Component { + static propTypes = { + results: PropTypes.arrayOf(PropTypes.string).isRequired, + selectedResult: PropTypes.string, + searchText: PropTypes.string.isRequired, + + // react-router + match: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + + // redux + dispatch: PropTypes.func.isRequired, + }; + + static defaultProps = { + selectedResult: null, + }; + constructor(props) { super(props); @@ -132,23 +149,6 @@ class Search extends React.Component { } } -Search.propTypes = { - results: PropTypes.arrayOf(PropTypes.string).isRequired, - selectedResult: PropTypes.string, - searchText: PropTypes.string.isRequired, - - // react-router - match: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, - - // redux - dispatch: PropTypes.func.isRequired, -}; - -Search.defaultProps = { - selectedResult: null, -}; - const mapStateToProps = state => ({ results: state.search.results, searchText: state.search.searchText, diff --git a/src/client/react/components/container/View.js b/src/client/react/components/container/View.js index f919dbc..96611d5 100644 --- a/src/client/react/components/container/View.js +++ b/src/client/react/components/container/View.js @@ -11,6 +11,20 @@ import Schedule from '../presentational/Schedule'; import Loading from '../presentational/Loading'; class View extends React.Component { + static propTypes = { + schedules: PropTypes.objectOf(PropTypes.objectOf(PropTypes.shape({ + state: PropTypes.string.isRequired, + htmlStr: PropTypes.string, + }))).isRequired, + + // react-router + match: PropTypes.object.isRequired, + location: PropTypes.object.isRequired, + + // redux + dispatch: PropTypes.func.isRequired, + }; + componentDidMount() { this.fetchScheduleIfNeeded(); } @@ -49,7 +63,6 @@ class View extends React.Component { switch (schedule.state) { case 'NOT_REQUESTED': - return ; case 'FETCHING': return ; case 'FINISHED': @@ -60,20 +73,6 @@ class View extends React.Component { } } -View.propTypes = { - schedules: PropTypes.objectOf(PropTypes.objectOf(PropTypes.shape({ - state: PropTypes.string.isRequired, - htmlStr: PropTypes.string, - }))).isRequired, - - // react-router - match: PropTypes.object.isRequired, - location: PropTypes.object.isRequired, - - // redux - dispatch: PropTypes.func.isRequired, -}; - const mapStateToProps = state => ({ schedules: state.view.schedules, }); diff --git a/src/client/react/components/container/WeekSelector.js b/src/client/react/components/container/WeekSelector.js index b8b5c2b..002128b 100644 --- a/src/client/react/components/container/WeekSelector.js +++ b/src/client/react/components/container/WeekSelector.js @@ -10,46 +10,50 @@ import ArrowForwardIcon from 'react-icons/lib/md/arrow-forward'; import purifyWeek from '../../lib/purifyWeek'; import { weekFromLocation } from '../../lib/url'; -function weekName(week) { - const currentWeek = moment().week(); - - if (currentWeek === week) { - return `Huidige week • ${week}`; - } else if (currentWeek + 1 === week) { - return `Volgende week • ${week}`; - } else if (currentWeek - 1 === week) { - return `Vorige week • ${week}`; - } +class WeekSelector extends React.Component { + static propTypes = { + // react-router + location: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + }; - return `Week ${week}`; -} + getWeekText() { + const week = weekFromLocation(this.props.location); -const WeekSelector = ({ location, history }) => { - const week = weekFromLocation(location); + const currentWeek = moment().week(); + + if (currentWeek === week) { + return `Huidige week • ${week}`; + } else if (currentWeek + 1 === week) { + return `Volgende week • ${week}`; + } else if (currentWeek - 1 === week) { + return `Vorige week • ${week}`; + } + + return `Week ${week}`; + } + + updateWeek(change) { + const week = weekFromLocation(this.props.location); - const updateWeek = (change) => { const newWeek = purifyWeek(week + change); const isCurrentWeek = moment().week() === newWeek; const query = queryString.stringify({ week: isCurrentWeek ? undefined : newWeek, }); - history.push(`${location.pathname}?${query}`); - }; + this.props.history.push(`${this.props.location.pathname}?${query}`); + } - return ( -
- -
{weekName(week)}
- -
- ); -}; - -WeekSelector.propTypes = { - // react-router - location: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, -}; + render() { + return ( +
+ +
{this.getWeekText()}
+ +
+ ); + } +} export default withRouter(WeekSelector); diff --git a/src/client/react/components/page/Index.js b/src/client/react/components/page/Index.js index e5e47c5..c48ebe0 100644 --- a/src/client/react/components/page/Index.js +++ b/src/client/react/components/page/Index.js @@ -2,14 +2,18 @@ import React from 'react'; import Search from '../container/Search'; import HelpBox from '../container/HelpBox'; -const IndexPage = () => ( -
-
- Metis - - -
-
-); +class IndexPage extends React.Component { + render() { + return ( +
+
+ Metis + + +
+
+ ); + } +} export default IndexPage; diff --git a/src/client/react/components/page/User.js b/src/client/react/components/page/User.js index ae1dcc2..b520f90 100644 --- a/src/client/react/components/page/User.js +++ b/src/client/react/components/page/User.js @@ -6,34 +6,36 @@ import View from '../container/View'; import { userFromMatch } from '../../lib/url'; import WeekSelector from '../container/WeekSelector'; -const UserPage = ({ match }) => { - const user = userFromMatch(match); +class UserPage extends React.Component { + static propTypes = { + // react-router + match: PropTypes.object.isRequired, + }; - if (!user) { - // Invalid user, redirect to index. - return ; - } + render() { + const user = userFromMatch(this.props.match); + + if (!user) { + // Invalid user, redirect to index. + return ; + } - return ( -
-
-
- + return ( +
+
+
+ +
-
-
-
- +
+
+ +
+
- -
- ); -}; - -UserPage.propTypes = { - // react-router - match: PropTypes.object.isRequired, -}; + ); + } +} export default UserPage; diff --git a/src/client/react/components/presentational/IconFromUserType.js b/src/client/react/components/presentational/IconFromUserType.js index ee0e04b..2ef95b7 100644 --- a/src/client/react/components/presentational/IconFromUserType.js +++ b/src/client/react/components/presentational/IconFromUserType.js @@ -5,33 +5,35 @@ import RoomIcon from 'react-icons/lib/md/room'; import ClassIcon from 'react-icons/lib/md/group'; import TeacherIcon from 'react-icons/lib/md/account-circle'; -const IconFromUserType = ({ userType, defaultIcon }) => { - switch (userType) { - case 'c': - return ; - case 't': - return ; - case 's': - return ; - case 'r': - return ; - default: - if (defaultIcon) { - return defaultIcon; - } +class IconFromUserType extends React.Component { + static propTypes = { + userType: PropTypes.string, + defaultIcon: PropTypes.element, + }; - throw new Error('`userType` was invalid or not given, but `defaultIcon` is not defined.'); - } -}; + static defaultProps = { + userType: null, + defaultIcon: null, + }; -IconFromUserType.propTypes = { - userType: PropTypes.string, - defaultIcon: PropTypes.element, -}; + render() { + switch (this.props.userType) { + case 'c': + return ; + case 't': + return ; + case 's': + return ; + case 'r': + return ; + default: + if (this.props.defaultIcon) { + return this.props.defaultIcon; + } -IconFromUserType.defaultProps = { - userType: null, - defaultIcon: null, -}; + throw new Error('`userType` was invalid or not given, but `defaultIcon` is not defined.'); + } + } +} export default IconFromUserType; diff --git a/src/client/react/components/presentational/Loading.js b/src/client/react/components/presentational/Loading.js index 84eaac7..2ab80da 100644 --- a/src/client/react/components/presentational/Loading.js +++ b/src/client/react/components/presentational/Loading.js @@ -1,5 +1,9 @@ import React from 'react'; -const Loading = () =>
Loading...
; +class Loading extends React.Component { + render() { + return
Loading...
; + } +} export default Loading; diff --git a/src/client/react/components/presentational/Result.js b/src/client/react/components/presentational/Result.js index 6d39a80..a30cf52 100644 --- a/src/client/react/components/presentational/Result.js +++ b/src/client/react/components/presentational/Result.js @@ -5,25 +5,29 @@ import users from '../../users'; import IconFromUserType from './IconFromUserType'; -const Result = ({ userId, isSelected, onClick }) => ( - // eslint-disable-next-line -
-
- -
-
{users.byId[userId].value}
-
-); +class Result extends React.Component { + static propTypes = { + userId: PropTypes.string.isRequired, + isSelected: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + }; -Result.propTypes = { - userId: PropTypes.string.isRequired, - isSelected: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, -}; + render() { + return ( + // eslint-disable-next-line +
+
+ +
+
{users.byId[this.props.userId].value}
+
+ ); + } +} export default Result; diff --git a/src/client/react/components/presentational/Schedule.js b/src/client/react/components/presentational/Schedule.js index 3f17adc..85cf72f 100644 --- a/src/client/react/components/presentational/Schedule.js +++ b/src/client/react/components/presentational/Schedule.js @@ -3,6 +3,10 @@ import PropTypes from 'prop-types'; import createDOMPurify from 'dompurify'; class Schedule extends React.Component { + static propTypes = { + htmlStr: PropTypes.string.isRequired, + }; + constructor(props) { super(props); @@ -58,8 +62,4 @@ class Schedule extends React.Component { } } -Schedule.propTypes = { - htmlStr: PropTypes.string.isRequired, -}; - export default Schedule; diff --git a/yarn.lock b/yarn.lock index 2a485d7..8382f1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,58 @@ # yarn lockfile v1 +"@babel/code-frame@7.0.0-beta.36": + version "7.0.0-beta.36" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.36.tgz#2349d7ec04b3a06945ae173280ef8579b63728e4" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +"@babel/helper-function-name@7.0.0-beta.36": + version "7.0.0-beta.36" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.36.tgz#366e3bc35147721b69009f803907c4d53212e88d" + dependencies: + "@babel/helper-get-function-arity" "7.0.0-beta.36" + "@babel/template" "7.0.0-beta.36" + "@babel/types" "7.0.0-beta.36" + +"@babel/helper-get-function-arity@7.0.0-beta.36": + version "7.0.0-beta.36" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.36.tgz#f5383bac9a96b274828b10d98900e84ee43e32b8" + dependencies: + "@babel/types" "7.0.0-beta.36" + +"@babel/template@7.0.0-beta.36": + version "7.0.0-beta.36" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.36.tgz#02e903de5d68bd7899bce3c5b5447e59529abb00" + dependencies: + "@babel/code-frame" "7.0.0-beta.36" + "@babel/types" "7.0.0-beta.36" + babylon "7.0.0-beta.36" + lodash "^4.2.0" + +"@babel/traverse@7.0.0-beta.36": + version "7.0.0-beta.36" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.36.tgz#1dc6f8750e89b6b979de5fe44aa993b1a2192261" + dependencies: + "@babel/code-frame" "7.0.0-beta.36" + "@babel/helper-function-name" "7.0.0-beta.36" + "@babel/types" "7.0.0-beta.36" + babylon "7.0.0-beta.36" + debug "^3.0.1" + globals "^11.1.0" + invariant "^2.2.0" + lodash "^4.2.0" + +"@babel/types@7.0.0-beta.36": + version "7.0.0-beta.36" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.36.tgz#64f2004353de42adb72f9ebb4665fc35b5499d23" + dependencies: + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^2.0.0" + JSONStream@^1.0.3: version "1.3.1" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" @@ -391,6 +443,17 @@ babel-core@^6.0.0, babel-core@^6.0.14, babel-core@^6.26.0: slash "^1.0.0" source-map "^0.5.6" +babel-eslint@^8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.1.tgz#136888f3c109edc65376c23ebf494f36a3e03951" + dependencies: + "@babel/code-frame" "7.0.0-beta.36" + "@babel/traverse" "7.0.0-beta.36" + "@babel/types" "7.0.0-beta.36" + babylon "7.0.0-beta.36" + eslint-scope "~3.7.1" + eslint-visitor-keys "^1.0.0" + babel-generator@^6.18.0, babel-generator@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" @@ -1006,6 +1069,10 @@ babelify@^7.3.0: babel-core "^6.0.14" object-assign "^4.0.0" +babylon@7.0.0-beta.36: + version "7.0.0-beta.36" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.36.tgz#3a3683ba6a9a1e02b0aa507c8e63435e39305b9e" + babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" @@ -1935,7 +2002,7 @@ debug@2.6.8, debug@^2.2.0, debug@^2.6.0, debug@^2.6.8: dependencies: ms "2.0.0" -debug@^3.1.0: +debug@^3.0.1, debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: @@ -2396,7 +2463,7 @@ eslint-restricted-globals@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7" -eslint-scope@^3.7.1: +eslint-scope@^3.7.1, eslint-scope@~3.7.1: version "3.7.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" dependencies: @@ -3057,6 +3124,10 @@ globals@^11.0.1: version "11.1.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.1.0.tgz#632644457f5f0e3ae711807183700ebf2e4633e4" +globals@^11.1.0: + version "11.3.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0" + globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -3518,7 +3589,7 @@ interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" -invariant@^2.0.0, invariant@^2.2.1, invariant@^2.2.2: +invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -6825,6 +6896,10 @@ to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + tough-cookie@^2.3.2, tough-cookie@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" -- cgit v1.1