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;
- }
-};