From efd7127575cf6436bd78cf36fed1ab008a22557c Mon Sep 17 00:00:00 2001 From: djmil Date: Wed, 18 Oct 2023 17:20:37 +0200 Subject: [PATCH] react: GlobalState: data, appCtx + polling feature - Polling - time beased URI polling - can be diabled - GlobaState - utilizes Flux pattern - data: for storing data for UI to work with - appData: for string global UI state - major refactoring --- webapp/src/App.css | 4 ++ webapp/src/App.js | 50 +++++++------------ webapp/src/Header.js | 17 ------- webapp/src/components/DataPolling/index.jsx | 16 ++++++ .../GameProposal}/GameProposalAction.js | 10 ++-- .../GameProposal/GameProposalCancel.js | 24 +++++++++ webapp/src/components/GameProposal/Reject.jsx | 21 ++++++++ .../GameProposal/index.css} | 0 .../GameProposal/index.jsx} | 50 +++++++++++-------- .../Header/index.css} | 0 webapp/src/components/Header/index.jsx | 17 +++++++ .../Leaderboard/index.css} | 0 .../Leaderboard/index.jsx} | 29 +++-------- webapp/src/context/app/index.jsx | 17 +++++++ webapp/src/context/app/reducer.js | 16 ++++++ webapp/src/context/data/Poll.js | 40 +++++++++++++++ webapp/src/context/data/index.jsx | 36 +++++++++++++ webapp/src/context/data/reducer.js | 14 ++++++ webapp/src/index.js | 15 ++++-- 19 files changed, 274 insertions(+), 102 deletions(-) delete mode 100644 webapp/src/Header.js create mode 100644 webapp/src/components/DataPolling/index.jsx rename webapp/src/{ => components/GameProposal}/GameProposalAction.js (66%) create mode 100644 webapp/src/components/GameProposal/GameProposalCancel.js create mode 100644 webapp/src/components/GameProposal/Reject.jsx rename webapp/src/{GameProposal.css => components/GameProposal/index.css} (100%) rename webapp/src/{GameProposal.js => components/GameProposal/index.jsx} (57%) rename webapp/src/{Header.css => components/Header/index.css} (100%) create mode 100644 webapp/src/components/Header/index.jsx rename webapp/src/{Leaderboard.css => components/Leaderboard/index.css} (100%) rename webapp/src/{Leaderboard.js => components/Leaderboard/index.jsx} (59%) create mode 100644 webapp/src/context/app/index.jsx create mode 100644 webapp/src/context/app/reducer.js create mode 100644 webapp/src/context/data/Poll.js create mode 100644 webapp/src/context/data/index.jsx create mode 100644 webapp/src/context/data/reducer.js diff --git a/webapp/src/App.css b/webapp/src/App.css index 50b63a3..f09c515 100644 --- a/webapp/src/App.css +++ b/webapp/src/App.css @@ -16,3 +16,7 @@ .App-link { color: #61dafb; } + +.Container { + margin-top: 25px; +} \ No newline at end of file diff --git a/webapp/src/App.js b/webapp/src/App.js index 9fac52a..f796e62 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -1,47 +1,31 @@ import './App.css'; -import React, { useState, useEffect, useCallback } from 'react'; +import React from 'react' import { BrowserRouter, Routes, Route, -} from "react-router-dom"; +} from "react-router-dom" -import Header from "./Header" -import Leaderboard from "./Leaderboard"; -import GameProposal from "./GameProposal"; +import Header from "./components/Header" +import Leaderboard from "./components/Leaderboard" +import GameProposal from "./components/GameProposal" +import DataPolling from './components/DataPolling'; + +//import { UserProvider } from "../contexts/UserProvider" +//import { GameProposalProvider } from './context/GameProposal'; function App() { - const [games, setGames] = useState(null); - const [polling, setPolling] = useState(false); - - const pollGames = useCallback(() => { - if (polling) - return; - - setPolling(true); - fetch('/api/gamestate') - .then(response => response.json()) - .then(data => { - setGames(data); - setPolling(false); - }) - .catch(err => console.log(err.message)); - }, [polling]); - - useEffect(() => { - const timer = setInterval(pollGames(), 35 * 1000); // <<-- poll new gamestates every 35 sec - return clearInterval(timer); - }, [pollGames]) return
-
-
- - } /> - } /> - -
+
+ +
+ + } /> + } /> + +
} diff --git a/webapp/src/Header.js b/webapp/src/Header.js deleted file mode 100644 index f9dc7dc..0000000 --- a/webapp/src/Header.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Link } from "react-router-dom"; -import './Header.css'; - -export default function Header() { - return ( -

-

CordaCheckers

- -

- ); - } \ No newline at end of file diff --git a/webapp/src/components/DataPolling/index.jsx b/webapp/src/components/DataPolling/index.jsx new file mode 100644 index 0000000..4b4a789 --- /dev/null +++ b/webapp/src/components/DataPolling/index.jsx @@ -0,0 +1,16 @@ +import React from "react" +import { AppData } from "../../context/data" +import { AppContext } from "../../context/app" + +export default function DataPolling() { + const [appData] = React.useContext(AppData) + const [appCtx, dispatchAppData] = React.useContext(AppContext) + + return
+ polling + + { appData.fetching } +
+} diff --git a/webapp/src/GameProposalAction.js b/webapp/src/components/GameProposal/GameProposalAction.js similarity index 66% rename from webapp/src/GameProposalAction.js rename to webapp/src/components/GameProposal/GameProposalAction.js index 0fdd4df..0c60782 100644 --- a/webapp/src/GameProposalAction.js +++ b/webapp/src/components/GameProposal/GameProposalAction.js @@ -6,11 +6,11 @@ export function Accept({uuid}) { } -export function Reject({uuid}) { - return -} +// export function Reject({uuid}) { +// return +// } export function Cancel({uuid}) { return +} diff --git a/webapp/src/components/GameProposal/Reject.jsx b/webapp/src/components/GameProposal/Reject.jsx new file mode 100644 index 0000000..6b59e81 --- /dev/null +++ b/webapp/src/components/GameProposal/Reject.jsx @@ -0,0 +1,21 @@ +import React, {useState} from 'react'; + +export default function Reject({uuid}) { + const [pending, setPending] = useState([]) + + for (const [key, value] of Object.entries(pending)) + console.log("pending ", key, value); + + function sendRequest(reject_uuid) { + setPending( // Replace the old array + [ // with a new array consisting of: + ...pending, // - all the old items + { uuid: reject_uuid } // - and a new item at the end + ] + ) + } + + return +} diff --git a/webapp/src/GameProposal.css b/webapp/src/components/GameProposal/index.css similarity index 100% rename from webapp/src/GameProposal.css rename to webapp/src/components/GameProposal/index.css diff --git a/webapp/src/GameProposal.js b/webapp/src/components/GameProposal/index.jsx similarity index 57% rename from webapp/src/GameProposal.js rename to webapp/src/components/GameProposal/index.jsx index 3cd85a6..3e7f25c 100644 --- a/webapp/src/GameProposal.js +++ b/webapp/src/components/GameProposal/index.jsx @@ -1,24 +1,25 @@ +import './index.css'; import React from 'react'; -import {Accept, Reject, Cancel} from './GameProposalAction'; -import './GameProposal.css'; +import {Accept} from './GameProposalAction'; +import Reject from './Reject' +import Cancel from './GameProposalCancel' -const State = { - WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT", - WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU", -} +import { AppData } from "../../context/data" -const GameProposal = ({games}) => { - if (games == null) - return

