diff --git a/junit.xml b/junit.xml deleted file mode 100644 index c2bd030..0000000 --- a/junit.xml +++ /dev/null @@ -1,209 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/package.json b/package.json index 602877c..928a561 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "7.4.7", + "version": "8.0.0", "description": "React frontend for the Cards 110", "author": "Daithi Hearn", "license": "MIT", @@ -18,6 +18,7 @@ "@mui/x-data-grid": "6.18.7", "@popperjs/core": "2.11.8", "@reduxjs/toolkit": "2.0.1", + "@tanstack/react-query": "^5.17.19", "@types/jest": "29.5.11", "@types/node": "20.10.8", "@types/react": "18.2.47", @@ -54,6 +55,7 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@tanstack/eslint-plugin-query": "^5.17.20", "@types/crypto-js": "4.2.1", "@types/enzyme": "3.10.18", "@types/enzyme-adapter-react-16": "1.0.9", diff --git a/public/manifest.json b/public/manifest.json index 707c028..8640ff7 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "short_name": "Cards 110", "name": "Cards 110", - "version": "7.4.7", + "version": "8.0.0", "icons": [ { "src": "./assets/favicon.png", diff --git a/src/App.tsx b/src/App.tsx index bfeaead..e28e98b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,6 +37,7 @@ import { ThemeProvider, useMediaQuery, } from "@mui/material" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" const AUTHO_DOMAIN = process.env.REACT_APP_AUTH0_DOMAIN as string const AUTH0_CLIENT_ID = process.env.REACT_APP_AUTH0_CLIENT_ID as string @@ -55,6 +56,8 @@ const router = createBrowserRouter( const App = () => { const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)") + const queryClient = new QueryClient() + const theme = React.useMemo( () => createTheme({ @@ -64,20 +67,22 @@ const App = () => { ) return ( - - - - - - - - - - - + + + + + + + + + + + + + ) } export default App diff --git a/src/auth/accessToken.tsx b/src/auth/accessToken.tsx new file mode 100644 index 0000000..2d035b0 --- /dev/null +++ b/src/auth/accessToken.tsx @@ -0,0 +1,49 @@ +import { useAuth0 } from "@auth0/auth0-react" +import { useEffect, useMemo, useState } from "react" +import jwt_decode from "jwt-decode" + +const AUTH0_AUDIENCE = process.env.REACT_APP_AUTH0_AUDIENCE as string +const AUTH0_SCOPE = process.env.REACT_APP_AUTH0_SCOPE as string + +interface JWTToken { + permissions: string[] +} + +const useAccessToken = () => { + const [accessToken, setAccessToken] = useState() + const { user, isAuthenticated, getAccessTokenSilently } = useAuth0() + + useEffect(() => { + if (isAuthenticated && user) { + getAccessTokenSilently({ + authorizationParams: { + audience: AUTH0_AUDIENCE, + redirect_uri: window.location.origin, + scope: AUTH0_SCOPE, + }, + }).then(token => { + setAccessToken(token) + }) + } + }, [user, isAuthenticated, getAccessTokenSilently]) + + const isPlayer = useMemo(() => { + if (!accessToken) { + return false + } + const decodedAccessToken = jwt_decode(accessToken) + return decodedAccessToken.permissions.indexOf("read:game") !== -1 + }, [accessToken]) + + const isAdmin = useMemo(() => { + if (!accessToken) { + return false + } + const decodedAccessToken = jwt_decode(accessToken) + return decodedAccessToken.permissions.indexOf("write:admin") !== -1 + }, [accessToken]) + + return { isPlayer, isAdmin, accessToken: accessToken } +} + +export default useAccessToken diff --git a/src/caches/GameSlice.ts b/src/caches/GameSlice.ts index c164980..2b108cc 100644 --- a/src/caches/GameSlice.ts +++ b/src/caches/GameSlice.ts @@ -1,46 +1,91 @@ import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit" -import { GameState, GameStatus, PlayedCard } from "model/Game" -import { Player } from "model/Player" +import { GameState, GameStateResponse, GameStatus } from "model/Game" import { RoundStatus } from "model/Round" import { RootState } from "./caches" +import { processOrderedCardsAfterGameUpdate } from "utils/GameUtils" +import { Card, EMPTY } from "model/Cards" +import { determineEvent } from "utils/EventUtils" -const initialState: GameState = { +export const initialGameState: GameState = { + revision: -1, iamSpectator: true, isMyGo: false, iamGoer: false, iamDealer: false, iamAdmin: false, cards: [], + cardsFull: [], status: GameStatus.NONE, players: [], } export const gameSlice = createSlice({ name: "game", - initialState: initialState, + initialState: initialGameState, reducers: { - updateGame: (_, action: PayloadAction) => action.payload, - updatePlayers: (state, action: PayloadAction) => { - state.players = action.payload + updateGame: (state, action: PayloadAction) => { + const updatedGame: GameState = { + ...action.payload, + cardsFull: processOrderedCardsAfterGameUpdate( + state.cardsFull, + action.payload.cards, + ), + } + + const event = determineEvent(state, updatedGame) + + console.log("event", event) + + return updatedGame + }, + selectCard: (state, action: PayloadAction) => { + state.cardsFull.forEach(c => { + if (c.name === action.payload.name) c.selected = true + }) + }, + selectCards: (state, action: PayloadAction) => { + state.cardsFull.forEach(c => { + if (action.payload.some(a => a.name === c.name)) + c.selected = true + }) }, - updatePlayedCards: (state, action: PayloadAction) => { - if (state.round) - state.round.currentHand.playedCards = action.payload + toggleSelect: (state, action: PayloadAction) => + state.cardsFull.forEach(c => { + if (c.name === action.payload.name) c.selected = !c.selected + }), + toggleUniqueSelect: (state, action: PayloadAction) => + state.cardsFull.forEach(c => { + if (c.name === action.payload.name) c.selected = !c.selected + else c.selected = false + }), + selectAll: state => { + state.cardsFull.forEach(c => { + if (c.name !== EMPTY.name) c.selected = true + }) }, - disableActions: state => { - state.isMyGo = false + clearSelectedCards: state => { + state.cardsFull.forEach(c => { + c.selected = false + }) }, - resetGame: () => initialState, + replaceMyCards: (state, action: PayloadAction) => { + state.cardsFull = action.payload + }, + resetGame: () => initialGameState, }, }) export const { updateGame, - disableActions, - updatePlayedCards, - updatePlayers, resetGame, + toggleSelect, + toggleUniqueSelect, + selectCard, + selectCards, + selectAll, + clearSelectedCards, + replaceMyCards, } = gameSlice.actions export const getGame = (state: RootState) => state.game @@ -53,6 +98,14 @@ export const getMe = createSelector(getGame, game => game.me) export const getRound = createSelector(getGame, game => game.round) export const getCards = createSelector(getGame, game => game.cards) +export const getCardsFull = createSelector(getGame, game => game.cardsFull) +export const getCardsWithoutBlanks = createSelector(getCardsFull, cards => + cards.filter(c => c.name !== EMPTY.name), +) +export const getSelectedCards = createSelector(getCardsFull, cards => + cards.filter(c => c.selected), +) + export const getSuit = createSelector(getRound, round => round?.suit) export const getGameId = createSelector(getGame, game => game.id) @@ -67,9 +120,9 @@ export const getIsGameActive = createSelector( status => status === GameStatus.ACTIVE, ) -export const getIsGameFinished = createSelector( +export const getIsGameCompleted = createSelector( getGameStatus, - status => status === GameStatus.FINISHED, + status => status === GameStatus.COMPLETED, ) export const getRoundStatus = createSelector(getRound, round => round?.status) @@ -124,3 +177,5 @@ export const getIsInBunker = createSelector( (isMyGo, isRoundCalling, me) => isMyGo && isRoundCalling && me && me?.score < -30, ) + +export const getRevision = createSelector(getGame, game => game.revision) diff --git a/src/caches/MyCardsSlice.ts b/src/caches/MyCardsSlice.ts deleted file mode 100644 index a55bf9b..0000000 --- a/src/caches/MyCardsSlice.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit" -import { CardName, EMPTY, Card } from "model/Cards" -import { processOrderedCardsAfterGameUpdate } from "utils/GameUtils" -import { RootState } from "./caches" - -export interface MyCardsState { - cards: Card[] -} - -const initialState: MyCardsState = { - cards: [], -} - -export const myCardsSlice = createSlice({ - name: "myCards", - initialState: initialState, - reducers: { - updateMyCards: (state, action: PayloadAction) => { - return { - cards: processOrderedCardsAfterGameUpdate( - state.cards, - action.payload, - ), - } - }, - replaceMyCards: (_, action: PayloadAction) => { - return { - cards: action.payload, - } - }, - removeCard: (state, action: PayloadAction) => { - const idx = state.cards.findIndex(c => c.name === action.payload) - if (idx > 0) state.cards[idx] = { ...EMPTY, selected: false } - }, - selectCard: (state, action: PayloadAction) => { - state.cards.forEach(c => { - if (c.name === action.payload.name) c.selected = true - }) - }, - selectCards: (state, action: PayloadAction) => { - state.cards.forEach(c => { - if (action.payload.some(a => a.name === c.name)) - c.selected = true - }) - }, - toggleSelect: (state, action: PayloadAction) => - state.cards.forEach(c => { - if (c.name === action.payload.name) c.selected = !c.selected - }), - toggleUniqueSelect: (state, action: PayloadAction) => - state.cards.forEach(c => { - if (c.name === action.payload.name) c.selected = !c.selected - else c.selected = false - }), - selectAll: state => { - state.cards.forEach(c => { - if (c.name !== EMPTY.name) c.selected = true - }) - }, - clearSelectedCards: state => { - state.cards.forEach(c => { - c.selected = false - }) - }, - clearMyCards: () => initialState, - }, -}) - -export const { - updateMyCards, - replaceMyCards, - removeCard, - clearSelectedCards, - selectAll, - selectCard, - selectCards, - toggleSelect, - toggleUniqueSelect, - clearMyCards, -} = myCardsSlice.actions - -export const getMyCards = (state: RootState) => state.myCards.cards - -export const getMyCardsWithoutBlanks = createSelector(getMyCards, cards => - cards.filter(c => c.name !== EMPTY.name), -) - -export const getSelectedCards = createSelector(getMyCards, cards => - cards.filter(c => c.selected), -) diff --git a/src/caches/MyGamesSlice.ts b/src/caches/MyGamesSlice.ts deleted file mode 100644 index 9210463..0000000 --- a/src/caches/MyGamesSlice.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit" -import { Game } from "model/Game" -import { RootState } from "./caches" - -export interface MyGamesState { - games: Game[] -} - -const initialState: MyGamesState = { - games: [], -} - -export const myGamesSlice = createSlice({ - name: "myGames", - initialState: initialState, - reducers: { - updateMyGames: (_, action: PayloadAction) => { - return { - games: action.payload, - } - }, - addMyGame: (state, action: PayloadAction) => { - return { - games: state.games.concat(action.payload), - } - }, - removeMyGame: (state, action: PayloadAction) => { - let updated = [...state.games] - let idx = updated.findIndex(x => x.id === action.payload) - if (idx === -1) { - return state - } - updated.splice(idx, 1) - return { - games: updated, - } - }, - }, -}) - -export const { updateMyGames, addMyGame, removeMyGame } = myGamesSlice.actions - -export const getMyGames = (state: RootState) => state.myGames.games diff --git a/src/caches/MyProfileSlice.ts b/src/caches/MyProfileSlice.ts deleted file mode 100644 index 0b96807..0000000 --- a/src/caches/MyProfileSlice.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit" -import { MyProfile } from "model/Player" -import { RootState } from "./caches" - -const initialProfileState: MyProfile = { - id: "", - name: "", - picture: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAMAAAAPkIrYAAAAV1BMVEVHcEz/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD5S+t0AAAAHHRSTlMAOAPXFwch/O/lLUINUFmymMWlhPbOYY7eebxrUg5LCAAAAu5JREFUWMOtmFeCgjAQQCEBQg+95v7nFGYCgoqkkC9Xl8f0Ese5PfHYCiEmz7E9cU0FnsIO5Hel2A+3IXl1+CbRIrYgFY1gG2iMiTkpfcsU9rGVofJoI7WdD19QykfX1ycFfCNNidQNnRnyTjMyuk29Mtm/KzY6K+dU3VKVfCrKj9FR71oLViWK+rXygf7j9SToqt0fbabgV1e+fnB//UqSPtppd6hMvrm6tklSyahr3f+hgP/GOqV8YMUfL2SICt377Gr+WAJsha+LApUMQ03ZeOHBSB212o1eW9ZvtVBLIGL8lj9g+Euok8d5+BuWo2sSnWQjGI2fMA/tPutWAYBN5yTAysC1Kx7CTq0gQWMZtBoXIu2QUAT9m5uUzQ6kCM5/i8msBhenZ30QixmW9TQ6aplhBBs3B2hVshdAQ2WBKQutjYLFdmJJwVr42AMrMWf5YLG1/pAB9LVppyBNv0YbiDXasAAxLB9qYAU2LAeyeYmp9m054wPp3DkeFNvajlVjhmOgunYsiIoSkcy3Y4HxI1S1tJxqPVAOp6HiGVZjUpsvWLYJ9MWydKMsD8iy3SmgX7BnWDOWh0dYsubTJ2zfYmBNxt3s040d1rH6AXMtNSd7IIcmTEcnXWsOS21QAduaz4S62lYvaGq5rZIpVOhoLVt+aJlFo9jakBTRfAP2mkPbh5mQGQtWndo+CNkSi7R+TyMpNe+2ODEdCnOyBkhjNH/hoBulHwMB9Ux9yI4LIIERbNLubB37caUSwLDDNWEJrmvlx2NxqA+TqO8VCmGTRpLnzdUKRRBGlb05oq2an4t3DDYLO6Wg3a4emosd3sMbKq4w17mDuNmnSQ9yh6N/tyFvF1D/LJJhSgy5r3IvxlOlV0bjRRYE/UYK74uxXMpFw/MvXDBP7zs/lXGZ5Nt1I2uL2fVAkdRL5oIyIZTvYHba4c5x0Xc54nSUSRhs/SAuTlNpjwzErcvmCxRVmWEvTd254BSJjPI+/3uf+QKDNWmJwx+g3AAAAABJRU5ErkJggg==", - isPlayer: false, - isAdmin: false, - lastAccess: "1970-01-01T00:00:00", -} - -export const myProfileSlice = createSlice({ - name: "myProfile", - initialState: initialProfileState, - reducers: { - updateMyProfile: (_, action: PayloadAction) => { - return action.payload - }, - }, -}) - -export const { updateMyProfile } = myProfileSlice.actions - -export const getMyProfile = (state: RootState) => state.myProfile - -export const getAccessToken = createSelector( - getMyProfile, - myProfile => myProfile.accessToken, -) diff --git a/src/caches/PlayerProfilesSlice.ts b/src/caches/PlayerProfilesSlice.ts deleted file mode 100644 index 3167273..0000000 --- a/src/caches/PlayerProfilesSlice.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit" -import { PlayerProfile } from "model/Player" -import { RootState } from "./caches" - -export interface PlayersState { - players: PlayerProfile[] -} - -const initialState: PlayersState = { - players: [], -} - -export const playerProfilesSlice = createSlice({ - name: "playerProfiles", - initialState: initialState, - reducers: { - updatePlayerProfiles: (_, action: PayloadAction) => { - return { - players: action.payload, - } - }, - }, -}) - -export const { updatePlayerProfiles } = playerProfilesSlice.actions - -export const getPlayerProfiles = (state: RootState) => - state.playerProfiles.players diff --git a/src/caches/SettingsSlice.ts b/src/caches/SettingsSlice.ts deleted file mode 100644 index defa30a..0000000 --- a/src/caches/SettingsSlice.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit" -import { RootState } from "./caches" -import { PlayerSettings } from "model/PlayerSettings" - -const initialState: PlayerSettings = { - autoBuyCards: true, -} - -export const settingsSlice = createSlice({ - name: "settings", - initialState: initialState, - reducers: { - updateSettings: (_, action: PayloadAction) => - action.payload, - }, -}) - -export const { updateSettings } = settingsSlice.actions - -export const getSettings = (state: RootState) => state.settings diff --git a/src/caches/caches.ts b/src/caches/caches.ts index 0148bd8..86755b0 100644 --- a/src/caches/caches.ts +++ b/src/caches/caches.ts @@ -1,28 +1,16 @@ import { - Action, AnyAction, combineReducers, configureStore, Reducer, - ThunkDispatch, } from "@reduxjs/toolkit" -import { myProfileSlice } from "./MyProfileSlice" import { gameSlice } from "./GameSlice" -import { myGamesSlice } from "./MyGamesSlice" -import { myCardsSlice } from "./MyCardsSlice" import { playCardSlice } from "./PlayCardSlice" -import { playerProfilesSlice } from "./PlayerProfilesSlice" -import { settingsSlice } from "./SettingsSlice" const combinedReducer = combineReducers({ - myProfile: myProfileSlice.reducer, game: gameSlice.reducer, - myGames: myGamesSlice.reducer, - playerProfiles: playerProfilesSlice.reducer, - myCards: myCardsSlice.reducer, playCard: playCardSlice.reducer, - settings: settingsSlice.reducer, }) export type RootState = ReturnType @@ -36,22 +24,3 @@ export const store = configureStore({ export type AppStore = typeof store export type AppDispatch = typeof store.dispatch - -export type GenericThunkAction< - TReturnType, - TState, - TExtraThunkArg, - TBasicAction extends Action, -> = ( - dispatch: ThunkDispatch, - getState: () => TState, - extraArgument: TExtraThunkArg, -) => TReturnType - -//Use this when your Thunk is async and has a return type -export type AppThunk = GenericThunkAction< - TReturnType, - RootState, - unknown, - Action -> diff --git a/src/components/Game/Actions/Buying.tsx b/src/components/Game/Actions/Buying.tsx index 772b485..60e8bfc 100644 --- a/src/components/Game/Actions/Buying.tsx +++ b/src/components/Game/Actions/Buying.tsx @@ -1,12 +1,6 @@ import { useCallback, useEffect, useState } from "react" -import GameService from "services/GameService" import { useAppDispatch, useAppSelector } from "caches/hooks" -import { - getMyCardsWithoutBlanks, - getSelectedCards, - selectAll, -} from "caches/MyCardsSlice" import { getGameId, getNumPlayers, @@ -14,12 +8,16 @@ import { getIHavePlayed, getIsMyGo, getSuit, + getCardsWithoutBlanks, + getSelectedCards, + selectAll, } from "caches/GameSlice" import { pickBestCards, riskOfMistakeBuyingCards } from "utils/GameUtils" import ThrowCardsWarningModal from "./ThrowCardsWarningModal" -import { Card } from "model/Cards" +import { Card, CardName } from "model/Cards" import { Button } from "@mui/material" -import { getSettings } from "caches/SettingsSlice" +import { useSettings } from "components/Hooks/useSettings" +import { useGameActions } from "components/Hooks/useGameActions" const WaitingForRoundToStart = () => ( {canCall10 ? ( @@ -79,7 +67,7 @@ const Calling = () => { disabled={!buttonsEnabled} type="button" color="primary" - onClick={() => call(10)}> + onClick={() => call({ gameId, call: "10" })}> 10 ) : null} @@ -88,7 +76,7 @@ const Calling = () => { disabled={!buttonsEnabled} type="button" color="primary" - onClick={() => call(15)}> + onClick={() => call({ gameId, call: "15" })}> 15 ) : null} @@ -97,7 +85,7 @@ const Calling = () => { disabled={!buttonsEnabled} type="button" color="primary" - onClick={() => call(20)}> + onClick={() => call({ gameId, call: "20" })}> 20 ) : null} @@ -106,7 +94,7 @@ const Calling = () => { disabled={!buttonsEnabled} type="button" color="primary" - onClick={() => call(25)}> + onClick={() => call({ gameId, call: "25" })}> 25 ) : null} @@ -114,7 +102,7 @@ const Calling = () => { disabled={!buttonsEnabled} type="button" color="warning" - onClick={() => call(30)}> + onClick={() => call({ gameId, call: "30" })}> {canCallJink ? "Jink" : "30"} diff --git a/src/components/Game/Actions/PlayCard.tsx b/src/components/Game/Actions/PlayCard.tsx index f14ce10..d56c7a7 100644 --- a/src/components/Game/Actions/PlayCard.tsx +++ b/src/components/Game/Actions/PlayCard.tsx @@ -1,10 +1,13 @@ import { useCallback, useEffect, useMemo, useState } from "react" -import GameService from "services/GameService" import { useAppDispatch, useAppSelector } from "caches/hooks" -import { getMyCardsWithoutBlanks, getSelectedCards } from "caches/MyCardsSlice" -import { getGameId, getIsMyGo, getRound } from "caches/GameSlice" -import parseError from "utils/ErrorUtils" +import { + getCardsWithoutBlanks, + getGameId, + getIsMyGo, + getRound, + getSelectedCards, +} from "caches/GameSlice" import { RoundStatus } from "model/Round" import { Button, @@ -18,18 +21,21 @@ import { } from "@mui/material" import { getCardToPlay, updateCardToPlay } from "caches/PlayCardSlice" import { bestCardLead, getBestCard, getWorstCard } from "utils/GameUtils" -import { CardName } from "model/Cards" +import { CARDS, CardName } from "model/Cards" +import { useGameActions } from "components/Hooks/useGameActions" type AutoPlayState = "off" | "best" | "worst" const PlayCard = () => { const dispatch = useAppDispatch() const theme = useTheme() + const { playCard } = useGameActions() const round = useAppSelector(getRound) - const [autoPlay, setAutoPlay] = useState("off") const gameId = useAppSelector(getGameId) - const myCards = useAppSelector(getMyCardsWithoutBlanks) + const myCards = useAppSelector(getCardsWithoutBlanks) const isMyGo = useAppSelector(getIsMyGo) + + const [autoPlay, setAutoPlay] = useState("off") const selectedCards = useAppSelector(getSelectedCards) const cardToPlay = useAppSelector(getCardToPlay) @@ -91,36 +97,10 @@ const PlayCard = () => { ) - const playCard = useCallback( - (card: CardName) => { - console.debug(`Playing card ${card}`) - dispatch(GameService.playCard(gameId!, card)).catch(e => { - console.error(parseError(e)) - }) - }, - [gameId], - ) - const selectCardToPlay = useCallback(() => { - if (selectedCards.length === 1) - dispatch(updateCardToPlay(selectedCards[0])) - }, [selectedCards]) - - // 1. Play card when you've pre-selected a card - // 2. If best card lead or lead from bottom enabled, play worst card - // 3. If lead from the top enabled, play best card - useEffect(() => { - if (round?.suit && isMyGo) { - if (cardToPlay) playCard(cardToPlay) - else if (autoPlay === "worst" || bestCardLead(round)) { - const worstCard = getWorstCard(myCards, round) - if (worstCard) playCard(worstCard.name) - } else if (autoPlay === "best") { - const bestCard = getBestCard(myCards, round) - if (bestCard) playCard(bestCard.name) - } - } - }, [playCard, autoPlay, round, isMyGo, myCards, cardToPlay]) + if (selectedCards.length === 1 && gameId) + playCard({ gameId, card: selectedCards[0].name }) + }, [gameId, selectedCards]) return ( diff --git a/src/components/Game/Actions/SelectSuit.tsx b/src/components/Game/Actions/SelectSuit.tsx index 4770c16..ad54c3c 100644 --- a/src/components/Game/Actions/SelectSuit.tsx +++ b/src/components/Game/Actions/SelectSuit.tsx @@ -1,16 +1,19 @@ import { useCallback, useState } from "react" -import GameService from "services/GameService" -import { useAppDispatch, useAppSelector } from "caches/hooks" -import { getGameId, getIamGoer } from "caches/GameSlice" import { Suit } from "model/Suit" import { useSnackbar } from "notistack" -import { getMyCardsWithoutBlanks, getSelectedCards } from "caches/MyCardsSlice" import { removeAllFromHand } from "utils/GameUtils" import ThrowCardsWarningModal from "./ThrowCardsWarningModal" import { CardName, Card } from "model/Cards" -import parseError from "utils/ErrorUtils" import { Button } from "@mui/material" +import { useAppSelector } from "caches/hooks" +import { + getCardsWithoutBlanks, + getGameId, + getIamGoer, + getSelectedCards, +} from "caches/GameSlice" +import { useGameActions } from "components/Hooks/useGameActions" const WaitingForSuit = () => ( - diff --git a/src/components/Hooks/useGame.tsx b/src/components/Hooks/useGame.tsx new file mode 100644 index 0000000..13798b2 --- /dev/null +++ b/src/components/Hooks/useGame.tsx @@ -0,0 +1,47 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import useAccessToken from "auth/accessToken" +import axios from "axios" +import { CreateGame, Game } from "model/Game" +import { useSnackbar } from "notistack" +import { getDefaultConfig } from "utils/AxiosUtils" +import parseError from "utils/ErrorUtils" + +export const useGame = () => { + const { enqueueSnackbar } = useSnackbar() + const { accessToken } = useAccessToken() + const queryClient = useQueryClient() + + const gamesResp = useQuery({ + queryKey: ["myGames", accessToken], + queryFn: async () => { + if (!accessToken) { + // Handle the case when accessToken is undefined + // For example, return a default value or throw an error + return [] + } + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/v1/game/all`, + getDefaultConfig(accessToken), + ) + return res.data + }, + refetchInterval: 5 * 60_000, + }) + + const createGame = useMutation({ + mutationFn: async (createGame: CreateGame) => { + if (accessToken) { + axios.put( + `${process.env.REACT_APP_API_URL}/api/v1/game`, + createGame, + getDefaultConfig(accessToken), + ) + } + }, + onSuccess: data => + queryClient.invalidateQueries({ queryKey: ["myGames"] }), + onError: e => enqueueSnackbar(parseError(e), { variant: "error" }), + }) + + return { games: gamesResp.data, createGame: createGame.mutate } +} diff --git a/src/components/Hooks/useGameActions.tsx b/src/components/Hooks/useGameActions.tsx new file mode 100644 index 0000000..bb5cfae --- /dev/null +++ b/src/components/Hooks/useGameActions.tsx @@ -0,0 +1,147 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query" +import useAccessToken from "auth/accessToken" +import axios from "axios" +import { initialGameState, updateGame } from "caches/GameSlice" +import { useAppDispatch } from "caches/hooks" +import { Card, CardName } from "model/Cards" +import { GameStateResponse } from "model/Game" +import { Suit } from "model/Suit" +import { useSnackbar } from "notistack" +import { getDefaultConfig } from "utils/AxiosUtils" +import parseError from "utils/ErrorUtils" + +export const useGameActions = () => { + const dispatch = useAppDispatch() + const { accessToken } = useAccessToken() + const { enqueueSnackbar } = useSnackbar() + const queryClient = useQueryClient() + + const call = useMutation({ + mutationFn: async ({ + gameId, + call, + }: { + gameId: string + call: string + }) => { + if (!accessToken) { + return initialGameState + } + const response = await axios.put( + `${process.env.REACT_APP_API_URL}/api/v1/game/${gameId}/call?call=${call}`, + null, + getDefaultConfig(accessToken), + ) + return response.data + }, + onSuccess: data => { + queryClient.setQueryData(["gameState"], data) + dispatch(updateGame(data)) + }, + onError: e => enqueueSnackbar(parseError(e), { variant: "error" }), + }) + + const buyCards = useMutation({ + mutationFn: async ({ + gameId, + cards, + }: { + gameId: string + cards: CardName[] + }) => { + if (!accessToken) { + return initialGameState + } + const response = await axios.put( + `${process.env.REACT_APP_API_URL}/api/v1/game/${gameId}/buy`, + { cards }, + getDefaultConfig(accessToken), + ) + return response.data + }, + onSuccess: data => { + queryClient.setQueryData(["gameState"], data) + dispatch(updateGame(data)) + }, + onError: e => enqueueSnackbar(parseError(e), { variant: "error" }), + }) + + const selectSuit = useMutation({ + mutationFn: async ({ + gameId, + cards, + suit, + }: { + gameId: string + cards: Card[] + suit: Suit + }) => { + if (!accessToken) { + return initialGameState + } + const response = await axios.put( + `${process.env.REACT_APP_API_URL}/api/v1/game/${gameId}/suit`, + { + suit: suit, + cards: cards.map(c => c.name), + }, + getDefaultConfig(accessToken), + ) + return response.data + }, + onSuccess: data => { + queryClient.setQueryData(["gameState"], data) + dispatch(updateGame(data)) + }, + onError: e => enqueueSnackbar(parseError(e), { variant: "error" }), + }) + + const playCard = useMutation({ + mutationFn: async ({ + gameId, + card, + }: { + gameId: string + card: CardName + }) => { + if (!accessToken) { + return initialGameState + } + const response = await axios.put( + `${process.env.REACT_APP_API_URL}/api/v1/game/${gameId}/play?card=${card}`, + null, + getDefaultConfig(accessToken), + ) + return response.data + }, + onSuccess: data => { + queryClient.setQueryData(["gameState"], data) + dispatch(updateGame(data)) + }, + onError: e => enqueueSnackbar(parseError(e), { variant: "error" }), + }) + + const deleteGame = useMutation({ + mutationFn: async ({ gameId }: { gameId: string }) => { + if (accessToken) { + axios.delete( + `${process.env.REACT_APP_API_URL}/api/v1/game/${gameId}`, + getDefaultConfig(accessToken), + ) + } + }, + onSuccess: () => + queryClient.invalidateQueries({ + queryKey: ["myGames", "gameState"], + }), + onError: e => enqueueSnackbar(parseError(e), { variant: "error" }), + }) + + return { + call: call.mutate, + buyCards: buyCards.mutate, + selectSuit: selectSuit.mutate, + playCard: playCard.mutate, + deleteGame: deleteGame.mutate, + } +} diff --git a/src/components/Hooks/useGameState.tsx b/src/components/Hooks/useGameState.tsx new file mode 100644 index 0000000..567fd77 --- /dev/null +++ b/src/components/Hooks/useGameState.tsx @@ -0,0 +1,38 @@ +import { useQuery } from "@tanstack/react-query" +import useAccessToken from "auth/accessToken" +import axios from "axios" +import { getRevision, initialGameState, updateGame } from "caches/GameSlice" +import { useAppDispatch, useAppSelector } from "caches/hooks" +import { GameStateResponse } from "model/Game" +import { getDefaultConfig } from "utils/AxiosUtils" + +export const useGameState = (gameId?: string) => { + const dispatch = useAppDispatch() + const { accessToken } = useAccessToken() + const revision = useAppSelector(getRevision) + + // Game state query + + useQuery({ + queryKey: ["gameState", accessToken, revision, gameId], + queryFn: async () => { + if (!accessToken || !gameId) { + // Handle the case when accessToken is undefined + // For example, return a default value or throw an error + return initialGameState + } + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/v1/game/${gameId}/state?revision=${revision}`, + getDefaultConfig(accessToken), + ) + + if (res.status === 200) { + dispatch(updateGame(res.data)) + } + return res.data + }, + // Refetch the data every 2 seconds + refetchInterval: 10_000, + enabled: !!accessToken && !!gameId, + }) +} diff --git a/src/components/Hooks/useProfiles.tsx b/src/components/Hooks/useProfiles.tsx new file mode 100644 index 0000000..340699d --- /dev/null +++ b/src/components/Hooks/useProfiles.tsx @@ -0,0 +1,87 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import useAccessToken from "auth/accessToken" +import axios from "axios" +import { PlayerProfile } from "model/Player" +import { useSnackbar } from "notistack" +import { getDefaultConfig } from "utils/AxiosUtils" + +export interface UpdateProfilePayload { + name: string + picture: string + forceUpdate?: boolean +} + +export const useProfiles = () => { + const { enqueueSnackbar } = useSnackbar() + const queryClient = useQueryClient() + const { accessToken } = useAccessToken() + + const myProfileRes = useQuery({ + queryKey: ["myProfile", accessToken], + queryFn: async () => { + if (!accessToken) { + // Handle the case when accessToken is undefined + // For example, return a default value or throw an error + return { + id: "", + name: "", + picture: "", + lastAccess: "", + } + } + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/v1/profile`, + getDefaultConfig(accessToken), + ) + return res.data + }, + // Refetch the data 15 minutes + refetchInterval: 15 * 60_000, + enabled: !!accessToken, + }) + + // Player Profiles query + const allProfiles = useQuery({ + queryKey: ["playerProfiles", accessToken], + queryFn: async () => { + if (!accessToken) { + // Handle the case when accessToken is undefined + // For example, return a default value or throw an error + return [] + } + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/v1/profile/all`, + getDefaultConfig(accessToken), + ) + return res.data + }, + // Refetch the data 15 minutes + refetchInterval: 15 * 60_000, + enabled: !!accessToken, + }) + + // Mutations + const updateProfile = useMutation({ + mutationFn: async (profile: UpdateProfilePayload) => { + if (accessToken) { + await axios.put( + `${process.env.REACT_APP_API_URL}/api/v1/profile`, + profile, + getDefaultConfig(accessToken), + ) + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["myProfile", "playerProfiles"], + }) + }, + onError: e => enqueueSnackbar(e.message, { variant: "error" }), + }) + + return { + updateProfile: updateProfile.mutate, + myProfile: myProfileRes.data, + allProfiles: allProfiles.data ?? [], + } +} diff --git a/src/components/Hooks/useSettings.tsx b/src/components/Hooks/useSettings.tsx new file mode 100644 index 0000000..916dae9 --- /dev/null +++ b/src/components/Hooks/useSettings.tsx @@ -0,0 +1,58 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import useAccessToken from "auth/accessToken" +import axios from "axios" +import { PlayerSettings } from "model/PlayerSettings" +import { useSnackbar } from "notistack" +import { getDefaultConfig } from "utils/AxiosUtils" +import parseError from "utils/ErrorUtils" + +export const useSettings = () => { + const { enqueueSnackbar } = useSnackbar() + const queryClient = useQueryClient() + const { accessToken } = useAccessToken() + + // Player Settings query + const { isLoading, data } = useQuery({ + queryKey: ["playerSettings", accessToken], + queryFn: async () => { + if (!accessToken) { + return { + autoBuyCards: false, + } + } + const res = await axios.get( + `${process.env.REACT_APP_API_URL}/api/v1/settings`, + getDefaultConfig(accessToken), + ) + return res.data + }, + // Refetch the data 15 minutes + refetchInterval: 15 * 60_000, + enabled: !!accessToken, + }) + + const updateSettings = useMutation({ + mutationFn: async (settings: PlayerSettings) => { + if (!accessToken) { + return { + autoBuyCards: false, + } + } + const res = await axios.put( + `${process.env.REACT_APP_API_URL}/api/v1/settings`, + settings, + getDefaultConfig(accessToken), + ) + return res.data + }, + onSuccess: data => + queryClient.setQueryData(["playerSettings", accessToken], data), + onError: e => enqueueSnackbar(parseError(e), { variant: "error" }), + }) + + return { + updateSettings: updateSettings.mutate, + isLoading, + settings: data, + } +} diff --git a/src/components/Hooks/useStats.tsx b/src/components/Hooks/useStats.tsx new file mode 100644 index 0000000..c9d8071 --- /dev/null +++ b/src/components/Hooks/useStats.tsx @@ -0,0 +1,33 @@ +import { useQuery } from "@tanstack/react-query" +import useAccessToken from "auth/accessToken" +import axios from "axios" +import { PlayerGameStats } from "model/Player" +import { getDefaultConfig } from "utils/AxiosUtils" + +export const useStats = (playerId: string) => { + const { accessToken } = useAccessToken() + + const statsResponse = useQuery({ + queryKey: ["playerStats", accessToken, playerId], + queryFn: async () => { + if (!accessToken) { + // Handle the case when accessToken is undefined + // For example, return a default value or throw an error + return [] + } + const response = await axios.get( + `${ + process.env.REACT_APP_API_URL + }/api/v1/stats/${encodeURIComponent(playerId)}`, + getDefaultConfig(accessToken), + ) + return response.data + }, + refetchInterval: 15 * 60_000, + enabled: !!accessToken, + }) + + return { + stats: statsResponse.data ?? [], + } +} diff --git a/src/components/Leaderboard/DoublesLeaderboard.tsx b/src/components/Leaderboard/DoublesLeaderboard.tsx index 32c8598..286f355 100644 --- a/src/components/Leaderboard/DoublesLeaderboard.tsx +++ b/src/components/Leaderboard/DoublesLeaderboard.tsx @@ -2,10 +2,10 @@ import React, { useCallback, useMemo } from "react" import VictoryIcon from "@mui/icons-material/EmojiEventsTwoTone" import { useAppSelector } from "caches/hooks" import { getGamePlayers, getIsGameActive, getRound } from "caches/GameSlice" -import { getPlayerProfiles } from "caches/PlayerProfilesSlice" import { compareScore, compareTeamIds } from "utils/PlayerUtils" import { Player } from "model/Player" import { Box, Grid, Typography } from "@mui/material" +import { useProfiles } from "components/Hooks/useProfiles" interface LeaderBoardPlayer { cardsBought?: number @@ -26,24 +26,25 @@ interface DoublesLeaderboardItem { const DoublesLeaderboard = () => { const round = useAppSelector(getRound) const players = useAppSelector(getGamePlayers) - const playerProfiles = useAppSelector(getPlayerProfiles) const isGameActive = useAppSelector(getIsGameActive) + const { allProfiles } = useProfiles() + const previousHand = useMemo(() => { if (round) return round.completedHands[round.completedHands.length - 1] }, [round]) const getProfile = useCallback( (player: Player) => - playerProfiles.find(p => p.id === player.id, [playerProfiles]), - [playerProfiles], + allProfiles.find(p => p.id === player.id, [allProfiles]), + [allProfiles], ) const mapToLeaderboard = useCallback( (player: Player): LeaderBoardPlayer => { const profile = getProfile(player) if (!profile) throw Error("No profile for player") - const previousCard = previousHand?.playedCards.find( + const previousCard = previousHand?.playedCards?.find( c => c.playerId === player.id, ) return { @@ -92,7 +93,7 @@ const DoublesLeaderboard = () => { return items.sort(compareScore) }, [players]) - if (!playerProfiles || playerProfiles.length === 0) { + if (allProfiles.length === 0) { return null } diff --git a/src/components/Leaderboard/SinglesLeaderboard.tsx b/src/components/Leaderboard/SinglesLeaderboard.tsx index 936782b..5add5ca 100644 --- a/src/components/Leaderboard/SinglesLeaderboard.tsx +++ b/src/components/Leaderboard/SinglesLeaderboard.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useMemo } from "react" import VictoryIcon from "@mui/icons-material/EmojiEventsTwoTone" -import { getGame, getIsGameActive } from "caches/GameSlice" -import { useAppSelector } from "caches/hooks" -import { getPlayerProfiles } from "caches/PlayerProfilesSlice" import { Player } from "model/Player" import { Box, Grid, Typography } from "@mui/material" import { CardName } from "model/Cards" +import { useProfiles } from "components/Hooks/useProfiles" +import { useAppSelector } from "caches/hooks" +import { getGamePlayers, getIsGameActive, getRound } from "caches/GameSlice" interface LeaderboardItem { previousCard?: CardName @@ -18,27 +18,25 @@ interface LeaderboardItem { } const SinglesLeaderboard = () => { - const game = useAppSelector(getGame) + const round = useAppSelector(getRound) + const players = useAppSelector(getGamePlayers) const isGameActive = useAppSelector(getIsGameActive) - const playerProfiles = useAppSelector(getPlayerProfiles) + const { allProfiles } = useProfiles() const previousHand = useMemo(() => { - if (game.round) - return game.round.completedHands[ - game.round.completedHands.length - 1 - ] - }, [game]) + if (round) return round.completedHands[round.completedHands.length - 1] + }, [round]) const getProfile = useCallback( (player: Player) => - playerProfiles.find(p => p.id === player.id, [playerProfiles]), - [playerProfiles], + allProfiles.find(p => p.id === player.id, [allProfiles]), + [allProfiles], ) const leaderboardData = useMemo(() => { const leaderboardData: LeaderboardItem[] = [] - game.players.forEach(player => { + players.forEach(player => { const profile = getProfile(player) if (!profile) { return null @@ -49,7 +47,7 @@ const SinglesLeaderboard = () => { score: player.score, cardsBought: player.cardsBought || 0, previousCard: previousHand - ? previousHand.playedCards.find( + ? previousHand?.playedCards?.find( p => p.playerId === profile.id, )?.card : undefined, @@ -58,11 +56,7 @@ const SinglesLeaderboard = () => { }) }) return leaderboardData.sort((a, b) => b.score - a.score) - }, [playerProfiles, game, getProfile, previousHand]) - - if (!game?.status || !playerProfiles || playerProfiles.length === 0) { - return null - } + }, [allProfiles, players, getProfile, previousHand]) return ( diff --git a/src/components/MyGames/MyGames.tsx b/src/components/MyGames/MyGames.tsx index a16af03..deba4ab 100644 --- a/src/components/MyGames/MyGames.tsx +++ b/src/components/MyGames/MyGames.tsx @@ -21,11 +21,10 @@ import { } from "@mui/material" import moment from "moment" -import { useAppSelector } from "caches/hooks" -import { getMyGames } from "caches/MyGamesSlice" -import { getMyProfile } from "caches/MyProfileSlice" import { Game, GameStatus } from "model/Game" import { Player } from "model/Player" +import { useGame } from "components/Hooks/useGame" +import { useProfiles } from "components/Hooks/useProfiles" const sortByDate = (a: Game, b: Game) => { return moment(b.timestamp).diff(moment(a.timestamp)) @@ -37,13 +36,14 @@ const getNumberOfPlayers = (players: Player[]) => { const MyGames = () => { const navigateTo = useNavigate() + const { games } = useGame() - const myGames = useAppSelector(getMyGames) - const myProfile = useAppSelector(getMyProfile) + const { myProfile } = useProfiles() const last10Games = useMemo(() => { - return [...myGames].sort(sortByDate).slice(0, 7) - }, [myGames]) + if (!games) return [] + return [...games].sort(sortByDate).slice(0, 7) + }, [games]) const isGameActive = (game: Game) => { return game.status === GameStatus.ACTIVE @@ -105,14 +105,16 @@ const MyGames = () => { }} /> - - {isWinner(row, myProfile.id!) && ( - - )} - {isLoser(row, myProfile.id!) && ( - - )} - + {myProfile && ( + + {isWinner(row, myProfile.id) && ( + + )} + {isLoser(row, myProfile.id) && ( + + )} + + )} ))} diff --git a/src/components/MyProfile/MyProfileSync.tsx b/src/components/MyProfile/MyProfileSync.tsx index f231079..e175db8 100644 --- a/src/components/MyProfile/MyProfileSync.tsx +++ b/src/components/MyProfile/MyProfileSync.tsx @@ -1,17 +1,11 @@ import React, { useEffect } from "react" -import ProfileService from "services/ProfileService" - import { useAuth0 } from "@auth0/auth0-react" -import { useAppDispatch } from "caches/hooks" import { useSnackbar } from "notistack" -import parseError from "utils/ErrorUtils" - -const AUTH0_AUDIENCE = process.env.REACT_APP_AUTH0_AUDIENCE as string -const AUTH0_SCOPE = process.env.REACT_APP_AUTH0_SCOPE as string +import { useProfiles } from "components/Hooks/useProfiles" const MyProfileSync: React.FC = () => { - const dispatch = useAppDispatch() + const { updateProfile } = useProfiles() const { user, error, isAuthenticated, getAccessTokenSilently } = useAuth0() const { enqueueSnackbar } = useSnackbar() @@ -19,33 +13,9 @@ const MyProfileSync: React.FC = () => { if (error) enqueueSnackbar(error.message, { variant: "error" }) }, [error]) - const updateProfile = async (name: string, picture: string) => { - const accessToken = await getAccessTokenSilently({ - authorizationParams: { - audience: AUTH0_AUDIENCE, - redirect_uri: window.location.origin, - scope: AUTH0_SCOPE, - }, - }) - - dispatch( - ProfileService.updateProfile( - { - name, - picture, - }, - accessToken, - ), - ).catch((e: Error) => - enqueueSnackbar(parseError(e), { - variant: "error", - }), - ) - } - useEffect(() => { if (isAuthenticated && user && user.name && user.picture) { - updateProfile(user.name, user.picture) + updateProfile({ name: user.name, picture: user.picture }) } }, [user, isAuthenticated, getAccessTokenSilently]) diff --git a/src/components/Settings/Avatar/ProfilePictureEditor.tsx b/src/components/Settings/Avatar/ProfilePictureEditor.tsx index 2ade6bf..9987e04 100644 --- a/src/components/Settings/Avatar/ProfilePictureEditor.tsx +++ b/src/components/Settings/Avatar/ProfilePictureEditor.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useRef, useState } from "react" import heic2any from "heic2any" -import { useSnackbar } from "notistack" import { Button, FormControl, @@ -12,15 +11,11 @@ import { ButtonGroup, Box, } from "@mui/material" -import { useAppDispatch, useAppSelector } from "caches/hooks" -import { getMyProfile } from "caches/MyProfileSlice" -import ProfileService from "services/ProfileService" import AvatarEditor from "react-avatar-editor" -import parseError from "utils/ErrorUtils" +import { useProfiles } from "components/Hooks/useProfiles" const ProfilePictureEditor: React.FC = () => { - const dispatch = useAppDispatch() - const { enqueueSnackbar } = useSnackbar() + const { myProfile, updateProfile } = useProfiles() const [selectedImage, updateSelectedImage] = useState() const [scale, updateScale] = useState(1.2) @@ -35,8 +30,6 @@ const ProfilePictureEditor: React.FC = () => { [fileInputRef], ) - const myProfile = useAppSelector(getMyProfile) - const reset = useCallback(() => { updateSelectedImage(undefined) updateScale(1.2) @@ -49,23 +42,15 @@ const ProfilePictureEditor: React.FC = () => { const handleSave = useCallback( (event: React.SyntheticEvent) => { event.preventDefault() - if (!editorRef.current) return + if (!editorRef.current || !myProfile) return const canvasScaled = editorRef.current.getImageScaledToCanvas() const croppedImg = canvasScaled.toDataURL() - dispatch( - ProfileService.updateProfile({ - name: myProfile.name, - picture: croppedImg, - forceUpdate: true, - }), - ) - .catch((e: Error) => - enqueueSnackbar(parseError(e), { variant: "error" }), - ) - .finally(() => { - reset() - }) + updateProfile({ + name: myProfile.name, + picture: croppedImg, + forceUpdate: true, + }) }, [myProfile, editorRef, fileInputRef], ) diff --git a/src/components/Settings/GamePlaySettings.tsx b/src/components/Settings/GamePlaySettings.tsx index 4386ac2..713e703 100644 --- a/src/components/Settings/GamePlaySettings.tsx +++ b/src/components/Settings/GamePlaySettings.tsx @@ -1,24 +1,17 @@ import { FormControl, FormLabel, Checkbox, Grid } from "@mui/material" -import { getSettings } from "caches/SettingsSlice" -import { useAppDispatch, useAppSelector } from "caches/hooks" -import { useSnackbar } from "notistack" +import { useSettings } from "components/Hooks/useSettings" +import { PlayerSettings } from "model/PlayerSettings" import React, { useCallback } from "react" -import SettingsService from "services/SettingsService" -import parseError from "utils/ErrorUtils" const GamePlaySettings: React.FC = () => { - const dispatch = useAppDispatch() - const { enqueueSnackbar } = useSnackbar() - const settings = useAppSelector(getSettings) + const { settings, updateSettings } = useSettings() const toggleAutoBuy = useCallback(async () => { - const updatedSettings = { + const updatedSettings: PlayerSettings = { ...settings, - autoBuyCards: !settings.autoBuyCards, + autoBuyCards: !settings?.autoBuyCards, } - await dispatch(SettingsService.updateSettings(updatedSettings)).catch( - (e: Error) => enqueueSnackbar(parseError(e), { variant: "error" }), - ) + updateSettings(updatedSettings) }, [settings]) return ( @@ -34,7 +27,7 @@ const GamePlaySettings: React.FC = () => { { const theme = useTheme() - const dispatch = useAppDispatch() const { enqueueSnackbar } = useSnackbar() + const { allProfiles } = useProfiles() + const { createGame } = useGame() - const [newGameName, updateNewGameName] = useState("") - const allPlayers = useAppSelector(getPlayerProfiles) + const [newGameName, setNewGameName] = useState("") - const [selectedPlayers, updateSelectedPlayers] = useState( - [], - ) + const [selectedPlayers, setSelectedPlayers] = useState([]) const orderedPlayers = useMemo(() => { - if (!allPlayers || allPlayers.length < 1) return [] + if (allProfiles.length < 1) return [] // Sort by last lastAccess which is a string timestamp in the form 1970-01-01T00:00:00 - return [...allPlayers].sort((a, b) => { + return [...allProfiles].sort((a, b) => { const aDate = new Date(a.lastAccess) const bDate = new Date(b.lastAccess) return bDate.getTime() - aDate.getTime() }) - }, [allPlayers]) + }, [allProfiles]) const togglePlayer = useCallback( (player: PlayerProfile) => { - if (selectedPlayers.includes(player)) { - updateSelectedPlayers( + if (selectedPlayers?.includes(player)) { + setSelectedPlayers( selectedPlayers.filter(p => p.id !== player.id), ) } else { - updateSelectedPlayers([...selectedPlayers, player]) + setSelectedPlayers([...selectedPlayers, player]) } }, [selectedPlayers], @@ -85,23 +80,14 @@ const StartNewGame = () => { name: newGameName, } - dispatch(GameService.put(payload)) - .then(() => { - updateNewGameName("") - enqueueSnackbar("Game started successfully", { - variant: "success", - }) - }) - .catch((e: Error) => - enqueueSnackbar(parseError(e), { variant: "error" }), - ) + createGame(payload) }, [selectedPlayers, newGameName], ) const handleNameChange = useCallback( (e: React.ChangeEvent) => { - updateNewGameName(e.target.value) + setNewGameName(e.target.value) }, [], ) @@ -173,7 +159,7 @@ const StartNewGame = () => { onClick={() => togglePlayer(player) } - selected={selectedPlayers.includes( + selected={selectedPlayers?.includes( player, )} sx={{ @@ -197,7 +183,7 @@ const StartNewGame = () => { "center", }}> Image Preview { - const dispatch = useAppDispatch() let { id } = useParams() - const { enqueueSnackbar } = useSnackbar() - const iamSpectator = useAppSelector(getIamSpectator) + useGameState(id) const isGameActive = useAppSelector(getIsGameActive) - - const fetchData = useCallback(async () => { - if (id) - await dispatch( - GameService.refreshGameState(id, iamSpectator), - ).catch((e: Error) => - enqueueSnackbar(parseError(e), { variant: "error" }), - ) - - await dispatch(GameService.getAllPlayers()).catch((e: Error) => - enqueueSnackbar(parseError(e), { variant: "error" }), - ) - }, [id, iamSpectator]) - - useEffect(() => { - fetchData() - }, [id, iamSpectator]) - - useEffect(() => { - return () => { - console.log("Clearing game") - dispatch(clearMyCards()) - dispatch(clearAutoPlay()) - dispatch(resetGame()) - } - }, []) + const isGameCompleted = useAppSelector(getIsGameCompleted) return (
- {isGameActive ? : } + {isGameActive && } + {isGameCompleted && }
diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 8388759..2758175 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react" +import React from "react" import { Card, CardHeader, Grid } from "@mui/material" import StartNewGame from "components/StartNewGame/StartNewGame" @@ -6,51 +6,16 @@ import MyGames from "components/MyGames/MyGames" import GameStats from "components/GameStats/GameStats" import { withAuthenticationRequired } from "@auth0/auth0-react" -import { useAppDispatch, useAppSelector } from "caches/hooks" -import { getMyProfile } from "caches/MyProfileSlice" -import GameService from "services/GameService" -import { useSnackbar } from "notistack" -import StatsService from "services/StatsService" -import parseError from "utils/ErrorUtils" -import SettingsService from "services/SettingsService" +import useAccessToken from "auth/accessToken" const Home = () => { - const dispatch = useAppDispatch() - const myProfile = useAppSelector(getMyProfile) - const { enqueueSnackbar } = useSnackbar() - - const getSettings = async () => { - await dispatch(SettingsService.getSettings()).catch((e: Error) => - enqueueSnackbar(parseError(e), { variant: "error" }), - ) - } - - const fetchData = async () => { - if (myProfile.isAdmin) - await dispatch(GameService.getAllPlayers()).catch((e: Error) => - enqueueSnackbar(parseError(e), { variant: "error" }), - ) - - await dispatch(GameService.getAll()) - - await dispatch(StatsService.gameStatsForPlayer()).catch((e: Error) => - enqueueSnackbar(parseError(e), { variant: "error" }), - ) - } - - useEffect(() => { - fetchData() - }, []) - - useEffect(() => { - getSettings() - }) + const { isPlayer, isAdmin } = useAccessToken() return (
- {!myProfile.isPlayer && !myProfile.isAdmin ? ( + {!isPlayer && !isAdmin ? ( @@ -60,18 +25,18 @@ const Home = () => { ) : (
- {myProfile.isPlayer ? ( + {isPlayer ? ( ) : null} - {myProfile.isPlayer && !myProfile.isAdmin ? ( + {isPlayer && !isAdmin ? ( ) : null} - {myProfile.isAdmin ? : null} + {isAdmin ? : null}
)}
diff --git a/src/pages/Layout/Layout.tsx b/src/pages/Layout/Layout.tsx index 9d1dfc9..ae85345 100644 --- a/src/pages/Layout/Layout.tsx +++ b/src/pages/Layout/Layout.tsx @@ -1,17 +1,16 @@ import { useAuth0 } from "@auth0/auth0-react" import { Outlet } from "react-router" -import { useAppSelector } from "caches/hooks" -import { getAccessToken } from "caches/MyProfileSlice" import DefaultHeader from "components/Header/Header" import { useEffect } from "react" import Loading from "components/icons/Loading" import { Box } from "@mui/material" +import useAccessToken from "auth/accessToken" const AUTH0_AUDIENCE = process.env.REACT_APP_AUTH0_AUDIENCE as string const AUTH0_SCOPE = process.env.REACT_APP_AUTH0_SCOPE as string const Layout = () => { - const accessToken = useAppSelector(getAccessToken) + const { accessToken } = useAccessToken() const { isLoading, isAuthenticated, loginWithRedirect } = useAuth0() useEffect(() => { diff --git a/src/services/GameService.ts b/src/services/GameService.ts deleted file mode 100644 index 045ad23..0000000 --- a/src/services/GameService.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { Card } from "model/Cards" -import { CreateGame, Game, GameState } from "model/Game" -import { Suit } from "model/Suit" -import axios from "axios" -import { getDefaultConfig } from "utils/AxiosUtils" -import { Player, PlayerProfile } from "model/Player" -import { AppThunk } from "caches/caches" -import { updateGame, updatePlayers } from "caches/GameSlice" -import { getAccessToken } from "caches/MyProfileSlice" -import { addMyGame, removeMyGame, updateMyGames } from "caches/MyGamesSlice" -import { updatePlayerProfiles } from "caches/PlayerProfilesSlice" -import { - clearSelectedCards, - removeCard, - updateMyCards, -} from "caches/MyCardsSlice" -import { clearAutoPlay } from "caches/PlayCardSlice" - -const getGame = - (gameId: string): AppThunk> => - async (_, getState) => { - const accessToken = getAccessToken(getState()) - - const response = await axios.get( - `${process.env.REACT_APP_API_URL}/api/v1/game?gameId=${gameId}`, - getDefaultConfig(accessToken), - ) - return response.data - } - -const refreshGameState = - (gameId: string, iamSpectator: boolean): AppThunk> => - async (dispatch, getState) => { - const accessToken = getAccessToken(getState()) - const response = await axios.get( - `${process.env.REACT_APP_API_URL}/api/v1/gameState?gameId=${gameId}`, - getDefaultConfig(accessToken), - ) - dispatch(updateGame(response.data)) - if (!iamSpectator) { - dispatch(updateMyCards(response.data.cards)) - dispatch(clearAutoPlay()) - } - } - -const getAll = (): AppThunk> => async (dispatch, getState) => { - const accessToken = getAccessToken(getState()) - const response = await axios.get( - `${process.env.REACT_APP_API_URL}/api/v1/game/all`, - getDefaultConfig(accessToken), - ) - dispatch(updateMyGames(response.data)) - return response.data -} - -const getAllPlayers = - (): AppThunk> => async (dispatch, getState) => { - const accessToken = getAccessToken(getState()) - const response = await axios.get( - `${process.env.REACT_APP_API_URL}/api/v1/admin/game/players/all`, - getDefaultConfig(accessToken), - ) - dispatch(updatePlayerProfiles(response.data)) - return response.data - } - -const getPlayersForGame = - (gameId: string): AppThunk> => - async (dispatch, getState) => { - const accessToken = getAccessToken(getState()) - const response = await axios.get( - `${process.env.REACT_APP_API_URL}/api/v1/game/players?gameId=${gameId}`, - getDefaultConfig(accessToken), - ) - - dispatch(updatePlayers(response.data)) - - return response.data - } - -const put = - (createGame: CreateGame): AppThunk> => - async (dispatch, getState) => { - const accessToken = getAccessToken(getState()) - const response = await axios.put( - `${process.env.REACT_APP_API_URL}/api/v1/admin/game`, - createGame, - getDefaultConfig(accessToken), - ) - - dispatch(addMyGame(response.data)) - return response.data - } - -const finish = - (gameId: string): AppThunk> => - async (_, getState) => { - const accessToken = getAccessToken(getState()) - - await axios.put( - `${process.env.REACT_APP_API_URL}/api/v1/admin/game/finish?gameId=${gameId}`, - null, - getDefaultConfig(accessToken), - ) - } - -const cancel = - (gameId: string): AppThunk> => - async (_, getState) => { - const accessToken = getAccessToken(getState()) - - await axios.put( - `${process.env.REACT_APP_API_URL}/api/v1/admin/game/cancel?gameId=${gameId}`, - null, - getDefaultConfig(accessToken), - ) - } - -const deleteGame = - (gameId: string): AppThunk> => - async (dispatch, getState) => { - const accessToken = getAccessToken(getState()) - - await axios.delete( - `${process.env.REACT_APP_API_URL}/api/v1/admin/game?gameId=${gameId}`, - getDefaultConfig(accessToken), - ) - dispatch(removeMyGame(gameId)) - } - -const replay = - (gameId: string): AppThunk> => - async (_, getState) => { - const accessToken = getAccessToken(getState()) - - await axios.put( - `${process.env.REACT_APP_API_URL}/api/v1/replay?gameId=${gameId}`, - null, - getDefaultConfig(accessToken), - ) - } - -const call = - (gameId: string, call: number): AppThunk> => - async (_, getState) => { - const accessToken = getAccessToken(getState()) - - await axios.put( - `${process.env.REACT_APP_API_URL}/api/v1/call?gameId=${gameId}&call=${call}`, - null, - getDefaultConfig(accessToken), - ) - } - -const buyCards = - (gameId: string, cards: Card[] | string[]): AppThunk> => - async (dispatch, getState) => { - const accessToken = getAccessToken(getState()) - - await axios.put( - `${process.env.REACT_APP_API_URL}/api/v1/buyCards?gameId=${gameId}`, - cards.map(c => (typeof c === "string" ? c : c.name)), - getDefaultConfig(accessToken), - ) - dispatch(clearSelectedCards) - } - -const chooseFromDummy = - (gameId: string, cards: Card[], suit: Suit): AppThunk> => - async (dispatch, getState) => { - const accessToken = getAccessToken(getState()) - await axios.put( - `${process.env.REACT_APP_API_URL}/api/v1/chooseFromDummy?gameId=${gameId}&suit=${suit}`, - cards.map(c => c.name), - getDefaultConfig(accessToken), - ) - dispatch(clearSelectedCards) - } - -const playCard = - (gameId: string, card: string): AppThunk> => - async (dispatch, getState) => { - const accessToken = getAccessToken(getState()) - await axios.put( - `${process.env.REACT_APP_API_URL}/api/v1/playCard?gameId=${gameId}&card=${card}`, - null, - getDefaultConfig(accessToken), - ) - - dispatch(removeCard(card)) - dispatch(clearAutoPlay()) - } - -export default { - getGame, - refreshGameState, - getAll, - getAllPlayers, - getPlayersForGame, - deleteGame, - playCard, - chooseFromDummy, - buyCards, - call, - replay, - put, - finish, - cancel, -} diff --git a/src/services/ProfileService.ts b/src/services/ProfileService.ts deleted file mode 100644 index 2f8d2ed..0000000 --- a/src/services/ProfileService.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { getDefaultConfig } from "utils/AxiosUtils" - -import axios from "axios" -import jwt_decode from "jwt-decode" -import { AppThunk } from "caches/caches" -import { getAccessToken, updateMyProfile } from "caches/MyProfileSlice" - -const hasProfile = (): AppThunk> => async (_, getState) => { - const accessToken = getAccessToken(getState()) - const response = await axios.get( - `${process.env.REACT_APP_API_URL}/api/v1/profile/has`, - getDefaultConfig(accessToken), - ) - return response.data -} - -export interface UpdateProfilePayload { - name: string - picture: string - forceUpdate?: boolean -} - -interface ProfileResponse { - id: string - name: string - picture: string - pictureLocked: boolean - lastAccess: string -} - -interface JWTToken { - permissions: string[] -} - -const updateProfile = - ( - payload: UpdateProfilePayload, - accessToken?: string, - ): AppThunk> => - async (dispatch, getState) => { - const token = accessToken ?? getAccessToken(getState()) - if (!token) throw Error("No access token found") - - const response = await axios.put( - `${process.env.REACT_APP_API_URL}/api/v1/profile`, - payload, - getDefaultConfig(token), - ) - const decodedAccessToken = jwt_decode(token) - dispatch( - updateMyProfile({ - id: response.data.id, - name: response.data.name, - picture: response.data.picture, - isPlayer: - decodedAccessToken.permissions.indexOf("read:game") !== -1, - isAdmin: - decodedAccessToken.permissions.indexOf("read:admin") !== -1, - accessToken: token, - lastAccess: response.data.lastAccess, - }), - ) - } - -export default { hasProfile, updateProfile } diff --git a/src/services/SettingsService.ts b/src/services/SettingsService.ts deleted file mode 100644 index 8db7254..0000000 --- a/src/services/SettingsService.ts +++ /dev/null @@ -1,34 +0,0 @@ -import axios from "axios" -import { AppThunk } from "caches/caches" -import { getAccessToken } from "caches/MyProfileSlice" -import { PlayerSettings } from "model/PlayerSettings" -import { getDefaultConfig } from "utils/AxiosUtils" -import { updateSettings as updateSettingsCache } from "caches/SettingsSlice" - -const getSettings = - (): AppThunk> => async (dispatch, getState) => { - const accessToken = getAccessToken(getState()) - - const response = await axios.get( - `${process.env.REACT_APP_API_URL}/api/v1/settings`, - getDefaultConfig(accessToken), - ) - dispatch(updateSettingsCache(response.data)) - return response.data - } - -const updateSettings = - (settings: PlayerSettings): AppThunk> => - async (dispatch, getState) => { - const accessToken = getAccessToken(getState()) - - await axios.put( - `${process.env.REACT_APP_API_URL}/api/v1/settings`, - settings, - getDefaultConfig(accessToken), - ) - - dispatch(updateSettingsCache(settings)) - } - -export default { getSettings, updateSettings } diff --git a/src/services/SpectatorService.ts b/src/services/SpectatorService.ts deleted file mode 100644 index 8dea8f0..0000000 --- a/src/services/SpectatorService.ts +++ /dev/null @@ -1,18 +0,0 @@ -import axios from "axios" -import { AppThunk } from "caches/caches" -import { getAccessToken } from "caches/MyProfileSlice" -import { getDefaultConfig } from "utils/AxiosUtils" - -const register = - (gameId: string): AppThunk> => - async (_, getState) => { - const accessToken = getAccessToken(getState()) - - await axios.put( - `${process.env.REACT_APP_API_URL}/api/v1/spectator/register?gameId=${gameId}`, - null, - getDefaultConfig(accessToken), - ) - } - -export default { register } diff --git a/src/services/StatsService.ts b/src/services/StatsService.ts deleted file mode 100644 index 6b9441e..0000000 --- a/src/services/StatsService.ts +++ /dev/null @@ -1,25 +0,0 @@ -import axios from "axios" -import { AppThunk } from "caches/caches" -import { getAccessToken } from "caches/MyProfileSlice" -import { PlayerGameStats } from "model/Player" -import { getDefaultConfig } from "utils/AxiosUtils" - -const gameStatsForPlayer = - (playerId?: string): AppThunk> => - async (_, getState) => { - const accessToken = getAccessToken(getState()) - - const url = playerId - ? `${ - process.env.REACT_APP_API_URL - }/api/v1/admin/stats/gameStatsForPlayer?playerId=${encodeURIComponent( - playerId, - )}` - : `${process.env.REACT_APP_API_URL}/api/v1/stats/gameStatsForPlayer` - - const response = await axios.get(url, getDefaultConfig(accessToken)) - - return response.data - } - -export default { gameStatsForPlayer } diff --git a/src/test/data/buy-cards/after-buy-cards.json b/src/test/data/buy-cards/after-buy-cards.json new file mode 100644 index 0000000..ccc9a39 --- /dev/null +++ b/src/test/data/buy-cards/after-buy-cards.json @@ -0,0 +1,65 @@ +{ + "id": "game1", + "revision": 24, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": false, + "iamGoer": true, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 15, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 25, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T11:38:38.687Z", + "number": 2, + "dealerId": "player2", + "goerId": "player1", + "suit": "CLUBS", + "status": "PLAYING", + "currentHand": { + "timestamp": "2024-01-25T11:38:38.687Z", + "currentPlayerId": "player2", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "JACK_CLUBS", + "JOKER", + "KING_CLUBS", + "ACE_CLUBS", + "FOUR_CLUBS" + ] +} diff --git a/src/test/data/buy-cards/before-buy-cards.json b/src/test/data/buy-cards/before-buy-cards.json new file mode 100644 index 0000000..58d71d5 --- /dev/null +++ b/src/test/data/buy-cards/before-buy-cards.json @@ -0,0 +1,65 @@ +{ + "id": "game1", + "revision": 22, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": true, + "iamGoer": true, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 15, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 25, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T11:38:38.687Z", + "number": 2, + "dealerId": "player2", + "goerId": "player1", + "suit": "CLUBS", + "status": "BUYING", + "currentHand": { + "timestamp": "2024-01-25T11:38:38.687Z", + "currentPlayerId": "player1", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "JACK_CLUBS", + "JOKER", + "KING_CLUBS", + "ACE_CLUBS", + "FOUR_CLUBS" + ] +} diff --git a/src/test/data/call/after-call.json b/src/test/data/call/after-call.json new file mode 100644 index 0000000..b34ab65 --- /dev/null +++ b/src/test/data/call/after-call.json @@ -0,0 +1,63 @@ +{ + "id": "game1", + "revision": 0, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": false, + "iamGoer": false, + "iamDealer": true, + "iamAdmin": true, + "maxCall": 0, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T09:25:58.651Z", + "number": 1, + "dealerId": "player1", + "status": "CALLING", + "currentHand": { + "timestamp": "2024-01-25T09:25:58.651Z", + "currentPlayerId": "player2", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "QUEEN_HEARTS", + "SIX_DIAMONDS", + "TEN_CLUBS", + "FIVE_DIAMONDS", + "TWO_SPADES" + ] +} diff --git a/src/test/data/call/before-call.json b/src/test/data/call/before-call.json new file mode 100644 index 0000000..b34ab65 --- /dev/null +++ b/src/test/data/call/before-call.json @@ -0,0 +1,63 @@ +{ + "id": "game1", + "revision": 0, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": false, + "iamGoer": false, + "iamDealer": true, + "iamAdmin": true, + "maxCall": 0, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T09:25:58.651Z", + "number": 1, + "dealerId": "player1", + "status": "CALLING", + "currentHand": { + "timestamp": "2024-01-25T09:25:58.651Z", + "currentPlayerId": "player2", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "QUEEN_HEARTS", + "SIX_DIAMONDS", + "TEN_CLUBS", + "FIVE_DIAMONDS", + "TWO_SPADES" + ] +} diff --git a/src/test/data/card-played/after-card-played.json b/src/test/data/card-played/after-card-played.json new file mode 100644 index 0000000..899b458 --- /dev/null +++ b/src/test/data/card-played/after-card-played.json @@ -0,0 +1,71 @@ +{ + "id": "game1", + "revision": 25, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": true, + "iamGoer": true, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 15, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 25, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T11:38:38.687Z", + "number": 2, + "dealerId": "player2", + "goerId": "player1", + "suit": "CLUBS", + "status": "PLAYING", + "currentHand": { + "timestamp": "2024-01-25T11:38:38.687Z", + "leadOut": "NINE_CLUBS", + "currentPlayerId": "player1", + "playedCards": [ + { + "playerId": "player2", + "card": "NINE_CLUBS" + } + ] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "JACK_CLUBS", + "JOKER", + "KING_CLUBS", + "ACE_CLUBS", + "FOUR_CLUBS" + ] +} diff --git a/src/test/data/card-played/before-card-played.json b/src/test/data/card-played/before-card-played.json new file mode 100644 index 0000000..ccc9a39 --- /dev/null +++ b/src/test/data/card-played/before-card-played.json @@ -0,0 +1,65 @@ +{ + "id": "game1", + "revision": 24, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": false, + "iamGoer": true, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 15, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 25, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T11:38:38.687Z", + "number": 2, + "dealerId": "player2", + "goerId": "player1", + "suit": "CLUBS", + "status": "PLAYING", + "currentHand": { + "timestamp": "2024-01-25T11:38:38.687Z", + "currentPlayerId": "player2", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "JACK_CLUBS", + "JOKER", + "KING_CLUBS", + "ACE_CLUBS", + "FOUR_CLUBS" + ] +} diff --git a/src/test/data/game-end/after-game-end.json b/src/test/data/game-end/after-game-end.json new file mode 100644 index 0000000..f94c132 --- /dev/null +++ b/src/test/data/game-end/after-game-end.json @@ -0,0 +1,135 @@ +{ + "id": "game1", + "revision": 188, + "status": "COMPLETED", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 125, + "rings": 2, + "teamId": "1", + "winner": true + }, + "iamSpectator": false, + "isMyGo": true, + "iamGoer": true, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 15, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 125, + "rings": 2, + "teamId": "1", + "winner": true + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 65, + "rings": 2, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T12:49:33.956Z", + "number": 14, + "dealerId": "player2", + "goerId": "player1", + "suit": "HEARTS", + "status": "PLAYING", + "currentHand": { + "timestamp": "2024-01-25T13:51:22.941201+01:00", + "currentPlayerId": "player1", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [ + { + "timestamp": "2024-01-25T12:49:33.956Z", + "leadOut": "QUEEN_HEARTS", + "currentPlayerId": "player1", + "playedCards": [ + { + "playerId": "player2", + "card": "QUEEN_HEARTS" + }, + { + "playerId": "player1", + "card": "THREE_HEARTS" + } + ] + }, + { + "timestamp": "2024-01-25T12:50:18.778Z", + "leadOut": "FOUR_HEARTS", + "currentPlayerId": "player1", + "playedCards": [ + { + "playerId": "player2", + "card": "FOUR_HEARTS" + }, + { + "playerId": "player1", + "card": "SIX_HEARTS" + } + ] + }, + { + "timestamp": "2024-01-25T12:50:33.96Z", + "leadOut": "FIVE_HEARTS", + "currentPlayerId": "player2", + "playedCards": [ + { + "playerId": "player1", + "card": "FIVE_HEARTS" + }, + { + "playerId": "player2", + "card": "FOUR_DIAMONDS" + } + ] + }, + { + "timestamp": "2024-01-25T12:50:42.35Z", + "leadOut": "SEVEN_HEARTS", + "currentPlayerId": "player2", + "playedCards": [ + { + "playerId": "player1", + "card": "SEVEN_HEARTS" + }, + { + "playerId": "player2", + "card": "SEVEN_CLUBS" + } + ] + }, + { + "timestamp": "2024-01-25T12:50:53.224Z", + "leadOut": "THREE_DIAMONDS", + "currentPlayerId": "player2", + "playedCards": [ + { + "playerId": "player1", + "card": "THREE_DIAMONDS" + }, + { + "playerId": "player2", + "card": "FIVE_SPADES" + } + ] + } + ] + }, + "cards": null +} diff --git a/src/test/data/game-end/before-game-end.json b/src/test/data/game-end/before-game-end.json new file mode 100644 index 0000000..067047d --- /dev/null +++ b/src/test/data/game-end/before-game-end.json @@ -0,0 +1,126 @@ +{ + "id": "game1", + "revision": 187, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 105, + "rings": 2, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": false, + "iamGoer": true, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 15, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 105, + "rings": 2, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 60, + "rings": 2, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T12:49:33.956Z", + "number": 14, + "dealerId": "player2", + "goerId": "player1", + "suit": "HEARTS", + "status": "PLAYING", + "currentHand": { + "timestamp": "2024-01-25T12:50:53.224Z", + "leadOut": "THREE_DIAMONDS", + "currentPlayerId": "player2", + "playedCards": [ + { + "playerId": "player1", + "card": "THREE_DIAMONDS" + } + ] + }, + "dealerSeeingCall": false, + "completedHands": [ + { + "timestamp": "2024-01-25T12:49:33.956Z", + "leadOut": "QUEEN_HEARTS", + "currentPlayerId": "player1", + "playedCards": [ + { + "playerId": "player2", + "card": "QUEEN_HEARTS" + }, + { + "playerId": "player1", + "card": "THREE_HEARTS" + } + ] + }, + { + "timestamp": "2024-01-25T12:50:18.778Z", + "leadOut": "FOUR_HEARTS", + "currentPlayerId": "player1", + "playedCards": [ + { + "playerId": "player2", + "card": "FOUR_HEARTS" + }, + { + "playerId": "player1", + "card": "SIX_HEARTS" + } + ] + }, + { + "timestamp": "2024-01-25T12:50:33.96Z", + "leadOut": "FIVE_HEARTS", + "currentPlayerId": "player2", + "playedCards": [ + { + "playerId": "player1", + "card": "FIVE_HEARTS" + }, + { + "playerId": "player2", + "card": "FOUR_DIAMONDS" + } + ] + }, + { + "timestamp": "2024-01-25T12:50:42.35Z", + "leadOut": "SEVEN_HEARTS", + "currentPlayerId": "player2", + "playedCards": [ + { + "playerId": "player1", + "card": "SEVEN_HEARTS" + }, + { + "playerId": "player2", + "card": "SEVEN_CLUBS" + } + ] + } + ] + }, + "cards": null +} diff --git a/src/test/data/hand-end/after-hand-end.json b/src/test/data/hand-end/after-hand-end.json new file mode 100644 index 0000000..8451e2e --- /dev/null +++ b/src/test/data/hand-end/after-hand-end.json @@ -0,0 +1,80 @@ +{ + "id": "game1", + "revision": 26, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": true, + "iamGoer": true, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 15, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 25, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T11:38:38.687Z", + "number": 2, + "dealerId": "player2", + "goerId": "player1", + "suit": "CLUBS", + "status": "PLAYING", + "currentHand": { + "timestamp": "2024-01-25T12:55:15.854232+01:00", + "currentPlayerId": "player1", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [ + { + "timestamp": "2024-01-25T11:38:38.687Z", + "leadOut": "NINE_CLUBS", + "currentPlayerId": "player1", + "playedCards": [ + { + "playerId": "player2", + "card": "NINE_CLUBS" + }, + { + "playerId": "player1", + "card": "FOUR_CLUBS" + } + ] + } + ] + }, + "cards": [ + "JACK_CLUBS", + "JOKER", + "KING_CLUBS", + "ACE_CLUBS" + ] +} diff --git a/src/test/data/hand-end/before-hand-end.json b/src/test/data/hand-end/before-hand-end.json new file mode 100644 index 0000000..899b458 --- /dev/null +++ b/src/test/data/hand-end/before-hand-end.json @@ -0,0 +1,71 @@ +{ + "id": "game1", + "revision": 25, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": true, + "iamGoer": true, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 15, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 25, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T11:38:38.687Z", + "number": 2, + "dealerId": "player2", + "goerId": "player1", + "suit": "CLUBS", + "status": "PLAYING", + "currentHand": { + "timestamp": "2024-01-25T11:38:38.687Z", + "leadOut": "NINE_CLUBS", + "currentPlayerId": "player1", + "playedCards": [ + { + "playerId": "player2", + "card": "NINE_CLUBS" + } + ] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "JACK_CLUBS", + "JOKER", + "KING_CLUBS", + "ACE_CLUBS", + "FOUR_CLUBS" + ] +} diff --git a/src/test/data/pass/after-pass.json b/src/test/data/pass/after-pass.json new file mode 100644 index 0000000..97547dc --- /dev/null +++ b/src/test/data/pass/after-pass.json @@ -0,0 +1,63 @@ +{ + "id": "game1", + "revision": 80, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 40, + "rings": 1, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": false, + "iamGoer": false, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 0, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 40, + "rings": 1, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 5, + "rings": 1, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T11:57:48.548Z", + "number": 6, + "dealerId": "player2", + "status": "CALLING", + "currentHand": { + "timestamp": "2024-01-25T11:57:48.548Z", + "currentPlayerId": "player2", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "FOUR_HEARTS", + "NINE_HEARTS", + "THREE_DIAMONDS", + "SEVEN_SPADES", + "TEN_CLUBS" + ] +} diff --git a/src/test/data/pass/before-pass.json b/src/test/data/pass/before-pass.json new file mode 100644 index 0000000..d7be93c --- /dev/null +++ b/src/test/data/pass/before-pass.json @@ -0,0 +1,63 @@ +{ + "id": "game1", + "revision": 79, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 40, + "rings": 1, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": true, + "iamGoer": false, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 0, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 40, + "rings": 1, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 5, + "rings": 1, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T12:57:48.548711+01:00", + "number": 6, + "dealerId": "player2", + "status": "CALLING", + "currentHand": { + "timestamp": "2024-01-25T12:57:48.548711+01:00", + "currentPlayerId": "player1", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "FOUR_HEARTS", + "NINE_HEARTS", + "THREE_DIAMONDS", + "SEVEN_SPADES", + "TEN_CLUBS" + ] +} diff --git a/src/test/data/round-end/after-round-end.json b/src/test/data/round-end/after-round-end.json new file mode 100644 index 0000000..9594e4c --- /dev/null +++ b/src/test/data/round-end/after-round-end.json @@ -0,0 +1,63 @@ +{ + "id": "game1", + "revision": 19, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": true, + "iamGoer": false, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 0, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 25, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T12:38:38.687034+01:00", + "number": 2, + "dealerId": "player2", + "status": "CALLING", + "currentHand": { + "timestamp": "2024-01-25T12:38:38.687034+01:00", + "currentPlayerId": "player1", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "JACK_CLUBS", + "JOKER", + "FOUR_HEARTS", + "KING_CLUBS", + "TWO_DIAMONDS" + ] +} diff --git a/src/test/data/round-end/before-round-end.json b/src/test/data/round-end/before-round-end.json new file mode 100644 index 0000000..b2eeab3 --- /dev/null +++ b/src/test/data/round-end/before-round-end.json @@ -0,0 +1,128 @@ +{ + "id": "game1", + "revision": 18, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": true, + "iamGoer": false, + "iamDealer": true, + "iamAdmin": true, + "maxCall": 25, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 0, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 25, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T09:25:58.651Z", + "number": 1, + "dealerId": "player1", + "goerId": "player2", + "suit": "SPADES", + "status": "PLAYING", + "currentHand": { + "timestamp": "2024-01-25T11:37:15.895Z", + "leadOut": "TEN_HEARTS", + "currentPlayerId": "player1", + "playedCards": [ + { + "playerId": "player2", + "card": "TEN_HEARTS" + } + ] + }, + "dealerSeeingCall": false, + "completedHands": [ + { + "timestamp": "2024-01-25T09:25:58.651Z", + "leadOut": "TWO_SPADES", + "currentPlayerId": "player2", + "playedCards": [ + { + "playerId": "player1", + "card": "TWO_SPADES" + }, + { + "playerId": "player2", + "card": "QUEEN_SPADES" + } + ] + }, + { + "timestamp": "2024-01-25T11:37:01.996Z", + "leadOut": "FIVE_SPADES", + "currentPlayerId": "player1", + "playedCards": [ + { + "playerId": "player2", + "card": "FIVE_SPADES" + }, + { + "playerId": "player1", + "card": "EIGHT_CLUBS" + } + ] + }, + { + "timestamp": "2024-01-25T11:37:07.397Z", + "leadOut": "JACK_SPADES", + "currentPlayerId": "player1", + "playedCards": [ + { + "playerId": "player2", + "card": "JACK_SPADES" + }, + { + "playerId": "player1", + "card": "THREE_HEARTS" + } + ] + }, + { + "timestamp": "2024-01-25T11:37:11.393Z", + "leadOut": "JACK_HEARTS", + "currentPlayerId": "player1", + "playedCards": [ + { + "playerId": "player2", + "card": "JACK_HEARTS" + }, + { + "playerId": "player1", + "card": "FIVE_HEARTS" + } + ] + } + ] + }, + "cards": [ + "SIX_HEARTS" + ] +} diff --git a/src/test/data/select-suit/after-select-suit.json b/src/test/data/select-suit/after-select-suit.json new file mode 100644 index 0000000..58d71d5 --- /dev/null +++ b/src/test/data/select-suit/after-select-suit.json @@ -0,0 +1,65 @@ +{ + "id": "game1", + "revision": 22, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": true, + "iamGoer": true, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 15, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 25, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T11:38:38.687Z", + "number": 2, + "dealerId": "player2", + "goerId": "player1", + "suit": "CLUBS", + "status": "BUYING", + "currentHand": { + "timestamp": "2024-01-25T11:38:38.687Z", + "currentPlayerId": "player1", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "JACK_CLUBS", + "JOKER", + "KING_CLUBS", + "ACE_CLUBS", + "FOUR_CLUBS" + ] +} diff --git a/src/test/data/select-suit/before-select-suit.json b/src/test/data/select-suit/before-select-suit.json new file mode 100644 index 0000000..8f426b2 --- /dev/null +++ b/src/test/data/select-suit/before-select-suit.json @@ -0,0 +1,69 @@ +{ + "id": "game1", + "revision": 21, + "status": "ACTIVE", + "me": { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + "iamSpectator": false, + "isMyGo": true, + "iamGoer": true, + "iamDealer": false, + "iamAdmin": true, + "maxCall": 15, + "players": [ + { + "id": "player1", + "seatNumber": 1, + "call": 15, + "cardsBought": 0, + "score": 0, + "rings": 0, + "teamId": "1", + "winner": false + }, + { + "id": "player2", + "seatNumber": 2, + "call": 0, + "cardsBought": 0, + "score": 25, + "rings": 0, + "teamId": "2", + "winner": false + } + ], + "round": { + "timestamp": "2024-01-25T11:38:38.687Z", + "number": 2, + "dealerId": "player2", + "goerId": "player1", + "status": "CALLED", + "currentHand": { + "timestamp": "2024-01-25T11:38:38.687Z", + "currentPlayerId": "player1", + "playedCards": [] + }, + "dealerSeeingCall": false, + "completedHands": [] + }, + "cards": [ + "JACK_CLUBS", + "JOKER", + "FOUR_HEARTS", + "KING_CLUBS", + "TWO_DIAMONDS", + "KING_SPADES", + "ACE_CLUBS", + "SIX_HEARTS", + "FOUR_DIAMONDS", + "FOUR_CLUBS" + ] +} diff --git a/src/utils/AxiosUtils.ts b/src/utils/AxiosUtils.ts index a13a7ac..d62748f 100644 --- a/src/utils/AxiosUtils.ts +++ b/src/utils/AxiosUtils.ts @@ -1,7 +1,6 @@ import { AxiosRequestConfig } from "axios" export const getDefaultConfig = (accessToken?: string): AxiosRequestConfig => { - if (!accessToken) throw Error("No access token found") return { headers: { Authorization: `Bearer ${accessToken}`, diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts new file mode 100644 index 0000000..b27b4c5 --- /dev/null +++ b/src/utils/EventUtils.ts @@ -0,0 +1,117 @@ +import { Actions } from "model/Events" +import { GameStateResponse, GameStatus } from "model/Game" +import { RoundStatus } from "model/Round" +import { Player } from "model/Player" + +const callsChanged = (prev: Player[], curr: Player[]): boolean => { + let changed = false + prev.forEach(p => { + curr.forEach(c => { + if (p.id === c.id && p.call !== c.call && c.call !== 0) { + changed = true + } + }) + }) + + return changed +} + +export const isCallEvent = ( + prevState: GameStateResponse, + currState: GameStateResponse, +): boolean => { + return ( + prevState.round?.status === RoundStatus.CALLING && + prevState.round.currentHand?.currentPlayerId !== + currState.round?.currentHand?.currentPlayerId && + callsChanged(prevState.players, currState.players) + ) +} + +export const isPassEvent = ( + prevState: GameStateResponse, + currState: GameStateResponse, +): boolean => { + return ( + prevState.round?.status === RoundStatus.CALLING && + prevState.round.currentHand?.currentPlayerId !== + currState.round?.currentHand?.currentPlayerId && + !callsChanged(prevState.players, currState.players) + ) +} + +export const isSelectSuitEvent = (prevState: GameStateResponse): boolean => { + return prevState.round?.status === RoundStatus.CALLED +} + +export const isBuyCardsEvent = (prevState: GameStateResponse): boolean => { + return prevState.round?.status === RoundStatus.BUYING +} + +export const isCardPlayedEvent = (prevState: GameStateResponse): boolean => { + return prevState.round?.status === RoundStatus.PLAYING +} + +export const isHandEndEvent = ( + prevState: GameStateResponse, + currState: GameStateResponse, +): boolean => { + return ( + prevState.round?.status === RoundStatus.PLAYING && + currState.round?.status === RoundStatus.PLAYING && + prevState.round?.completedHands.length !== + currState.round?.completedHands.length + ) +} + +export const isRoundEndEvent = ( + prevState: GameStateResponse, + currState: GameStateResponse, +): boolean => { + return ( + prevState.round?.status === RoundStatus.PLAYING && + currState.round?.status === RoundStatus.PLAYING && + prevState.round?.number !== currState.round?.number + ) +} + +export const isGameOverEvent = ( + prevState: GameStateResponse, + currState: GameStateResponse, +): boolean => { + return ( + prevState.status === GameStatus.ACTIVE && + currState.status === GameStatus.COMPLETED + ) +} + +export const determineEvent = ( + prevState: GameStateResponse, + currState: GameStateResponse, +): Actions => { + if (isCallEvent(prevState, currState)) { + return Actions.Call + } + if (isPassEvent(prevState, currState)) { + return Actions.Pass + } + if (isSelectSuitEvent(prevState)) { + return Actions.SelectSuit + } + if (isBuyCardsEvent(prevState)) { + return Actions.BuyCards + } + if (isRoundEndEvent(prevState, currState)) { + return Actions.RoundEnd + } + if (isHandEndEvent(prevState, currState)) { + return Actions.HandEnd + } + if (isCardPlayedEvent(prevState)) { + return Actions.CardPlayed + } + if (isGameOverEvent(prevState, currState)) { + return Actions.GameOver + } + return Actions.Unknown +} diff --git a/src/utils/GameUtils.ts b/src/utils/GameUtils.ts index bfa7080..ff49ce8 100644 --- a/src/utils/GameUtils.ts +++ b/src/utils/GameUtils.ts @@ -53,7 +53,7 @@ export const processOrderedCardsAfterGameUpdate = ( // Find the delta between the existing cards and the updated cards we got from the api const delta = currentCardsNoBlanks.filter( - x => !updatedCardNames.includes(x.name), + x => !updatedCardNames?.includes(x.name), ) // 1. If cards in payload match ordered cards then don't change orderedCards @@ -160,7 +160,7 @@ export const bestCardLead = (round: Round) => { // Remove played trump cards round.completedHands.forEach(hand => { - hand.playedCards.forEach(p => { + hand.playedCards?.forEach(p => { const card = CARDS[p.card] if ( (card && card.suit === round.suit) ||