Refactor game creation logic
This commit is contained in:
parent
ff53798849
commit
b8837c3eed
12 changed files with 202 additions and 193 deletions
15
src/App.js
15
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()}`}
|
||||
/>
|
||||
<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 />
|
||||
|
|
|
@ -163,6 +163,13 @@ const Selector = ({ children }) => {
|
|||
};
|
||||
}, [onMouseUp]);
|
||||
|
||||
// Reset selected on unmount
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
setSelected([]);
|
||||
};
|
||||
}, [setSelected]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseDown={onMouseDown}
|
||||
|
|
|
@ -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", {});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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
84
src/hooks/useGame.js
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue