From 0bd3034ea7bc2822a6798293d2577db22e4ab2b0 Mon Sep 17 00:00:00 2001 From: Melvin Valster Date: Mon, 29 Jul 2019 13:16:07 +0200 Subject: [PATCH] Add redux, fix routing, add normalize --- TODO.md | 6 +- package.json | 4 ++ src/App.tsx | 3 +- src/components/Calculator.tsx | 69 ++++++++--------------- src/containers/Home.tsx | 96 ++++++++++++++++++++++++++------ src/index.tsx | 14 ++++- src/lib/tree.ts | 10 ++-- src/store/calculator/actions.ts | 29 ++++++++++ src/store/calculator/reducers.ts | 76 +++++++++++++++++++++++++ src/store/calculator/types.ts | 38 +++++++++++++ src/store/index.ts | 17 ++++++ src/types.d.ts | 3 +- yarn.lock | 70 +++++++++++++++++++++-- 13 files changed, 353 insertions(+), 82 deletions(-) create mode 100644 src/store/calculator/actions.ts create mode 100644 src/store/calculator/reducers.ts create mode 100644 src/store/calculator/types.ts create mode 100644 src/store/index.ts diff --git a/TODO.md b/TODO.md index c404e2f..b975b95 100644 --- a/TODO.md +++ b/TODO.md @@ -1,12 +1,8 @@ # TODO - [ ] Fix: Initial load `pointString` validation (make sure all talents are valid and their deps are met) -- [ ] Fix: Navigating between talent links for same class does not trigger re-render +- [ ] Fix: Tooltips cause horizontal scroll on less-wide screens. Investigate. -- [ ] Styling: - - [ ] SCSS: Normalize -- [ ] General: - - [ ] Add redux - [ ] Responsiveness: - [ ] Tooltips on mobile need different UX - [ ] Talent tree: diff --git a/package.json b/package.json index 3b39b0d..17c6384 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "react": "^16.8.6", "react-dom": "^16.8.6", "react-loadable": "^5.5.0", + "react-redux": "^7.1.0", "react-router-dom": "^5.0.1", "react-scripts": "3.0.1", + "redux": "^4.0.4", "typescript": "3.5.3" }, "scripts": { @@ -47,11 +49,13 @@ "@types/cheerio": "^0.22.12", "@types/node-fetch": "^2.5.0", "@types/react-loadable": "^5.5.1", + "@types/react-redux": "^7.1.1", "@types/request": "^2.48.2", "@welldone-software/why-did-you-render": "^3.2.1", "cheerio": "^1.0.0-rc.3", "gh-pages": "^2.0.1", "node-fetch": "^2.6.0", + "redux-devtools": "^3.5.0", "request": "^2.88.0", "ts-node": "^8.3.0" } diff --git a/src/App.tsx b/src/App.tsx index d9dfa03..2445a19 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ -import React from 'react' import './App.scss' +import React from 'react' import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom' import Loadable from 'react-loadable' import { PageLoader } from './components/PageLoader' @@ -17,7 +17,6 @@ const LoadablePlayground = Loadable({ const App: React.FC = () => { return ( - {/* */}
diff --git a/src/components/Calculator.tsx b/src/components/Calculator.tsx index 6971699..0af8976 100644 --- a/src/components/Calculator.tsx +++ b/src/components/Calculator.tsx @@ -2,21 +2,18 @@ import './Calculator.scss' import React from 'react' import { Map } from 'immutable' import { TalentTree } from './TalentTree' -import { - modifyTalentPoint, - calcAvailablePoints, - encodeKnownTalents, -} from '../lib/tree' -import { talentsBySpec } from '../data/talents' -import { classByName } from '../data/classes' -import { History } from 'history' -// import { debugPrintKnown } from '../lib/debug' +import { calcAvailablePoints } from '../lib/tree' +import { classById } from '../data/classes' import { Link } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { addPoint, removePoint } from '../store/calculator/actions' +import { Points } from '../store/calculator/types' interface Props { - selectedClass: string - history: History - initialTalents?: Map + classId: number + points: Points + addPoint: typeof addPoint + removePoint: typeof removePoint } const EMPTY_TALENTS = Map() @@ -28,46 +25,19 @@ export class Calculator extends React.PureComponent { knownTalents: EMPTY_TALENTS } - componentDidMount() { - if (this.props.initialTalents) { - this.setState({ knownTalents: this.props.initialTalents }) - this.updateURL(this.props.initialTalents) - } - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.selectedClass !== this.props.selectedClass) { - this.setState({ - knownTalents: EMPTY_TALENTS - }) - } - } - - updateURL(knownTalents: Map) { - const { selectedClass } = this.props - const pointString = encodeKnownTalents(knownTalents, selectedClass) - this.props.history.replace(`/${selectedClass}` + (pointString ? `/${pointString}` : '')) - } - handleTalentPress = (specId: number, talentId: number, modifier: 1 | -1) => { - const talent = talentsBySpec[specId][talentId] - console.log('Clicked talent: ', talentId) - - const newKnownTalents = modifyTalentPoint(this.state.knownTalents, talent, modifier) - if (newKnownTalents !== this.state.knownTalents) { - this.updateURL(newKnownTalents) + if (modifier === 1) { + this.props.addPoint(talentId) + } else { + this.props.removePoint(talentId) } - this.setState({ knownTalents: newKnownTalents }) - - // Debug - // debugPrintKnown(newKnownTalents) } render() { - const { selectedClass } = this.props - const { knownTalents } = this.state + const { classId } = this.props + const knownTalents = this.props.points - const classData = classByName[selectedClass] + const classData = classById[classId] const availablePoints = calcAvailablePoints(knownTalents) return ( @@ -99,3 +69,10 @@ export class Calculator extends React.PureComponent { } } +export default connect( + null, + { + addPoint, + removePoint + } +)(Calculator) \ No newline at end of file diff --git a/src/containers/Home.tsx b/src/containers/Home.tsx index bd8ae67..882d5b5 100644 --- a/src/containers/Home.tsx +++ b/src/containers/Home.tsx @@ -1,33 +1,90 @@ import React from 'react' -import { Calculator } from '../components/Calculator' -import { ClassPicker } from '../components/ClassPicker' +import { connect } from 'react-redux' import { match } from 'react-router-dom' import { RouteComponentProps } from 'react-router' -import { decodeKnownTalents } from '../lib/tree' -import { classByName } from '../data/classes' +import Calculator from '../components/Calculator' +import { ClassPicker } from '../components/ClassPicker' +import { classByName, classById } from '../data/classes' +import { AppState } from '../store' +import { setClass, setPoints } from '../store/calculator/actions' +import { Points } from '../store/calculator/types' +import { decodeKnownTalents, encodeKnownTalents } from '../lib/tree' interface Props extends RouteComponentProps { match: match<{ selectedClass: string pointString: string }> + classId: number + points: Points + setClass: typeof setClass + setPoints: typeof setPoints } -export default class Home extends React.PureComponent { +export class Home extends React.PureComponent { static whyDidYouRender = true + get classSlug() { + return classById[this.props.classId] && classById[this.props.classId].name.toLowerCase() + } + componentDidMount() { - const { selectedClass } = this.props.match.params - if (selectedClass && !classByName[selectedClass]) { + this.loadFromUrlParams() + } + + componentDidUpdate(prevProps: Props) { + const prevParams = prevProps.match.params + const { params } = this.props.match + + if (prevParams.selectedClass !== params.selectedClass) { + // Class changed in route + this.loadFromUrlParams() + } else { + // Changes within same class + if (prevParams.pointString !== params.pointString) { + // Same class but point string changed + const decoded = decodeKnownTalents(params.pointString || '', this.props.classId) + if (!this.props.points.equals(decoded)) { + this.props.setPoints(decoded) + } + } else if (prevProps.points !== this.props.points) { + // Points map changed, update the URL + this.updateURL(this.props.points) + } + } + } + + componentWillUnmount() { + this.props.setClass(null) + } + + loadFromUrlParams() { + const { selectedClass, pointString } = this.props.match.params + const c = selectedClass && classByName[selectedClass] + if (c) { + const points = pointString && decodeKnownTalents(pointString || '', c.id) + this.props.setClass(c.id, points) + } else { + this.props.setClass(null) this.props.history.replace('/') } } - render() { - const { match, history } = this.props - const { selectedClass, pointString } = match.params + updateURL(points: Points) { + const { classId } = this.props + const pointsString = encodeKnownTalents(points, classId) + if (pointsString !== this.props.match.params.pointString) { + this.props.history.replace(`/${this.classSlug}` + (pointsString ? `/${pointsString}` : '')) + } + } - if (selectedClass && !classByName[selectedClass]) { + render() { + const { match, classId } = this.props + const { selectedClass } = match.params + + const currentClass = classById[classId] + if (classId && !currentClass) { + // We're redirecting to / return null } @@ -39,14 +96,21 @@ export default class Home extends React.PureComponent { selected={selectedClass} /> - {selectedClass && + {currentClass && }
) } -} \ No newline at end of file +} + +export default connect( + ({ calculator }: AppState) => ({ + classId: calculator.classId, + points: calculator.points, + }), + { setClass, setPoints } +)(Home) \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 2652ecb..7718cfb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,12 +3,22 @@ import ReactDOM from 'react-dom' import App from './App' import * as serviceWorker from './serviceWorker' +import { Provider } from 'react-redux' +import store from './store' + if (process.env.NODE_ENV !== 'production') { const whyDidYouRender = require('@welldone-software/why-did-you-render/dist/no-classes-transpile/umd/whyDidYouRender.min.js') - whyDidYouRender(React) + whyDidYouRender(React, { + include: [/^ConnectFunction$/] + }) } -ReactDOM.render(, document.getElementById('root')) +ReactDOM.render( + + + , + document.getElementById('root') +) // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/src/lib/tree.ts b/src/lib/tree.ts index d42239d..168a3b8 100644 --- a/src/lib/tree.ts +++ b/src/lib/tree.ts @@ -5,7 +5,7 @@ import { talentsBySpecArray, talentsById } from '../data/talents'; -import { classByName } from '../data/classes' +import { classById } from '../data/classes' import spells from '../data/spells.json' export const MAX_POINTS = 51 @@ -204,9 +204,9 @@ export const modifyTalentPoint = (known: Map, talent: TalentData /** * Encodes a Map of known talents into a URL-friendly string. */ -export function encodeKnownTalents(known: Map, className: string): string { +export function encodeKnownTalents(known: Map, classId: number): string { let string = '' - const { specs } = classByName[className] + const { specs } = classById[classId] for (let i = 0; i < specs.length; i++) { const specId = specs[i] const talents = talentsBySpecArray[specId].sort(SORT_TALENTS) @@ -222,8 +222,8 @@ export function encodeKnownTalents(known: Map, className: string /** * Decodes a string of points into a Map of talents. */ -export function decodeKnownTalents(pointString: string, className: string): Map { - const { specs } = classByName[className] +export function decodeKnownTalents(pointString: string, classId: number): Map { + const { specs } = classById[classId] let known = Map() // TODO: Make sure we validate the point string diff --git a/src/store/calculator/actions.ts b/src/store/calculator/actions.ts new file mode 100644 index 0000000..b0683fd --- /dev/null +++ b/src/store/calculator/actions.ts @@ -0,0 +1,29 @@ +import { + CalculatorActionTypes, + SET_CLASS, + ADD_POINT, + REMOVE_POINT, + SET_POINTS, + Points, +} from './types' + +export const setClass = (classId: number, points?: Points): CalculatorActionTypes => ({ + type: SET_CLASS, + classId, + points +}) + +export const addPoint = (talentId: number): CalculatorActionTypes => ({ + type: ADD_POINT, + talentId +}) + +export const removePoint = (talentId: number): CalculatorActionTypes => ({ + type: REMOVE_POINT, + talentId +}) + +export const setPoints = (points: Points): CalculatorActionTypes => ({ + type: SET_POINTS, + points +}) \ No newline at end of file diff --git a/src/store/calculator/reducers.ts b/src/store/calculator/reducers.ts new file mode 100644 index 0000000..4b43a3a --- /dev/null +++ b/src/store/calculator/reducers.ts @@ -0,0 +1,76 @@ +import { Map } from 'immutable' +import { + CalculatorState, + CalculatorActionTypes, + SET_CLASS, + ADD_POINT, + REMOVE_POINT, + SET_POINTS +} from './types' +import { canLearnTalent, canUnlearnTalent, encodeKnownTalents } from '../../lib/tree' +import { talentsById } from '../../data/talents' + +const initialState: CalculatorState = { + classId: null, + points: Map(), + pointsEncoded: '' +} + +export default function(state = initialState, action: CalculatorActionTypes): CalculatorState { + const { classId, points } = state + + switch (action.type) { + case SET_CLASS: { + if (classId === action.classId) { + return state + } + return { + ...state, + classId: action.classId, + points: action.points || Map(), + pointsEncoded: '' + } + } + + case ADD_POINT: { + const { talentId } = action + const talent = talentsById[talentId] + if (!canLearnTalent(points, talent)) { + return state + } + const nextPoints = points.set(talentId, points.get(talentId, 0) + 1) + return { + ...state, + points: nextPoints, + pointsEncoded: encodeKnownTalents(nextPoints, classId) + } + } + + case REMOVE_POINT: { + const { talentId } = action + const talent = talentsById[talentId] + if (!canUnlearnTalent(points, talent)) { + return state + } + const nextPoints = points.set(talentId, points.get(talentId, 1) - 1) + return { + ...state, + points: nextPoints, + pointsEncoded: encodeKnownTalents(nextPoints, classId) + } + } + + case SET_POINTS: { + if (points.equals(action.points)) { + return state + } + return { + ...state, + points: action.points + } + } + + default: + return state + } +} \ No newline at end of file diff --git a/src/store/calculator/types.ts b/src/store/calculator/types.ts new file mode 100644 index 0000000..cdb7d24 --- /dev/null +++ b/src/store/calculator/types.ts @@ -0,0 +1,38 @@ +import { Map } from 'immutable' + +export type Points = Map + +export interface CalculatorState { + classId: number + points: Points + pointsEncoded: string +} + +export const SET_CLASS = 'SET_CLASS' +export const ADD_POINT = 'ADD_POINT' +export const REMOVE_POINT = 'REMOVE_POINT' +export const SET_POINTS = 'SET_POINTS' + +interface SetClassAction { + type: typeof SET_CLASS + classId: number + points?: Points +} + +interface AddPointAction { + type: typeof ADD_POINT + talentId: number +} + +interface RemovePointAction { + type: typeof REMOVE_POINT + talentId: number +} + +interface SetPointsAction { + type: typeof SET_POINTS + points: Points +} + +export type CalculatorActionTypes = SetClassAction | AddPointAction | RemovePointAction | + SetPointsAction \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..46a8c4e --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,17 @@ +import { createStore, combineReducers, compose } from 'redux' +import calculator from './calculator/reducers' + +const rootReducer = combineReducers({ + calculator, +}) + +export type AppState = ReturnType + +const store = createStore( + rootReducer, + compose( + (window as any).__REDUX_DEVTOOLS_EXTENSION__ && (window as any).__REDUX_DEVTOOLS_EXTENSION__() + ), +) + +export default store diff --git a/src/types.d.ts b/src/types.d.ts index 1b6dbc5..1083c2c 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -51,4 +51,5 @@ interface Talent { type TalentClickHandler = (specId: number, talentId: number, modifier: 1 | -1) => void -type TooltipPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'left' | 'right' \ No newline at end of file +type TooltipPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'left' | 'right' + diff --git a/yarn.lock b/yarn.lock index 865b9c0..bd12869 100644 --- a/yarn.lock +++ b/yarn.lock @@ -881,7 +881,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== @@ -1293,6 +1293,14 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220" integrity sha512-ui3WwXmjTaY73fOQ3/m3nnajU/Orhi6cEu5rzX+BrAAJxa3eITXZ5ch9suPqtM03OWhAHhPSyBGCN4UKoxO20Q== +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -1367,6 +1375,16 @@ "@types/react" "*" "@types/webpack" "*" +"@types/react-redux@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.1.tgz#eb01e89cf71cad77df9f442b819d5db692b997cb" + integrity sha512-owqNahzE8en/jR4NtrUJDJya3tKru7CIEGSRL/pVS84LtSCdSoT7qZTkrbBd3S4Lp11sAp+7LsvxIeONJVKMnw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-router-dom@^4.3.4": version "4.3.4" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.4.tgz#63a7a8558129d2f4ff76e4bdd099bf4b98e25a0d" @@ -4745,7 +4763,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.1.0: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== @@ -6273,7 +6291,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -"lodash@>=3.5 <5", lodash@^4, lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.5, lodash@~4.17.10: +"lodash@>=3.5 <5", lodash@^4, lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.5, lodash@^4.2.0, lodash@~4.17.10: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -8216,7 +8234,7 @@ prompts@^2.0.1: kleur "^3.0.2" sisteransi "^1.0.0" -prop-types@^15.5.0, prop-types@^15.6.2: +prop-types@^15.5.0, prop-types@^15.5.7, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -8443,7 +8461,7 @@ react-error-overlay@^5.1.6: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.6.tgz#0cd73407c5d141f9638ae1e0c63e7b2bf7e9929d" integrity sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q== -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: +react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== @@ -8455,6 +8473,18 @@ react-loadable@^5.5.0: dependencies: prop-types "^15.5.0" +react-redux@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2" + integrity sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw== + dependencies: + "@babel/runtime" "^7.4.5" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.8.6" + react-router-dom@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.1.tgz#ee66f4a5d18b6089c361958e443489d6bab714be" @@ -8658,6 +8688,31 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" +redux-devtools-instrument@^1.9.0: + version "1.9.6" + resolved "https://registry.yarnpkg.com/redux-devtools-instrument/-/redux-devtools-instrument-1.9.6.tgz#6b412595f74b9d48cfd4ecc13e585b1588ed6e7e" + integrity sha512-MwvY4cLEB2tIfWWBzrUR02UM9qRG2i7daNzywRvabOSVdvAY7s9BxSwMmVRH1Y/7QWjplNtOwgT0apKhHg2Qew== + dependencies: + lodash "^4.2.0" + symbol-observable "^1.0.2" + +redux-devtools@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/redux-devtools/-/redux-devtools-3.5.0.tgz#d69ab76d4f0f8abdf6d24bcf5954d7a1aa2b6827" + integrity sha512-pGU8TZNvWxPaCCE432AGm6H6alQbAz80gQM5CzM3SjX9/oSNu/HPF17xFdPQJOXasqyih1Gv167kZDTRe7r0iQ== + dependencies: + lodash "^4.2.0" + prop-types "^15.5.7" + redux-devtools-instrument "^1.9.0" + +redux@^4.0.0, redux@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" + integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + regenerate-unicode-properties@^8.0.2: version "8.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" @@ -9694,6 +9749,11 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" +symbol-observable@^1.0.2, symbol-observable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + symbol-tree@^3.2.2: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"