From 703a6a0326d698f26a984ae107e7c5d72432d7bd Mon Sep 17 00:00:00 2001 From: djmil Date: Sat, 11 Nov 2023 11:08:48 +0100 Subject: [PATCH 1/4] skeleton --- webapp/src/container/Games.jsx | 9 ++-- webapp/src/container/games/action/Create.jsx | 44 ++++++++++++++++++-- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/webapp/src/container/Games.jsx b/webapp/src/container/Games.jsx index 43da121..595c98b 100644 --- a/webapp/src/container/Games.jsx +++ b/webapp/src/container/Games.jsx @@ -29,7 +29,7 @@ export default function Games({ context: { games, dispatchGames, gamesApi }, pla
- + {/* @@ -80,11 +80,14 @@ function ViewProvider({ players, dispatchGames }) { ) } -function ActionPanel() { +function ActionPanel({ players }) { return (
- } /> + + } /> , , ]} /> , , ]} /> , ]} /> diff --git a/webapp/src/container/games/action/Create.jsx b/webapp/src/container/games/action/Create.jsx index 2cee6c6..d29f925 100644 --- a/webapp/src/container/games/action/Create.jsx +++ b/webapp/src/container/games/action/Create.jsx @@ -1,6 +1,44 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { GamesContext } from '../../../context/games'; +import Wobler from '../../../components/Wobler'; -export default function Create() { - return + +export default function Create({ isCurrentUser }) { + const newGameCtx = useContext(GamesContext).newGame; + + const hasPlayers = checkPlayers(newGameCtx); + const hasCurrentUser = checkCurrentUser(newGameCtx, isCurrentUser); + + const pushNewGame = () => { + if (!hasPlayers) + return alert("Black and White players must be selected for the game"); + + if (!hasCurrentUser) + return alert("You must be one of the selected players"); + + if (newGameCtx.pushing) + return; // current request is still being processed + + console.log("send request"); + } + + return ( + + ) } + +function checkPlayers({ whitePlayer, blackPlayer }) { + return whitePlayer && blackPlayer + && whitePlayer !== blackPlayer; +} + +function checkCurrentUser({ whitePlayer, blackPlayer }, isCurrentUser) { + return isCurrentUser(whitePlayer) || isCurrentUser(blackPlayer); +} \ No newline at end of file -- 2.45.2 From 82f5b07256cc54f7e2707a20f6bd025149e18db6 Mon Sep 17 00:00:00 2001 From: djmil Date: Sat, 11 Nov 2023 11:37:17 +0100 Subject: [PATCH 2/4] action readyiness indication --- webapp/src/container/Games.css | 9 ++++++--- webapp/src/container/games/action/Create.jsx | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/webapp/src/container/Games.css b/webapp/src/container/Games.css index 3d8680a..2508acd 100644 --- a/webapp/src/container/Games.css +++ b/webapp/src/container/Games.css @@ -97,16 +97,19 @@ margin: 2px; } -.ActionPanel .Create:hover, /* OR */ -.game-action.busy +.ActionPanel .Create.ready /* , */ /* OR .game-action.busy */ { background-color:#00b0ff60; } -.ActionPanel .Create.enabled:active { +.ActionPanel .Create.ready:hover { background-color:#00b0ffa0; } +.ActionPanel .Create.ready:active { + background-color:#00b0fff0; +} + .ActionPanel .Cancel:hover, .ActionPanel .Reject:hover { background-color:#ff000030 diff --git a/webapp/src/container/games/action/Create.jsx b/webapp/src/container/games/action/Create.jsx index d29f925..893258f 100644 --- a/webapp/src/container/games/action/Create.jsx +++ b/webapp/src/container/games/action/Create.jsx @@ -25,7 +25,7 @@ export default function Create({ isCurrentUser }) { return ( diff --git a/webapp/src/container/games/view/GameSelector.jsx b/webapp/src/container/games/view/GameSelector.jsx index 5578e7c..23c6d34 100644 --- a/webapp/src/container/games/view/GameSelector.jsx +++ b/webapp/src/container/games/view/GameSelector.jsx @@ -7,14 +7,14 @@ import Loading from '../../../components/Loading'; export default function GameSelector({ yours, opponents, onClick }) { - const games = useContext(GamesContext); - if (games.list === null) + const gamesList = useContext(GamesContext).gamesList; + if (gamesList === null) return - const yoursList = games.list.filter(game => game.status === yours) + const yoursList = gamesList.filter(game => game.status === yours) .map(game => ) - const opponentsList = games.list.filter(game => game.status === opponents) + const opponentsList = gamesList.filter(game => game.status === opponents) .map(game => ) return ( diff --git a/webapp/src/container/games/view/NewGame.jsx b/webapp/src/container/games/view/NewGame.jsx index 0ce7d3e..a729a70 100644 --- a/webapp/src/container/games/view/NewGame.jsx +++ b/webapp/src/container/games/view/NewGame.jsx @@ -11,9 +11,9 @@ export default function NewGame({ players, onSelectPlayer }) { /* * Name options */ - const nameOptions = !players.leaderboard + const nameOptions = !players.leaderboard.table ? [] - : Object.keys(players.leaderboard).map(playerName => + : Object.keys(players.leaderboard.table).map(playerName => ) diff --git a/webapp/src/reducer/config.js b/webapp/src/reducer/config.js new file mode 100644 index 0000000..1d2f3a4 --- /dev/null +++ b/webapp/src/reducer/config.js @@ -0,0 +1,44 @@ +import { useReducer } from 'react'; +import { namedLocalStorage } from '../util/PersistentStorage'; +import { nextState } from '../util/StateHelper'; + +const Persistent = (() => { + const [getOnline, setOnline] = namedLocalStorage('config.online', true); + + return { + getOnline, + setOnline + } +})(); // <<--- Execute + +const initialState = { + online: Persistent.getOnline() === 'true', + + intervalMode +}; + +function dispatch(state, action) { + switch (action.type) { + + case 'toggleOnline': return { + ...state, + online: Persistent.setOnline(!state.online) + }; + + case 'next': + return nextState(state, action); + + default: + throw Error('ConfigReducer: unknown action.type', action.type); + } +} + +export default function useConfigReducer() { + return useReducer(dispatch, initialState); +} + +function intervalMode(interval_sec) { + return (this.online === true) + ? { interval_sec } // fetch from API every interval_sec + : { interval_stop: true } // user has fliped OfflineToggel +} \ No newline at end of file diff --git a/webapp/src/reducer/games.js b/webapp/src/reducer/games.js index 854ef37..073bd6a 100644 --- a/webapp/src/reducer/games.js +++ b/webapp/src/reducer/games.js @@ -1,16 +1,20 @@ import { useReducer } from 'react'; import { nextState } from '../util/StateHelper'; -export const gamesInitialState = { - list: null, +const initialState = { + gamesList: null, newGame: { whitePlayer: '', blackPlayer: '' - } + }, + + // Network + isPollingGamesList: false, + isPushingNewGame: false, }; -export function gamesReducer(state, action) { +function reducer(state, action) { switch (action.type) { case 'next': @@ -22,5 +26,5 @@ export function gamesReducer(state, action) { } export default function useGamesReducer() { - return useReducer(gamesReducer, gamesInitialState); + return useReducer(reducer, initialState); } \ No newline at end of file diff --git a/webapp/src/reducer/leaderboard.js b/webapp/src/reducer/leaderboard.js new file mode 100644 index 0000000..f2b0141 --- /dev/null +++ b/webapp/src/reducer/leaderboard.js @@ -0,0 +1,24 @@ +import { useReducer } from 'react'; +import { nextState } from '../util/StateHelper'; + +const initialState = { + table: null, + + // Network + isPollingTable: false +}; + +function reducer(state, action) { + switch (action.type) { + + case 'next': + return nextState(state, action); + + default: + throw Error('LeaderboardReducer: unknown action.type', action.type); + } +} + +export default function useLeaderboardReducer() { + return useReducer(reducer, initialState); +} \ No newline at end of file diff --git a/webapp/src/reducer/polling.js b/webapp/src/reducer/polling.js deleted file mode 100644 index 750f2ca..0000000 --- a/webapp/src/reducer/polling.js +++ /dev/null @@ -1,39 +0,0 @@ -import { useReducer } from 'react'; -import { namedLocalStorage } from '../util/PersistentStorage'; -import { nextState } from '../util/StateHelper'; - -const Persistent = (() => { - const [getEnabled, setEnabled] = namedLocalStorage('polling.enabled', true); - - return { - getEnabled, - setEnabled - } -})(); // <<--- Execute - -export const pollingInitialState = { - enabled: Persistent.getEnabled() === 'true', - - games: false, - leaderboard: false -}; - -export function pollingReducer(curntState, action) { - switch (action.type) { - - case 'toggleOnOff': return { - ...curntState, - enabled: Persistent.setEnabled(!curntState.enabled) - }; - - case 'next': - return nextState(curntState, action); - - default: - throw Error('Unknown action.type:' + action.type); - } -} - -export default function usePollingReducer() { - return useReducer(pollingReducer, pollingInitialState); -} \ No newline at end of file diff --git a/webapp/src/reducer/user.js b/webapp/src/reducer/user.js index 0351553..1cf1265 100644 --- a/webapp/src/reducer/user.js +++ b/webapp/src/reducer/user.js @@ -1,7 +1,7 @@ import { useReducer } from 'react'; import { localeCompare } from '../util/Locale'; -export const userInitialState = { +const initialState = { username: '', isCurrentUser: function (otherUsername) { @@ -9,13 +9,13 @@ export const userInitialState = { } }; -export function userReducer(state, action) { +function reducer(state, action) { switch (action.type) { case 'parse': return { ...state, - username: action.json.username + username: action.userJson.username }; default: @@ -24,5 +24,5 @@ export function userReducer(state, action) { } export default function useUserReducer() { - return useReducer(userReducer, userInitialState); + return useReducer(reducer, initialState); } \ No newline at end of file -- 2.45.2 From 04f0b86527445f7032c544db405e53a368ac3340 Mon Sep 17 00:00:00 2001 From: djmil Date: Mon, 13 Nov 2023 16:55:10 +0100 Subject: [PATCH 4/4] api.push (#36) Reviewed-on: http://192.168.8.55:3000/HQLAx/CordaCheckers/pulls/36 --- webapp/src/api/games.js | 13 +++--- webapp/src/api/leaderboard.js | 2 +- webapp/src/api/user.js | 2 +- webapp/src/container/Games.jsx | 5 +- webapp/src/container/games/action/Create.jsx | 49 ++++++++++++++------ webapp/src/hook/{Polling.js => api.js} | 46 +++++++++++++----- 6 files changed, 81 insertions(+), 36 deletions(-) rename webapp/src/hook/{Polling.js => api.js} (60%) diff --git a/webapp/src/api/games.js b/webapp/src/api/games.js index 8a4f3ed..58b800d 100644 --- a/webapp/src/api/games.js +++ b/webapp/src/api/games.js @@ -1,4 +1,4 @@ -import usePolling from '../hook/Polling'; +import { usePolling, doPushing } from '../hook/api'; export default function useGamesApi(gamesReducer, config) { const [games, dispatchGames] = gamesReducer; @@ -16,14 +16,13 @@ export default function useGamesApi(gamesReducer, config) { return games; } - const usePushNewGame = () => { - const onSuccess = (game) => { - dispatchGames({ type: 'next', game }); - } - } return { pollGamesList: usePollingGamesList, - pushNewGame: usePushNewGame + + pushNewGame: (reqParams) => doPushing('/api/gameproposal', 'POST', reqParams, { + onPushing: (isPushingNewGame) => dispatchGames({ type: 'next', isPushingNewGame }), + onSuccess: (game) => dispatchGames({ type: 'next', gamesList: [...games.gamesList, game] }) + }), } } \ No newline at end of file diff --git a/webapp/src/api/leaderboard.js b/webapp/src/api/leaderboard.js index fe279a3..280afaa 100644 --- a/webapp/src/api/leaderboard.js +++ b/webapp/src/api/leaderboard.js @@ -1,4 +1,4 @@ -import usePolling from "../hook/Polling"; +import { usePolling } from "../hook/api"; export default function useLeaderboardApi(leaderboardReducer, config) { const [leaderboard, dispatchLeaderboard] = leaderboardReducer; diff --git a/webapp/src/api/user.js b/webapp/src/api/user.js index 39c3a68..9d8df5c 100644 --- a/webapp/src/api/user.js +++ b/webapp/src/api/user.js @@ -1,4 +1,4 @@ -import usePolling from "../hook/Polling"; +import { usePolling } from "../hook/api"; export default function useUserApi(userReducer) { const [user, dispatchUser] = userReducer; diff --git a/webapp/src/container/Games.jsx b/webapp/src/container/Games.jsx index 3ac2c34..e94d41b 100644 --- a/webapp/src/container/Games.jsx +++ b/webapp/src/container/Games.jsx @@ -30,7 +30,7 @@ export default function Games({ context: { gamesReducer, gamesApi }, players })
- + {/* @@ -81,12 +81,13 @@ function ViewProvider({ players, dispatchGames }) { ) } -function ActionPanel({ players }) { +function ActionPanel({ players, gamesApi }) { return (
gamesApi.pushNewGame(reqParams)} /> } /> , , ]} /> diff --git a/webapp/src/container/games/action/Create.jsx b/webapp/src/container/games/action/Create.jsx index 9fd59ca..cddb347 100644 --- a/webapp/src/container/games/action/Create.jsx +++ b/webapp/src/container/games/action/Create.jsx @@ -1,36 +1,45 @@ import React, { useContext } from 'react'; import { GamesContext } from '../../../context/games'; import Wobler from '../../../components/Wobler'; +import { Color } from '../../../components/Checkers'; +export default function Create({ isCurrentUser, pushNewGame }) { + const games = useContext(GamesContext); -export default function Create({ isCurrentUser, onClick }) { - const newGameCtx = useContext(GamesContext).newGame; + const hasPlayers = checkPlayers(games.newGame); + const hasCurrentUser = checkCurrentUser(games.newGame, isCurrentUser); - const hasPlayers = checkPlayers(newGameCtx); - const hasCurrentUser = checkCurrentUser(newGameCtx, isCurrentUser); - - const prepareRequest = () => { + const prepareNewGameRequest = () => { if (!hasPlayers) return alert("Black and White players must be selected for the game"); if (!hasCurrentUser) return alert("You must be one of the selected players"); - if (newGameCtx.pushing) + if (games.isPushingNewGame) return; // current request is still being processed - //onClick - console.log("send request"); + /* + * Prepare & send NewGame request + */ + const [opponentName, opponentColor] = getOpponent(games.newGame, isCurrentUser); + + const reqParams = { + opponentName, + opponentColor, + board: null, // default board configuration + message: 'default NewGame req message' + } + + pushNewGame(reqParams); } return ( - ) } @@ -42,4 +51,16 @@ function checkPlayers({ whitePlayer, blackPlayer }) { function checkCurrentUser({ whitePlayer, blackPlayer }, isCurrentUser) { return isCurrentUser(whitePlayer) || isCurrentUser(blackPlayer); +} + +function getOpponent({whitePlayer, blackPlayer}, isCurrentUser) { + if (isCurrentUser(whitePlayer)) { + return [blackPlayer, Color.black]; + } + + if (isCurrentUser(blackPlayer)) { + return [whitePlayer, Color.white]; + } + + return ['', '']; } \ No newline at end of file diff --git a/webapp/src/hook/Polling.js b/webapp/src/hook/api.js similarity index 60% rename from webapp/src/hook/Polling.js rename to webapp/src/hook/api.js index abfa3b7..28e759c 100644 --- a/webapp/src/hook/Polling.js +++ b/webapp/src/hook/api.js @@ -8,22 +8,21 @@ import { useState, useRef, useCallback, useEffect, } from "react" - interval_stop */ -export default function usePolling(uri, onResponce, mode = null) { +export function usePolling(uri, onSuccess, mode = null) { const [isPolling, setPolling] = useState(false); const initialPollRef = useRef(true); const initialPoll = initialPollRef.current; - + const intervalTimerIdRef = useRef(null); const intervalTimerId = intervalTimerIdRef.current; - const pollData = useCallback(() => { setPolling(true); initialPollRef.current = false; fetch(uri) - .then((responce) => { + .then((response) => { setPolling(false); if (typeof mode?.interval_sec === 'number') { @@ -31,15 +30,15 @@ export default function usePolling(uri, onResponce, mode = null) { intervalTimerIdRef.current = setTimeout(pollData, mode.interval_sec * 1000); } - return responce.json(); + return response.json(); }) .then((json) => { - onResponce(json); + onSuccess(json); }) .catch((err) => { console.warn(err.message); }) - }, [uri, mode, onResponce, initialPollRef, intervalTimerIdRef]); + }, [uri, mode, onSuccess, initialPollRef, intervalTimerIdRef]); const stopPollInterval = useCallback(() => { console.log("Cancel scheduled fetch for", uri); @@ -51,11 +50,36 @@ export default function usePolling(uri, onResponce, mode = null) { useEffect(() => { if ((initialPoll || (typeof mode?.interval_sec === 'number' && intervalTimerId === null)) && !isPolling) { pollData(); - } else - if (mode?.interval_stop && intervalTimerId) { - stopPollInterval(); - } + } else + if (mode?.interval_stop && intervalTimerId) { + stopPollInterval(); + } }, [initialPoll, mode, intervalTimerId, isPolling, pollData, stopPollInterval]); return isPolling; +} + +export async function doPushing(uri, method, data, { onSuccess, onPushing }) { + if (onPushing) + onPushing(true); + + try { + const response = await fetch(uri, { + method, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), // body data type must match "Content-Type" header + }); + + if (!response.ok) + throw new Error(`Error! status: ${response.status}`); + + if (onSuccess) + onSuccess(await response.json()); +// } catch (err) { + } finally { + if (onPushing) + onPushing(false); + } } \ No newline at end of file -- 2.45.2