From 1b3f4ea79f947558573fbce5a2e2d0c2c5dd6a8d Mon Sep 17 00:00:00 2001 From: Noah Loomans Date: Wed, 17 Jan 2018 16:26:04 +0100 Subject: Add view code --- .eslintrc.js | 2 + package.json | 1 + src/client/react/actions/view.js | 28 +++++++ src/client/react/components/container/Search.js | 21 +++++- src/client/react/components/container/View.js | 70 +++++++++++++++++ src/client/react/components/page/User.js | 2 + src/client/react/index.js | 2 +- src/client/react/reducers.js | 2 + src/client/react/reducers/view.js | 38 ++++++++++ src/server/routes/getSchedule.js | 99 +++++++++++-------------- yarn.lock | 4 + 11 files changed, 210 insertions(+), 59 deletions(-) create mode 100644 src/client/react/actions/view.js create mode 100644 src/client/react/components/container/View.js create mode 100644 src/client/react/reducers/view.js diff --git a/.eslintrc.js b/.eslintrc.js index 376aa38..b253a04 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,5 +8,7 @@ module.exports = { "rules": { "react/jsx-filename-extension": ["error", { "extensions": [".js"] }], "no-underscore-dangle": ["error", { "allow": ["_test"] }], + "class-methods-use-this": "off", + "no-prototype-builtins": "off", } }; diff --git a/package.json b/package.json index 0363675..83d624b 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "css-loader": "^0.28.7", "debug": "^2.6.0", "diacritics": "^1.2.3", + "dompurify": "^1.0.3", "encoding": "^0.1.12", "eslint": "^4.14.0", "express": "^4.13.4", diff --git a/src/client/react/actions/view.js b/src/client/react/actions/view.js new file mode 100644 index 0000000..f9f0be2 --- /dev/null +++ b/src/client/react/actions/view.js @@ -0,0 +1,28 @@ +// eslint-disable-next-line import/prefer-default-export +export const fetchSchedule = user => (dispatch) => { + dispatch({ + type: 'VIEW/FETCH_SCHEDULE_REQUEST', + user, + }); + + fetch(`/get/${user}`).then( + // success + (r) => { + r.text().then((htmlStr) => { + dispatch({ + type: 'VIEW/FETCH_SCHEDULE_SUCCESS', + user, + htmlStr, + }); + }); + }, + + // error + () => { + dispatch({ + type: 'VIEW/FETCH_SCHEDULE_FAILURE', + user, + }); + }, + ); +}; diff --git a/src/client/react/components/container/Search.js b/src/client/react/components/container/Search.js index 27b0563..9a99833 100644 --- a/src/client/react/components/container/Search.js +++ b/src/client/react/components/container/Search.js @@ -29,6 +29,12 @@ class Search extends React.Component { 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, @@ -51,10 +57,18 @@ class Search extends React.Component { case 'ArrowDown': this.props.dispatch(changeSelectedResult(+1)); break; - case 'Enter': - if (this.props.selectedResult) { - this.props.history.push(`/${this.props.selectedResult}`); + 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?'); @@ -100,6 +114,7 @@ class Search extends React.Component { } Search.propTypes = { + results: PropTypes.arrayOf(PropTypes.string).isRequired, selectedResult: PropTypes.string, urlUser: PropTypes.string, isExactMatch: PropTypes.bool.isRequired, diff --git a/src/client/react/components/container/View.js b/src/client/react/components/container/View.js new file mode 100644 index 0000000..9bac66f --- /dev/null +++ b/src/client/react/components/container/View.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import createDOMPurify from 'dompurify'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import { fetchSchedule } from '../../actions/view'; + +function cleanMeetingpointHTML(htmlStr) { + const DOMPurify = createDOMPurify(window); + + return DOMPurify.sanitize(htmlStr, { + ADD_ATTR: ['rules'], + }); +} + +class View extends React.Component { + componentDidMount() { + if (!this.loadingFinished(this.props.user)) { + this.props.dispatch(fetchSchedule(this.props.user)); + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.user !== this.props.user && !this.loadingFinished(nextProps.user)) { + this.props.dispatch(fetchSchedule(nextProps.user)); + } + } + + loadingFinished(user) { + return this.props.schedules.hasOwnProperty(user) && + this.props.schedules[user].state === 'finished'; + } + + render() { + if (!this.loadingFinished(this.props.user)) { + return ( +
+ Loading... +
+ ); + } + + const cleanHTML = cleanMeetingpointHTML(this.props.schedules[this.props.user].htmlStr); + + return ( + // eslint-disable-next-line react/no-danger +
+ ); + } +} + +View.propTypes = { + user: PropTypes.string, + dispatch: PropTypes.func.isRequired, + schedules: PropTypes.objectOf(PropTypes.shape({ + state: PropTypes.string.isRequired, + htmlStr: PropTypes.string, + })).isRequired, +}; + +View.defaultProps = { + user: null, +}; + +const mapStateToProps = state => ({ + schedules: state.view.schedules, +}); + +export default withRouter(connect(mapStateToProps)(View)); diff --git a/src/client/react/components/page/User.js b/src/client/react/components/page/User.js index 2ad65a6..ea8cd10 100644 --- a/src/client/react/components/page/User.js +++ b/src/client/react/components/page/User.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Redirect } from 'react-router-dom'; import Search from '../container/Search'; +import View from '../container/View'; import users from '../../users'; const App = ({ match }) => { @@ -15,6 +16,7 @@ const App = ({ match }) => { return (
+
); }; diff --git a/src/client/react/index.js b/src/client/react/index.js index ffa5403..a7006d4 100644 --- a/src/client/react/index.js +++ b/src/client/react/index.js @@ -13,7 +13,7 @@ import User from './components/page/User'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const store = createStore( reducer, - composeEnhancers(applyMiddleware(logger, thunk)), + composeEnhancers(applyMiddleware(thunk, logger)), ); ReactDOM.render( diff --git a/src/client/react/reducers.js b/src/client/react/reducers.js index 9fdf2c4..fb97228 100644 --- a/src/client/react/reducers.js +++ b/src/client/react/reducers.js @@ -1,8 +1,10 @@ import { combineReducers } from 'redux'; import search from './reducers/search'; +import view from './reducers/view'; const rootReducer = combineReducers({ search, + view, }); export default rootReducer; diff --git a/src/client/react/reducers/view.js b/src/client/react/reducers/view.js new file mode 100644 index 0000000..276d8ae --- /dev/null +++ b/src/client/react/reducers/view.js @@ -0,0 +1,38 @@ +const DEFAULT_STATE = { + schedules: {}, +}; + +const view = (state = DEFAULT_STATE, action) => { + switch (action.type) { + case 'VIEW/FETCH_SCHEDULE_REQUEST': + return { + ...state, + schedules: { + ...state.schedules, + [action.user]: { + state: 'fetching', + }, + }, + }; + case 'VIEW/FETCH_SCHEDULE_SUCCESS': + return { + ...state, + schedules: { + ...state.schedules, + [action.user]: { + ...state.schedules[action.user], + state: 'finished', + htmlStr: action.htmlStr, + }, + }, + }; + default: + return state; + } +}; + +export default view; + +export const _test = { + DEFAULT_STATE, +}; diff --git a/src/server/routes/getSchedule.js b/src/server/routes/getSchedule.js index 9c31d66..f6bc256 100644 --- a/src/server/routes/getSchedule.js +++ b/src/server/routes/getSchedule.js @@ -1,81 +1,70 @@ -const express = require('express') -const router = express.Router() -const request = require('request') -const iconv = require('iconv-lite') -const webshot = require('webshot') +const express = require('express'); -const getUserIndex = require('../lib/getUserIndex') -const getURLOfUser = require('../lib/getURLOfUser') +const router = express.Router(); +const request = require('request'); +const iconv = require('iconv-lite'); +const webshot = require('webshot'); + +const getUserIndex = require('../lib/getUserIndex'); +const getURLOfUser = require('../lib/getURLOfUser'); // copied from http://www.meetingpointmco.nl/Roosters-AL/doc/dagroosters/untisscripts.js, // were using the same code as they do to be sure that we always get the same // week number. -function getWeekNumber (target) { - const dayNr = (target.getDay() + 6) % 7 - target.setDate(target.getDate() - dayNr + 3) - const firstThursday = target.valueOf() - target.setMonth(0, 1) +function getWeekNumber(target) { + const dayNr = (target.getDay() + 6) % 7; + // eslint-disable-next-line + target.setDate(target.getDate() - dayNr + 3); + const firstThursday = target.valueOf(); + target.setMonth(0, 1); if (target.getDay() !== 4) { - target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7) + // eslint-disable-next-line + target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7); } - return 1 + Math.ceil((firstThursday - target) / 604800000) + return 1 + Math.ceil((firstThursday - target) / 604800000); } -router.get('/:type/:value.png', function (req, res, next) { - port = process.env.PORT || 3000; - const { type, value } = req.params - const stream = webshot( - `http://localhost:${port}/get/${type}/${value}`, - { customCSS: "body { background-color: white; }" } - ) - stream.pipe(res) -}) - -router.get('/:type/:value.jpg', function (req, res, next) { - port = process.env.PORT || 3000; - const { type, value } = req.params - const stream = webshot( - `http://localhost:${port}/get/${type}/${value}`, - { customCSS: "body { background-color: white; }", streamType: 'jpg' } - ) - stream.pipe(res) -}) +// router.get('/:type/:value.png', (req, res) => { +// const port = process.env.PORT || 3000; +// const { type, value } = req.params; +// const stream = webshot( +// `http://localhost:${port}/get/${type}/${value}`, +// { customCSS: 'body { background-color: white; }' }, +// ); +// stream.pipe(res); +// }); -router.get('/:type/:value', function (req, res, next) { - getUserIndex().then(users => { - const { type, value } = req.params - let { week } = req.query +router.get('/:type/:value', (req, res, next) => { + getUserIndex().then((users) => { + const { type, value } = req.params; + let { week } = req.query; const user = - users.filter(user => user.type === type && user.value === value)[0] + users.filter(user_ => user_.type === type && user_.value === value)[0]; if (!user) { - next(new Error(`${type}${value} is not in the user index.`)) + next(new Error(`${type}${value} is not in the user index.`)); } if (!week) { - week = getWeekNumber(new Date()) + week = getWeekNumber(new Date()); } - const { index } = user + const { index } = user; - const url = getURLOfUser(type, index, week) + const url = getURLOfUser(type, index, week); - request(url, { encoding: null }, function (err, data) { + request(url, { encoding: null }, (err, data) => { if (err) { - next(err) - return + next(err); + return; } - let utf8Body = iconv.decode(data.body, 'ISO-8859-1') - - users.forEach(function (user) { - let utf8Body = utf8Body.replace(/oko/g, "test") - }) + const utf8Body = iconv.decode(data.body, 'ISO-8859-1'); - res.status(data.statusCode).end(utf8Body) - }) - }) -}) + res.status(data.statusCode).end(utf8Body); + }); + }); +}); -module.exports = router +module.exports = router; diff --git a/yarn.lock b/yarn.lock index 1db80c0..1886b4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2113,6 +2113,10 @@ domhandler@^2.3.0: dependencies: domelementtype "1" +dompurify@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-1.0.3.tgz#3f2f6ecb6ecd27599a506b410ff47d6eb90fd05d" + domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" -- cgit v1.1