diff --git a/webapp/src/App.js b/webapp/src/App.js index 16d5a50..84e5661 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -9,40 +9,36 @@ import About from "./components/About" import Games from './container/Games'; import Leaderboard from './container/Leaderboard'; -import useUserReducer from './reducer/user'; import usePollingReducer from './reducer/polling'; -import useLeaderboardReducer from './reducer/leaderboard'; -import useGamesReducer from './reducer/games'; +//import useGamesReducer from './reducer/games'; import useUserApi from './api/user'; import useLeaderboardApi from './api/leaderboard'; -import useGamesApi from './api/games'; +//import useGamesApi from './api/games'; export default function App() { - const userReducer = useUserReducer(); - const pollingFlux = usePollingReducer(); - const leaderboardReducer = useLeaderboardReducer(); - const gamesReducer = useGamesReducer(); + const pollingReducer = usePollingReducer(); + //const gamesReducer = useGamesReducer(); - const user = useUserApi(userReducer).get(); - const leaderboard = useLeaderboardApi(leaderboardReducer).poll(pollingFlux); - /*const gamesApi = */ useGamesApi(gamesReducer).list(pollingFlux); + //const games = useGamesApi(gamesReducer).list(pollingReducer); + const leaderboard = useLeaderboardApi().poll(pollingReducer); + const user = useUserApi().get(); return ( -
+
- } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> ) } -function Header({ pollingFlux }) { - const [polling, dispatchPolling] = pollingFlux; +function Header({ pollingReducer }) { + const [polling, dispatchPolling] = pollingReducer; return (
@@ -52,20 +48,20 @@ function Header({ pollingFlux }) { dispatchPolling({ type: "toggleOnOff" })} + onClick={() => dispatchPolling({ type: 'toggleOnOff' })} />
diff --git a/webapp/src/api/games.js b/webapp/src/api/games.js index aefbdbb..df2a168 100644 --- a/webapp/src/api/games.js +++ b/webapp/src/api/games.js @@ -1,25 +1,19 @@ import usePolling from "../util/Polling" -const uri = '/api/games'; - -export default function useGamesApi(gamesReducer) { - const [games, dispatchGames] = gamesReducer; +export default function useGamesApi(gamesState) { + const [games, setGames] = gamesState; const useList = (pollingReducer) => { const [polling, dispatchPolling] = pollingReducer; const mode = (polling.enabled === true) - ? { interval_sec: 30 } // update games list half a minue + ? { interval_sec: 30 } // update games list every half a minue : { interval_stop: true } // user has fliped OfflineToggel - const [list, isFetching] = usePolling(uri, mode); + const isPolling = usePolling('/api/games', setGames, mode); - if (polling.games !== isFetching) { - dispatchPolling({ type: 'next', games: isFetching }); - } - - if (games.list !== list) { - dispatchGames({ type: 'next', list }); + if (isPolling !== polling.games) { + dispatchPolling({ type: 'next', games: isPolling }); } return games; diff --git a/webapp/src/api/leaderboard.js b/webapp/src/api/leaderboard.js index f5d01f9..a653e35 100644 --- a/webapp/src/api/leaderboard.js +++ b/webapp/src/api/leaderboard.js @@ -1,9 +1,8 @@ -import usePolling from "../util/Polling" +import { useState } from "react"; +import usePolling from "../util/Polling"; -const uri = '/api/leaderboard'; - -export default function useLeaderboardApi(leaderboardReducer) { - const [leaderboard, dispatchLeaderboaed] = leaderboardReducer; +export default function useLeaderboardApi() { + const [leaderboard, setLeaderboard] = useState(null); const usePoll = (pollingReducer) => { const [polling, dispatchPolling] = pollingReducer; @@ -12,14 +11,10 @@ export default function useLeaderboardApi(leaderboardReducer) { ? { interval_sec: 300 } // update leaderbord stats every 5 min : { interval_stop: true } // user has fliped OfflineToggel - const [table, isFetching] = usePolling(uri, mode); + const isPolling = usePolling('/api/leaderboard', setLeaderboard, mode); - if (polling.leaderboard !== isFetching) { - dispatchPolling({ type: 'next', leaderboard: isFetching }); - } - - if (leaderboard.table !== table) { - dispatchLeaderboaed({ type: 'next', table }); + if (isPolling !== polling.leaderboard) { + dispatchPolling({ type: 'next', leaderboard: isPolling }); } return leaderboard; diff --git a/webapp/src/api/user.js b/webapp/src/api/user.js index 7f8606a..83106b3 100644 --- a/webapp/src/api/user.js +++ b/webapp/src/api/user.js @@ -1,16 +1,15 @@ -import usePolling from "../util/Polling" +import usePolling from "../util/Polling"; +import useUserReducer from "../reducer/user"; -const uri = '/api/user'; - -export default function useUserApi([user, dispatchUser]) { +export default function useUserApi() { + const [user, dispatchUser] = useUserReducer(); const useGet = () => { - const [nextUser] = usePolling(uri); - - if (typeof nextUser?.username === 'string' && nextUser.username !== user.username) { - dispatchUser({ type: "next", username: nextUser.username }); + const onResponce = (json) => { + dispatchUser({ type: "parse", json }); } + usePolling('/api/user', onResponce); // <<-- fetch once return user; } diff --git a/webapp/src/container/Leaderboard.jsx b/webapp/src/container/Leaderboard.jsx index 6224487..6d8863e 100644 --- a/webapp/src/container/Leaderboard.jsx +++ b/webapp/src/container/Leaderboard.jsx @@ -4,15 +4,14 @@ import Loading from '../components/Loading'; export default function Leaderboard({ leaderboard, user }) { - const table = leaderboard?.table; - if (!table) + if (leaderboard == null) return const isCurrentUser = (playerName) => user?.isCurrentUser(playerName) === true ? true : null; - const tableRows = Object.keys(table).map(playerName => { - var rank = table[playerName]; + const tableRows = Object.keys(leaderboard).map(playerName => { + var rank = leaderboard[playerName]; return {playerName} diff --git a/webapp/src/context/app/reducer.js b/webapp/src/context/app/reducer.js deleted file mode 100644 index bb53b1d..0000000 --- a/webapp/src/context/app/reducer.js +++ /dev/null @@ -1,84 +0,0 @@ -export const reducer = (state, action) => { - - switch (action.update) { - - case "game-selector": - return GameSelector_update(state, action) - - case "newGame": - return updateNewGame(state, action) - - default: - console.warn("Unknown action.component", action.component) - return state - } -} - -export const initialState = { - gameSelector: { - selectedGameProposal: null, - selectedActiveGame: null, - selectedArchiveGame: null, - }, - - newGame: { - whitePlayer: '', - blackPlayer: '', - message: '', - fetching: false, - }, - -} - -function GameSelector_update(state, action) { - if (Object.hasOwn(action, 'selectedGameProposal')) { - return { - ...state, - gameSelector: { - ...state.gameSelector, - selectedGameProposal: action.selectedGameProposal - } - } - } - - if (Object.hasOwn(action, 'selectedActiveGame')) { - return { - ...state, - gameSelector: { - ...state.gameSelector, - selectedActiveGame: action.selectedActiveGame - } - } - } - - if (Object.hasOwn(action, 'selectedArchiveGame')) { - return { - ...state, - gameSelector: { - ...state.gameSelector, - selectedArchiveGame: action.selectedArchiveGame - } - } - } - - console.warn(action.component, "- bad property") -} - -function updateNewGame(state, action) { - const newGame = {...state.newGame} - - Object.keys(action) - .slice(1) // skip 'update' property - .forEach(actionKey => { - if (Object.hasOwn(newGame, actionKey)) { - newGame[actionKey] = action[actionKey] - } else { - console.warn("NewGame update: bad action property\n", actionKey + ":", action[actionKey]) - } - }) - - return { - ...state, - newGame - } -} diff --git a/webapp/src/context/data/Poll.js b/webapp/src/context/data/Poll.js deleted file mode 100644 index 9d6d209..0000000 --- a/webapp/src/context/data/Poll.js +++ /dev/null @@ -1,46 +0,0 @@ -import { useState, useCallback, useEffect, } from "react" - -/* - TODO: Poll(uri, flavour) - - uri: string - - execution_flvour: - - once (i.e. now) - - interval (sec) - - stop -*/ - -export default function Poll(url, interval_sec, offlineMode) { - const [dataCache, setDataCache] = useState(null) - const [fetching , setFetching ] = useState(false) - const [timeoutID, setTimeoutID] = useState(null) - - const fecthData = useCallback(() => { - setTimeoutID(null) - setFetching(true) - - fetch(url) - .then((response) => { - setFetching(false) - return response.json() - }) - .then((freshData) => setDataCache(freshData)) - .catch((err) => console.log(err.message)) - }, [url]) - - useEffect(() => { - if (dataCache == null) { - fecthData() // <<-- run immediatly on startup - } - else if (offlineMode === true) { - clearTimeout(timeoutID) // cancel already scheduled fetch - setTimeoutID(null) // & stop interval fetching - } - else if (timeoutID === null && typeof interval_sec === 'number') { - const timeoutID = setTimeout(fecthData, interval_sec * 1000) - setTimeoutID(timeoutID) - console.log("Fetch '" +url +"' scheduled in " +interval_sec +" sec") - } - }, [url, dataCache, fecthData, timeoutID, offlineMode, interval_sec]); - - return [ dataCache, fetching ] -} diff --git a/webapp/src/context/data/index.jsx b/webapp/src/context/data/index.jsx deleted file mode 100644 index e7fa95e..0000000 --- a/webapp/src/context/data/index.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react" -import { reducer, initialState } from "./reducer" - -import Poll from "./Poll" - -export const AppData = React.createContext({ - state: initialState, - dispatch: () => null -}) - -export const AppDataProvider = ({ children }) => { - - const [data, dispatchData] = React.useReducer(reducer, initialState) - - const [games, gamesFetching ] = Poll('/api/gamestate' , 30, data.offlineMode) - const [leaderboard, leaderboardFetching ] = Poll('/api/leaderboard', 60, data.offlineMode) - const [user] = Poll('/api/user') // once - - data.games = games - data.gamesFetching = gamesFetching - - data.leaderboard = leaderboard - data.leaderboardFetching = leaderboardFetching - - data.isCurrentUser = (otherUsername) => { - return user?.username && ciEquals(user.username, otherUsername) ? true : null - } - - return ( - - {children} - - ) -} - -function ciEquals(a, b) { - return typeof a === 'string' && typeof b === 'string' - ? a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0 - : a === b; -} \ No newline at end of file diff --git a/webapp/src/context/data/reducer.js b/webapp/src/context/data/reducer.js deleted file mode 100644 index 6e9e065..0000000 --- a/webapp/src/context/data/reducer.js +++ /dev/null @@ -1,25 +0,0 @@ -export const reducer = (state, action) => { - switch (action.type) { - - case "toggleOfflineMode": - return { ...state, - offlineMode: !state.offlineMode // on/off - } - - default: - console.warn("Unknown action.type", action) - return state - } -} - -export const initialState = { - games: null, - gamesFetching: false, - - leaderboard: null, - leaderboardFetching: false, - - isCurrentUser: () => null, - - offlineMode: false -} diff --git a/webapp/src/context/app/index.jsx b/webapp/src/context/games.jsx similarity index 55% rename from webapp/src/context/app/index.jsx rename to webapp/src/context/games.jsx index f4be27c..d747333 100644 --- a/webapp/src/context/app/index.jsx +++ b/webapp/src/context/games.jsx @@ -1,17 +1,17 @@ import React from "react" import { reducer, initialState } from "./reducer" -export const AppContext = React.createContext({ +export const Games = React.createContext({ state: initialState, dispatch: () => null }) -export const AppContextProvider = ({ children }) => { +export const GamesProvider = ({ children }) => { const [state, dispatch] = React.useReducer(reducer, initialState) return ( - + { children } - + ) } diff --git a/webapp/src/index.js b/webapp/src/index.js index d65c14e..71bb868 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -3,18 +3,11 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import reportWebVitals from './reportWebVitals'; import App from './App'; -import { AppDataProvider } from "./context/data" -import { AppContextProvider } from "./context/app" const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - - - - - - + ); diff --git a/webapp/src/reducer/leaderboard.js b/webapp/src/reducer/leaderboard.js deleted file mode 100644 index 06352a4..0000000 --- a/webapp/src/reducer/leaderboard.js +++ /dev/null @@ -1,21 +0,0 @@ -import { useReducer } from 'react'; -import { nextState } from '../util/StateHelper'; - -export const leaderboardInitialState = { - table: null, -}; - -export function leaderboardReducer(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(leaderboardReducer, leaderboardInitialState); -} \ No newline at end of file diff --git a/webapp/src/reducer/user.js b/webapp/src/reducer/user.js index 09b8f86..151c220 100644 --- a/webapp/src/reducer/user.js +++ b/webapp/src/reducer/user.js @@ -1,6 +1,6 @@ import { useReducer } from 'react'; import { localeCompare } from '../util/Locale'; -import { nextState } from '../util/StateHelper'; +//import { nextState } from '../util/StateHelper'; export const userInitialState = { username: '', @@ -13,8 +13,13 @@ export const userInitialState = { export function userReducer(state, action) { switch (action.type) { - case 'next': - return nextState(state, action); + case 'parse': + const apiData = parse(action.json); + + return { + ...state, + ...apiData + }; default: throw Error('UserReducer: unknown action.type', action.type); @@ -23,4 +28,11 @@ export function userReducer(state, action) { export default function useUserReducer() { return useReducer(userReducer, userInitialState); +} + +function parse(json) { + console.log("userreducer.parse", json); + return { + username: json.username + } } \ No newline at end of file diff --git a/webapp/src/util/Polling.js b/webapp/src/util/Polling.js index 49a1f9a..4446656 100644 --- a/webapp/src/util/Polling.js +++ b/webapp/src/util/Polling.js @@ -3,51 +3,53 @@ import { useState, useCallback, useEffect, } from "react" /* - uri: string - mode: - - null - default, return cashed value while polling fresh one from server) + - null - default, fetch data ONCE - interval_sec - interval_stop */ -export default function usePolling(url, mode) { - const [cache, setCache] = useState(null); - const [isFetching, setFetching] = useState(false); - const [delayID, setDelayID] = useState(null); +export default function usePolling(uri, onResponce, mode = null) { + const [initialPoll, setInitialPoll] = useState(true); + const [isPolling, setPolling] = useState(false); + const [intervalTimer, setIntervalTimer] = useState(null); - const fetchData = useCallback(() => { - setDelayID(null); - setFetching(true); + const pollData = useCallback(() => { + setPolling(true); + setInitialPoll(false); - fetch(url) - .then((response) => response.json()) - .then((freshData) => { - setCache(freshData); - setFetching(false); + fetch(uri) + .then((responce) => { + setPolling(false); + + if (typeof mode?.interval_sec === 'number') { + console.log("Schedule", uri, "fetch in", mode.interval_sec, "sec"); + const intervalTimer = setTimeout(pollData, mode.interval_sec * 1000); + setIntervalTimer(intervalTimer); + } + + return responce.json(); + }) + .then((json) => { + onResponce(json); }) .catch((err) => { console.warn(err.message); - setFetching(false); }) - }, [url]) + }, [uri, mode, onResponce]); useEffect(() => { - if (cache === null && isFetching === false) { - fetchData(); + if ((initialPoll || (typeof mode?.interval_sec === 'number' && intervalTimer === null)) && !isPolling) { + pollData(); } + }, [initialPoll, mode, intervalTimer, isPolling, pollData]); - if (mode?.interval_sec && delayID === null) { - const timeoutID = setTimeout(fetchData, mode.interval_sec * 1000) - setDelayID(timeoutID) - console.log("Fetch '" + url + "' scheduled in " + mode.interval_sec + " sec") - } - else if (mode?.interval_stop) { - clearTimeout(delayID); // cancel already scheduled fetch - setDelayID(null); - } + if (mode?.interval_stop && intervalTimer) { + console.log("Cancel scheduled fetch for", uri); + clearTimeout(intervalTimer); + setIntervalTimer(null); + setInitialPoll(true); + } - }, [url, mode, isFetching, cache, fetchData, delayID]); - return [ - cache, // API responce - isFetching // true / false - ] -} + return isPolling; +} \ No newline at end of file