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
This commit is contained in:
parent
fa29d2a631
commit
efd7127575
@ -16,3 +16,7 @@
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
.Container {
|
||||
margin-top: 25px;
|
||||
}
|
@ -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 <div className="App">
|
||||
<BrowserRouter>
|
||||
<Header/>
|
||||
<div className="Container">
|
||||
<Routes>
|
||||
<Route path="/leaderboard" element={<Leaderboard/>} />
|
||||
<Route path="/gameproposal" element={<GameProposal games={games}/>} />
|
||||
</Routes>
|
||||
</div>
|
||||
<Header/>
|
||||
<DataPolling/>
|
||||
<div className="Container">
|
||||
<Routes>
|
||||
<Route path="/leaderboard" element={<Leaderboard/>} />
|
||||
<Route path="/gameproposal" element={<GameProposal/>} />
|
||||
</Routes>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import './Header.css';
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<p>
|
||||
<h1>CordaCheckers</h1>
|
||||
<nav>
|
||||
<Link to="/leaderboard">Leaderboard</Link> {"| "}
|
||||
<Link to="/gameproposal">Game Proposal</Link> {"| "}
|
||||
<Link to="/game">Active Games</Link> {"| "}
|
||||
<Link to="/archive">Archive</Link> {"| "}
|
||||
<Link to="about">About</Link>
|
||||
</nav>
|
||||
</p>
|
||||
);
|
||||
}
|
16
webapp/src/components/DataPolling/index.jsx
Normal file
16
webapp/src/components/DataPolling/index.jsx
Normal file
@ -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 <div className={DataPolling.name}>
|
||||
polling
|
||||
<button onClick={() => dispatchAppData({type: "togglePolling"})}>
|
||||
{ appCtx.disablePolling === true ? "off" : "on" }
|
||||
</button>
|
||||
{ appData.fetching }
|
||||
</div>
|
||||
}
|
@ -6,11 +6,11 @@ export function Accept({uuid}) {
|
||||
</button>
|
||||
}
|
||||
|
||||
export function Reject({uuid}) {
|
||||
return <button className="action" id={uuid} type="submit">
|
||||
Reject
|
||||
</button>
|
||||
}
|
||||
// export function Reject({uuid}) {
|
||||
// return <button className="action" id={uuid} type="submit" >
|
||||
// Reject
|
||||
// </button>
|
||||
// }
|
||||
|
||||
export function Cancel({uuid}) {
|
||||
return <button className="action" id={uuid} type="submit">
|
24
webapp/src/components/GameProposal/GameProposalCancel.js
Normal file
24
webapp/src/components/GameProposal/GameProposalCancel.js
Normal file
@ -0,0 +1,24 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
export default function Cancel({uuid}) {
|
||||
const [pending, setPending] = useState(new Map())
|
||||
|
||||
for (const [key, value] of pending)
|
||||
console.log("cancel", key, value);
|
||||
|
||||
function sendRequest(uuid2reject) {
|
||||
const nextPending = new Map(pending)
|
||||
nextPending.set(uuid2reject, null)
|
||||
|
||||
setPending(nextPending)
|
||||
}
|
||||
|
||||
const status = pending.get(uuid)
|
||||
const isPending = status !== undefined
|
||||
|
||||
return <button
|
||||
className={isPending ? "visible" : "action"}
|
||||
onClick={() => sendRequest(uuid) }>
|
||||
Cancel
|
||||
</button>
|
||||
}
|
21
webapp/src/components/GameProposal/Reject.jsx
Normal file
21
webapp/src/components/GameProposal/Reject.jsx
Normal file
@ -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 <button className="action" onClick={() => sendRequest(uuid) }>
|
||||
Reject
|
||||
</button>
|
||||
}
|
@ -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 <p>Loading..</p>
|
||||
export default function GameProposal() {
|
||||
const [data] = React.useContext(AppData)
|
||||
|
||||
// for (const [key, value] of Object.entries(games))
|
||||
if (data.games == null)
|
||||
return <div>Loading..</div>
|
||||
|
||||
// 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 <div class="li" key={game.uuid}>
|
||||
return <div className="li" key={game.uuid}>
|
||||
<p>
|
||||
You {Stone(game.myColor)} <i>vs</i> {game.opponentName} {Stone(oppositeColor(game.myColor))}
|
||||
<br/>
|
||||
@ -30,10 +31,10 @@ const GameProposal = ({games}) => {
|
||||
</div>
|
||||
});
|
||||
|
||||
const WaitForOpponent = games
|
||||
.filter(game => game.status === State.WaitForOpponent)
|
||||
const WaitForOpponent = data.games
|
||||
.filter(game => game.status === Status.WaitForOpponent)
|
||||
.map(game => {
|
||||
return <div class="li" key={game.uuid}>
|
||||
return <div className="li" key={game.uuid}>
|
||||
<p>
|
||||
You {Stone(game.myColor)} <i>vs</i> {game.opponentName} {Stone(oppositeColor(game.myColor))}
|
||||
<br/>
|
||||
@ -47,7 +48,7 @@ const GameProposal = ({games}) => {
|
||||
return <div className="GameProposal">
|
||||
{waitForYou}
|
||||
{WaitForOpponent.length > 0 &&
|
||||
<div class="separator">
|
||||
<div className="separator">
|
||||
waiting for opponent ({WaitForOpponent.length})
|
||||
</div>
|
||||
}
|
||||
@ -55,14 +56,21 @@ const GameProposal = ({games}) => {
|
||||
</div>
|
||||
};
|
||||
|
||||
|
||||
|
||||
const Status = {
|
||||
WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT",
|
||||
WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU",
|
||||
}
|
||||
|
||||
function Stone(color) {
|
||||
if (color === "WHITE")
|
||||
return <span class="stone">⛀</span>
|
||||
return <span className="stone">⛀</span>
|
||||
|
||||
if (color === "BLACK")
|
||||
return <span class="stone">⛂</span>
|
||||
return <span className="stone">⛂</span>
|
||||
|
||||
return <span class="stone">{color}</span>
|
||||
return <span className="stone">{color}</span>
|
||||
}
|
||||
|
||||
function oppositeColor(color) {
|
||||
@ -74,5 +82,3 @@ function oppositeColor(color) {
|
||||
|
||||
return color
|
||||
}
|
||||
|
||||
export default GameProposal;
|
17
webapp/src/components/Header/index.jsx
Normal file
17
webapp/src/components/Header/index.jsx
Normal file
@ -0,0 +1,17 @@
|
||||
import './index.css';
|
||||
import React from "react"
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function Header() {
|
||||
|
||||
return <div>
|
||||
<h1>CordaCheckers</h1>
|
||||
<nav>
|
||||
<Link to="/leaderboard">Leaderboard</Link> {"| "}
|
||||
<Link to="/gameproposal">Game Proposal</Link> {"| "}
|
||||
<Link to="/game">Active Games</Link> {"| "}
|
||||
<Link to="/archive">Archive</Link> {"| "}
|
||||
<Link to="about">About</Link>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
@ -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 <p>Loading...</p>
|
||||
|
||||
// var listItems = Object.keys(data).map(playerName => {
|
||||
@ -28,8 +17,8 @@ const Leaderboard = () => {
|
||||
// });
|
||||
// return <ul>{listItems}</ul>;
|
||||
|
||||
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 <tr key={playerName}>
|
||||
<td>{playerName}</td>
|
||||
@ -55,5 +44,3 @@ const Leaderboard = () => {
|
||||
</table>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default Leaderboard;
|
17
webapp/src/context/app/index.jsx
Normal file
17
webapp/src/context/app/index.jsx
Normal file
@ -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 (
|
||||
<AppContext.Provider value={[ state, dispatch ]}>
|
||||
{ children }
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
16
webapp/src/context/app/reducer.js
Normal file
16
webapp/src/context/app/reducer.js
Normal file
@ -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,
|
||||
}
|
40
webapp/src/context/data/Poll.js
Normal file
40
webapp/src/context/data/Poll.js
Normal file
@ -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
|
||||
}
|
||||
}
|
36
webapp/src/context/data/index.jsx
Normal file
36
webapp/src/context/data/index.jsx
Normal file
@ -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 (
|
||||
<AppData.Provider value={[data, dispatchData]}>
|
||||
{children}
|
||||
</AppData.Provider>
|
||||
)
|
||||
}
|
14
webapp/src/context/data/reducer.js
Normal file
14
webapp/src/context/data/reducer.js
Normal file
@ -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: []
|
||||
}
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
|
||||
//<React.StrictMode>
|
||||
<AppContextProvider>
|
||||
<AppDataProvider>
|
||||
<App />
|
||||
</AppDataProvider>
|
||||
</AppContextProvider>
|
||||
//</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
Loading…
Reference in New Issue
Block a user