Add redux, fix routing, add normalize
This commit is contained in:
+1
-2
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Vendored
+2
-1
@@ -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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user