Преглед на файлове

Big refactor to take all the state/callback passing and move it up to the observable stores

Kirk Trombley преди 5 години
родител
ревизия
b824a8916b

+ 2 - 0
client/src/App.js

@@ -14,7 +14,9 @@ const Wrapper = styled.div`
 `
 
 export default () => (
+  <React.StrictMode>
     <Wrapper>
       <Game/>
     </Wrapper>
+  </React.StrictMode>
 );

+ 33 - 60
client/src/components/Game/Game.jsx

@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React from "react";
 import {
   PRE_GAME,
   PRE_ROUND,
@@ -6,19 +6,15 @@ import {
   POST_ROUND,
   POST_GAME,
   ERROR,
-} from "./GameState";
-import HeaderAndFooter from "./HeaderAndFooter";
+} from "../../domain/GameState";
 import PreGame from '../screens/PreGame';
 import PreRound from '../screens/PreRound';
 import GamePanel from "../screens/GamePanel";
 import RoundSummary from '../screens/RoundSummary';
 import PlayerScores from "../screens/PlayerScores";
-import { gameIdStore } from "../../domain/store";
-
-const initialState = {
-  gameState: PRE_GAME,
-  joined: false,
-}
+import { gameIdStore, gameStateStore } from "../../domain/store";
+import useObservable from "../../hooks/useObservable";
+import { useEffect } from "react";
 
 const extractAndRemoveSearchParam = param => {
   const u = new URL(window.location.href);
@@ -28,60 +24,37 @@ const extractAndRemoveSearchParam = param => {
   return extracted;
 }
 
+const componentMap = {
+  [PRE_GAME]: PreGame,
+  [PRE_ROUND]: PreRound,
+  [IN_ROUND]: GamePanel,
+  [POST_ROUND]: RoundSummary,
+  [POST_GAME]: PlayerScores,
+  [ERROR]: () => <p>Application encountered unrecoverable error, please refresh the page.</p>,
+}
+
 const Game = () => {
-  const [ state, setState ] = useState(initialState);
-  const setGameState = gameState => setState({ ...state, gameState });
-  const onGameJoined = () => setState({ gameState: PRE_ROUND, joined: true });
-  
-  const joinCode = extractAndRemoveSearchParam("join");
-  if (joinCode) {
-    gameIdStore.set(joinCode);
-    setGameState(PRE_ROUND);
-  }
+  const gameState = useObservable(gameStateStore);
+
+  useEffect(() => {
+    const joinCode = extractAndRemoveSearchParam("join");
+    if (joinCode) {
+      gameIdStore.set(joinCode);
+      gameStateStore.set(PRE_ROUND);
+    }
+  }, [])
+
+  useEffect(() => {
+    const summaryCode = extractAndRemoveSearchParam("summary");
+    if (summaryCode) {
+      gameIdStore.set(summaryCode);
+      gameStateStore.set(POST_GAME);
+    }
+  }, [])
 
-  const summaryCode = extractAndRemoveSearchParam("summary");
-  if (summaryCode) {
-    gameIdStore.set(summaryCode);
-    setGameState(POST_GAME);
-  }
+  const Screen = componentMap[gameState];
 
-  switch (state.gameState) {
-    case PRE_GAME:
-      return (
-        <HeaderAndFooter>
-          <PreGame onGameJoined={onGameJoined} />
-        </HeaderAndFooter>
-      );
-    case PRE_ROUND:
-      return (
-        <HeaderAndFooter>
-          <PreRound
-            joined={state.joined}
-            onGameJoined={onGameJoined}
-            onStart={() => setGameState(IN_ROUND)}
-          />
-        </HeaderAndFooter>
-      );
-    case IN_ROUND:
-      return <GamePanel
-        onRoundEnd={() => setGameState(POST_ROUND)}
-        onGameEnd={() => setGameState(POST_GAME)}
-      />
-    case POST_ROUND:
-      return <RoundSummary onNext={() => setGameState(IN_ROUND)} />
-    case POST_GAME:
-      return (
-        <HeaderAndFooter>
-          <PlayerScores onReturnToStart={() => setState(initialState)} />
-        </HeaderAndFooter>
-      );
-    case ERROR:
-      // TODO - would be nice to hook this into the sub-components, maybe with a HOC?
-      return <p>Application encountered unrecoverable error, please refresh the page.</p>
-    default:
-      setGameState(ERROR);
-      return <p>Application state is inconsistent, please refresh and rejoin your previous game.</p>
-  }
+  return <Screen />
 }
 
 export default Game;

+ 5 - 4
client/src/components/screens/GamePanel/GamePanel.jsx

@@ -5,7 +5,8 @@ import Loading from '../../util/Loading';
 import GuessPane from "./GuessPane";
 import PositionedStreetView from "./PositionedStreetView";
 import useRoundInfo from "../../../hooks/useRoundInfo";
-import { lastRoundStore } from '../../../domain/store';
+import { lastRoundStore, gameStateStore } from '../../../domain/store';
+import { POST_ROUND, POST_GAME } from '../../../domain/GameState';
 
 const Container = styled.div`
   height: 100%;
@@ -30,13 +31,13 @@ const StreetViewWrapper = styled.div`
   flex: 3;
 `
 
-const GamePanelContainer = ({ onRoundEnd, onGameEnd }) => {
+const GamePanelContainer = ({ onGameEnd }) => {
   const [submitDisabled, setSubmitDisabled] = useState(false);
   const [selectedPoint, setSelectedPoint] = useState(null);
   const [finished, roundInfo] = useRoundInfo();
 
   if (finished) {
-    onGameEnd();
+    gameStateStore.set(POST_GAME);
     return <Loading/>
   }
 
@@ -55,7 +56,7 @@ const GamePanelContainer = ({ onRoundEnd, onGameEnd }) => {
       score,
       totalScore,
     });
-    onRoundEnd();
+    gameStateStore.set(POST_ROUND);
   }
 
   return (

+ 25 - 17
client/src/components/screens/PlayerScores/PlayerScores.jsx

@@ -6,7 +6,9 @@ import PlayerScoreTile from "./PlayerScoreTile";
 import usePlayerScores from '../../../hooks/usePlayerScores';
 import ClickToCopy from '../../util/ClickToCopy';
 import useObservable from '../../../hooks/useObservable';
-import { gameIdStore } from '../../../domain/store';
+import { gameIdStore, gameJoinedStore, gameStateStore } from '../../../domain/store';
+import { PRE_ROUND } from '../../../domain/GameState';
+import HeaderAndFooter from '../../util/HeaderAndFooter';
 
 const Container = styled.div`
   display: flex;
@@ -49,29 +51,35 @@ const getUrl = gameId => {
   return u.href;
 }
 
-export default ({ onReturnToStart }) => {
+export default () => {
   const gameId = useObservable(gameIdStore);
   const scores = usePlayerScores();
+  const onReturnToStart = () => {
+    gameJoinedStore.set(false);
+    gameStateStore.set(PRE_ROUND);
+  }
 
   if (!scores) {
     return <Loading/>
   }
 
   return (
-    <Container>
-      <ScoreBoard>
-        {
-          scores
-            .filter(({ currentRound }) => currentRound === null)
-            .sort((p1, p2) => p1.totalScore > p2.totalScore ? -1 : (p1.totalScore < p2.totalScore ? 1 : 0))
-            .map((data) => <PlayerScoreTile key={data.name} {...data} />)
-        }
-      </ScoreBoard>
-      <LowerContainer>
-        <Label>This page can be directly linked with:</Label>
-        <Label><ClickToCopy text={getUrl(gameId)} /></Label>
-        <StyledButton onClick={onReturnToStart}>Return to Start</StyledButton>
-      </LowerContainer>
-    </Container>
+    <HeaderAndFooter>
+      <Container>
+        <ScoreBoard>
+          {
+            scores
+              .filter(({ currentRound }) => currentRound === null)
+              .sort((p1, p2) => p1.totalScore > p2.totalScore ? -1 : (p1.totalScore < p2.totalScore ? 1 : 0))
+              .map((data) => <PlayerScoreTile key={data.name} {...data} />)
+          }
+        </ScoreBoard>
+        <LowerContainer>
+          <Label>This page can be directly linked with:</Label>
+          <Label><ClickToCopy text={getUrl(gameId)} /></Label>
+          <StyledButton onClick={onReturnToStart}>Return to Start</StyledButton>
+        </LowerContainer>
+      </Container>
+    </HeaderAndFooter>
   );
 }

+ 13 - 8
client/src/components/screens/PreGame/PreGame.jsx

@@ -5,7 +5,9 @@ import PlayerNameInput from "./../../util/PlayerNameInput";
 import { createGame } from "../../../domain/GGSHService";
 import NewGameInput from "./NewGameInput";
 import useObservable from "../../../hooks/useObservable";
-import { playerNameStore, gameIdStore } from "../../../domain/store";
+import { playerNameStore, gameIdStore, gameJoinedStore, gameStateStore } from "../../../domain/store";
+import HeaderAndFooter from "../../util/HeaderAndFooter";
+import { PRE_ROUND } from "../../../domain/GameState";
 
 const Container = styled.div`
   display: flex;
@@ -31,7 +33,7 @@ const Divider = styled.hr`
   }
 `
 
-export default ({ onGameJoined }) => {
+export default () => {
   const [loading, setLoading] = useState(false);
   const playerName = useObservable(playerNameStore);
   const [timer, setTimer] = useState(300);
@@ -44,15 +46,18 @@ export default ({ onGameJoined }) => {
     setLoading(true);
     const gameId = await createGame(timer);
     gameIdStore.set(gameId);
-    onGameJoined();
+    gameJoinedStore.set(true);
+    gameStateStore.set(PRE_ROUND);
   };
   const cannotCreateGame = !playerName || playerName.length === 0;
 
   return (
-    <Container>
-      <PlayerNameInput/>
-      <Divider/>
-      <NewGameInput {...{ onCreateGame, cannotCreateGame, timer, setTimer }} />
-    </Container>
+    <HeaderAndFooter>
+      <Container>
+        <PlayerNameInput/>
+        <Divider/>
+        <NewGameInput {...{ onCreateGame, cannotCreateGame, timer, setTimer }} />
+      </Container>
+    </HeaderAndFooter>
   );
 }

+ 23 - 15
client/src/components/screens/PreRound/PreRound.jsx

@@ -8,7 +8,9 @@ import LobbyPlayerList from "./LobbyPlayerList";
 import { joinGame } from "../../../domain/GGSHService";
 import DelayedButton from "../../util/DelayedButton";
 import useObservable from "../../../hooks/useObservable";
-import { playerNameStore, gameIdStore } from "../../../domain/store";
+import { playerNameStore, gameIdStore, gameJoinedStore, gameStateStore } from "../../../domain/store";
+import { IN_ROUND } from "../../../domain/GameState";
+import HeaderAndFooter from "../../util/HeaderAndFooter";
 
 const InfoContainer = styled.div`
   flex: 3;
@@ -50,7 +52,7 @@ const getUrl = gameId => {
   return u.href;
 }
 
-const LobbyInfo = ({ onStart }) => {
+const LobbyInfo = () => {
   const gameId = useObservable(gameIdStore);
   const playerName = useObservable(playerNameStore);
 
@@ -60,7 +62,7 @@ const LobbyInfo = ({ onStart }) => {
       <Label>Use this link to invite other players:</Label>
       <Label><ClickToCopy text={getUrl(gameId)} /></Label>
       <StyledDelayButton 
-        onEnd={onStart} 
+        onEnd={() => gameStateStore.set(IN_ROUND)} 
         countDownFormatter={rem => `Click to cancel, ${rem}s...`}
       >
         Start Game
@@ -69,7 +71,7 @@ const LobbyInfo = ({ onStart }) => {
   )
 };
 
-const JoinForm = ({ onGameJoined }) => {
+const JoinForm = () => {
   const gameId = useObservable(gameIdStore);
   const playerName = useObservable(playerNameStore);
   const [loading, setLoading] = useState(false);
@@ -92,7 +94,7 @@ const JoinForm = ({ onGameJoined }) => {
     }
     setFailed(false);
     playerNameStore.set(playerName);
-    onGameJoined();
+    gameJoinedStore.set(true);
   };
   const cannotJoinGame = !playerName || playerName.length === 0;
 
@@ -106,15 +108,21 @@ const JoinForm = ({ onGameJoined }) => {
   );
 }
 
-const PreRound = ({ joined, onGameJoined, onStart }) => (
-  <PageContainer>
-    {
-      joined
-        ? <LobbyInfo onStart={onStart}/>
-        : <JoinForm onGameJoined={onGameJoined}/>
-    }
-    <LobbyPlayerList/>
-  </PageContainer>
-);
+const PreRound = ({ onStart }) => {
+  const joined = useObservable(gameJoinedStore);
+
+  return (
+    <HeaderAndFooter>
+      <PageContainer>
+        {
+          joined
+            ? <LobbyInfo onStart={onStart}/>
+            : <JoinForm />
+        }
+        <LobbyPlayerList/>
+      </PageContainer>
+    </HeaderAndFooter>
+  )
+};
 
 export default PreRound;

+ 4 - 2
client/src/components/screens/RoundSummary/RoundInfoPane.jsx

@@ -6,7 +6,8 @@ import DelayedButton from "../../util/DelayedButton";
 import useRoundInfo from "../../../hooks/useRoundInfo";
 import Button from "../../util/Button";
 import useObservable from "../../../hooks/useObservable";
-import { lastRoundStore } from "../../../domain/store";
+import { lastRoundStore, gameStateStore } from "../../../domain/store";
+import { IN_ROUND } from "../../../domain/GameState";
 
 const useTopScorer = (roundNum) => {
   const scores = usePlayerScores();
@@ -55,7 +56,7 @@ const NextButton = styled(DelayedButton)`
   padding: 1em;
 `
 
-export default ({ onNext }) => {
+export default () => {
   const {
     roundNum,
     selectedPoint,
@@ -65,6 +66,7 @@ export default ({ onNext }) => {
   } = useObservable(lastRoundStore);
   const { topPlayer, topScore } = useTopScorer(roundNum);
   const [gameFinished] = useRoundInfo()
+  const onNext = () => gameStateStore.set(IN_ROUND);
 
   return (
     <Container>

+ 2 - 2
client/src/components/screens/RoundSummary/RoundSummary.jsx

@@ -27,7 +27,7 @@ const MapDiv = styled.div`
   }
 `
 
-export default ({ onNext }) => {
+export default () => {
   const {
     selectedPoint,
     targetPoint,
@@ -40,7 +40,7 @@ export default ({ onNext }) => {
   return (
     <Container>
       <MapDiv ref={mapDivRef} />
-      <RoundInfoPane onNext={onNext} />
+      <RoundInfoPane/>
     </Container>
   );
 };

+ 1 - 1
client/src/components/Game/HeaderAndFooter.jsx → client/src/components/util/HeaderAndFooter.jsx

@@ -1,6 +1,6 @@
 import React from "react";
 import styled from "styled-components";
-import ApiInfo from "../util/ApiInfo";
+import ApiInfo from "./ApiInfo";
 
 const Container = styled.div`
   display: block;

+ 0 - 0
client/src/components/Game/GameState.js → client/src/domain/GameState.js


+ 3 - 0
client/src/domain/store.js

@@ -1,5 +1,8 @@
 import { createObservable } from "../hooks/useObservable";
+import { PRE_GAME } from "./GameState";
 
 export const gameIdStore = createObservable(null);
 export const playerNameStore = createObservable(null);
 export const lastRoundStore = createObservable(null);
+export const gameJoinedStore = createObservable(false);
+export const gameStateStore = createObservable(PRE_GAME);