Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce redux-saga #1

Merged
merged 5 commits into from Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 6 additions & 2 deletions package.json
Expand Up @@ -25,7 +25,7 @@
"react-scripts": "3.4.1",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.8",
"redux-thunk": "^2.3.0",
"redux-saga": "^1.1.3",
"typescript": "^3.8.3"
},
"scripts": {
Expand Down Expand Up @@ -58,7 +58,11 @@
"extends": "react-app"
},
"browserslist": {
"production": [ ">0.2%", "not dead", "not op_mini all" ],
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
Expand Down
21 changes: 11 additions & 10 deletions src/App.tsx
Expand Up @@ -3,10 +3,10 @@ import { useSelector, useDispatch } from 'react-redux';
import { Box, Typography, CircularProgress, Container, Fade } from '@material-ui/core';
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';

import { Store } from './store/types';
import { signInPlayer } from "./store/effects";
import { signInPlayerRequest } from "./state/player/player.actions";
import Home from "./pages/Home";
import Game from "./pages/Game";
import { Root } from "./state/root.types";

const useStyles = makeStyles((theme: Theme) => createStyles({
root: {
Expand All @@ -29,21 +29,22 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
const App: React.FC = () => {
const classes = useStyles();
const dispatch = useDispatch();
const isLoggedIn = useSelector((state: Store.ApplicationState) => !!state.player.id);
const currentGameId = useSelector((state: Store.ApplicationState) => state.player.currentGameId);
const leavingGame = useSelector((state: Store.ApplicationState) => state.loading.leavingGame);
const isLoggedIn = useSelector((state: Root.State) => !!state.player.data.id);
const gameLoaded = useSelector((state: Root.State) => state.game.loaded);
const gameLoading = useSelector((state: Root.State) => state.game.loading);
const gameId = useSelector((state: Root.State) => state.game.data.id);

useEffect(() => {
dispatch(signInPlayer());
dispatch(signInPlayerRequest());
}, [dispatch])

return (
<Box className={classes.root}>
<Fade in={!isLoggedIn}><CircularProgress className={classes.loading} /></Fade>
{isLoggedIn && <Container className={classes.root} maxWidth="md">
<Fade in={!isLoggedIn || gameLoading}><CircularProgress className={classes.loading} /></Fade>
{isLoggedIn && !gameLoading && <Container className={classes.root} maxWidth="md">
<Typography variant="h1" className={classes.title}>CODYSNAMES</Typography>
{!leavingGame && currentGameId && <Game gameId={currentGameId} />}
{(!currentGameId || leavingGame) && <Home />}
{gameLoaded && <Game gameId={gameId} />}
{!gameLoaded && <Home />}
</Container>}
</Box>
);
Expand Down
40 changes: 22 additions & 18 deletions src/components/GameBoard.tsx
Expand Up @@ -3,14 +3,17 @@ import { Grid, Button, Box, Typography } from "@material-ui/core";
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
import classnames from 'classnames';
import isEqual from 'lodash/isEqual';
import { useDispatch, useSelector } from "react-redux";

import GameCard from "../components/GameCard";
import { Store } from "../store/types";
import { useDispatch, useSelector } from "react-redux";
import { doneWithTurn } from "../store/effects";
import { getCurrentPlayerType, getCurrentPlayerColor, getGameCardsWithState } from "../store/selectors";
import { Root } from "../state/root.types";
import { Game } from "../state/game/game.types";
import { GameCard as IGameCard } from "../state/gameCard/gameCard.types";
import { endTurnRequest } from "../state/game/game.actions";
import { databasePushGameCardUpdate, databasePushGameCardStateUpdate } from "../state/gameCard/gameCard.actions";
import { getCurrentPlayerType, getCurrentPlayerColor } from "../state/player/player.selectors";
import { getGameCardsWithState } from "../state/gameCard/gameCard.selectors";
import { useCollection } from "../hooks/useCollection";
import { setGameCardsData, setGameCardStateMap } from "../store/actions";
import { db } from "../services/firebase";

const useStyles = makeStyles((theme: Theme) => createStyles({
Expand Down Expand Up @@ -71,48 +74,49 @@ const GameBoard: React.FC = () => {
const classes = useStyles();
const dispatch = useDispatch();
const cards = useSelector(getGameCardsWithState, isEqual);
const game = useSelector((state: Store.ApplicationState) => state.game, isEqual);
const game = useSelector((state: Root.State) => state.game.data, isEqual);
const currentPlayerType = useSelector(getCurrentPlayerType);
const currentPlayerColor = useSelector(getCurrentPlayerColor);
const leavingGame = useSelector((state: Root.State) => state.game.leaving);

const whoWon = game.whoWon;
const whosTurn = game.turn;
const gameStatus = game.status;
const yourTurn = currentPlayerColor === whosTurn;
const isSpymaster = currentPlayerType === Store.PlayerType.Spymaster;
const isSpymaster = currentPlayerType === Game.PlayerType.Spymaster;

const turnClass = classnames({
[classes.redTurn]: whosTurn === Store.TeamColor.Red,
[classes.blueTurn]: whosTurn === Store.TeamColor.Blue
[classes.redTurn]: whosTurn === Game.TeamColor.Red,
[classes.blueTurn]: whosTurn === Game.TeamColor.Blue
});

useCollection<Store.GameCard>(
useCollection<IGameCard.GameCardEntity>(
() => db
.collection('gameCards')
.where('gameId', '==', game.id)
.orderBy('order', 'asc'),
{
data: gameCards => dispatch(setGameCardsData(gameCards)),
data: gameCards => !leavingGame && dispatch(databasePushGameCardUpdate(gameCards)),
}, [game.id]
)

useCollection<Store.GameCardState>(
useCollection<IGameCard.GameCardStateEntity>(
() => {
const query = db
.collection('gameCardState')
.where('gameId', '==', game.id)

if (
currentPlayerType === Store.PlayerType.Agent &&
game.status !== Store.Status.Over
currentPlayerType === Game.PlayerType.Agent &&
game.status !== Game.Status.Over
) {
return query.where('flipped', '==', true)
}

return query;
},
{
data: gameCardState => dispatch(setGameCardStateMap(gameCardState)),
data: gameCardState => !leavingGame && dispatch(databasePushGameCardStateUpdate(gameCardState)),
}, [game.id, currentPlayerType, game.status]
)

Expand All @@ -123,7 +127,7 @@ const GameBoard: React.FC = () => {
return (
<Box className={classes.root}>
{
gameStatus === Store.Status.Over &&
gameStatus === Game.Status.Over &&
(
<React.Fragment>
<Typography variant="h4" className={classes.gameOver}>Game over, man! Game over!</Typography>
Expand All @@ -132,13 +136,13 @@ const GameBoard: React.FC = () => {
)
}

{gameStatus !== Store.Status.Over && (<Typography variant="h5" className={classes.whosTurn}>
{gameStatus !== Game.Status.Over && (<Typography variant="h5" className={classes.whosTurn}>
Team Turn: <span className={turnClass}>{whosTurn}</span>&nbsp;&nbsp;
{
!isSpymaster && yourTurn && (
<Button
variant="outlined"
onClick={() => dispatch(doneWithTurn())}>
onClick={() => dispatch(endTurnRequest())}>
Done With Turn
</Button>
)
Expand Down
32 changes: 17 additions & 15 deletions src/components/GameCard.tsx
Expand Up @@ -5,8 +5,10 @@ import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
import { useSelector, useDispatch } from "react-redux";
import classnames from 'classnames';

import { Store } from "../store/types";
import { flipCard } from "../store/effects";
import { flipGameCardRequest } from '../state/gameCard/gameCard.actions';
import { Root } from "../state/root.types";
import { GameCard as IGameCard } from "../state/gameCard/gameCard.types";
import { Game } from "../state/game/game.types";

const useStyles = makeStyles((theme: Theme) => createStyles({
root: {
Expand Down Expand Up @@ -55,32 +57,32 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
}
}))

const GameCard: React.FC<{ card: Store.GameCardWithState }> = ({ card }) => {
const currentPlayerType = useSelector((state: Store.ApplicationState) => {
const userId = state.player.id;
if (state.game.blueSpymaster.id === userId || state.game.redSpymaster.id === userId) {
return Store.PlayerType.Spymaster;
const GameCard: React.FC<{ card: IGameCard.GameCardEntityWithStateEntity }> = ({ card }) => {
const currentPlayerType = useSelector((state: Root.State) => {
const userId = state.player.data.id;
if (state.game.data.blueSpymaster.id === userId || state.game.data.redSpymaster.id === userId) {
return Game.PlayerType.Spymaster;
}

return Store.PlayerType.Agent;
return Game.PlayerType.Agent;
});
const isSpymaster = currentPlayerType === Store.PlayerType.Spymaster;
const gameOver = useSelector((state: Store.ApplicationState) => state.game?.status === Store.Status.Over);
const isSpymaster = currentPlayerType === Game.PlayerType.Spymaster;
const gameOver = useSelector((state: Root.State) => state.game.data.status === Game.Status.Over);
const dispatch = useDispatch();

const classes = useStyles();
const paperClasses = classnames(classes.root, {
[classes.red]: (card.state.flipped || isSpymaster || gameOver) && card.state.type === Store.CardType.RedTeam,
[classes.blue]: (card.state.flipped || isSpymaster || gameOver) && card.state.type === Store.CardType.BlueTeam,
[classes.bystander]: (card.state.flipped || isSpymaster || gameOver) && card.state.type === Store.CardType.Bystander,
[classes.assassin]: (card.state.flipped || isSpymaster || gameOver) && card.state.type === Store.CardType.Assassin,
[classes.red]: (card.state.flipped || isSpymaster || gameOver) && card.state.type === IGameCard.CardType.RedTeam,
[classes.blue]: (card.state.flipped || isSpymaster || gameOver) && card.state.type === IGameCard.CardType.BlueTeam,
[classes.bystander]: (card.state.flipped || isSpymaster || gameOver) && card.state.type === IGameCard.CardType.Bystander,
[classes.assassin]: (card.state.flipped || isSpymaster || gameOver) && card.state.type === IGameCard.CardType.Assassin,
[classes.flipped]: card.state.flipped,
[classes.fontSize30]: card.name.length >= 10,
[classes.fontSize35]: card.name.length >= 8 && card.name.length < 10,
[classes.fontSize45]: card.name.length < 8,
})
return (
<Paper className={paperClasses} onClick={() => dispatch(flipCard(card))}>
<Paper className={paperClasses} onClick={() => dispatch(flipGameCardRequest(card.state))}>
{(!card.state.flipped || gameOver) && card.name}
</Paper>
);
Expand Down
17 changes: 9 additions & 8 deletions src/components/GameInfo.tsx
Expand Up @@ -3,10 +3,11 @@ import classnames from 'classnames';
import { Box, Typography, Button } from "@material-ui/core";
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';

import { Store } from "../store/types";
import { useDispatch, useSelector } from "react-redux";
import { leaveGame } from "../store/effects";
import { getCurrentPlayerColor } from "../store/selectors";
import { leaveGameRequest } from "../state/game/game.actions";
import { getCurrentPlayerColor } from "../state/player/player.selectors";
import { Root } from "../state/root.types";
import { Game } from "../state/game/game.types";


const useStyles = makeStyles((theme: Theme) => createStyles({
Expand Down Expand Up @@ -42,13 +43,13 @@ const GameInfo: React.FC = () => {
const classes = useStyles();
const dispatch = useDispatch();

const playerName = useSelector((state: Store.ApplicationState) => state.player.name);
const gameId = useSelector((state: Store.ApplicationState) => state.game.id);
const playerName = useSelector((state: Root.State) => state.player.data.name);
const gameId = useSelector((state: Root.State) => state.game.data.id);
const currentPlayerColor = useSelector(getCurrentPlayerColor);

const colorTag = classnames({
[classes.redColor]: currentPlayerColor === Store.TeamColor.Red,
[classes.blueColor]: currentPlayerColor === Store.TeamColor.Blue
[classes.redColor]: currentPlayerColor === Game.TeamColor.Red,
[classes.blueColor]: currentPlayerColor === Game.TeamColor.Blue
})

return (
Expand All @@ -61,7 +62,7 @@ const GameInfo: React.FC = () => {
<Button
size="small"
variant="outlined"
onClick={() => dispatch(leaveGame())}>
onClick={() => dispatch(leaveGameRequest(gameId))}>
Leave Game
</Button>
</Box>
Expand Down
11 changes: 6 additions & 5 deletions src/components/GameStart.tsx
Expand Up @@ -3,8 +3,9 @@ import { useDispatch, useSelector } from 'react-redux';
import { TextField, Button, Grid, Typography } from '@material-ui/core';
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';

import { createGame, joinGame } from '../store/effects';
import { Store } from "../store/types";
import { createGameRequest } from '../state/game/game.actions';
import { joinGameRequest } from '../state/player/player.actions';
import { Root } from "../state/root.types";


const useStyles = makeStyles((theme: Theme) => createStyles({
Expand All @@ -31,7 +32,7 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
const GameStart: React.FC = () => {
const classes = useStyles();
const [gameId, setGameId] = useState<string | undefined>();
const creatingGame = useSelector((state: Store.ApplicationState) => state.loading.game);
const creatingGame = useSelector((state: Root.State) => state.game.creating);
const dispatch = useDispatch();

return (
Expand All @@ -58,7 +59,7 @@ const GameStart: React.FC = () => {
onChange={(ev) => setGameId(ev.target.value)}
onKeyPress={(ev) => {
if (ev.key === 'Enter' && gameId && gameId.length === 4) {
dispatch(joinGame(gameId))
dispatch(joinGameRequest(gameId.toLowerCase()))
ev.preventDefault();
}
}} />
Expand All @@ -70,7 +71,7 @@ const GameStart: React.FC = () => {
variant="contained"
fullWidth
disabled={creatingGame}
onClick={() => dispatch(createGame())}
onClick={() => dispatch(createGameRequest())}
>
{creatingGame ? 'Creating Game...' : 'Create Game'}
</Button>
Expand Down
31 changes: 16 additions & 15 deletions src/components/Lobby.tsx
Expand Up @@ -3,10 +3,12 @@ import { useSelector, useDispatch } from "react-redux";
import { Grid, Box, Button, Typography, Paper } from '@material-ui/core';
import { makeStyles, Theme, createStyles } from "@material-ui/core/styles";

import { Store } from "../store/types";
import PlayerList from "./PlayerList";
import { promotePlayerToSpymaster, switchTeams, startGame } from "../store/effects";
import { getCurrentPlayerType, getCurrentPlayerColor } from "../store/selectors";
import { promoteToSpymasterRequest, switchTeamsRequest, startGameRequest } from "../state/game/game.actions";
import { getCurrentPlayerType, getCurrentPlayerColor } from "../state/player/player.selectors";
import { Root } from "../state/root.types";
import { Game } from "../state/game/game.types";
import { Player } from "../state/player/player.types";


const useStyles = makeStyles((theme: Theme) => createStyles({
Expand All @@ -21,24 +23,24 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
const Lobby: React.FC = () => {
const classes = useStyles();
const dispatch = useDispatch();
const [gameStarting, setGameStarting] = useState(false);
const gameStarting = useSelector((state: Root.State) => state.game.starting);
const {
redSpymaster,
blueSpymaster,
redAgents,
blueAgents,
id
} = useSelector((state: Store.ApplicationState) => state.game);
} = useSelector((state: Root.State) => state.game.data);
const playerType = useSelector(getCurrentPlayerType);
const playerColor = useSelector(getCurrentPlayerColor);
const isBlueSpymaster = playerType === Store.PlayerType.Spymaster && playerColor === Store.TeamColor.Blue;
const isBlueSpymaster = playerType === Game.PlayerType.Spymaster && playerColor === Game.TeamColor.Blue;

const promoteToSpymasterHandler = (player: Store.Player) => {
dispatch(promotePlayerToSpymaster(player))
const promoteToSpymasterHandler = (player: Player.Entity) => {
dispatch(promoteToSpymasterRequest(player))
}

const switchTeamsHandler = () => {
dispatch(switchTeams())
dispatch(switchTeamsRequest())
}

const canStartGame =
Expand All @@ -55,8 +57,7 @@ const Lobby: React.FC = () => {
color="primary"
disabled={gameStarting}
onClick={() => {
setGameStarting(true)
dispatch(startGame(id))
dispatch(startGameRequest(id))
}}>
{gameStarting ? 'Game Starting...' : 'Start Game'}
</Button>}
Expand All @@ -70,8 +71,8 @@ const Lobby: React.FC = () => {
agents={blueAgents}
spymaster={blueSpymaster}
teamName="Blue Team"
showJoinTeamButton={playerType === Store.PlayerType.Agent && playerColor !== Store.TeamColor.Blue}
showPromoteButton={playerType === Store.PlayerType.Spymaster && playerColor === Store.TeamColor.Blue}
showJoinTeamButton={playerType === Game.PlayerType.Agent && playerColor !== Game.TeamColor.Blue}
showPromoteButton={playerType === Game.PlayerType.Spymaster && playerColor === Game.TeamColor.Blue}
promoteToSpymaster={promoteToSpymasterHandler}
switchTeams={switchTeamsHandler} />
</Paper>
Expand All @@ -83,8 +84,8 @@ const Lobby: React.FC = () => {
agents={redAgents}
spymaster={redSpymaster}
teamName="Red Team"
showJoinTeamButton={playerType === Store.PlayerType.Agent && playerColor !== Store.TeamColor.Red}
showPromoteButton={playerType === Store.PlayerType.Spymaster && playerColor === Store.TeamColor.Red}
showJoinTeamButton={playerType === Game.PlayerType.Agent && playerColor !== Game.TeamColor.Red}
showPromoteButton={playerType === Game.PlayerType.Spymaster && playerColor === Game.TeamColor.Red}
promoteToSpymaster={promoteToSpymasterHandler}
switchTeams={switchTeamsHandler} />
</Paper>
Expand Down