diff --git a/.gitignore b/.gitignore index 4d29575..4306416 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# typescript +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/package.json b/package.json index 423154f..f1380b2 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,37 @@ { "name": "owlbear-rodeo", - "version": "1.9.0", + "version": "1.10.0", "private": true, "dependencies": { "@babylonjs/core": "^4.2.0", "@babylonjs/loaders": "^4.2.0", - "@dnd-kit/core": "^3.0.4", - "@dnd-kit/sortable": "^3.1.0", + "@dnd-kit/core": "^3.1.1", + "@dnd-kit/sortable": "^4.0.0", "@mitchemmc/dexie-export-import": "^1.0.1", - "@msgpack/msgpack": "^2.4.1", - "@react-spring/konva": "^9.2.3", - "@sentry/integrations": "^6.3.0", - "@sentry/react": "^6.3.0", - "@stripe/stripe-js": "^1.13.1", - "@tensorflow/tfjs": "^3.3.0", + "@msgpack/msgpack": "^2.7.0", + "@react-spring/konva": "^9.2.4", + "@sentry/integrations": "^6.11.0", + "@sentry/react": "^6.11.0", + "@stripe/stripe-js": "^1.16.0", + "@tensorflow/tfjs": "^3.8.0", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.0.2", - "ammo.js": "kripken/ammo.js#aab297a4164779c3a9d8dc8d9da26958de3cb778", + "ajv": "^8.6.2", + "ammo.js": "kripken/ammo.js", "case": "^1.6.3", - "color": "^3.1.3", - "comlink": "^4.3.0", + "color": "^3.2.1", + "comlink": "^4.3.1", "deep-diff": "^1.0.2", "dexie": "3.1.0-beta.13", - "dexie-react-hooks": "^1.0.6", + "dexie-react-hooks": "^1.0.7", "err-code": "^3.0.1", - "fake-indexeddb": "^3.1.2", + "fake-indexeddb": "^3.1.3", "file-saver": "^2.0.5", "fuse.js": "^6.4.6", "image-outline": "^0.1.0", "intersection-observer": "^0.12.0", - "konva": "^8.1.1", + "konva": "^8.1.3", "lodash.chunk": "^4.2.0", "lodash.clonedeep": "^4.5.0", "lodash.get": "^4.4.2", @@ -38,36 +39,37 @@ "lodash.unset": "^4.5.2", "normalize-wheel": "^1.0.1", "pepjs": "^0.5.3", - "polygon-clipping": "^0.15.2", + "polygon-clipping": "^0.15.3", "pretty-bytes": "^5.6.0", "raw.macro": "^0.4.2", - "react": "^17.0.1", - "react-dom": "^17.0.1", + "react": "^17.0.2", + "react-dom": "^17.0.2", "react-intersection-observer": "^8.32.0", "react-konva": "^17.0.2-5", + "react-konva-utils": "^0.1.7", "react-markdown": "4", "react-media": "^2.0.0-rc.1", - "react-modal": "^3.12.1", - "react-resize-detector": "4.2.3", + "react-modal": "^3.14.3", + "react-resize-detector": "^6.7.4", "react-router-dom": "^5.1.2", - "react-router-hash-link": "^2.2.2", + "react-router-hash-link": "^2.4.3", "react-scripts": "^4.0.3", - "react-select": "^4.2.1", - "react-spring": "^9.2.3", + "react-select": "^4.3.1", + "react-spring": "^9.2.4", "react-textarea-autosize": "^8.3.3", - "react-toast-notifications": "^2.4.3", + "react-toast-notifications": "^2.5.1", "react-use-gesture": "^9.1.3", "shortid": "^2.2.15", - "simple-peer": "feross/simple-peer#694/head", - "simplebar-react": "^2.1.0", + "simple-peer": "^9.11.0", + "simplebar-react": "^2.3.5", "simplify-js": "^1.2.4", - "socket.io-client": "^4.0.0", + "socket.io-client": "^4.1.3", "socket.io-msgpack-parser": "^3.0.1", "source-map-explorer": "^2.5.2", - "theme-ui": "^0.3.1", - "use-image": "^1.0.7", + "theme-ui": "^0.10.0", + "use-image": "^1.0.8", "uuid": "^8.3.2", - "webrtc-adapter": "^7.7.1" + "webrtc-adapter": "^8.1.0" }, "resolutions": { "simple-peer/get-browser-rtc": "substack/get-browser-rtc#4/head" @@ -95,6 +97,25 @@ ] }, "devDependencies": { + "@types/color": "^3.0.1", + "@types/deep-diff": "^1.0.0", + "@types/file-saver": "^2.0.2", + "@types/jest": "^26.0.23", + "@types/lodash.chunk": "^4.2.6", + "@types/lodash.clonedeep": "^4.5.6", + "@types/lodash.get": "^4.4.6", + "@types/lodash.set": "^4.3.6", + "@types/node": "^15.6.0", + "@types/normalize-wheel": "^1.0.0", + "@types/react": "^17.0.6", + "@types/react-dom": "^17.0.5", + "@types/react-modal": "^3.12.0", + "@types/react-router-dom": "^5.1.7", + "@types/react-select": "^4.0.17", + "@types/shortid": "^0.0.29", + "@types/simple-peer": "^9.11.1", + "@types/uuid": "^8.3.1", + "typescript": "^4.2.4", "worker-loader": "^3.0.8" } } diff --git a/src/App.js b/src/App.tsx similarity index 97% rename from src/App.js rename to src/App.tsx index 99058b5..94eb780 100644 --- a/src/App.js +++ b/src/App.tsx @@ -1,8 +1,7 @@ -import React from "react"; import { ThemeProvider } from "theme-ui"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; -import theme from "./theme.js"; +import theme from "./theme"; import Home from "./routes/Home"; import Game from "./routes/Game"; import About from "./routes/About"; diff --git a/src/actions/Action.js b/src/actions/Action.ts similarity index 67% rename from src/actions/Action.js rename to src/actions/Action.ts index a147f2b..a6f12fe 100644 --- a/src/actions/Action.js +++ b/src/actions/Action.ts @@ -1,40 +1,31 @@ -// Load Diff for auto complete -// eslint-disable-next-line no-unused-vars import { Diff } from "deep-diff"; import { diff, revertChanges } from "../helpers/diff"; import cloneDeep from "lodash.clonedeep"; -/** - * @callback ActionUpdate - * @param {any} state - */ - /** * Implementation of the Command Pattern * Wraps an update function with internal state to support undo */ -class Action { +class Action { /** * The update function called with the current state and should return the updated state * This is implemented in the child class - * - * @type {ActionUpdate} */ - update; + update(state: State): State { + return state; + } /** * The changes caused by the last state update - * @type {Diff} */ - changes; + changes: Diff[] | undefined; /** * Executes the action update on the state - * @param {any} state The current state to update - * @returns {any} The updated state + * @param {State} state The current state to update */ - execute(state) { + execute(state: State): State { if (state && this.update) { let newState = this.update(cloneDeep(state)); this.changes = diff(state, newState); @@ -45,10 +36,10 @@ class Action { /** * Reverts the changes caused by the last call of `execute` - * @param {any} state The current state to perform the undo on - * @returns {any} The state with the last changes reverted + * @param {State} state The current state to perform the undo on + * @returns {State} The state with the last changes reverted */ - undo(state) { + undo(state: State): State { if (state && this.changes) { let revertedState = cloneDeep(state); revertChanges(revertedState, this.changes); diff --git a/src/actions/AddShapeAction.js b/src/actions/AddShapeAction.js deleted file mode 100644 index 5147d05..0000000 --- a/src/actions/AddShapeAction.js +++ /dev/null @@ -1,15 +0,0 @@ -import Action from "./Action"; - -class AddShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - for (let shape of shapes) { - shapesById[shape.id] = shape; - } - return shapesById; - }; - } -} - -export default AddShapeAction; diff --git a/src/actions/AddStatesAction.ts b/src/actions/AddStatesAction.ts new file mode 100644 index 0000000..14d9053 --- /dev/null +++ b/src/actions/AddStatesAction.ts @@ -0,0 +1,21 @@ +import Action from "./Action"; + +import { ID } from "../types/Action"; + +class AddStatesAction extends Action> { + states: State[]; + + constructor(states: State[]) { + super(); + this.states = states; + } + + update(statesById: Record) { + for (let state of this.states) { + statesById[state.id] = state; + } + return statesById; + } +} + +export default AddStatesAction; diff --git a/src/actions/CutFogAction.ts b/src/actions/CutFogAction.ts new file mode 100644 index 0000000..779a921 --- /dev/null +++ b/src/actions/CutFogAction.ts @@ -0,0 +1,41 @@ +import polygonClipping from "polygon-clipping"; + +import Action from "./Action"; +import { + addPolygonDifferenceToFog, + addPolygonIntersectionToFog, + fogToGeometry, +} from "../helpers/actions"; + +import { Fog, FogState } from "../types/Fog"; + +class CutFogAction extends Action { + fogs: Fog[]; + + constructor(fog: Fog[]) { + super(); + this.fogs = fog; + } + + update(fogsById: FogState): FogState { + let actionGeom = this.fogs.map(fogToGeometry); + let cutFogs: FogState = {}; + for (let fog of Object.values(fogsById)) { + const fogGeom = fogToGeometry(fog); + try { + const difference = polygonClipping.difference(fogGeom, ...actionGeom); + const intersection = polygonClipping.intersection( + fogGeom, + ...actionGeom + ); + addPolygonDifferenceToFog(fog, difference, cutFogs); + addPolygonIntersectionToFog(fog, intersection, cutFogs); + } catch { + console.error("Unable to find intersection for fogs"); + } + } + return cutFogs; + } +} + +export default CutFogAction; diff --git a/src/actions/CutShapeAction.js b/src/actions/CutShapeAction.js deleted file mode 100644 index 59688e6..0000000 --- a/src/actions/CutShapeAction.js +++ /dev/null @@ -1,38 +0,0 @@ -import polygonClipping from "polygon-clipping"; - -import Action from "./Action"; -import { - addPolygonDifferenceToShapes, - addPolygonIntersectionToShapes, - shapeToGeometry, -} from "../helpers/actions"; - -class CutShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - let actionGeom = shapes.map(shapeToGeometry); - let cutShapes = {}; - for (let shape of Object.values(shapesById)) { - const shapeGeom = shapeToGeometry(shape); - try { - const difference = polygonClipping.difference( - shapeGeom, - ...actionGeom - ); - const intersection = polygonClipping.intersection( - shapeGeom, - ...actionGeom - ); - addPolygonDifferenceToShapes(shape, difference, cutShapes); - addPolygonIntersectionToShapes(shape, intersection, cutShapes); - } catch { - console.error("Unable to find intersection for shapes"); - } - } - return cutShapes; - }; - } -} - -export default CutShapeAction; diff --git a/src/actions/EditShapeAction.js b/src/actions/EditShapeAction.js deleted file mode 100644 index e531df5..0000000 --- a/src/actions/EditShapeAction.js +++ /dev/null @@ -1,17 +0,0 @@ -import Action from "./Action"; - -class EditShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - for (let edit of shapes) { - if (edit.id in shapesById) { - shapesById[edit.id] = { ...shapesById[edit.id], ...edit }; - } - } - return shapesById; - }; - } -} - -export default EditShapeAction; diff --git a/src/actions/EditStatesAction.ts b/src/actions/EditStatesAction.ts new file mode 100644 index 0000000..247828c --- /dev/null +++ b/src/actions/EditStatesAction.ts @@ -0,0 +1,23 @@ +import Action from "./Action"; + +import { ID } from "../types/Action"; + +class EditStatesAction extends Action> { + edits: Partial[]; + + constructor(edits: Partial[]) { + super(); + this.edits = edits; + } + + update(statesById: Record) { + for (let edit of this.edits) { + if (edit.id !== undefined && edit.id in statesById) { + statesById[edit.id] = { ...statesById[edit.id], ...edit }; + } + } + return statesById; + } +} + +export default EditStatesAction; diff --git a/src/actions/RemoveShapeAction.js b/src/actions/RemoveShapeAction.js deleted file mode 100644 index baa2df0..0000000 --- a/src/actions/RemoveShapeAction.js +++ /dev/null @@ -1,13 +0,0 @@ -import Action from "./Action"; -import { omit } from "../helpers/shared"; - -class RemoveShapeAction extends Action { - constructor(shapeIds) { - super(); - this.update = (shapesById) => { - return omit(shapesById, shapeIds); - }; - } -} - -export default RemoveShapeAction; diff --git a/src/actions/RemoveStatesAction.ts b/src/actions/RemoveStatesAction.ts new file mode 100644 index 0000000..aa422ad --- /dev/null +++ b/src/actions/RemoveStatesAction.ts @@ -0,0 +1,21 @@ +import Action from "./Action"; +import { omit } from "../helpers/shared"; + +import { ID } from "../types/Action"; + +class RemoveStatesAction extends Action< + Record +> { + stateIds: string[]; + + constructor(stateIds: string[]) { + super(); + this.stateIds = stateIds; + } + + update(statesById: Record) { + return omit(statesById, this.stateIds); + } +} + +export default RemoveStatesAction; diff --git a/src/actions/SubtractFogAction.ts b/src/actions/SubtractFogAction.ts new file mode 100644 index 0000000..9fa6ff7 --- /dev/null +++ b/src/actions/SubtractFogAction.ts @@ -0,0 +1,32 @@ +import polygonClipping from "polygon-clipping"; + +import Action from "./Action"; +import { addPolygonDifferenceToFog, fogToGeometry } from "../helpers/actions"; + +import { Fog, FogState } from "../types/Fog"; + +class SubtractFogAction extends Action { + fogs: Fog[]; + + constructor(fogs: Fog[]) { + super(); + this.fogs = fogs; + } + + update(fogsById: FogState): FogState { + const actionGeom = this.fogs.map(fogToGeometry); + let subtractedFogs: FogState = {}; + for (let fog of Object.values(fogsById)) { + const fogGeom = fogToGeometry(fog); + try { + const difference = polygonClipping.difference(fogGeom, ...actionGeom); + addPolygonDifferenceToFog(fog, difference, subtractedFogs); + } catch { + console.error("Unable to find difference for fogs"); + } + } + return subtractedFogs; + } +} + +export default SubtractFogAction; diff --git a/src/actions/SubtractShapeAction.js b/src/actions/SubtractShapeAction.js deleted file mode 100644 index 13f915a..0000000 --- a/src/actions/SubtractShapeAction.js +++ /dev/null @@ -1,32 +0,0 @@ -import polygonClipping from "polygon-clipping"; - -import Action from "./Action"; -import { - addPolygonDifferenceToShapes, - shapeToGeometry, -} from "../helpers/actions"; - -class SubtractShapeAction extends Action { - constructor(shapes) { - super(); - this.update = (shapesById) => { - const actionGeom = shapes.map(shapeToGeometry); - let subtractedShapes = {}; - for (let shape of Object.values(shapesById)) { - const shapeGeom = shapeToGeometry(shape); - try { - const difference = polygonClipping.difference( - shapeGeom, - ...actionGeom - ); - addPolygonDifferenceToShapes(shape, difference, subtractedShapes); - } catch { - console.error("Unable to find difference for shapes"); - } - } - return subtractedShapes; - }; - } -} - -export default SubtractShapeAction; diff --git a/src/actions/index.js b/src/actions/index.js deleted file mode 100644 index 7822cc1..0000000 --- a/src/actions/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import AddShapeAction from "./AddShapeAction"; -import CutShapeAction from "./CutShapeAction"; -import EditShapeAction from "./EditShapeAction"; -import RemoveShapeAction from "./RemoveShapeAction"; -import SubtractShapeAction from "./SubtractShapeAction"; - -export { - AddShapeAction, - CutShapeAction, - EditShapeAction, - RemoveShapeAction, - SubtractShapeAction, -}; diff --git a/src/actions/index.ts b/src/actions/index.ts new file mode 100644 index 0000000..4b37526 --- /dev/null +++ b/src/actions/index.ts @@ -0,0 +1,13 @@ +import AddStatesAction from "./AddStatesAction"; +import CutFogAction from "./CutFogAction"; +import EditStatesAction from "./EditStatesAction"; +import RemoveStatesAction from "./RemoveStatesAction"; +import SubtractFogAction from "./SubtractFogAction"; + +export { + AddStatesAction, + CutFogAction, + EditStatesAction, + RemoveStatesAction, + SubtractFogAction, +}; diff --git a/src/components/Accordion.js b/src/components/Accordion.tsx similarity index 84% rename from src/components/Accordion.js rename to src/components/Accordion.tsx index bbb2488..736da4f 100644 --- a/src/components/Accordion.js +++ b/src/components/Accordion.tsx @@ -3,7 +3,13 @@ import { Box, Flex, Text, IconButton, Divider } from "theme-ui"; import ExpandMoreIcon from "../icons/ExpandMoreIcon"; -function Accordion({ heading, children, defaultOpen }) { +type AccordianProps = { + heading: string; + children: React.ReactNode; + defaultOpen: boolean; +}; + +function Accordion({ heading, children, defaultOpen }: AccordianProps) { const [open, setOpen] = useState(defaultOpen); return ( diff --git a/src/components/DatabaseDisabledMessage.tsx b/src/components/DatabaseDisabledMessage.tsx new file mode 100644 index 0000000..f00b673 --- /dev/null +++ b/src/components/DatabaseDisabledMessage.tsx @@ -0,0 +1,24 @@ +import { Box, Text } from "theme-ui"; + +import Link from "./Link"; + +function DatabaseDisabledMessage({ type }: { type: string }) { + return ( + + + {type} saving is unavailable. See FAQ{" "} + for more information. + + + ); +} + +export default DatabaseDisabledMessage; diff --git a/src/components/Divider.js b/src/components/Divider.tsx similarity index 63% rename from src/components/Divider.js rename to src/components/Divider.tsx index 0d51b0a..37b5a54 100644 --- a/src/components/Divider.js +++ b/src/components/Divider.tsx @@ -1,7 +1,16 @@ -import React from "react"; -import { Divider } from "theme-ui"; +import { Divider, DividerProps } from "theme-ui"; -function StyledDivider({ vertical, color, fill }) { +type StyledDividerProps = { + vertical: boolean; + fill: boolean; +} & DividerProps; + +function StyledDivider({ + vertical, + color, + fill, + ...props +}: StyledDividerProps) { return ( ); } diff --git a/src/components/Footer.js b/src/components/Footer.tsx similarity index 96% rename from src/components/Footer.js rename to src/components/Footer.tsx index aa83ee9..fd1e6b8 100644 --- a/src/components/Footer.js +++ b/src/components/Footer.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Flex } from "theme-ui"; import Link from "./Link"; diff --git a/src/components/Link.js b/src/components/Link.tsx similarity index 61% rename from src/components/Link.js rename to src/components/Link.tsx index 5f29f97..ddd4b5c 100644 --- a/src/components/Link.js +++ b/src/components/Link.tsx @@ -1,8 +1,7 @@ -import React from "react"; -import { Link as ThemeLink } from "theme-ui"; +import { Link as ThemeLink, LinkProps } from "theme-ui"; import { HashLink as RouterLink } from "react-router-hash-link"; -function Link({ to, ...rest }) { +function Link({ to, ...rest }: { to: string } & LinkProps) { return ( diff --git a/src/components/LoadingBar.js b/src/components/LoadingBar.tsx similarity index 69% rename from src/components/LoadingBar.js rename to src/components/LoadingBar.tsx index 68e56dc..3f1c1e9 100644 --- a/src/components/LoadingBar.js +++ b/src/components/LoadingBar.tsx @@ -1,9 +1,14 @@ import React, { useEffect, useRef } from "react"; import { Progress } from "theme-ui"; -function LoadingBar({ isLoading, loadingProgressRef }) { - const requestRef = useRef(); - const progressBarRef = useRef(); +type LoadingBarProps = { + isLoading: boolean; + loadingProgressRef: React.MutableRefObject; +}; + +function LoadingBar({ isLoading, loadingProgressRef }: LoadingBarProps) { + const requestRef = useRef(); + const progressBarRef = useRef(null); // Use an animation frame to update the progress bar // This bypasses react allowing the animation to be smooth @@ -21,7 +26,9 @@ function LoadingBar({ isLoading, loadingProgressRef }) { requestRef.current = requestAnimationFrame(animate); return () => { - cancelAnimationFrame(requestRef.current); + if (requestRef.current !== undefined) { + cancelAnimationFrame(requestRef.current); + } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoading]); diff --git a/src/components/LoadingOverlay.js b/src/components/LoadingOverlay.tsx similarity index 79% rename from src/components/LoadingOverlay.js rename to src/components/LoadingOverlay.tsx index 5e46bc0..87b24ce 100644 --- a/src/components/LoadingOverlay.js +++ b/src/components/LoadingOverlay.tsx @@ -3,7 +3,12 @@ import { Box } from "theme-ui"; import Spinner from "./Spinner"; -function LoadingOverlay({ bg, children }) { +type LoadingOverlayProps = { + bg: string; + children?: React.ReactNode; +}; + +function LoadingOverlay({ bg, children }: LoadingOverlayProps) { return ( ; } -function Heading({ level, ...props }) { +function Heading({ level, ...props }: { level: number } & TextProps) { const fontSize = level === 1 ? 5 : level === 2 ? 3 : 1; return ( ; + return ( + +