Loading..

+export default function GameProposal() { + const [data] = React.useContext(AppData) - // for (const [key, value] of Object.entries(games)) + if (data.games == null) + return
Loading..
+ + // for (const [key, value] of Object.entries(data.games)) // console.log(key, value); - const waitForYou = games - .filter(game => game.status === State.WaitForYou) + const waitForYou = data.games + .filter(game => game.status === Status.WaitForYou) .map(game => { - return
+ return

You {Stone(game.myColor)} vs {game.opponentName} {Stone(oppositeColor(game.myColor))}
@@ -30,10 +31,10 @@ const GameProposal = ({games}) => {

}); - const WaitForOpponent = games - .filter(game => game.status === State.WaitForOpponent) + const WaitForOpponent = data.games + .filter(game => game.status === Status.WaitForOpponent) .map(game => { - return
+ return

You {Stone(game.myColor)} vs {game.opponentName} {Stone(oppositeColor(game.myColor))}
@@ -47,7 +48,7 @@ const GameProposal = ({games}) => { return

{waitForYou} {WaitForOpponent.length > 0 && -
+
waiting for opponent ({WaitForOpponent.length})
} @@ -55,14 +56,21 @@ const GameProposal = ({games}) => {
}; + + +const Status = { + WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT", + WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU", +} + function Stone(color) { if (color === "WHITE") - return + return if (color === "BLACK") - return + return - return {color} + return {color} } function oppositeColor(color) { @@ -74,5 +82,3 @@ function oppositeColor(color) { return color } - -export default GameProposal; diff --git a/webapp/src/Header.css b/webapp/src/components/Header/index.css similarity index 100% rename from webapp/src/Header.css rename to webapp/src/components/Header/index.css diff --git a/webapp/src/components/Header/index.jsx b/webapp/src/components/Header/index.jsx new file mode 100644 index 0000000..2528bbb --- /dev/null +++ b/webapp/src/components/Header/index.jsx @@ -0,0 +1,17 @@ +import './index.css'; +import React from "react" +import { Link } from "react-router-dom"; + +export default function Header() { + + return
+

CordaCheckers

+ +
+} \ No newline at end of file diff --git a/webapp/src/Leaderboard.css b/webapp/src/components/Leaderboard/index.css similarity index 100% rename from webapp/src/Leaderboard.css rename to webapp/src/components/Leaderboard/index.css diff --git a/webapp/src/Leaderboard.js b/webapp/src/components/Leaderboard/index.jsx similarity index 59% rename from webapp/src/Leaderboard.js rename to webapp/src/components/Leaderboard/index.jsx index f10c1a1..fe0eaf3 100644 --- a/webapp/src/Leaderboard.js +++ b/webapp/src/components/Leaderboard/index.jsx @@ -1,22 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import './Leaderboard.css'; +import React from "react" +import './index.css'; +import { AppData } from "../../context/data" -const Leaderboard = () => { +export default function Leaderboard() { + const [data] = React.useContext(AppData) - const [data, setData] = useState(null); - - useEffect(() => { - fetch('/api/leaderboard') - .then((response) => response.json()) - .then((data) => { - setData(data); - }) - .catch((err) => { - console.log(err.message); - }); - }, []); - - if (data == null) + if (data.leaderboard == null) return

Loading...

// var listItems = Object.keys(data).map(playerName => { @@ -28,8 +17,8 @@ const Leaderboard = () => { // }); // return
    {listItems}
; - const tableRows = Object.keys(data).map(playerName => { - var rank = data[playerName]; + const tableRows = Object.keys(data.leaderboard).map(playerName => { + var rank = data.leaderboard[playerName]; return {playerName} @@ -55,5 +44,3 @@ const Leaderboard = () => {
}; - -export default Leaderboard; diff --git a/webapp/src/context/app/index.jsx b/webapp/src/context/app/index.jsx new file mode 100644 index 0000000..f4be27c --- /dev/null +++ b/webapp/src/context/app/index.jsx @@ -0,0 +1,17 @@ +import React from "react" +import { reducer, initialState } from "./reducer" + +export const AppContext = React.createContext({ + state: initialState, + dispatch: () => null +}) + +export const AppContextProvider = ({ children }) => { + const [state, dispatch] = React.useReducer(reducer, initialState) + + return ( + + { children } + + ) +} diff --git a/webapp/src/context/app/reducer.js b/webapp/src/context/app/reducer.js new file mode 100644 index 0000000..cc1536a --- /dev/null +++ b/webapp/src/context/app/reducer.js @@ -0,0 +1,16 @@ +export const reducer = (state, action) => { + switch (action.type) { + + case "togglePolling": + return { ...state, + disablePolling: !state.disablePolling // on/off + } + + default: + return state + } +} + +export const initialState = { + disablePolling: false, +} diff --git a/webapp/src/context/data/Poll.js b/webapp/src/context/data/Poll.js new file mode 100644 index 0000000..55b37cf --- /dev/null +++ b/webapp/src/context/data/Poll.js @@ -0,0 +1,40 @@ +import { useState, useCallback, useEffect, } from "react" + +export default function Poll(url, interval_sec, disabled) { + const [cache, setCache] = 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) => setCache(freshData)) + .catch((err) => console.log(err.message)) + }, [url]) + + useEffect(() => { + if (cache == null) { + fecthData() // <<-- run immediatly on startup + } + else if (disabled === true) { + clearTimeout(timeoutID) // cancel already scheduled fetch + setTimeoutID(null) + } + else if (timeoutID === null) { + const timeoutID = setTimeout(fecthData, interval_sec * 1000) + setTimeoutID(timeoutID) + console.log("Fetch '" +url +"' scheduled in " +interval_sec +" sec") + } + }, [url, cache, fecthData, timeoutID, disabled, interval_sec]); + + return { + data: cache, + fetching + } +} diff --git a/webapp/src/context/data/index.jsx b/webapp/src/context/data/index.jsx new file mode 100644 index 0000000..c1dc614 --- /dev/null +++ b/webapp/src/context/data/index.jsx @@ -0,0 +1,36 @@ +import React from "react" +import { reducer, initialState } from "./reducer" +import { AppContext } from "../app" + +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 [appContext] = React.useContext(AppContext) + + const games = Poll('api/gamestate', 30, appContext.disablePolling) + const leaderboard = Poll('api/leaderboard', 60, appContext.disablePolling) + + data.games = games.data + data.leaderboard = leaderboard.data + + var fetching = [] + if (games.fetching === true) + fetching = [...fetching, "games"] + if (leaderboard.fetching === true) + fetching = [...fetching, "leaderboard"] + + data.fetching = fetching + + return ( + + {children} + + ) +} diff --git a/webapp/src/context/data/reducer.js b/webapp/src/context/data/reducer.js new file mode 100644 index 0000000..b6fcb2e --- /dev/null +++ b/webapp/src/context/data/reducer.js @@ -0,0 +1,14 @@ +export const reducer = (state, action) => { + switch (action.type) { + + default: + console.warn("Unknown action.type", action) + return state + } +} + +export const initialState = { + games: null, + leaderboard: null, + fetching: [] +} diff --git a/webapp/src/index.js b/webapp/src/index.js index d563c0f..58da36a 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -1,14 +1,21 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; -import App from './App'; 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( - - - + + // + + + + + + // ); // If you want to start measuring performance in your app, pass a function