From 53bb2628748e5967514251606d1fece9f4388b35 Mon Sep 17 00:00:00 2001 From: Noah Loomans Date: Fri, 6 Jul 2018 16:51:41 +0200 Subject: client: Switch to connected-react-router --- package-lock.json | 48 +++++++++++++++++++++------------ package.json | 1 + src/client/react/App.js | 6 ++--- src/client/react/index.js | 12 ++++----- src/client/react/lib/getHistory.js | 35 ------------------------ src/client/react/lib/url.js | 11 -------- src/client/react/store/actions.js | 54 ++++++++++++++++++++++++++----------- src/client/react/store/reducers.js | 1 + src/client/react/store/selectors.js | 30 +++++++++++++++++++++ 9 files changed, 110 insertions(+), 88 deletions(-) delete mode 100644 src/client/react/lib/getHistory.js create mode 100644 src/client/react/store/selectors.js diff --git a/package-lock.json b/package-lock.json index b3eb738..06c9573 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2802,6 +2802,15 @@ "typedarray": "0.0.6" } }, + "connected-react-router": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-4.3.0.tgz", + "integrity": "sha512-HUXyhpA2EyOcxKftkrETWwGIsJ/PQ6yjtWMAYPfwqnjVVmMOex0RfkPpub8e+/VAU/XV+DZ62YjFeDvPo6boZw==", + "requires": { + "immutable": "3.8.2", + "react-router": "4.3.1" + } + }, "console-browserify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", @@ -4073,7 +4082,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "1.9.2" } } } @@ -4776,7 +4785,7 @@ "dev": true, "optional": true, "requires": { - "minimatch": "^3.0.4" + "minimatch": "3.0.4" } }, "inflight": { @@ -4805,7 +4814,7 @@ "bundled": true, "dev": true, "requires": { - "number-is-nan": "^1.0.0" + "number-is-nan": "1.0.1" } }, "isarray": { @@ -4819,7 +4828,7 @@ "bundled": true, "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "1.1.11" } }, "minimist": { @@ -4910,8 +4919,8 @@ "dev": true, "optional": true, "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" } }, "npmlog": { @@ -5626,6 +5635,11 @@ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", "dev": true }, + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" + }, "import-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", @@ -7702,11 +7716,11 @@ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" } }, "cross-spawn": { @@ -7715,8 +7729,8 @@ "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", "dev": true, "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" + "lru-cache": "4.1.3", + "which": "1.3.1" } }, "form-data": { @@ -7799,7 +7813,7 @@ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "2.1.1" } }, "tough-cookie": { @@ -8839,7 +8853,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "1.9.2" } } } @@ -10370,8 +10384,8 @@ "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", "dev": true, "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" + "ajv": "6.5.1", + "ajv-keywords": "3.2.0" } } } diff --git a/package.json b/package.json index e4480e8..d46178f 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "cheerio": "^0.22.0", "classnames": "^2.2.5", "compression": "^1.6.2", + "connected-react-router": "^4.3.0", "dompurify": "^1.0.3", "express": "^4.16.3", "express-handlebars": "^3.0.0", diff --git a/src/client/react/App.js b/src/client/react/App.js index 522b746..f4cb848 100644 --- a/src/client/react/App.js +++ b/src/client/react/App.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { ConnectedRouter } from 'connected-react-router'; import AppRouter from './AppRouter'; @@ -17,9 +17,9 @@ export default class App extends React.Component { return ( - + - + ); } diff --git a/src/client/react/index.js b/src/client/react/index.js index f01a361..f41c9e3 100644 --- a/src/client/react/index.js +++ b/src/client/react/index.js @@ -24,12 +24,12 @@ import 'whatwg-fetch'; import React from 'react'; import ReactDOM from 'react-dom'; import { createStore, applyMiddleware, compose as reduxCompose } from 'redux'; +import { connectRouter, routerMiddleware } from 'connected-react-router'; import thunk from 'redux-thunk'; import moment from 'moment'; import createHistory from 'history/createBrowserHistory'; -import makeGetHistory from './lib/getHistory'; import reducer from './store/reducers'; import App from './App'; import './index.scss'; @@ -43,14 +43,14 @@ const history = createHistory(); const compose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || reduxCompose; const store = createStore( - reducer, + connectRouter(history)(reducer), // Redux devtools extension // https://github.com/zalmoxisus/redux-devtools-extension compose( - applyMiddleware(thunk.withExtraArgument({ - getHistory: makeGetHistory(history), - moment, - })), + applyMiddleware( + routerMiddleware(history), + thunk, + ), ), ); diff --git a/src/client/react/lib/getHistory.js b/src/client/react/lib/getHistory.js deleted file mode 100644 index daa984b..0000000 --- a/src/client/react/lib/getHistory.js +++ /dev/null @@ -1,35 +0,0 @@ -import { - weekFromLocation, - userFromLocation, - makeUpdatePathname, - makeUpdateQuery, -} from './url'; - -/** - * Make a getHistory function. This function is used in index.js when creating - * the redux store. - * @param {history} history - * The history object from the `history` package. - * There may only be a single shared history object in the application, which - * is why it's delivered from `../index.js`. - */ -export default function makeGetHistory(history) { - /** - * Get a collection of helpers for common browser history interactions, and a - * collection of precalculated values from the address bar. This function is - * used in actions. - */ - return function getHistory() { - const user = userFromLocation(history.location); - const week = weekFromLocation(history.location); - const updatePathname = makeUpdatePathname(history); - const updateQuery = makeUpdateQuery(history); - - return { - user, - week, - updatePathname, - updateQuery, - }; - }; -} diff --git a/src/client/react/lib/url.js b/src/client/react/lib/url.js index 442e5eb..bb5b483 100644 --- a/src/client/react/lib/url.js +++ b/src/client/react/lib/url.js @@ -71,17 +71,6 @@ export function makeSetWeek(history) { }; } -export function makeUpdatePathname(history) { - return function updatePathname(pathname) { - const query = history.location.search; - if (pathname) { - history.push(`/${pathname}${query}`); - } else { - history.push(`/${query}`); - } - }; -} - export function makeUpdateQuery(history) { return function updateQuery(newQuery) { const query = queryString.stringify({ diff --git a/src/client/react/store/actions.js b/src/client/react/store/actions.js index 2d2cd30..e7ef91d 100644 --- a/src/client/react/store/actions.js +++ b/src/client/react/store/actions.js @@ -1,19 +1,40 @@ +import { push } from 'connected-react-router'; +import queryString from 'query-string'; import users from '../users'; +import { selectUser, selectWeek, selectCurrentWeek } from './selectors'; import purifyWeek from '../lib/purifyWeek'; import withinRange from '../lib/withinRange'; -export function setUser(newUser) { - return (dispatch, getState, { getHistory }) => { - const { updatePathname } = getHistory(); +function updatePathname(pathname = '') { + return (dispatch, getState) => { + const query = getState().router.location.search; + dispatch(push(`/${pathname}${query}`)); + }; +} +function updateQuery(newQuery) { + return (dispatch, getState) => { + const { location } = getState().router; + const query = queryString.stringify({ + ...queryString.parse(location.search), + ...newQuery, + }); + + dispatch(push(`${location.pathname}?${query}`)); + }; +} + +export function setUser(newUser) { + return (dispatch) => { dispatch({ type: 'SEARCH/RESET' }); - updatePathname(newUser); + dispatch(updatePathname(newUser)); }; } export function shiftRoom(shift) { - return (dispatch, getState, { getHistory }) => { - const { user } = getHistory(); + return (dispatch, getState) => { + const state = getState(); + const user = selectUser(state); const { allRoomIds } = users; if (users.byId[user].type !== 'r') throw new Error('User must be a room'); @@ -31,27 +52,28 @@ export function shiftRoom(shift) { } export function setWeek(newWeek) { - return (dispatch, getState, { getHistory, moment }) => { - const { updateQuery } = getHistory(); + return (dispatch, getState) => { + const state = getState(); + const isCurrentWeek = selectCurrentWeek(state) === newWeek; - const isCurrentWeek = moment().week() === newWeek; - - updateQuery({ + dispatch(updateQuery({ week: isCurrentWeek ? undefined : newWeek, - }); + })); }; } export function shiftWeek(shift) { - return (dispatch, getState, { getHistory }) => { - const { week } = getHistory(); + return (dispatch, getState) => { + const state = getState(); + const week = selectWeek(state); dispatch(setWeek(purifyWeek(week + shift))); }; } export function showRoomFinder() { - return (dispatch, getState, { getHistory }) => { - const { user } = getHistory(); + return (dispatch, getState) => { + const state = getState(); + const user = selectUser(state); if (user == null || users.byId[user].type !== 'r') { // We are not currently viewing a room, correct the situation. diff --git a/src/client/react/store/reducers.js b/src/client/react/store/reducers.js index 4a3576d..cd68d96 100644 --- a/src/client/react/store/reducers.js +++ b/src/client/react/store/reducers.js @@ -22,6 +22,7 @@ import getSearchResults from '../lib/getSearchResults'; import withinRange from '../lib/withinRange'; const DEFAULT_STATE = { + timestamp: Date.now(), search: null, isRoomFinderVisible: false, schedules: {}, diff --git a/src/client/react/store/selectors.js b/src/client/react/store/selectors.js new file mode 100644 index 0000000..8264cdb --- /dev/null +++ b/src/client/react/store/selectors.js @@ -0,0 +1,30 @@ +import moment from 'moment'; +import queryString from 'query-string'; +import purifyWeek from '../lib/purifyWeek'; +import users from '../users'; + +export function selectUser(store) { + const { location } = store.router; + const match = location.pathname.match(/^\/([stcr])\/([-0-9a-zA-Z]+)/); + if (!match) return null; + + const user = `${match[1]}/${match[2]}`; + if (!users.allIds.includes(user)) return null; + + return user; +} + +export function selectCurrentWeek(store) { + return moment(store.now).week(); +} + +export function selectWeek(store) { + const { location } = store.router; + const weekStr = queryString.parse(location.search).week; + + if (!weekStr) { + return selectCurrentWeek(store); + } + + return purifyWeek(parseInt(weekStr, 10)); +} -- cgit v1.1