diff --git a/package-lock.json b/package-lock.json index 6faad46..36d5a99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1909,17 +1909,6 @@ } } }, - "@reduxjs/toolkit": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.5.0.tgz", - "integrity": "sha512-E/FUraRx+8guw9Hlg/Ja8jI/hwCrmIKed8Annt9YsZw3BQp+F24t5I5b2OWR6pkEHY4hn1BgP08FrTZFRKsdaQ==", - "requires": { - "immer": "^8.0.0", - "redux": "^4.0.0", - "redux-thunk": "^2.3.0", - "reselect": "^4.0.0" - } - }, "@rollup/plugin-node-resolve": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", @@ -2331,15 +2320,6 @@ "@types/node": "*" } }, - "@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", - "requires": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "@types/html-minifier-terser": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", @@ -2438,17 +2418,6 @@ "@types/react": "*" } }, - "@types/react-redux": { - "version": "7.1.16", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz", - "integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==", - "requires": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "@types/react-transition-group": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", @@ -2801,6 +2770,15 @@ "@xtuc/long": "4.2.2" } }, + "@xstate/react": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@xstate/react/-/react-1.3.1.tgz", + "integrity": "sha512-wgAHr4tWVQQwH6dIQTcZQYGLXiK9V8gMqMxOWyiLQzoKchqXFfb0LlCcw0FAc4jmpuGHSFSk6fAXjEpFpV+Jxw==", + "requires": { + "use-isomorphic-layout-effect": "^1.0.0", + "use-subscription": "^1.3.0" + } + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -12729,18 +12707,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "react-redux": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz", - "integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==", - "requires": { - "@babel/runtime": "^7.12.1", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^16.13.1" - } - }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -12938,20 +12904,6 @@ "strip-indent": "^3.0.0" } }, - "redux": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", - "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", - "requires": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" - } - }, - "redux-thunk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", - "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" - }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -13178,11 +13130,6 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, - "reselect": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", - "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" - }, "resolve": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", @@ -15275,6 +15222,19 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-isomorphic-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz", + "integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==" + }, + "use-subscription": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz", + "integrity": "sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA==", + "requires": { + "object-assign": "^4.1.1" + } + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", @@ -16834,6 +16794,11 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "xstate": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.16.2.tgz", + "integrity": "sha512-EY39NNZnwM4tRYNmQAi1c2qHuZ1lJmuDpEo1jxiRcfS+1jPtKRAjGRLNx3fYKcK0ohW6mL41Wze3mdCF0SqavA==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 377cba8..75eea52 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "dependencies": { "@material-ui/core": "^4.11.3", "@material-ui/icons": "^4.11.2", - "@reduxjs/toolkit": "^1.5.0", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.8.3", @@ -13,17 +12,17 @@ "@types/node": "^12.20.5", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.2", - "@types/react-redux": "^7.1.16", "@types/webpack-env": "^1.16.0", "@use-it/interval": "^1.0.0", + "@xstate/react": "^1.3.1", "bottleneck": "^2.19.5", "react": "^17.0.1", "react-dom": "^17.0.1", - "react-redux": "^7.2.2", "react-scripts": "4.0.3", "typescript": "^4.2.3", "unique-names-generator": "^4.4.0", - "web-vitals": "^1.1.1" + "web-vitals": "^1.1.1", + "xstate": "^4.16.2" }, "scripts": { "start": "react-scripts start", diff --git a/src/api/index.ts b/src/api/index.ts index 67381a7..57c0f5c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -15,11 +15,12 @@ const limiter = new Bottleneck({ minTime: 500, }); -type Status = { +export type GetStatusResponse = { status: string; }; + export const getStatus = async () => { - const json: Status = await get("game/status"); + const json: GetStatusResponse = await get("game/status"); return json; }; diff --git a/src/components/App.tsx b/src/components/App.tsx index 7d6cb46..3a75fcc 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,23 +1,14 @@ -import React, { useEffect } from "react"; -import { useDispatch } from "react-redux"; +import React from "react"; import "./App.css"; import { GithubFork } from "./GithubFork"; import { Status } from "./Status"; -import { startup } from "../store/gameSlice"; -import { Player } from "./Player"; function App() { - const dispatch = useDispatch(); - useEffect(() => { - dispatch(startup()); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); return (
-
); diff --git a/src/components/Player.tsx b/src/components/Player.tsx index f06ebed..248818e 100644 --- a/src/components/Player.tsx +++ b/src/components/Player.tsx @@ -2,15 +2,12 @@ import React, { useState } from "react"; import Typography from "@material-ui/core/Typography"; import Paper from "@material-ui/core/Paper"; import { makeStyles } from "@material-ui/core/styles"; -import { useDispatch, useSelector } from "react-redux"; -import { RootState } from "../store/rootReducer"; import PersonIcon from "@material-ui/icons/Person"; import RefreshIcon from "@material-ui/icons/Refresh"; import { CircularProgress } from "@material-ui/core"; import { IconButton } from "@material-ui/core"; import { Grid } from "@material-ui/core"; import { ConfirmDialog } from "./ConfirmDialog"; -import { getToken } from "../store/gameSlice"; import { newPlayerName } from "../newPlayerName"; const useStyles = makeStyles((theme) => ({ @@ -32,13 +29,12 @@ const useStyles = makeStyles((theme) => ({ export const Player = () => { const classes = useStyles(); - const player = useSelector((p: RootState) => p.game?.player); + const player = newPlayerName(); const [confirmOpen, setConfirmOpen] = useState(false); - const dispatch = useDispatch(); const handleNew = () => { console.log("handle new"); - dispatch(getToken(newPlayerName())); + //dispatch(getToken(newPlayerName())); }; return ( @@ -48,7 +44,7 @@ export const Player = () => { - {player.user.username} + {player} ({ paper: { @@ -21,30 +24,19 @@ const useStyles = makeStyles((theme) => ({ export const Status = () => { const classes = useStyles(); - const [status, setStatus] = useState(""); - - const updateStatus = async () => { - try { - setStatus(""); - const response = await getStatus(); - setStatus(response.status); - } catch (e) { - console.error(e); - setStatus(e.message); - } - }; - useInterval(updateStatus, 60000); + const [state, send] = useMachine(statusMachine); useEffect(() => { - updateStatus(); + send({ type: "FETCH" }); }, []); return ( - {status ? ( + {state.matches("success") ? ( <> - {status} + {" "} + {state.context.result.status} ) : ( diff --git a/src/components/apiMachine.ts b/src/components/apiMachine.ts new file mode 100644 index 0000000..2774e07 --- /dev/null +++ b/src/components/apiMachine.ts @@ -0,0 +1,94 @@ +import { assign, createMachine, StateMachine } from "xstate"; +import { + TimerContext, + TimerEvent, + timerMachine, + TimerState, +} from "./timerMachine"; + +type ApiContext = { + result?: T; + error?: string; +}; +type ApiState = + | { + value: "idle"; + context: ApiContext & { result: undefined; error: undefined }; + } + | { + value: "loading"; + context: ApiContext & { result: undefined; error: undefined }; + } + | { + value: "failure"; + context: ApiContext & { result: undefined; error: string }; + } + | { + value: "success"; + context: ApiContext & { + result: T; + error: undefined; + }; + }; +type ApiEvent = { type: "FETCH" } | { type: "LOADING" } | { type: "RETRY" }; + +type Success = {}; +type SuccessWithRetry = { + invoke: { + src: StateMachine; + onDone: string; + }; +}; + +const successWithRetry: SuccessWithRetry = { + invoke: { + src: timerMachine, + onDone: "loading", + }, +}; + +export const apiPollMachine = (apiCall: () => Promise) => + apiMachine(apiCall, successWithRetry); + +export const apiCallMachine = (apiCall: () => Promise) => + apiMachine(apiCall); + +const apiMachine = ( + apiCall: () => Promise, + success: Success | SuccessWithRetry = {} +) => + createMachine, ApiEvent, ApiState>({ + id: "api", + initial: "idle", + context: { + result: undefined, + error: undefined, + }, + states: { + idle: { + on: { + FETCH: "loading", + }, + }, + loading: { + invoke: { + id: "apiCall", + src: apiCall, + onDone: { + target: "success", + actions: assign({ result: (_, event) => event.data }), + }, + onError: { + target: "failure", + actions: assign({ error: (_, event) => event.data }), + }, + }, + }, + success, + failure: { + on: { + RETRY: "loading", + }, + }, + }, + }); diff --git a/src/components/timerMachine.tsx b/src/components/timerMachine.tsx new file mode 100644 index 0000000..3a0338d --- /dev/null +++ b/src/components/timerMachine.tsx @@ -0,0 +1,82 @@ +import { assign, createMachine } from "xstate"; + +export interface TimerContext { + // The elapsed time (in seconds) + elapsed: number; + // The maximum time (in seconds) + duration: number; + // The interval to send TICK events (in seconds) + interval: number; +} +export type TimerEvent = + | { + // The TICK event sent by the spawned interval service + type: "TICK"; + } + | { + // User intent to update the duration + type: "DURATION.UPDATE"; + value: number; + } + | { + // User intent to reset the elapsed time to 0 + type: "RESET"; + }; +export type TimerState = + | { value: "running"; context: TimerContext } + | { value: "paused"; context: TimerContext }; +export const timerMachine = createMachine( + { + initial: "running", + context: { + elapsed: 0, + duration: 5, + interval: 1, + }, + states: { + running: { + invoke: { + src: (context) => (cb) => { + const interval = setInterval(() => { + console.log(context.elapsed); + cb("TICK"); + }, 1000 * context.interval); + + return () => { + clearInterval(interval); + }; + }, + }, + on: { + "": { + target: "paused", + cond: (context) => { + return context.elapsed >= context.duration; + }, + }, + TICK: { + actions: assign({ + elapsed: (context) => + +(context.elapsed + context.interval).toFixed(2), + }), + }, + }, + }, + paused: { + type: "final", + }, + }, + on: { + "DURATION.UPDATE": { + actions: assign({ + duration: (_, event) => event.value, + }), + }, + RESET: { + actions: assign({ + elapsed: 0, + }) as any, + }, + }, + } +); diff --git a/src/index.tsx b/src/index.tsx index a27a0cc..f6ff5df 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,14 +3,10 @@ import ReactDOM from "react-dom"; import "./index.css"; import App from "./components/App"; import reportWebVitals from "./reportWebVitals"; -import { Provider } from "react-redux"; -import { store } from "./store/store"; ReactDOM.render( - - - + , document.getElementById("root") ); diff --git a/src/store/Player.ts b/src/store/Player.ts deleted file mode 100644 index 93398e0..0000000 --- a/src/store/Player.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { User } from "../api/User"; - -export type Player = { - user: User; - token: string; -}; diff --git a/src/store/gameMiddleware.test.ts b/src/store/gameMiddleware.test.ts deleted file mode 100644 index 5806fb1..0000000 --- a/src/store/gameMiddleware.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { startup } from "./gameSlice"; -import { createStore } from "./store"; - -jest.mock("../api", () => ({ - getToken: () => ({ - user: {}, - token: "123", - }), -})); - -function wait(ms: number = 0) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -describe.only("gameMiddleware", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - it("should create new player if one does not exist", async () => { - const store = createStore(); - store.dispatch(startup()); - await wait(); - const state = store.getState(); - expect(state.game.player!.token).toBe("123"); - }); - it("should use cached player", async () => { - jest - .spyOn(Storage.prototype, "getItem") - .mockImplementation(() => JSON.stringify({ token: "hi!" })); - const store = createStore(); - store.dispatch(startup()); - await wait(); - const state = store.getState(); - expect(state.game.player!.token).toBe("hi!"); - }); -}); diff --git a/src/store/gameMiddleware.ts b/src/store/gameMiddleware.ts deleted file mode 100644 index 39b2fc6..0000000 --- a/src/store/gameMiddleware.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Middleware } from "redux"; -import { newPlayerName } from "../newPlayerName"; -import { - buyShip, - getAvailableLoans, - getAvailableShips, - getFlightPlans, - getLoans, - getMarket, - getShips, - getSystems, - getToken, - newFlightPlan, - purchaseOrder, - requestNewLoan, - setPlayer, - startup, -} from "./gameSlice"; -import { RootState } from "./rootReducer"; -import { updateShip } from "./shipSlice"; -import { AppDispatch } from "./store"; - -type MiddlewareProps = { - dispatch: AppDispatch; - getState: () => RootState; -}; - -export const gameMiddleware: Middleware< - {}, // legacy type parameter added to satisfy interface signature - RootState -> = ({ getState, dispatch }: MiddlewareProps) => { - return (next) => (action) => { - const result = next(action); - const token = getState().game.player?.token || ""; - const username = getState().game.player?.user?.username || ""; - const state = getState(); - const fuel = - state.game.ships[0]?.cargo.filter((c) => c.good === "FUEL").length ?? 0; - - const getStartupData = () => { - dispatch(getSystems(token)); - dispatch( - getLoans({ - token, - username, - }) - ); - dispatch( - getShips({ - token, - username, - }) - ); - }; - - if (startup.match(action)) { - if (!state.game.player) { - dispatch(getToken(newPlayerName())); - } else getStartupData(); - return result; - } else if (setPlayer.match(action)) { - getStartupData(); - } else if (getLoans.fulfilled.match(action)) { - if (!action.payload.loans.length) { - dispatch(getAvailableLoans(token)); - } - } else if (getShips.fulfilled.match(action)) { - if (!action.payload.ships.length) { - dispatch(getAvailableShips(token)); - } else { - dispatch(updateShip(action.payload.ships[0])); - - // if (state.game.ships[0].spaceAvailable) { - // dispatch(getMarket({ token, symbol: state.game.ships[0].location })); - // } else { - // dispatch( - // newFlightPlan({ - // token, - // username, - // shipId: state.game.ships[0].id, - // destination: "OE-PM", - // }) - // ); - // } - } - } else if (getMarket.fulfilled.match(action)) { - // dispatch( - // purchaseOrder({ - // token, - // username, - // good: "METALS", - // quantity: state.game.ships[0].spaceAvailable, - // shipId: state.game.ships[0].id, - // }) - // ); - // dispatch(getFlightPlans({ token, symbol: state.game.systems[0].symbol })); - } - - // TODO: buy loan logic? - if ( - getAvailableLoans.fulfilled.match(action) && - !getState().game.loans.length - ) { - dispatch( - requestNewLoan({ token, username, type: action.payload.loans[0].type }) - ); - } - - // TODO: buy ship logic? - if ( - getAvailableShips.fulfilled.match(action) && - !getState().game.ships.length - ) { - const orderedShips = [...action.payload.ships].sort( - (a, b) => a.purchaseLocations[0].price - b.purchaseLocations[0].price - ); - const cheapestShip = orderedShips[0]; - - dispatch( - buyShip({ - token, - username, - type: cheapestShip.type, - location: cheapestShip.purchaseLocations[0].location, - }) - ); - } - - return result; - }; -}; diff --git a/src/store/gameSlice.ts b/src/store/gameSlice.ts deleted file mode 100644 index 0daf565..0000000 --- a/src/store/gameSlice.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { - AsyncThunk, - CaseReducer, - createSlice, - PayloadAction, -} from "@reduxjs/toolkit"; -import * as api from "../api"; -import { AvailableLoan } from "../api/AvailableLoan"; -import { AvailableShip } from "../api/AvailableShip"; -import { Loan } from "../api/Loan"; -import { LoanType } from "../api/LoanType"; -import { Ship } from "../api/Ship"; -import { System } from "../api/System"; -import { Player } from "./Player"; -import { wrappedThunk } from "./wrappedThunk"; - -type Game = { - player?: Player; - loans: Loan[]; - ships: Ship[]; - availableLoans: AvailableLoan[]; - availableShips: AvailableShip[]; - systems: System[]; - credits: number; -}; - -const getPlayer = () => { - const player = window.localStorage.getItem("player"); - if (player) return JSON.parse(player) as Player; -}; - -const initialState = { - loans: [], - availableLoans: [], - availableShips: [], - ships: [], - systems: [], - credits: 0, -} as Game; - -export const getToken = wrappedThunk("getToken", (username: string) => - api.getToken(username) -); - -export const getAvailableLoans = wrappedThunk( - "getAvailableLoans", - (token: string) => api.getAvailableLoans(token) -); - -export const getSystems = wrappedThunk("getSystems", (token: string) => - api.getSystems(token) -); - -type GetFlightPlansParams = { - token: string; - symbol: string; -}; - -export const getFlightPlans = wrappedThunk( - "getFlightPlans", - ({ token, symbol }: GetFlightPlansParams) => api.getFlightPlans(token, symbol) -); - -export const getUser = wrappedThunk( - "getUser", - ({ token, username }: UserParams) => api.getUser(token, username) -); - -type NewFlightPlanParams = UserParams & { - shipId: string; - destination: string; -}; - -export const newFlightPlan = wrappedThunk( - "newFlightPlan", - ({ token, username, shipId, destination }: NewFlightPlanParams) => - api.newFlightPlan(token, username, shipId, destination) -); - -type OrderParams = UserParams & { - shipId: string; - good: string; - quantity: number; -}; - -export const purchaseOrder = wrappedThunk( - "purchaseOrder", - ({ token, username, shipId, good, quantity }: OrderParams) => - api.purchaseOrder(token, username, shipId, good, quantity) -); -export const sellOrder = wrappedThunk( - "sellOrder", - ({ token, username, shipId, good, quantity }: OrderParams) => - api.sellOrder(token, username, shipId, good, quantity) -); - -type GetMarketParams = { - token: string; - symbol: string; -}; - -export const getMarket = wrappedThunk( - "getMarket", - ({ token, symbol }: GetMarketParams) => api.getMarket(token, symbol) -); - -export const getAvailableShips = wrappedThunk( - "getAvailableShips", - (token: string) => api.getAvailableShips(token) -); - -type RequestNewLoanParams = UserParams & { type: LoanType }; - -export const requestNewLoan = wrappedThunk( - "requestNewLoan", - ({ token, username, type }: RequestNewLoanParams) => - api.requestNewLoan(token, username, type) -); - -type BuyShipParams = UserParams & { type: string; location: string }; - -export const buyShip = wrappedThunk( - "buyShip", - ({ token, username, type, location }: BuyShipParams) => - api.buyShip(token, username, location, type) -); - -type UserParams = { - token: string; - username: string; -}; -export const getLoans = wrappedThunk( - "getLoans", - ({ token, username }: UserParams) => api.getLoans(token, username) -); - -export const getShips = wrappedThunk( - "getShips", - ({ token, username }: UserParams) => api.getShips(token, username) -); - -const gameSlice = createSlice({ - name: "game", - initialState, - reducers: { - setPlayer: (state, action: PayloadAction) => ({ - ...state, - player: action.payload, - }), - startup: (state) => { - const player = getPlayer(); - if (player) return { ...state, player }; - return state; - }, - instructShip: (state, action: PayloadAction) => {}, - }, - extraReducers: (builder) => { - const reduceThunk = ( - thunk: AsyncThunk, - fulfilledCaseReducer: - | CaseReducer> - | undefined = undefined, - fulfilledPayloadLogConverter = (payload: Returned) => payload, - pendingCaseReducer: - | CaseReducer> - | undefined = undefined - ) => { - builder.addCase(thunk.pending, (state, action) => { - console.log(`${thunk.typePrefix}-pending`); - if (pendingCaseReducer) return pendingCaseReducer(state, action); - }); - builder.addCase(thunk.fulfilled, (state, action) => { - console.log( - `${thunk.typePrefix}-fulfilled`, - JSON.stringify(fulfilledPayloadLogConverter(action.payload), null, 2) - ); - if (fulfilledCaseReducer) return fulfilledCaseReducer(state, action); - }); - builder.addCase(thunk.rejected, (state, action) => { - handleRejection(thunk.typePrefix, action.payload); - }); - }; - - reduceThunk( - getToken, - (state, action) => { - localStorage.setItem("player", JSON.stringify(action.payload)); - return { - ...state, - player: { - ...action.payload, - }, - }; - }, - undefined, - (state) => ({ ...state, player: undefined }) - ); - reduceThunk(requestNewLoan); - reduceThunk(buyShip); - reduceThunk(getSystems, (state, action) => ({ - ...state, - systems: action.payload.systems, - })); - reduceThunk(getAvailableLoans, (state, action) => ({ - ...state, - availableLoans: action.payload.loans, - })); - reduceThunk(getLoans, (state, action) => ({ - ...state, - loans: action.payload.loans, - })); - reduceThunk(getShips, (state, action) => ({ - ...state, - ships: action.payload.ships, - })); - reduceThunk(getMarket, (state, action) => ({ - ...state, - systems: [ - ...state.systems.map((system) => ({ - ...system, - locations: [ - ...system.locations.map((location) => - location.symbol === action.payload.planet.symbol - ? action.payload.planet - : location - ), - ], - })), - ], - })); - - reduceThunk(getUser, (state, action) => ({ - ...state, - ...action.payload.user, - })); - reduceThunk(getFlightPlans); - reduceThunk(newFlightPlan); - reduceThunk(purchaseOrder); - reduceThunk(sellOrder); - reduceThunk( - getAvailableShips, - (state, action) => ({ - ...state, - availableShips: action.payload.ships.sort( - (a, b) => a.purchaseLocations[0].price - b.purchaseLocations[0].price - ), - }), - (p) => ({ - ships: p.ships.sort( - (a, b) => a.purchaseLocations[0].price - b.purchaseLocations[0].price - ), - }) - ); - }, -}); - -export const { setPlayer, startup } = gameSlice.actions; - -export default gameSlice.reducer; - -const handleRejection = (name: string, payload: any) => { - console.warn(`${name}-rejected |`, payload.message); -}; diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts deleted file mode 100644 index 826071a..0000000 --- a/src/store/rootReducer.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { combineReducers } from "@reduxjs/toolkit"; -import gameReducer from "./gameSlice"; -import shipReducer from "./shipSlice"; - -export const rootReducer = combineReducers({ - game: gameReducer, - ships: shipReducer, -}); - -export type RootState = ReturnType; diff --git a/src/store/shipSlice.ts b/src/store/shipSlice.ts deleted file mode 100644 index 3d7f216..0000000 --- a/src/store/shipSlice.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { Ship } from "../api/Ship"; - -type ShipInstructions = Ship & { - instructions: string[]; -}; - -const initialState: ShipInstructions[] = []; - -const shipSlice = createSlice({ - name: "ship", - initialState, - reducers: { - instructShip: (state, action: PayloadAction) => {}, - updateShip: (state, action: PayloadAction) => { - console.warn("updateShip", action.payload); - return [ - ...state.filter((s) => s.id !== action.payload.id), - { - ...action.payload, - instructions: - state.find((s) => s.id === action.payload.id)?.instructions ?? [], - }, - ]; - }, - }, -}); - -export const { instructShip, updateShip } = shipSlice.actions; - -export default shipSlice.reducer; diff --git a/src/store/store.ts b/src/store/store.ts deleted file mode 100644 index 980a198..0000000 --- a/src/store/store.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { configureStore } from "@reduxjs/toolkit"; -import { gameMiddleware } from "./gameMiddleware"; -import { rootReducer } from "./rootReducer"; - -export const createStore = () => - configureStore({ - reducer: rootReducer, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware().concat(gameMiddleware), - }); - -export const store = createStore(); - -if (process.env.NODE_ENV === "development" && module.hot) { - module.hot.accept("./rootReducer", () => { - const newRootReducer = require("./rootReducer").default; - store.replaceReducer(newRootReducer); - }); -} - -export type AppDispatch = typeof store.dispatch; diff --git a/src/store/wrappedThunk.ts b/src/store/wrappedThunk.ts deleted file mode 100644 index ba45665..0000000 --- a/src/store/wrappedThunk.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; - -export const wrappedThunk = ( - name: string, - getPromise: (p: T1) => Promise -) => { - return createAsyncThunk(name, async (payload: T1, { rejectWithValue }) => { - return await errorWrapper(getPromise(payload), rejectWithValue); - }); -}; -const errorWrapper = async ( - promise: Promise, - rejectWithValue: (value: unknown) => any -) => { - try { - return await promise; - } catch (e) { - return rejectWithValue({ message: e.message }) as Promise; - } -};