leaderboard api polling

This commit is contained in:
djmil 2023-11-03 22:25:56 +01:00
parent ba7f9ce7d1
commit 472f5de928
5 changed files with 104 additions and 23 deletions

View File

@ -8,10 +8,15 @@ import Game from "./components/Game"
import About from "./components/About" import About from "./components/About"
import Polling from './reducer/polling'; import Polling from './reducer/polling';
import { LeaderboardApi } from './api/leaderboard';
// import { UserApi } from './api/user';
function App() { function App() {
const [polling, dispatchPolling] = useReducer(Polling.reducer, Polling.initialState) const [polling, dispatchPolling] = useReducer(Polling.reducer, Polling.initialState)
const leaderboard = LeaderboardApi(polling, dispatchPolling).get();
// const user = UserApi(polling, dispatchPolling).get();
return <div className="App"> return <div className="App">
<BrowserRouter> <BrowserRouter>
<Header polling={polling} dispatchPolling={dispatchPolling} /> <Header polling={polling} dispatchPolling={dispatchPolling} />
@ -22,7 +27,7 @@ function App() {
<Route path="/game/proposal" element={<Game />} /> <Route path="/game/proposal" element={<Game />} />
<Route path="/game/active" element={<Game />} /> <Route path="/game/active" element={<Game />} />
<Route path="/game/archive" element={<Game />} /> <Route path="/game/archive" element={<Game />} />
<Route path="/leaderboard" element={<Leaderboard />} /> <Route path="/leaderboard" element={<Leaderboard leaderboard={leaderboard} />} />
<Route path="/about" element={<About />} /> <Route path="/about" element={<About />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>

View File

@ -0,0 +1,24 @@
import usePolling from "../util/Polling"
const uri = '/api/leaderboard';
export function LeaderboardApi(polling, dispatchPolling) {
const useGet = () => {
const mode = (polling.enabled === true)
? { interval_sec: 300 } // update leaderbord stats every 5 min
: { interval_stop: true } // user has fliped OfflineToggel
const [leaderboard, isFetching] = usePolling(uri, mode);
if (polling.leaderboard !== isFetching) {
dispatchPolling({ type: 'setLeaderboard', value: isFetching });
}
return leaderboard;
}
return {
get: useGet
}
}

View File

@ -1,31 +1,30 @@
import React from "react" import React from "react"
import './index.css'; import './index.css';
import { AppData } from "../../context/data"
export default function Leaderboard() { export default function Leaderboard({ leaderboard }) {
const [data] = React.useContext(AppData)
if (data.leaderboard == null) if (leaderboard == null)
return <p>Loading...</p> return <p>Loading...</p>
// var listItems = Object.keys(data).map(playerName => { // var listItems = Object.keys(data).map(playerName => {
// var rank = data[playerName]; // var rank = data[playerName];
// //
// return <li key={playerName}> // return <li key={playerName}>
// {playerName}: played {rank.gamesPlayed}, won {rank.gamesWon}, draw {rank.gamesDraw} // {playerName}: played {rank.gamesPlayed}, won {rank.gamesWon}, draw {rank.gamesDraw}
// </li> // </li>
// }); // });
// return <ul>{listItems}</ul>; // return <ul>{listItems}</ul>;
const tableRows = Object.keys(data.leaderboard).map(playerName => { const tableRows = Object.keys(leaderboard).map(playerName => {
var rank = data.leaderboard[playerName]; var rank = leaderboard[playerName];
return <tr key={playerName} className={data.isCurrentUser(playerName) && 'username'}> // TODO tr: className={data.isCurrentUser(playerName) && 'username'}
return <tr key={playerName} >
<td>{playerName}</td> <td>{playerName}</td>
<td>{rank.gamesPlayed}</td> <td>{rank.gamesPlayed}</td>
<td>{rank.gamesWon}</td> <td>{rank.gamesWon}</td>
<td>{rank.gamesDraw}</td> <td>{rank.gamesDraw}</td>
</tr> </tr>
}); });
return <div className="Leaderboard"> return <div className="Leaderboard">
@ -39,8 +38,8 @@ export default function Leaderboard() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ tableRows } {tableRows}
</tbody> </tbody>
</table> </table>
</div> </div>
}; };

View File

@ -1,4 +1,4 @@
import { useLocalStorage } from './util/PersistentStorage' import { useLocalStorage } from '../util/PersistentStorage'
const Persistent = (() => { const Persistent = (() => {
const [getEnabled, setEnabled] = useLocalStorage('polling.enabled', true); const [getEnabled, setEnabled] = useLocalStorage('polling.enabled', true);
@ -26,18 +26,18 @@ export function pollingReducer(state, action) {
enabled: Persistent.setEnabled(!state.enabled) enabled: Persistent.setEnabled(!state.enabled)
}; };
case 'games': return { case 'setGames': return {
...state, ...state,
games: action.value games: action.value
}; };
case 'leaderboard': return { case 'setLeaderboard': return {
...state, ...state,
leaderboard: action.value leaderboard: action.value
}; };
default: default:
throw Error('Unknown action:' + action.type); throw Error('Unknown action.type:' + action.type);
} }
} }

View File

@ -0,0 +1,53 @@
import { useState, useCallback, useEffect, } from "react"
/*
- uri: string
- mode:
- null - default, return cashed value while polling fresh one from server)
- 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);
const fetchData = useCallback(() => {
setDelayID(null);
setFetching(true);
fetch(url)
.then((response) => response.json())
.then((freshData) => {
setCache(freshData);
setFetching(false);
})
.catch((err) => {
console.warn(err.message);
setFetching(false);
})
}, [url])
useEffect(() => {
if (cache === null && isFetching === false) {
fetchData();
}
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);
}
}, [url, mode, isFetching, cache, fetchData, delayID]);
return [
cache, // API response
isFetching // true / false
]
}