Add redux, fix routing, add normalize

This commit is contained in:
Melvin Valster
2019-07-29 13:16:07 +02:00
parent 2fcf238446
commit 0bd3034ea7
13 changed files with 353 additions and 82 deletions
+1 -2
View File
@@ -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 (
<Router>
{/* <Router basename={process.env.NODE_ENV !== 'development' ? '%PUBLIC_URL%' : ''}> */}
<div className="App">
<main>
<Switch>
+23 -46
View File
@@ -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<number, number>
classId: number
points: Points
addPoint: typeof addPoint
removePoint: typeof removePoint
}
const EMPTY_TALENTS = Map<number, number>()
@@ -28,46 +25,19 @@ export class Calculator extends React.PureComponent<Props> {
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<number, number>) {
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<Props> {
}
}
export default connect(
null,
{
addPoint,
removePoint
}
)(Calculator)
+80 -16
View File
@@ -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<Props> {
export class Home extends React.PureComponent<Props> {
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<Props> {
selected={selectedClass}
/>
{selectedClass &&
{currentClass &&
<Calculator
initialTalents={pointString && decodeKnownTalents(pointString, selectedClass)}
selectedClass={selectedClass}
history={history}
classId={classId}
points={this.props.points}
/>
}
</div>
)
}
}
}
export default connect(
({ calculator }: AppState) => ({
classId: calculator.classId,
points: calculator.points,
}),
{ setClass, setPoints }
)(Home)
+12 -2
View File
@@ -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(<App />, document.getElementById('root'))
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
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.
+5 -5
View File
@@ -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<number, number>, talent: TalentData
/**
* Encodes a Map of known talents into a URL-friendly string.
*/
export function encodeKnownTalents(known: Map<number, number>, className: string): string {
export function encodeKnownTalents(known: Map<number, number>, 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<number, number>, className: string
/**
* Decodes a string of points into a Map of talents.
*/
export function decodeKnownTalents(pointString: string, className: string): Map<number, number> {
const { specs } = classByName[className]
export function decodeKnownTalents(pointString: string, classId: number): Map<number, number> {
const { specs } = classById[classId]
let known = Map<number, number>()
// TODO: Make sure we validate the point string
+29
View File
@@ -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
})
+76
View File
@@ -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<number, number>(),
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
}
}
+38
View File
@@ -0,0 +1,38 @@
import { Map } from 'immutable'
export type Points = Map<number, number>
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
+17
View File
@@ -0,0 +1,17 @@
import { createStore, combineReducers, compose } from 'redux'
import calculator from './calculator/reducers'
const rootReducer = combineReducers({
calculator,
})
export type AppState = ReturnType<typeof rootReducer>
const store = createStore(
rootReducer,
compose(
(window as any).__REDUX_DEVTOOLS_EXTENSION__ && (window as any).__REDUX_DEVTOOLS_EXTENSION__()
),
)
export default store
+2 -1
View File
@@ -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'
type TooltipPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'left' | 'right'