From df431eb4f148dd5385c9b9cc69fda128946fd7af Mon Sep 17 00:00:00 2001 From: djmil Date: Fri, 24 Nov 2023 10:38:52 +0100 Subject: [PATCH] UUIDprops - guide.selectedUUID is used to determina current uuid - all the game related midifications are stored independantly per UUID - no global indicators --- .../cordacheckers/api/GameController.java | 1 - webapp/src/App.js | 28 ++- webapp/src/api/games.js | 163 +++++++------ webapp/src/api/leaderboard.js | 27 +-- webapp/src/api/user.js | 16 +- webapp/src/components/Checkers.jsx | 5 +- webapp/src/container/Games.jsx | 202 ++++++++++------ webapp/src/container/Leaderboard.jsx | 28 +-- webapp/src/container/games/ActionPanel.jsx | 221 ++++++++++-------- webapp/src/container/games/GameBoard.jsx | 45 ++-- .../src/container/games/Message2Opponent.jsx | 55 ++--- .../src/container/games/view/GameSelector.jsx | 72 ++++-- webapp/src/container/games/view/NewGame.jsx | 37 ++- webapp/src/context/games.js | 3 +- webapp/src/hook/api.js | 17 +- webapp/src/reducer/games.js | 150 ++++++++---- webapp/src/reducer/leaderboard.js | 44 +++- webapp/src/reducer/user.js | 21 +- webapp/src/util/StateHelper.js | 29 ++- 19 files changed, 670 insertions(+), 494 deletions(-) diff --git a/backend/src/main/java/djmil/cordacheckers/api/GameController.java b/backend/src/main/java/djmil/cordacheckers/api/GameController.java index 9452d77..f6f6d9c 100644 --- a/backend/src/main/java/djmil/cordacheckers/api/GameController.java +++ b/backend/src/main/java/djmil/cordacheckers/api/GameController.java @@ -16,7 +16,6 @@ import org.springframework.web.bind.annotation.RestController; import djmil.cordacheckers.cordaclient.CordaClient; import djmil.cordacheckers.cordaclient.dao.GameView; import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameBoardMove; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameProposalCreate; import djmil.cordacheckers.user.HoldingIdentityResolver; import djmil.cordacheckers.user.User; diff --git a/webapp/src/App.js b/webapp/src/App.js index 07dffe8..d4d5288 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -10,9 +10,6 @@ import Games from './container/Games'; import Leaderboard from './container/Leaderboard'; import useConfigReducer from './reducer/config'; -import useUserReducer from './reducer/user'; -import useLeaderboardReducer from './reducer/leaderboard'; -import useGamesReducer from './reducer/games'; import useUserApi from './api/user'; import useLeaderboardApi from './api/leaderboard'; @@ -21,22 +18,23 @@ import useGamesApi from './api/games'; export default function App() { const [config, dispatcConfig] = useConfigReducer(); - const user = useUserApi(useUserReducer()).getUser(); - const leaderboard = useLeaderboardApi(useLeaderboardReducer(), config).pollTable(); + const user = useUserApi(); + const leaderboard = useLeaderboardApi(); + const games = useGamesApi(); + + user.api.useGetUser(); + leaderboard.api.useTablePolling(config); + games.api.useGamesPolling(config); + const players = { - leaderboard, - currentUser: user.username, - isCurrentUser: (playerName) => user?.isCurrentUser(playerName) === true ? true : null + user: user.state, + leaderboard: leaderboard.state, }; - const gamesReducer = useGamesReducer(); - const gamesApi = useGamesApi(gamesReducer, config); - const games = gamesApi.pollGamesList(); - const isPolling = { - games: games.isPollingGamesList, - leaderboard: leaderboard.isPollingTable + games: games.guide.isPolling, + leaderboard: leaderboard.guide.isPolling } return ( @@ -45,7 +43,7 @@ export default function App() { } /> } /> - } /> + } /> } /> diff --git a/webapp/src/api/games.js b/webapp/src/api/games.js index 675e829..7700c3f 100644 --- a/webapp/src/api/games.js +++ b/webapp/src/api/games.js @@ -1,82 +1,101 @@ import { usePolling, doPushing } from '../hook/api'; -import { gamesInitialState } from '../reducer/games'; -import { Color } from '../components/Checkers'; +import { useGamesGuideReducer, useGamesStateReducer, gamesGuideTemplate } from '../reducer/games'; -export default function useGamesApi(gamesReducer, config) { - const [games, dispatchGames] = gamesReducer; +export default function useGamesApi() { + const [state, dispatchState] = useGamesStateReducer(); + const [guide, dispatchGuide] = useGamesGuideReducer(); - const usePollingGamesList = () => { - const onSuccess = (gamesList) => { - dispatchGames({ type: 'next', gamesList }); - } + const useGamesPolling = (config) => { + const onPolling = (isPolling) => dispatchGuide({ type: 'next', isPolling }); + const onSuccess = (games) => dispatchState({ type: 'next', games }); - const isPollingGamesList = usePolling('/api/game', onSuccess, config.intervalMode(30)); - if (games.isPollingGamesList !== isPollingGamesList) { - dispatchGames({ type: 'next', isPollingGamesList }); - } + usePolling('/api/game', { onPolling, onSuccess }, config.intervalMode(30)); + } - return games; + const pushNewGame = ({ opponentName, opponentColor, board, message }) => { + doPushing('/api/gameproposal', 'POST', { opponentName, opponentColor, board, message }, { + onPushing: (isPushing) => dispatchGuide({ type: 'nextNewGame', isPushing }), + onSuccess: (game) => { + dispatchState({ type: 'add', game }); + dispatchGuide({ type: 'nextNewGame', ...gamesGuideTemplate.newGame }); + } + }) + } + + const pushGameProposalAccept = ({ uuid }) => { + doPushing(`/api/gameproposal/${uuid}/accept`, 'PUT', null, { + onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'accept' }), + onSuccess: (game) => dispatchState({ type: 'update', game }) + }) + } + + const pushGameProposalReject = ({ uuid }) => { + doPushing(`/api/gameproposal/${uuid}/reject`, 'PUT', null, { + onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'reject' }), + onSuccess: (game) => dispatchState({ type: 'update', game }) + }) + } + + const pushGameProposalCancel = ({ uuid }) => { + doPushing(`/api/gameproposal/${uuid}/cancel`, 'PUT', null, { + onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'cancel' }), + onSuccess: (game) => dispatchState({ type: 'update', game }) + }) + } + + const pushGameSurrender = ({ uuid }) => { + doPushing(`/api/game/${uuid}/surrender`, 'PUT', null, { + onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'surrender' }), + onSuccess: (game) => dispatchState({ type: 'update', game }) + }) + } + + const pushGameDrawRequest = ({ uuid }) => { + doPushing(`/api/game/${uuid}/drawreq`, 'PUT', null, { + onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'draw_request' }), + onSuccess: (game) => dispatchState({ type: 'update', game }) + }) + } + + const pushGameDrawAccept = ({ uuid }) => { + doPushing(`/api/game/${uuid}/drawacc`, 'PUT', null, { + onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'draw_accept' }), + onSuccess: (game) => dispatchState({ type: 'update', game }) + }) + } + + const pushGameDrawReject = ({ uuid }) => { + doPushing(`/api/game/${uuid}/drawrej`, 'PUT', null, { + onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'draw_reject' }), + onSuccess: (game) => dispatchState({ type: 'update', game }) + }) + } + + const pushGameMove = ({ uuid, move, message }) => { + doPushing(`/api/game/${uuid}/move`, 'PUT', { move, message }, { + onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'move' }), + onSuccess: (game) => { + dispatchState({ type: 'update', game }); + dispatchGuide({ type: 'UUIDmessage', uuid, message: ''}); + } + }) } return { - pollGamesList: usePollingGamesList, - - pushNewGame: ({ opponentName, myColor, board, message }) => ifNot(games.isPushingNewGame) && - doPushing('/api/gameproposal', 'POST', { opponentName, opponentColor: Color.opposite(myColor), board, message }, { - onPushing: (isPushingNewGame) => dispatchGames({ type: 'next', isPushingNewGame }), - onSuccess: (game) => dispatchGames({ type: 'next', gamesList: [game, ...games.gamesList], newGame: gamesInitialState.newGame }) - }), - - pushGameProposalCancel: ({ uuid }) => ifNot(games.isPushingGameProposalCancel) && - doPushing(`/api/gameproposal/${uuid}/cancel`, 'PUT', null, { - onPushing: (isPushingGameProposalCancel) => dispatchGames({ type: 'next', isPushingGameProposalCancel }), - onSuccess: (canceledGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(canceledGame), proposal: gamesInitialState.proposal }) - }), - - pushGameProposalReject: ({ uuid }) => ifNot(games.isPushingGameProposalReject) && - doPushing(`/api/gameproposal/${uuid}/reject`, 'PUT', null, { - onPushing: (isPushingGameProposalReject) => dispatchGames({ type: 'next', isPushingGameProposalReject }), - onSuccess: (rejectedGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(rejectedGame), proposal: gamesInitialState.proposal }) - }), - - pushGameProposalAccept: ({ uuid }) => ifNot(games.isPushingGameProposalAccept) && - doPushing(`/api/gameproposal/${uuid}/accept`, 'PUT', null, { - onPushing: (isPushingGameProposalAccept) => dispatchGames({ type: 'next', isPushingGameProposalAccept }), - onSuccess: (acceptedGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(acceptedGame), proposal: gamesInitialState.proposal }) - }), - - pushGameSurrender: ({ uuid }) => ifNot(games.isPushingGameSurrender) && - doPushing(`/api/game/${uuid}/surrender`, 'PUT', null, { - onPushing: (isPushingGameSurrender) => dispatchGames({ type: 'next', isPushingGameSurrender }), - onSuccess: (finishedGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(finishedGame), active: gamesInitialState.active }) - }), - - pushGameDrawRequest: ({ uuid }) => ifNot(games.isPushingGameDrawRequest) && - doPushing(`/api/game/${uuid}/drawreq`, 'PUT', null, { - onPushing: (isPushingGameDrawRequest) => dispatchGames({ type: 'next', isPushingGameDrawRequest }), - onSuccess: (drawReqGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(drawReqGame), active: gamesInitialState.active }) - }), - - pushGameDrawAccept: ({ uuid }) => ifNot(games.isPushingGameDrawAccept) && - doPushing(`/api/game/${uuid}/drawacc`, 'PUT', null, { - onPushing: (isPushingGameDrawAccept) => dispatchGames({ type: 'next', isPushingGameDrawAccept }), - onSuccess: (drawAccGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(drawAccGame), active: gamesInitialState.active }) - }), - - pushGameDrawReject: ({ uuid }) => ifNot(games.isPushingGameDrawReject) && - doPushing(`/api/game/${uuid}/drawrej`, 'PUT', null, { - onPushing: (isPushingGameDrawReject) => dispatchGames({ type: 'next', isPushingGameDrawReject }), - onSuccess: (drawRejGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(drawRejGame), active: gamesInitialState.active }) - }), - - pushGameMove: ({ uuid, move, message }) => ifNot(games.isPushingGameMove) && - doPushing(`/api/game/${uuid}/move`, 'PUT', { move, message }, { - onPushing: (isPushingGameMove) => dispatchGames({ type: 'next', isPushingGameMove }), - onSuccess: (game) => dispatchGames({ type: 'next', gamesList: games.nextGame(game), active: gamesInitialState.active }) - }), + state, + guide, + dispatchGuide, + api: { + useGamesPolling, + pushNewGame, + pushGameProposalAccept, + pushGameProposalReject, + pushGameProposalCancel, + pushGameSurrender, + pushGameDrawRequest, + pushGameDrawAccept, + pushGameDrawReject, + pushGameMove + } } -} - -function ifNot(expression) { - return !expression; } \ No newline at end of file diff --git a/webapp/src/api/leaderboard.js b/webapp/src/api/leaderboard.js index 280afaa..6f4479a 100644 --- a/webapp/src/api/leaderboard.js +++ b/webapp/src/api/leaderboard.js @@ -1,22 +1,23 @@ import { usePolling } from "../hook/api"; +import { useLeaderboardStateReducer, useLeaderboardGuideReducer } from '../reducer/leaderboard'; -export default function useLeaderboardApi(leaderboardReducer, config) { - const [leaderboard, dispatchLeaderboard] = leaderboardReducer; +export default function useLeaderboardApi() { + const [state, dispatchState] = useLeaderboardStateReducer(); + const [guide, dispatchGuide] = useLeaderboardGuideReducer(); - const usePollingTable = () => { - const onSuccess = (table) => { - dispatchLeaderboard({ type: 'next', table }); - } + const useTablePolling = (config) => { + const onPolling = (isPolling) => dispatchGuide({ type: 'next', isPolling }); + const onSuccess = (table) => dispatchState({type: 'next', table }); - const isPollingTable = usePolling('/api/leaderboard', onSuccess, config.intervalMode(300)); - if (leaderboard.isPollingTable !== isPollingTable) { - dispatchLeaderboard({ type: 'next', isPollingTable }); - } - - return leaderboard; + usePolling('/api/leaderboard', { onSuccess, onPolling }, config.intervalMode(300)); } return { - pollTable: usePollingTable + state, + guide, + dispatchGuide, + api: { + useTablePolling + } } } \ No newline at end of file diff --git a/webapp/src/api/user.js b/webapp/src/api/user.js index 9d8df5c..d4d52c7 100644 --- a/webapp/src/api/user.js +++ b/webapp/src/api/user.js @@ -1,19 +1,21 @@ import { usePolling } from "../hook/api"; +import { useUserStateReducer } from "../reducer/user"; -export default function useUserApi(userReducer) { - const [user, dispatchUser] = userReducer; +export default function useUserApi() { + const [state, dispatchState] = useUserStateReducer(); const useGetUser = () => { const onSuccess = (userJson) => { - dispatchUser({ type: "parse", userJson }); + dispatchState({ type: "parse", userJson }); } - usePolling('/api/user', onSuccess); // <<-- fetch once - - return user; + usePolling('/api/user', { onSuccess }); // <<-- fetch once } return { - getUser: useGetUser + state, + api : { + useGetUser + } } } \ No newline at end of file diff --git a/webapp/src/components/Checkers.jsx b/webapp/src/components/Checkers.jsx index 13c3120..248bac4 100644 --- a/webapp/src/components/Checkers.jsx +++ b/webapp/src/components/Checkers.jsx @@ -74,10 +74,7 @@ export function Player({ color, name }) { export function Board({ board, flip, onStoneClick, onStoneMove }) { const [[moveId, moveX, moveY], setMove] = useState([0, 0, 0]); - if (!board) - board = []; - - const isInteractive = (typeof onStoneClick === 'function' || typeof onStoneMove === 'function') ? ' interactive' : ''; + const isInteractive = (board && (typeof onStoneClick === 'function' || typeof onStoneMove === 'function')) ? ' interactive' : ''; const WhiteTile = ({ id }) => { const stone = board[id]; diff --git a/webapp/src/container/Games.jsx b/webapp/src/container/Games.jsx index f78a2a8..7a3e880 100644 --- a/webapp/src/container/Games.jsx +++ b/webapp/src/container/Games.jsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { GamesContext } from '../context/games'; +import React, { useContext, useEffect } from 'react'; +import { GamesStateContext, GamesGuideContext } from '../context/games'; import { NavLink, Routes, Route } from 'react-router-dom'; import NewGame from './games/view/NewGame'; @@ -12,67 +12,75 @@ import Counter from '../components/Counter'; import './Games.css'; -export default function Games({ context: { gamesReducer, gamesApi }, players }) { - const [games, dispatchGames] = gamesReducer; +export default function Games({ games, players }) { + const gamesState = games.state; + const gamesDispatchGuide = games.dispatchGuide; + + useEffect(() => { + gamesDispatchGuide({ type: 'sync', gamesState }); + }, [gamesState, gamesDispatchGuide]); return ( - -
+ + +
-
- - -
+
+ + +
-
- - {/* */} - - -
+
+ + + + {/* */} +
-
- +
+ + ) }; -function ViewSelector({ games }) { - const awaiting = countGames(games.gamesList); +function ViewSelector() { + const guide = useContext(GamesGuideContext); return ( ) } -function ViewProvider({ dispatchGames, players }) { +function ViewProvider({ dispatchGuide, players }) { return (
dispatchGames({ type: 'nextNewGame', opponentName, myColor })} + dispatchGuide({ type: 'nextNewGame', opponentName, myColor })} /> } /> dispatchGames({ type: 'nextProposal', selectedUUID })} /> + dispatchGuide({ type: 'selectedUUID', proposal: uuid })} /> } /> dispatchGames({ type: 'nextActive', selectedUUID })} /> + dispatchGuide({ type: 'selectedUUID', active: uuid })} /> } /> dispatchGames({ type: 'nextArchive', selectedUUID })} /> + dispatchGuide({ type: 'selectedUUID', archive: uuid })} /> } /> @@ -81,25 +89,57 @@ function ViewProvider({ dispatchGames, players }) { } function ActionPanel({ gamesApi }) { + const games = useContext(GamesStateContext); + const guide = useContext(GamesGuideContext); + + const fromUUID = (uuid) => (!uuid) ? [{}, null] : + [ + games.find((game) => game.uuid === uuid) || {}, // game + guide.UUIDpushing[uuid] // pushing + ]; + return (
gamesApi.pushNewGame(reqParams)} /> + [guide.newGame, guide.newGame.isPushing]} + onClick={(req) => gamesApi.pushNewGame(req)} + /> } /> gamesApi.pushGameProposalAccept({ uuid })} />, - gamesApi.pushGameProposalReject({ uuid })} />, - gamesApi.pushGameProposalCancel({ uuid })} /> + fromUUID(guide.selectedUUID.proposal)} + onClick={(req) => gamesApi.pushGameProposalAccept(req)} + />, + fromUUID(guide.selectedUUID.proposal)} + onClick={(req) => gamesApi.pushGameProposalReject(req)} + />, + fromUUID(guide.selectedUUID.proposal)} + onClick={(req) => gamesApi.pushGameProposalCancel(req)} + /> ]} /> gamesApi.pushGameDrawRequest({ uuid })} />, - gamesApi.pushGameDrawAccept({ uuid })} />, - gamesApi.pushGameDrawReject({ uuid })} />, - gamesApi.pushGameSurrender({ uuid })} /> + fromUUID(guide.selectedUUID.active)} + onClick={(req) => gamesApi.pushGameDrawRequest(req)} + />, + fromUUID(guide.selectedUUID.active)} + onClick={(req) => gamesApi.pushGameDrawAccept(req)} + />, + fromUUID(guide.selectedUUID.active)} + onClick={(req) => gamesApi.pushGameDrawReject(req)} + />, + fromUUID(guide.selectedUUID.active)} + onClick={(req) => gamesApi.pushGameSurrender(req)} /> ]} /> (!uuid) ? [{}, null] : + [ + games.find((game) => game.uuid === uuid) || {}, // game + guide.UUIDpushing[uuid] // pushing + ]; const onStoneClick = (uuid, cellId) => { - let board = { ...games.newGame.board }; + let board = { ...guide.newGame.board }; board[cellId] = nextStone(board[cellId]); - dispatchGames({ type: 'nextNewGame', board }); + dispatchGuide({ type: 'nextNewGame', board }); } - const onStoneMove = (uuid, move) => gamesApi.pushGameMove({ uuid, move, message: games.active.message }); - return ( - } /> - } /> - } /> - } /> + + [guide.newGame, null]} + onStoneClick={onStoneClick} + /> + } /> + + fromUUID(guide.selectedUUID.proposal)} + /> + } /> + + fromUUID(guide.selectedUUID.active)} + onStoneMove={(uuid, move) => gamesApi.pushGameMove({ uuid, move, message: guide.UUIDmessage[uuid] })} + /> + } /> + + fromUUID(guide.selectedUUID.archive)} + /> + } /> + ) } -function countGames(gamesList) { +function Message2OpponentRoutes({ dispatchGuide }) { + const guide = useContext(GamesGuideContext); - var awaiting = { - proposals: 0, - active: 0 - }; + const getMessage = (uuid) => !uuid ? undefined : // <<-- appears as inactive message field + guide.UUIDmessage[uuid] || ''; - if (!gamesList) - return awaiting; + return ( + - for (const game of gamesList) { - switch (game.status) { - case 'GAME_PROPOSAL_WAIT_FOR_YOU': - awaiting.proposals++; - break; - case 'GAME_BOARD_WAIT_FOR_YOU': - case 'DRAW_REQUEST_WAIT_FOR_YOU': - awaiting.active++; - break; - default: - break; - } - } + guide.newGame.message} + setMessage={(message) => dispatchGuide({ type: 'nextNewGame', message })} /> + } /> - return awaiting; + getMessage(guide.selectedUUID.active)} + setMessage={(message) => dispatchGuide({ type: 'UUIDmessage', message, uuid: guide.selectedUUID.active })} /> + } /> + + + ) } \ No newline at end of file diff --git a/webapp/src/container/Leaderboard.jsx b/webapp/src/container/Leaderboard.jsx index 9493dd7..ec46483 100644 --- a/webapp/src/container/Leaderboard.jsx +++ b/webapp/src/container/Leaderboard.jsx @@ -1,26 +1,22 @@ import './Leaderboard.css'; -import React from "react" -import Loading from '../components/Loading'; +import React from 'react'; export default function Leaderboard({ players }) { + const tableRows = Object.keys(players.leaderboard).map(name => { + var rank = players.leaderboard[name]; - const table = players.leaderboard.table; - if (table == null) - return - - const tableRows = Object.keys(table).map(playerName => { - var rank = table[playerName]; - - return - {playerName} - {rank.total} - {rank.victory} - {rank.draw} - + return ( + + {name} + {rank.total} + {rank.victory} + {rank.draw} + + ) }); return ( -
+
diff --git a/webapp/src/container/games/ActionPanel.jsx b/webapp/src/container/games/ActionPanel.jsx index 55912a5..bc104f4 100644 --- a/webapp/src/container/games/ActionPanel.jsx +++ b/webapp/src/container/games/ActionPanel.jsx @@ -1,28 +1,33 @@ -import React, { useContext } from 'react'; -import { GamesContext } from '../../context/games'; +import React from 'react'; +import { Color } from '../../components/Checkers'; import Wobler from '../../components/Wobler'; import './ActionPanel.css' /* * NewGame actoins */ -export function Create({ onClick }) { - const games = useContext(GamesContext); +export function Create({ getGame, onClick }) { + const [game, isPushing] = getGame(); + const hasPlayers = (game.opponentName && game.myColor) ? true : ''; - const hasPlayers = games.newGame.opponentName && games.newGame.myColor; - - const validateNewGame = () => { + const validate = () => { if (!hasPlayers) return alert("You have to select an opponent"); - onClick(games.newGame); + if (isPushing) + return; // busy + + onClick({ + opponentName: game.opponentName, + opponentColor: Color.opposite(game.myColor), // TODO: by fixing this i can simply return GAME + board: game.board, + message: game.message + }); } return ( - ) } @@ -30,149 +35,165 @@ export function Create({ onClick }) { /* * GameProposal actions */ +export function Accept({ getGame, onClick }) { + const [game, isPushing] = getGame(); + const hasGame = (game.status === 'GAME_PROPOSAL_WAIT_FOR_YOU') ? true : ''; -export function Accept({ onClick }) { - const games = useContext(GamesContext); + const validate = () => { + if (!hasGame) + return alert('You have to select pending GameProposal'); - const selectedGame = games.findGame({ uuid: games.proposal.selectedUUID }); - const isReady = selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_YOU' ? true : ''; + if (isPushing) + return; // busy - if (selectedGame?.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') + onClick({ uuid: game.uuid }); + } + + if (game.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') return ( - ) } -export function Reject({ onClick }) { - const games = useContext(GamesContext); +export function Reject({ getGame, onClick }) { + const [game, isPushing] = getGame(); + const hasGame = (game.status === 'GAME_PROPOSAL_WAIT_FOR_YOU') ? true : ''; - const selectedGame = games.findGame({ uuid: games.proposal.selectedUUID }); - const isReady = selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_YOU' ? true : ''; + const validate = () => { + if (!hasGame) + return alert('You have to select some GameProposal'); - if (selectedGame?.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') + if (isPushing) + return; // busy + + onClick({ uuid: game.uuid }); + } + + if (game.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') return ( - ) } -export function Cancel({ onClick }) { - const games = useContext(GamesContext); +export function Cancel({ getGame, onClick }) { + const [game, isPushing] = getGame(); - const selectedGame = games.findGame({ uuid: games.proposal.selectedUUID }); - const isReady = selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT' ? true : ''; + if (game.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') + return; - if (selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') - return ( - - ) + const hasGame = (game.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') ? true : ''; + + const validate = () => { + if (!hasGame) + return alert('You have to select pending GameProposal'); + + if (isPushing) + return; // busy + + onClick({ uuid: game.uuid }); + } + + return ( + + ) } /* * Game actions */ +export function Surrender({ getGame, onClick }) { + const [game, isPushing] = getGame(); -export function Surrender({ onClick }) { - const games = useContext(GamesContext); - - const selectedGame = games.findGame({ uuid: games.active.selectedUUID }); - const gameStatus = selectedGame?.status; - const isReady = (gameStatus === 'GAME_BOARD_WAIT_FOR_OPPONENT' || gameStatus === 'GAME_BOARD_WAIT_FOR_YOU') ? true : ''; - - if (gameStatus === 'DRAW_REQUEST_WAIT_FOR_YOU' || gameStatus === 'DRAW_REQUEST_WAIT_FOR_OPPONENT') + if (game.status === 'DRAW_REQUEST_WAIT_FOR_YOU' || game.status === 'DRAW_REQUEST_WAIT_FOR_OPPONENT') return; // You shall not surrender if there is an active tie negotiations + const hasGame = (game.status === 'GAME_BOARD_WAIT_FOR_OPPONENT' || game.status === 'GAME_BOARD_WAIT_FOR_YOU') ? true : ''; + + const validate = () => { + if (!hasGame) + return alert('You have to select a Game'); + + if (isPushing) + return; // busy + + onClick({ uuid: game.uuid }); + } + return ( - + ) } -export function DrawAccept({ onClick }) { - const games = useContext(GamesContext); +export function DrawAccept({ getGame, onClick }) { + const [game, isPushing] = getGame(); - const selectedGame = games.findGame({ uuid: games.active.selectedUUID }); - const gameStatus = selectedGame?.status; + if (game.status !== 'DRAW_REQUEST_WAIT_FOR_YOU') + return; - if (gameStatus === 'DRAW_REQUEST_WAIT_FOR_YOU') - return ( - - ) + return ( + + ) } -export function DrawReject({ onClick }) { - const games = useContext(GamesContext); +export function DrawReject({ getGame, onClick }) { + const [game, isPushing] = getGame(); - const selectedGame = games.findGame({ uuid: games.active.selectedUUID }); + if (game.status !== 'DRAW_REQUEST_WAIT_FOR_YOU') + return; - if (selectedGame?.status === 'DRAW_REQUEST_WAIT_FOR_YOU') - return ( - - ) + return ( + + ) } /* * GameArchive actions */ - export function Backward() { - return } export function Forward() { - return } \ No newline at end of file diff --git a/webapp/src/container/games/GameBoard.jsx b/webapp/src/container/games/GameBoard.jsx index c2fb429..4e36817 100644 --- a/webapp/src/container/games/GameBoard.jsx +++ b/webapp/src/container/games/GameBoard.jsx @@ -1,12 +1,10 @@ -import React, { useContext } from 'react'; -import { useLocation, matchPath } from 'react-router-dom'; -import { GamesContext } from '../../context/games'; +import React from 'react'; import { Color, Player, Board } from '../../components/Checkers'; import './GameBoard.css'; -export default function GameBoard({ username, onStoneClick, onStoneMove }) { - const game = useSelectedGame() || {}; +export default function GameBoard({ username, getGame, onStoneClick, onStoneMove }) { + const [game, isPushing] = getGame(); const myName = game.opponentName ? username : ''; const opponentColor = Color.opposite(game.myColor); @@ -15,37 +13,26 @@ export default function GameBoard({ username, onStoneClick, onStoneMove }) { const optionalOnStoneClick = (typeof onStoneClick !== 'function') ? null : (cellId) => onStoneClick(game.uuid, cellId); - const optionalOnStoneMove = (typeof onStoneMove !== 'function') ? null : + const optionalOnStoneMove = (typeof onStoneMove !== 'function' || isPushing) ? null : (move) => onStoneMove(game.uuid, move); return (
- - + - - {game.isPushingGameMove ? Moving... : null /* TODO: isPushing shall be stored per game. curernty it is global indicator */} + + {(isPushing === 'move') ? Moving... : null}
) -} - -export function useSelectedGame() { - const games = useContext(GamesContext); - const { pathname } = useLocation(); - - if (matchPath('/games/new', pathname)) - return games.newGame; - - if (matchPath('/games/proposal', pathname)) - return games.findGame({ uuid: games.proposal.selectedUUID }); - - if (matchPath('/games/active', pathname)) - return games.findGame({ uuid: games.active.selectedUUID }); - - if (matchPath('/games/archive', pathname)) - return games.findGame({ uuid: games.archive.selectedUUID }); - - return undefined; } \ No newline at end of file diff --git a/webapp/src/container/games/Message2Opponent.jsx b/webapp/src/container/games/Message2Opponent.jsx index 0d1f258..5fa3f1b 100644 --- a/webapp/src/container/games/Message2Opponent.jsx +++ b/webapp/src/container/games/Message2Opponent.jsx @@ -1,65 +1,38 @@ -import React, { useContext, useRef, useState } from 'react'; -import { useLocation, matchPath } from 'react-router-dom'; -import { GamesContext } from '../../context/games'; +import React, { useRef, useState } from 'react'; -export default function Message2Opponent({ dispatchGames }) { - const games = useContext(GamesContext); - const { pathname } = useLocation(); +export default function Message2Opponent({ getMessage, setMessage }) { const [value, setValue] = useState(''); const syncTimeoutRef = useRef(null); - if (matchPath('/games/archive', pathname)) - return; // Shhh.. no chatting in the archives! - - if (matchPath('/games/proposal', pathname)) - return; // TODO: Enable GameProposal messages, as soon as it has been supported by the server side - - if (syncTimeoutRef.current === null) { // <<--- Absorb external value ONLY if there is no scheduled sync - var externalValue = ''; - - if (matchPath('/games/new', pathname)) - externalValue = games.newGame.message; - else if (matchPath('/games/proposal', pathname)) - externalValue = games.proposal.message; - else if (matchPath('/games/active', pathname)) - externalValue = games.active.message; - - if (value !== externalValue) - setValue(externalValue); + const message = getMessage(); + if (value !== message && syncTimeoutRef.current === null) { + // Absorb external value ONLY if there is no scheduled sync + setValue(message); } + const disabled = message === undefined; + /* --- */ - const sync = (message) => { + const sync = (nextMessage) => { syncTimeoutRef.current = null; - - if (matchPath('/games/new', pathname)) - return dispatchGames({ type: 'nextNewGame', message }); - - if (matchPath('/games/proposal', pathname)) - return dispatchGames({ type: 'nextProposal', message }); - - if (matchPath('/games/active', pathname)) - return dispatchGames({ type: 'nextActive', message }); - - console.warn('unknown path'); + setMessage(nextMessage); } const update = (value) => { setValue(value); - if (syncTimeoutRef.current) - clearTimeout(syncTimeoutRef.current); // <<--- Cancel previous sync - + clearTimeout(syncTimeoutRef.current); // <<--- Cancel previous sync syncTimeoutRef.current = setTimeout(() => sync(value), 500); } return ( update(e.target.value)} + onChange={(e) => update(e.target.value)} /> ) } \ No newline at end of file diff --git a/webapp/src/container/games/view/GameSelector.jsx b/webapp/src/container/games/view/GameSelector.jsx index 86e1e52..0a87ef9 100644 --- a/webapp/src/container/games/view/GameSelector.jsx +++ b/webapp/src/container/games/view/GameSelector.jsx @@ -1,17 +1,16 @@ import './GameSelector.css'; import React, { useContext } from 'react'; -import { GamesContext } from '../../../context/games'; +import { GamesStateContext, GamesGuideContext } from '../../../context/games'; import { Board, Color, Player } from '../../../components/Checkers'; import Loading from '../../../components/Loading'; import Counter from '../../../components/Counter'; export function GameProposalSelector({ onSelect }) { - const games = useContext(GamesContext); - if (games.gamesList === null) - return + const games = useContext(GamesStateContext); + const guide = useContext(GamesGuideContext); - const isSelected = (uuid) => uuid === games.proposal.selectedUUID; + const isSelected = (uuid) => uuid === guide.selectedUUID.proposal; const onClick = (uuid) => { if (isSelected(uuid)) @@ -20,12 +19,19 @@ export function GameProposalSelector({ onSelect }) { onSelect(uuid); } - const yoursList = games.gamesList.filter(game => game.status === 'GAME_PROPOSAL_WAIT_FOR_YOU') + const yoursList = games.filter(game => game.status === 'GAME_PROPOSAL_WAIT_FOR_YOU') .map(game => ) - const opponentsList = games.gamesList.filter(game => game.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') + const opponentsList = games.filter(game => game.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') .map(game => ) + if (yoursList.length === 0 && opponentsList.length === 0) { + if (guide.isPolling) + return + else + return <>There are no pending GameProposals.. + } + return (
{yoursList} @@ -36,11 +42,10 @@ export function GameProposalSelector({ onSelect }) { } export function ActiveGameSelector({ onSelect }) { - const games = useContext(GamesContext); - if (games.gamesList === null) - return + const games = useContext(GamesStateContext); + const guide = useContext(GamesGuideContext); - const isSelected = (uuid) => uuid === games.active.selectedUUID; + const isSelected = (uuid) => uuid === guide.selectedUUID.active; const onClick = (uuid) => { if (isSelected(uuid)) @@ -49,12 +54,19 @@ export function ActiveGameSelector({ onSelect }) { onSelect(uuid); } - const yoursList = games.gamesList.filter(game => (game.status === 'GAME_BOARD_WAIT_FOR_YOU' || game.status === 'DRAW_REQUEST_WAIT_FOR_YOU')) + const yoursList = games.filter(game => (game.status === 'GAME_BOARD_WAIT_FOR_YOU' || game.status === 'DRAW_REQUEST_WAIT_FOR_YOU')) .map(game => ) - const opponentsList = games.gamesList.filter(game => (game.status === 'GAME_BOARD_WAIT_FOR_OPPONENT' || game.status === 'DRAW_REQUEST_WAIT_FOR_OPPONENT')) + const opponentsList = games.filter(game => (game.status === 'GAME_BOARD_WAIT_FOR_OPPONENT' || game.status === 'DRAW_REQUEST_WAIT_FOR_OPPONENT')) .map(game => ) + if (yoursList.length === 0 && opponentsList.length === 0) { + if (guide.isPolling) + return + else + return <>There are no pending Games.. + } + return (
{yoursList} @@ -65,11 +77,10 @@ export function ActiveGameSelector({ onSelect }) { } export function GameArchiveSelector({ onSelect }) { - const games = useContext(GamesContext); - if (games.gamesList === null) - return + const games = useContext(GamesStateContext); + const guide = useContext(GamesGuideContext); - const isSelected = (uuid) => uuid === games.archive.selectedUUID; + const isSelected = (uuid) => uuid === guide.selectedUUID.archive; const onClick = (uuid) => { if (isSelected(uuid)) @@ -78,21 +89,28 @@ export function GameArchiveSelector({ onSelect }) { onSelect(uuid); } - const rejectedList = games.gamesList.filter(game => game.status === 'GAME_PROPOSAL_REJECTED') + const rejectedList = games.filter(game => game.status === 'GAME_PROPOSAL_REJECTED') .map(game => ) - const canceledList = games.gamesList.filter(game => game.status === 'GAME_PROPOSAL_CANCELED') + const canceledList = games.filter(game => game.status === 'GAME_PROPOSAL_CANCELED') .map(game => ) - const victoryList = games.gamesList.filter(game => game.status === 'GAME_RESULT_YOU_WON') + const victoryList = games.filter(game => game.status === 'GAME_RESULT_YOU_WON') .map(game => ) - const defeatList = games.gamesList.filter(game => game.status === 'GAME_RESULT_YOU_LOOSE') + const defeatList = games.filter(game => game.status === 'GAME_RESULT_YOU_LOOSE') .map(game => ) - const drawList = games.gamesList.filter(game => game.status === 'GAME_RESULT_DRAW') + const drawList = games.filter(game => game.status === 'GAME_RESULT_DRAW') .map(game => ) + if (rejectedList.length === 0 && canceledList.length === 0 && victoryList.length === 0 && defeatList.length === 0 && drawList.length === 0) { + if (guide.isPolling) + return + else + return <>Finished Games will be shown here.. + } + return (
{rejectedList.length > 0 && rejected proposals} @@ -119,9 +137,15 @@ function Selectable({ game, selected, onClick }) {
onClick(game.uuid)} > - +
- + {game.message}
diff --git a/webapp/src/container/games/view/NewGame.jsx b/webapp/src/container/games/view/NewGame.jsx index f0040af..01c9ffd 100644 --- a/webapp/src/container/games/view/NewGame.jsx +++ b/webapp/src/container/games/view/NewGame.jsx @@ -1,19 +1,19 @@ import './NewGame.css' import React, { useContext } from 'react'; -import { GamesContext } from '../../../context/games'; +import { GamesGuideContext } from '../../../context/games'; import DropdownList from '../../../components/DropdownList'; import { Color, WhiteStone, BlackStone } from '../../../components/Checkers'; export default function NewGame({ players, setPlayers }) { - const games = useContext(GamesContext); - - const [whitePlayer, blackPlayer] = (() => { - if (games.newGame.myColor === Color.white) - return [players.currentUser, games.newGame.opponentName]; + const newGame = useContext(GamesGuideContext).newGame; - if (games.newGame.myColor === Color.black) - return [games.newGame.opponentName, players.currentUser]; + const [whitePlayer, blackPlayer] = (() => { + if (newGame.myColor === Color.white) + return [players.user.name, newGame.opponentName]; + + if (newGame.myColor === Color.black) + return [newGame.opponentName, players.user.name]; return ['', '']; })(); // <<-- Execute! @@ -21,25 +21,24 @@ export default function NewGame({ players, setPlayers }) { /* * Name options */ - const nameOptions = !players.leaderboard.table - ? [] - : Object.keys(players.leaderboard.table).map(playerName => - ] : + Object.keys(players.leaderboard).map(name => + ) - const whiteOptions = Array(nameOptions) - whiteOptions.push() + const whiteOptions = Array(nameOptions); + whiteOptions.push(); - const blackOptions = Array(nameOptions) - blackOptions.push() + const blackOptions = Array(nameOptions); + blackOptions.push(); /* * The Component */ const onSelect = (name, myColor) => { - if (players.isCurrentUser(name)) - setPlayers(games.newGame.opponentName, myColor); + if (players.user.isCurrentUser(name)) + setPlayers(newGame.opponentName, myColor); else setPlayers(name, Color.opposite(myColor)); } diff --git a/webapp/src/context/games.js b/webapp/src/context/games.js index 537ab8a..1fea84d 100644 --- a/webapp/src/context/games.js +++ b/webapp/src/context/games.js @@ -1,6 +1,7 @@ import { createContext } from 'react'; -export const GamesContext = createContext(null); +export const GamesStateContext = createContext(null); +export const GamesGuideContext = createContext(null); // export const Games = React.createContext({ // state: initialState, diff --git a/webapp/src/hook/api.js b/webapp/src/hook/api.js index 11e4b17..2e5b71b 100644 --- a/webapp/src/hook/api.js +++ b/webapp/src/hook/api.js @@ -8,7 +8,7 @@ import { useState, useRef, useCallback, useEffect, } from "react" - interval_stop */ -export function usePolling(uri, onSuccess, mode = null) { +export function usePolling(uri, { onSuccess, onPolling }, mode = null) { const [isPolling, setPolling] = useState(false); const initialPollRef = useRef(true); @@ -19,11 +19,16 @@ export function usePolling(uri, onSuccess, mode = null) { const pollData = useCallback(() => { setPolling(true); + if (onPolling) + onPolling(true); + initialPollRef.current = false; fetch(uri) .then((response) => { setPolling(false); + if (onPolling) + onPolling(false); if (typeof mode?.interval_sec === 'number') { console.log("Schedule", uri, "fetch in", mode.interval_sec, "sec"); @@ -38,7 +43,7 @@ export function usePolling(uri, onSuccess, mode = null) { .catch((err) => { console.warn(err.message); }) - }, [uri, mode, onSuccess, initialPollRef, intervalTimerIdRef]); + }, [uri, mode, onSuccess, onPolling, initialPollRef, intervalTimerIdRef]); const stopPollInterval = useCallback(() => { console.log("Cancel scheduled fetch for", uri); @@ -55,8 +60,6 @@ export function usePolling(uri, onSuccess, mode = null) { stopPollInterval(); } }, [initialPoll, mode, intervalTimerId, isPolling, pollData, stopPollInterval]); - - return isPolling; } export async function doPushing(uri, method, data, { onSuccess, onPushing }) { @@ -77,13 +80,13 @@ export async function doPushing(uri, method, data, { onSuccess, onPushing }) { } if (onSuccess) { - var content = (response.headers.get('Content-Type') === "application/json") - ? await response.json() + var content = (response.headers.get('Content-Type') === "application/json") + ? await response.json() : null; onSuccess(content); } -// } catch (err) { + // } catch (err) { } finally { if (onPushing) onPushing(false); diff --git a/webapp/src/reducer/games.js b/webapp/src/reducer/games.js index 6d79c0a..14aa963 100644 --- a/webapp/src/reducer/games.js +++ b/webapp/src/reducer/games.js @@ -2,91 +2,143 @@ import { useReducer } from 'react'; import { nextState } from '../util/StateHelper'; import { defaultBoard } from '../components/Checkers'; -export const gamesInitialState = { - gamesList: null, +/* + * State + */ +const gameTemplate = { + status: '', + myColor: '', + opponentName: '', + board: null, + moveNumber: 0, + previousMove: [], + + message: '', + uuid: '' +} + +const gamesStateTemplate = [/* gameTemplate */]; + +function gamesStateReducer(state, action) { + switch (action.type) { + + case 'next': + return action.games; + + case 'add': + return [ + ...state, + nextState(gameTemplate, action.game, 'Game.create') + ]; + + case 'update': + return state.map((game) => + game.uuid !== action.game.uuid ? game : + nextState(gameTemplate, action.game, 'Game.update') + ); + + default: + throw Error('GamesState: unknown action.type', action.type); + } +} + +export function useGamesStateReducer() { + return useReducer(gamesStateReducer, gamesStateTemplate); +} + +/* + * Guide + */ +export const gamesGuideTemplate = { newGame: { opponentName: '', myColor: '', board: defaultBoard, message: '', + isPushing: false }, - proposal: { - selectedUUID: null, - message: '', + awaiting: { + proposal: 0, + active: 0 }, - active: { - selectedUUID: null, - message: '', + selectedUUID: { + proposal: null, + active: null, + archive: null, }, - archive: { - selectedUUID: null, + UUIDmessage: { // UUIDmessage[uuid] }, - // Network - isPollingGamesList: false, - isPushingNewGame: false, + UUIDpushing: { // UUIDpushing[uuid] + }, - isPushingGameProposalCancel: false, - isPushingGameProposalReject: false, - isPushingGameProposalAccept: false, - - isPushingGameSurrender: false, - isPushingGameDrawRequest: false, - isPushingGameDrawAccept: false, - isPushingGameDrawReject: false, - isPushingGameMove: false, - - findGame, - nextGame, + isPolling: false, }; -function reducer(state, action) { +function gamesGuideReducer(state, action) { switch (action.type) { case 'next': - return nextState(state, action); + return nextState(state, action, 'GamesGuide'); + + case 'sync': + //console.log('sync'); + return { + ...state, + awaiting: calcAwating(action.gamesState) + } case 'nextNewGame': return { ...state, - newGame: nextState(state.newGame, action) + newGame: nextState(state.newGame, action, 'GamesGuide.newGame') }; - case 'nextProposal': + case 'selectedUUID': return { ...state, - proposal: nextState(state.proposal, action) + selectedUUID: nextState(state.selectedUUID, action, 'GamesGuide.selectedUUID') }; - case 'nextActive': - return { - ...state, - active: nextState(state.active, action) - }; + case 'UUIDmessage': { + const next = { ...state }; + next.UUIDmessage[action.uuid] = action.message; + return next; + } - case 'nextArchive': - return { - ...state, - archive: nextState(state.archive, action) - }; + case 'UUIDpushing': { + const next = { ...state }; + next.UUIDpushing[action.uuid] = action.what + return next; + } default: - throw Error('GamesReducer: unknown action.type', action.type); + throw Error('GamesGuide: unknown action.type: ' + action.type); } } -export default function useGamesReducer() { - return useReducer(reducer, gamesInitialState); +//const uuidRegex = new RegExp("^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}"); + +export function useGamesGuideReducer() { + return useReducer(gamesGuideReducer, gamesGuideTemplate); } -function findGame({ uuid }) { - return this.gamesList?.find((game) => game.uuid === uuid); -} - -function nextGame(nextGame) { - return this.gamesList?.map((game) => (game.uuid === nextGame?.uuid) ? nextGame : game); +function calcAwating(games) { + return games.reduce((awaiting, game) => { + switch (game.status) { + case 'GAME_PROPOSAL_WAIT_FOR_YOU': + awaiting.proposal++; + return awaiting; + case 'GAME_BOARD_WAIT_FOR_YOU': + case 'DRAW_REQUEST_WAIT_FOR_YOU': + awaiting.active++; + return awaiting; + default: + return awaiting; + } + }, structuredClone(gamesGuideTemplate.awaiting)); } \ No newline at end of file diff --git a/webapp/src/reducer/leaderboard.js b/webapp/src/reducer/leaderboard.js index f2b0141..feb86de 100644 --- a/webapp/src/reducer/leaderboard.js +++ b/webapp/src/reducer/leaderboard.js @@ -1,24 +1,48 @@ import { useReducer } from 'react'; import { nextState } from '../util/StateHelper'; -const initialState = { - table: null, - - // Network - isPollingTable: false +/* + * State + */ +const stateTemplate = { + // name : { rank } + // Bobik: {total: 10, victory 5: draw: 1} }; -function reducer(state, action) { +function stateReducer(state, action) { switch (action.type) { case 'next': - return nextState(state, action); + return action.table; default: - throw Error('LeaderboardReducer: unknown action.type', action.type); + throw Error('LeaderboardState: unknown action.type ' +action.type); } } -export default function useLeaderboardReducer() { - return useReducer(reducer, initialState); +export function useLeaderboardStateReducer() { + return useReducer(stateReducer, stateTemplate); +} + +/* + * Guide + */ +const guideTemplate = { + + isPolling: false +} + +function guideReducer(state, action) { + switch (action.type) { + + case 'next': + return nextState(state, action, 'LeaderboardGuide'); + + default: + throw Error('LeaderboardGuide: unknown action.type ' +action.type); + } +} + +export function useLeaderboardGuideReducer() { + return useReducer(guideReducer, guideTemplate); } \ No newline at end of file diff --git a/webapp/src/reducer/user.js b/webapp/src/reducer/user.js index b019d0f..b42ca38 100644 --- a/webapp/src/reducer/user.js +++ b/webapp/src/reducer/user.js @@ -1,28 +1,31 @@ import { useReducer } from 'react'; import { localeCompare } from '../util/Locale'; -const initialState = { - username: '', +/* + * State + */ +const stateTemplate = { + name: '', - isCurrentUser: function (otherUsername) { - return localeCompare(this.username, otherUsername) + isCurrentUser: function (otherName) { + return localeCompare(this.name, otherName) || null; // true -or- null } }; -function reducer(state, action) { +function stateReducer(state, action) { switch (action.type) { case 'parse': return { ...state, - username: action.userJson.holdingIdentity.name + name: action.userJson.holdingIdentity.name }; default: - throw Error('UserReducer: unknown action.type', action.type); + throw Error('UserState: unknown action.type ' +action.type); } } -export default function useUserReducer() { - return useReducer(reducer, initialState); +export function useUserStateReducer() { + return useReducer(stateReducer, stateTemplate); } \ No newline at end of file diff --git a/webapp/src/util/StateHelper.js b/webapp/src/util/StateHelper.js index 4262349..e6608fb 100644 --- a/webapp/src/util/StateHelper.js +++ b/webapp/src/util/StateHelper.js @@ -1,16 +1,23 @@ -export function nextState(state, delta) { +export function nextState(state, delta, coment) { const nextState = { ...state }; - Object.keys(delta) - .slice(1) // skip first property i.e. 'next' - .forEach(key => { - if (Object.hasOwn(nextState, key)) { - console.log("next [", key, "] = ", delta[key]); - nextState[key] = delta[key]; - } else { - console.warn("nextState: bad action property\n", key + ":", delta[key]); - } - }) + let logMsg = ''; + Object.keys(delta).forEach(key => { + if (key === 'type') + return; + + if (nextState.hasOwnProperty(key)) { + if (coment) + logMsg += '\n ' + key + ': ' + delta[key]; + + nextState[key] = delta[key]; + } else { + console.warn("nextState: bad action property\n", key + ":", delta[key]); + } + }) + + if (coment) + console.log('Next', coment, logMsg); return nextState; }