From b8837c3eeda5dbca3c903947c534bb207c82e7cd Mon Sep 17 00:00:00 2001 From: Jeremie Pardou-Piquemal <571533+jrmi@users.noreply.github.com> Date: Sat, 3 Oct 2020 22:15:38 +0200 Subject: [PATCH] Refactor game creation logic --- src/App.js | 15 +-- src/components/Board/Selector.js | 7 + src/components/Board/game/useGameStorage.js | 2 +- src/components/BoardMenuEdit.js | 8 +- src/hooks/useAuth.js | 9 ++ src/hooks/useGame.js | 84 ++++++++++++ src/ui/formUtils/ImageField.js | 2 +- src/views/AuthView.js | 8 ++ src/views/BoardView.js | 4 +- src/views/{GamesView.js => GameListView.js} | 4 +- src/views/GameProvider.js | 140 -------------------- src/views/GameView.js | 112 +++++++++++----- 12 files changed, 202 insertions(+), 193 deletions(-) create mode 100644 src/hooks/useGame.js rename src/views/{GamesView.js => GameListView.js} (98%) delete mode 100644 src/views/GameProvider.js diff --git a/src/App.js b/src/App.js index 36a6f3e..abed4af 100644 --- a/src/App.js +++ b/src/App.js @@ -10,7 +10,7 @@ import { import { RecoilRoot } from "recoil"; import { nanoid } from "nanoid"; -import GamesView from "./views/GamesView"; +import GameListView from "./views/GameListView"; import GameView from "./views/GameView"; import LoginView from "./views/LoginView"; import AuthView from "./views/AuthView"; @@ -30,18 +30,11 @@ function App() { path="/game/:gameId/session/" to={`/game/:gameId/session/${nanoid()}`} /> - - {/* - - */} - - - - - + + - + diff --git a/src/components/Board/Selector.js b/src/components/Board/Selector.js index e7b493a..09d743e 100644 --- a/src/components/Board/Selector.js +++ b/src/components/Board/Selector.js @@ -163,6 +163,13 @@ const Selector = ({ children }) => { }; }, [onMouseUp]); + // Reset selected on unmount + React.useEffect(() => { + return () => { + setSelected([]); + }; + }, [setSelected]); + return (
{ const [gameLocalSave, setGameLocalSave] = useLocalStorage("savedGame", {}); diff --git a/src/components/BoardMenuEdit.js b/src/components/BoardMenuEdit.js index bc077ee..bd8d399 100644 --- a/src/components/BoardMenuEdit.js +++ b/src/components/BoardMenuEdit.js @@ -4,8 +4,8 @@ import { useTranslation } from "react-i18next"; import { useC2C } from "../hooks/useC2C"; -import { updateGame, createGame } from "../utils/api"; -import { useGame } from "../views/GameProvider"; +import { updateGame } from "../utils/api"; +import { useGame } from "../hooks/useGame"; import DownloadGameLink from "../components/DownloadGameLink"; @@ -83,11 +83,9 @@ const BoardMenuEdit = ({ isOpen, setMenuOpen, setShowLoadGameModal }) => { const handleSave = async () => { const currentGame = await getGame(); if (gameId && gameId.length > 8) { - // FIXME - console.log(gameId); await updateGame(gameId, currentGame); } else { - await createGame(currentGame); + console.log("Game not created. It's not a real one."); } setMenuOpen(false); }; diff --git a/src/hooks/useAuth.js b/src/hooks/useAuth.js index 1045dfb..807f5be 100644 --- a/src/hooks/useAuth.js +++ b/src/hooks/useAuth.js @@ -8,10 +8,12 @@ const useAuth = () => { false ); const [userId, setUserId] = useLocalStorage("userId", null); + const mountedRef = React.useRef(true); const login = React.useCallback( async (userHash, token) => { await loginAPI(userHash, token); + if (!mountedRef.current) return; setIsAuthenticated(true); setUserId(userHash); }, @@ -20,10 +22,17 @@ const useAuth = () => { const logout = React.useCallback(async () => { await logoutAPI(); + if (!mountedRef.current) return; setIsAuthenticated(false); setUserId(null); }, [setIsAuthenticated, setUserId]); + React.useEffect(() => { + return () => { + mountedRef.current = false; + }; + }, []); + return { isAuthenticated, userId, diff --git a/src/hooks/useGame.js b/src/hooks/useGame.js new file mode 100644 index 0000000..f7abae4 --- /dev/null +++ b/src/hooks/useGame.js @@ -0,0 +1,84 @@ +import React, { useContext } from "react"; +import { useSetRecoilState, useRecoilCallback } from "recoil"; +import { nanoid } from "nanoid"; + +import { getGame } from "../utils/api"; + +import SubscribeGameEvents from "../components/SubscribeGameEvents"; +import { useItems } from "../components/Board/Items"; +import { + AvailableItemListAtom, + AllItemsSelector, + BoardConfigAtom, +} from "../components/Board"; +import useBoardConfig from "../components/useBoardConfig"; + +export const GameContext = React.createContext({}); + +export const GameProvider = ({ gameId, game, children }) => { + const { setItemList } = useItems(); + const setAvailableItemList = useSetRecoilState(AvailableItemListAtom); + const [, setBoardConfig] = useBoardConfig(); + + const [gameLoaded, setGameLoaded] = React.useState(false); + + const setGame = React.useCallback( + async (newGame) => { + try { + const originalGame = await getGame(gameId); + setAvailableItemList( + originalGame.availableItems.map((item) => ({ + ...item, + id: nanoid(), + })) + ); + } catch { + setAvailableItemList( + newGame.availableItems.map((item) => ({ ...item, id: nanoid() })) + ); + } + setItemList(newGame.items); + setBoardConfig(newGame.board, false); + setGameLoaded(true); + }, + [setAvailableItemList, setBoardConfig, setItemList, gameId] + ); + + const getCurrentGame = useRecoilCallback( + ({ snapshot }) => async () => { + const availableItemList = await snapshot.getPromise( + AvailableItemListAtom + ); + const boardConfig = await snapshot.getPromise(BoardConfigAtom); + const itemList = await snapshot.getPromise(AllItemsSelector); + const currentGame = { + items: itemList, + board: boardConfig, + availableItems: availableItemList, + }; + return currentGame; + }, + [] + ); + + React.useEffect(() => { + if (game) { + setGame(game); + } + }, [game, setGame]); + + return ( + + {gameLoaded && children} + + + ); +}; + +export const useGame = () => { + return useContext(GameContext); +}; + +export default GameProvider; diff --git a/src/ui/formUtils/ImageField.js b/src/ui/formUtils/ImageField.js index ce11258..3394d85 100644 --- a/src/ui/formUtils/ImageField.js +++ b/src/ui/formUtils/ImageField.js @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import styled from "styled-components"; import { useDropzone } from "react-dropzone"; import { uploadImage } from "../../utils/api"; -import { useGame } from "../../views/GameProvider"; +import { useGame } from "../../hooks/useGame"; const Thumbnail = styled.img` height: 50px; diff --git a/src/views/AuthView.js b/src/views/AuthView.js index 6dfefc7..b696626 100644 --- a/src/views/AuthView.js +++ b/src/views/AuthView.js @@ -9,11 +9,19 @@ const AuthView = () => { const { login } = useAuth(); React.useEffect(() => { + let isMounted = true; + const verify = async () => { await login(userHash, token); + if (!isMounted) return; setLogged(true); }; + verify(); + + return () => { + isMounted = false; + }; }, [login, token, userHash]); if (logged) { diff --git a/src/views/BoardView.js b/src/views/BoardView.js index a049120..90947b6 100644 --- a/src/views/BoardView.js +++ b/src/views/BoardView.js @@ -16,7 +16,7 @@ import NavBar from "./NavBar"; import AutoSave from "../components/AutoSave"; import ImageDropNPaste from "../components/ImageDropNPaste"; import { getComponent } from "../components/boardComponents"; -import { useGame } from "../views/GameProvider"; +import { useGame } from "../hooks/useGame"; const StyledBoardView = styled.div` width: 100vw; @@ -32,7 +32,7 @@ const BoardContainer = styled.div` background-color: #202b38; `; -export const BoardView = ({ namespace, editMode = false }) => { +export const BoardView = ({ namespace, edit: editMode = false }) => { const { currentUser, users } = useUsers(); const [showLoadGameModal, setShowLoadGameModal] = React.useState(false); const [showHelpModal, setShowHelpModal] = React.useState(false); diff --git a/src/views/GamesView.js b/src/views/GameListView.js similarity index 98% rename from src/views/GamesView.js rename to src/views/GameListView.js index 32c3e4c..e5208bf 100644 --- a/src/views/GamesView.js +++ b/src/views/GameListView.js @@ -70,7 +70,7 @@ const Game = styled.li` } `; -const GamesView = () => { +const GameListView = () => { const { t } = useTranslation(); const [gameList, setGameList] = React.useState([]); const { isAuthenticated, userId } = useAuth(); @@ -137,4 +137,4 @@ const GamesView = () => { ); }; -export default GamesView; +export default GameListView; diff --git a/src/views/GameProvider.js b/src/views/GameProvider.js deleted file mode 100644 index dab57ad..0000000 --- a/src/views/GameProvider.js +++ /dev/null @@ -1,140 +0,0 @@ -import React, { useContext } from "react"; -import { useSetRecoilState, useRecoilCallback } from "recoil"; -import { nanoid } from "nanoid"; - -import { useC2C } from "../hooks/useC2C"; -import { getGame } from "../utils/api"; - -import SubscribeGameEvents from "../components/SubscribeGameEvents"; -import { useItems } from "../components/Board/Items"; -import { - AvailableItemListAtom, - AllItemsSelector, - BoardConfigAtom, -} from "../components/Board"; -import useBoardConfig from "../components/useBoardConfig"; - -export const GameContext = React.createContext({}); - -export const GameProvider = ({ gameId, create, children }) => { - const [c2c, joined, isMaster] = useC2C(); - const { setItemList } = useItems(); - const setAvailableItemList = useSetRecoilState(AvailableItemListAtom); - const [, setBoardConfig] = useBoardConfig(); - - const [gameLoaded, setGameLoaded] = React.useState(false); - const gameLoadingRef = React.useRef(false); - - const sendLoadGameEvent = React.useCallback( - (game) => { - game.items = game.items.map((item) => ({ ...item, id: nanoid() })); - c2c.publish("loadGame", game); - }, - [c2c] - ); - - const setGame = React.useCallback( - async (game) => { - const originalGame = await getGame(gameId); - if (originalGame) { - setAvailableItemList( - originalGame.availableItems.map((item) => ({ ...item, id: nanoid() })) - ); - } else { - setAvailableItemList( - game.availableItems.map((item) => ({ ...item, id: nanoid() })) - ); - } - setItemList(game.items); - setBoardConfig(game.board, false); - setGameLoaded(true); - }, - [setAvailableItemList, setBoardConfig, setItemList, gameId] - ); - - const getCurrentGame = useRecoilCallback( - ({ snapshot }) => async () => { - const availableItemList = await snapshot.getPromise( - AvailableItemListAtom - ); - const boardConfig = await snapshot.getPromise(BoardConfigAtom); - const itemList = await snapshot.getPromise(AllItemsSelector); - const game = { - items: itemList, - board: boardConfig, - availableItems: availableItemList, - }; - return game; - }, - [] - ); - - React.useEffect(() => { - let isMounted = true; - - const loadGameData = async () => { - try { - let gameData; - - if (create) { - gameData = { - board: { - name: "No name", - }, - items: [], - availableItems: [], - }; - } else { - gameData = await getGame(gameId); - } - - if (!isMounted) return; - setGame(gameData); - sendLoadGameEvent(gameData); - } catch (e) { - console.log(e); - } - }; - - if (gameId && isMaster && !gameLoaded) { - gameLoadingRef.current = true; - loadGameData(); - } - - return () => { - isMounted = false; - }; - }, [gameId, sendLoadGameEvent, isMaster, gameLoaded, setGame, create]); - - React.useEffect(() => { - return () => { - setItemList([]); - setBoardConfig({}, false); - setAvailableItemList([]); - setGameLoaded(false); - }; - }, [setAvailableItemList, setBoardConfig, setItemList]); - - // Load game from master if any - React.useEffect(() => { - if (!gameLoaded && joined && !isMaster && !gameLoadingRef.current) { - gameLoadingRef.current = true; - c2c.call("getGame").then(setGame, () => {}); - } - }, [c2c, isMaster, joined, gameLoaded, setGame]); - - return ( - - {children} - - - ); -}; - -export const useGame = () => { - return useContext(GameContext); -}; - -export default GameProvider; diff --git a/src/views/GameView.js b/src/views/GameView.js index 152a3a4..0a6f646 100644 --- a/src/views/GameView.js +++ b/src/views/GameView.js @@ -1,16 +1,18 @@ -import React, { useRef } from "react"; -import { useParams, useHistory } from "react-router-dom"; +import React from "react"; +import { useParams } from "react-router-dom"; import { nanoid } from "nanoid"; import { Provider } from "@scripters/use-socket.io"; -import { C2CProvider } from "../hooks/useC2C"; +import { C2CProvider, useC2C } from "../hooks/useC2C"; import { SOCKET_URL, SOCKET_OPTIONS } from "../utils/settings"; -import { createGame } from "../utils/api"; import BoardView from "../views/BoardView"; import Waiter from "../ui/Waiter"; -import GameProvider from "./GameProvider"; +import { getGame } from "../utils/api"; + +import GameProvider from "../hooks/useGame"; +import { useTranslation } from "react-i18next"; const newGameData = { items: [], @@ -18,42 +20,90 @@ const newGameData = { board: { size: 1000, scale: 1, name: "New game" }, }; -export const ConnectedGameProvider = ({ create = false, editMode = false }) => { - const { room = nanoid(), gameId } = useParams(); - const history = useHistory(); - const creationRef = useRef(false); +export const GameView = ({ edit }) => { + const [c2c, joined, isMaster] = useC2C(); + const { gameId } = useParams(); + const [realGameId, setRealGameId] = React.useState(); + const [gameLoaded, setGameLoaded] = React.useState(false); + const [game, setGame] = React.useState(null); + const gameLoadingRef = React.useRef(false); + const { t } = useTranslation(); - // 3 cas - // Create -> jeux vide - // Edit -> on va chercher du serveur - // Play master -> on va chercher du serveur - // Play slave -> on récupère du master - - // Create a new game as asked and redirect to it React.useEffect(() => { - const createNewGame = async () => { - const { _id: newGameId } = await createGame(newGameData); - history.push(`/game/${newGameId}/`); - }; - if (create && !creationRef.current) { - createNewGame(); - creationRef.current = true; - } - }, [create, history]); + let isMounted = true; - if (create) { - return ; + const loadGameData = async () => { + try { + let gameData; + + if (!gameId) { + // Create new game + gameData = JSON.parse(JSON.stringify(newGameData)); + setRealGameId(nanoid()); + } else { + // Load game from server + gameData = await getGame(gameId); + setRealGameId(gameId); + } + + // Add id if necessary + gameData.items = gameData.items.map((item) => ({ + ...item, + id: nanoid(), + })); + + if (!isMounted) return; + + setGame(gameData); + // Send loadGame event for other user + c2c.publish("loadGame", gameData); + setGameLoaded(true); + } catch (e) { + console.log(e); + } + }; + + if (joined && isMaster && !gameLoaded && !gameLoadingRef.current) { + gameLoadingRef.current = true; + loadGameData(); + } + + return () => { + isMounted = false; + }; + }, [c2c, gameId, gameLoaded, isMaster, joined]); + + // Load game from master if any + React.useEffect(() => { + if (joined && !isMaster && !gameLoaded && !gameLoadingRef.current) { + gameLoadingRef.current = true; + c2c.call("getGame").then((receivedGame) => { + setGame(receivedGame); + setGameLoaded(true); + }); + } + }, [c2c, isMaster, joined, gameLoaded]); + + if (!gameLoaded) { + return ; } + return ( + + + + ); +}; + +const ConnectedGameView = ({ edit = false }) => { + const { room = nanoid() } = useParams(); return ( - - - + ); }; -export default ConnectedGameProvider; +export default ConnectedGameView;