Refactor game creation logic

This commit is contained in:
Jeremie Pardou-Piquemal 2020-10-03 22:15:38 +02:00 committed by Jérémie Pardou-Piquemal
parent ff53798849
commit b8837c3eed
12 changed files with 202 additions and 193 deletions

View file

@ -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()}`}
/>
<Redirect path="/game/" to={`/game/${nanoid()}/new`} />
{/*<Route exact path="/game/">
<GameSessionView create editMode />
</Route>*/}
<Route path="/game/:gameId/new">
<GameView create editMode />
</Route>
<Route path="/game/:gameId/">
<GameView editMode />
<Route path="/game/:gameId?">
<GameView edit />
</Route>
<Route exact path="/games">
<GamesView />
<GameListView />
</Route>
<Route exact path="/login">
<LoginView />

View file

@ -163,6 +163,13 @@ const Selector = ({ children }) => {
};
}, [onMouseUp]);
// Reset selected on unmount
React.useEffect(() => {
return () => {
setSelected([]);
};
}, [setSelected]);
return (
<div
onMouseDown={onMouseDown}

View file

@ -1,5 +1,5 @@
import React from "react";
import useLocalStorage from "react-use-localstorage";
import useLocalStorage from "../../../hooks/useLocalStorage";
export const useGameStorage = () => {
const [gameLocalSave, setGameLocalSave] = useLocalStorage("savedGame", {});

View file

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

View file

@ -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,

84
src/hooks/useGame.js Normal file
View file

@ -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 (
<GameContext.Provider
value={{ setGame, getGame: getCurrentGame, gameId, gameLoaded }}
>
{gameLoaded && children}
<SubscribeGameEvents getGame={getCurrentGame} setGame={setGame} />
</GameContext.Provider>
);
};
export const useGame = () => {
return useContext(GameContext);
};
export default GameProvider;

View file

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

View file

@ -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) {

View file

@ -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);

View file

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

View file

@ -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 (
<GameContext.Provider
value={{ setGame, getGame: getCurrentGame, gameId, gameLoaded }}
>
{children}
<SubscribeGameEvents getGame={getCurrentGame} setGame={setGame} />
</GameContext.Provider>
);
};
export const useGame = () => {
return useContext(GameContext);
};
export default GameProvider;

View file

@ -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 <Waiter message={"Loading…"} />;
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 <Waiter message={t("Game loading...")} />;
}
return (
<GameProvider game={game} gameId={realGameId}>
<BoardView namespace={gameId} edit={edit} />
</GameProvider>
);
};
const ConnectedGameView = ({ edit = false }) => {
const { room = nanoid() } = useParams();
return (
<Provider url={SOCKET_URL} options={SOCKET_OPTIONS}>
<C2CProvider room={room}>
<GameProvider gameId={gameId} room={room} create={create}>
<BoardView namespace={gameId} editMode={editMode} />
</GameProvider>
<GameView edit={edit} />
</C2CProvider>
</Provider>
);
};
export default ConnectedGameProvider;
export default ConnectedGameView;