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 {
|
.App-link {
|
||||||
color: #61dafb;
|
color: #61dafb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Container {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
@ -1,45 +1,29 @@
|
|||||||
import './App.css';
|
import './App.css';
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
BrowserRouter,
|
BrowserRouter,
|
||||||
Routes,
|
Routes,
|
||||||
Route,
|
Route,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom"
|
||||||
|
|
||||||
import Header from "./Header"
|
import Header from "./components/Header"
|
||||||
import Leaderboard from "./Leaderboard";
|
import Leaderboard from "./components/Leaderboard"
|
||||||
import GameProposal from "./GameProposal";
|
import GameProposal from "./components/GameProposal"
|
||||||
|
import DataPolling from './components/DataPolling';
|
||||||
|
|
||||||
|
//import { UserProvider } from "../contexts/UserProvider"
|
||||||
|
//import { GameProposalProvider } from './context/GameProposal';
|
||||||
|
|
||||||
function App() {
|
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">
|
return <div className="App">
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Header/>
|
<Header/>
|
||||||
|
<DataPolling/>
|
||||||
<div className="Container">
|
<div className="Container">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/leaderboard" element={<Leaderboard/>} />
|
<Route path="/leaderboard" element={<Leaderboard/>} />
|
||||||
<Route path="/gameproposal" element={<GameProposal games={games}/>} />
|
<Route path="/gameproposal" element={<GameProposal/>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
@ -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>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Reject({uuid}) {
|
// export function Reject({uuid}) {
|
||||||
return <button className="action" id={uuid} type="submit">
|
// return <button className="action" id={uuid} type="submit" >
|
||||||
Reject
|
// Reject
|
||||||
</button>
|
// </button>
|
||||||
}
|
// }
|
||||||
|
|
||||||
export function Cancel({uuid}) {
|
export function Cancel({uuid}) {
|
||||||
return <button className="action" id={uuid} type="submit">
|
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 React from 'react';
|
||||||
import {Accept, Reject, Cancel} from './GameProposalAction';
|
import {Accept} from './GameProposalAction';
|
||||||
import './GameProposal.css';
|
import Reject from './Reject'
|
||||||
|
import Cancel from './GameProposalCancel'
|
||||||
|
|
||||||
const State = {
|
import { AppData } from "../../context/data"
|
||||||
WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT",
|
|
||||||
WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU",
|
|
||||||
}
|
|
||||||
|
|
||||||
const GameProposal = ({games}) => {
|
|
||||||
|
|
||||||
if (games == null)
|
export default function GameProposal() {
|
||||||
return <p>Loading..</p>
|
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);
|
// console.log(key, value);
|
||||||
|
|
||||||
const waitForYou = games
|
const waitForYou = data.games
|
||||||
.filter(game => game.status === State.WaitForYou)
|
.filter(game => game.status === Status.WaitForYou)
|
||||||
.map(game => {
|
.map(game => {
|
||||||
return <div class="li" key={game.uuid}>
|
return <div className="li" key={game.uuid}>
|
||||||
<p>
|
<p>
|
||||||
You {Stone(game.myColor)} <i>vs</i> {game.opponentName} {Stone(oppositeColor(game.myColor))}
|
You {Stone(game.myColor)} <i>vs</i> {game.opponentName} {Stone(oppositeColor(game.myColor))}
|
||||||
<br/>
|
<br/>
|
||||||
@ -30,10 +31,10 @@ const GameProposal = ({games}) => {
|
|||||||
</div>
|
</div>
|
||||||
});
|
});
|
||||||
|
|
||||||
const WaitForOpponent = games
|
const WaitForOpponent = data.games
|
||||||
.filter(game => game.status === State.WaitForOpponent)
|
.filter(game => game.status === Status.WaitForOpponent)
|
||||||
.map(game => {
|
.map(game => {
|
||||||
return <div class="li" key={game.uuid}>
|
return <div className="li" key={game.uuid}>
|
||||||
<p>
|
<p>
|
||||||
You {Stone(game.myColor)} <i>vs</i> {game.opponentName} {Stone(oppositeColor(game.myColor))}
|
You {Stone(game.myColor)} <i>vs</i> {game.opponentName} {Stone(oppositeColor(game.myColor))}
|
||||||
<br/>
|
<br/>
|
||||||
@ -47,7 +48,7 @@ const GameProposal = ({games}) => {
|
|||||||
return <div className="GameProposal">
|
return <div className="GameProposal">
|
||||||
{waitForYou}
|
{waitForYou}
|
||||||
{WaitForOpponent.length > 0 &&
|
{WaitForOpponent.length > 0 &&
|
||||||
<div class="separator">
|
<div className="separator">
|
||||||
waiting for opponent ({WaitForOpponent.length})
|
waiting for opponent ({WaitForOpponent.length})
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -55,14 +56,21 @@ const GameProposal = ({games}) => {
|
|||||||
</div>
|
</div>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const Status = {
|
||||||
|
WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT",
|
||||||
|
WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU",
|
||||||
|
}
|
||||||
|
|
||||||
function Stone(color) {
|
function Stone(color) {
|
||||||
if (color === "WHITE")
|
if (color === "WHITE")
|
||||||
return <span class="stone">⛀</span>
|
return <span className="stone">⛀</span>
|
||||||
|
|
||||||
if (color === "BLACK")
|
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) {
|
function oppositeColor(color) {
|
||||||
@ -74,5 +82,3 @@ function oppositeColor(color) {
|
|||||||
|
|
||||||
return 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 React from "react"
|
||||||
import './Leaderboard.css';
|
import './index.css';
|
||||||
|
import { AppData } from "../../context/data"
|
||||||
|
|
||||||
const Leaderboard = () => {
|
export default function Leaderboard() {
|
||||||
|
const [data] = React.useContext(AppData)
|
||||||
|
|
||||||
const [data, setData] = useState(null);
|
if (data.leaderboard == null)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('/api/leaderboard')
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
setData(data);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err.message);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (data == null)
|
|
||||||
return <p>Loading...</p>
|
return <p>Loading...</p>
|
||||||
|
|
||||||
// var listItems = Object.keys(data).map(playerName => {
|
// var listItems = Object.keys(data).map(playerName => {
|
||||||
@ -28,8 +17,8 @@ const Leaderboard = () => {
|
|||||||
// });
|
// });
|
||||||
// return <ul>{listItems}</ul>;
|
// return <ul>{listItems}</ul>;
|
||||||
|
|
||||||
const tableRows = Object.keys(data).map(playerName => {
|
const tableRows = Object.keys(data.leaderboard).map(playerName => {
|
||||||
var rank = data[playerName];
|
var rank = data.leaderboard[playerName];
|
||||||
|
|
||||||
return <tr key={playerName}>
|
return <tr key={playerName}>
|
||||||
<td>{playerName}</td>
|
<td>{playerName}</td>
|
||||||
@ -55,5 +44,3 @@ const Leaderboard = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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 React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
|
||||||
import reportWebVitals from './reportWebVitals';
|
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'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
|
||||||
|
//<React.StrictMode>
|
||||||
|
<AppContextProvider>
|
||||||
|
<AppDataProvider>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</AppDataProvider>
|
||||||
|
</AppContextProvider>
|
||||||
|
//</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
Loading…
Reference in New Issue
Block a user