Better useXxxApi #34
@ -9,7 +9,9 @@ import About from "./components/About"
|
||||
import Games from './container/Games';
|
||||
import Leaderboard from './container/Leaderboard';
|
||||
|
||||
import usePollingReducer from './reducer/polling';
|
||||
import useConfigReducer from './reducer/config';
|
||||
import useUserReducer from './reducer/user';
|
||||
import useLeaderboardReducer from './reducer/leaderboard';
|
||||
import useGamesReducer from './reducer/games';
|
||||
|
||||
import useUserApi from './api/user';
|
||||
@ -17,35 +19,40 @@ import useLeaderboardApi from './api/leaderboard';
|
||||
import useGamesApi from './api/games';
|
||||
|
||||
export default function App() {
|
||||
const pollingReducer = usePollingReducer();
|
||||
const [config, dispatcConfig] = useConfigReducer();
|
||||
|
||||
const leaderboard = useLeaderboardApi().poll(pollingReducer);
|
||||
const user = useUserApi().get();
|
||||
|
||||
const [games, dispatchGames] = useGamesReducer();
|
||||
const gamesApi = useGamesApi(dispatchGames);
|
||||
gamesApi.list(pollingReducer);
|
||||
const user = useUserApi(useUserReducer()).getUser();
|
||||
const leaderboard = useLeaderboardApi(useLeaderboardReducer(), config).pollTable();
|
||||
|
||||
const players = {
|
||||
leaderboard,
|
||||
isCurrentUser: (playerName) => user?.isCurrentUser(playerName) === true ? true : null
|
||||
};
|
||||
|
||||
const gamesReducer = useGamesReducer();
|
||||
const gamesApi = useGamesApi(gamesReducer, config);
|
||||
const games = gamesApi.pollGamesList();
|
||||
|
||||
const isPolling = {
|
||||
games: games.isPollingGamesList,
|
||||
leaderboard: leaderboard.isPollingTable
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Header pollingReducer={pollingReducer} />
|
||||
<Header configReducer={[config, dispatcConfig]} isPolling={isPolling}/>
|
||||
<Routes>
|
||||
<Route path='/' element={<About />} />
|
||||
<Route path='/about' element={<About />} />
|
||||
<Route path='/games/*' element={<Games context={{ games, dispatchGames, gamesApi }} players={players} />} />
|
||||
<Route path='/games/*' element={<Games context={{ gamesReducer, gamesApi }} players={players} />} />
|
||||
<Route path='/leaderboard' element={<Leaderboard players={players} />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
function Header({ pollingReducer }) {
|
||||
const [polling, dispatchPolling] = pollingReducer;
|
||||
function Header({ configReducer, isPolling }) {
|
||||
const [config, dispatcConfig] = configReducer;
|
||||
|
||||
return (
|
||||
<div className='Header'>
|
||||
@ -54,8 +61,8 @@ function Header({ pollingReducer }) {
|
||||
</h1>
|
||||
|
||||
<OnlineToggle
|
||||
isOnline={polling.enabled}
|
||||
onClick={() => dispatchPolling({ type: 'toggleOnOff' })}
|
||||
isOnline={config.online}
|
||||
onClick={() => dispatcConfig({ type: 'toggleOnline' })}
|
||||
/>
|
||||
|
||||
<nav>
|
||||
@ -64,11 +71,11 @@ function Header({ pollingReducer }) {
|
||||
</NavLink>
|
||||
|
||||
<NavLink to='/games'>
|
||||
<Wobler text="Games" dance={polling.games} />
|
||||
<Wobler text="Games" dance={isPolling.games} />
|
||||
</NavLink>
|
||||
|
||||
<NavLink to='/leaderboard'>
|
||||
<Wobler text='Leaderboard' dance={polling.leaderboard} />
|
||||
<Wobler text='Leaderboard' dance={isPolling.leaderboard} />
|
||||
</NavLink>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -1,26 +1,29 @@
|
||||
import usePolling from '../hook/Polling';
|
||||
|
||||
export default function useGamesApi(dispatchGames) {
|
||||
export default function useGamesApi(gamesReducer, config) {
|
||||
const [games, dispatchGames] = gamesReducer;
|
||||
|
||||
const useList = (pollingReducer) => {
|
||||
const [polling, dispatchPolling] = pollingReducer;
|
||||
|
||||
const onResponce = (json) => {
|
||||
dispatchGames({ type: 'next', list: json });
|
||||
const usePollingGamesList = () => {
|
||||
const onSuccess = (gamesList) => {
|
||||
dispatchGames({ type: 'next', gamesList });
|
||||
}
|
||||
|
||||
const mode = (polling.enabled === true)
|
||||
? { interval_sec: 30 } // fetch gamesList every half a minue
|
||||
: { interval_stop: true } // user has fliped OfflineToggel
|
||||
const isPollingGamesList = usePolling('/api/games', onSuccess, config.intervalMode(30));
|
||||
if (games.isPollingGamesList !== isPollingGamesList) {
|
||||
dispatchGames({ type: 'next', isPollingGamesList });
|
||||
}
|
||||
|
||||
const isPolling = usePolling('/api/games', onResponce, mode);
|
||||
return games;
|
||||
}
|
||||
|
||||
if (isPolling !== polling.games) {
|
||||
dispatchPolling({ type: 'next', games: isPolling });
|
||||
const usePushNewGame = () => {
|
||||
const onSuccess = (game) => {
|
||||
dispatchGames({ type: 'next', game });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
list: useList
|
||||
pollGamesList: usePollingGamesList,
|
||||
pushNewGame: usePushNewGame
|
||||
}
|
||||
}
|
@ -1,26 +1,22 @@
|
||||
import { useState } from "react";
|
||||
import usePolling from "../hook/Polling";
|
||||
|
||||
export default function useLeaderboardApi() {
|
||||
const [leaderboard, setLeaderboard] = useState(null);
|
||||
export default function useLeaderboardApi(leaderboardReducer, config) {
|
||||
const [leaderboard, dispatchLeaderboard] = leaderboardReducer;
|
||||
|
||||
const usePoll = (pollingReducer) => {
|
||||
const [polling, dispatchPolling] = pollingReducer;
|
||||
const usePollingTable = () => {
|
||||
const onSuccess = (table) => {
|
||||
dispatchLeaderboard({ type: 'next', table });
|
||||
}
|
||||
|
||||
const mode = (polling.enabled === true)
|
||||
? { interval_sec: 300 } // update leaderbord stats every 5 min
|
||||
: { interval_stop: true } // user has fliped OfflineToggel
|
||||
|
||||
const isPolling = usePolling('/api/leaderboard', setLeaderboard, mode);
|
||||
|
||||
if (isPolling !== polling.leaderboard) {
|
||||
dispatchPolling({ type: 'next', leaderboard: isPolling });
|
||||
const isPollingTable = usePolling('/api/leaderboard', onSuccess, config.intervalMode(300));
|
||||
if (leaderboard.isPollingTable !== isPollingTable) {
|
||||
dispatchLeaderboard({ type: 'next', isPollingTable });
|
||||
}
|
||||
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
return {
|
||||
poll: usePoll
|
||||
pollTable: usePollingTable
|
||||
}
|
||||
}
|
@ -1,20 +1,19 @@
|
||||
import usePolling from "../hook/Polling";
|
||||
import useUserReducer from "../reducer/user";
|
||||
|
||||
export default function useUserApi() {
|
||||
const [user, dispatchUser] = useUserReducer();
|
||||
export default function useUserApi(userReducer) {
|
||||
const [user, dispatchUser] = userReducer;
|
||||
|
||||
const useGet = () => {
|
||||
const onResponce = (json) => {
|
||||
dispatchUser({ type: "parse", json });
|
||||
const useGetUser = () => {
|
||||
const onSuccess = (userJson) => {
|
||||
dispatchUser({ type: "parse", userJson });
|
||||
}
|
||||
|
||||
usePolling('/api/user', onResponce); // <<-- fetch once
|
||||
usePolling('/api/user', onSuccess); // <<-- fetch once
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
return {
|
||||
get: useGet
|
||||
getUser: useGetUser
|
||||
}
|
||||
}
|
@ -19,7 +19,8 @@ import GameBoard from './games/GameBoard';
|
||||
|
||||
import './Games.css';
|
||||
|
||||
export default function Games({ context: { games, dispatchGames, gamesApi }, players }) {
|
||||
export default function Games({ context: { gamesReducer, gamesApi }, players }) {
|
||||
const [games, dispatchGames] = gamesReducer;
|
||||
|
||||
return (
|
||||
<GamesContext.Provider value={games} >
|
||||
|
@ -4,13 +4,12 @@ import Loading from '../components/Loading';
|
||||
|
||||
export default function Leaderboard({ players }) {
|
||||
|
||||
const leaderboard = players.leaderboard;
|
||||
|
||||
if (leaderboard == null)
|
||||
const table = players.leaderboard.table;
|
||||
if (table == null)
|
||||
return <Loading />
|
||||
|
||||
const tableRows = Object.keys(leaderboard).map(playerName => {
|
||||
var rank = leaderboard[playerName];
|
||||
const tableRows = Object.keys(table).map(playerName => {
|
||||
var rank = table[playerName];
|
||||
|
||||
return <tr key={playerName} className={players.isCurrentUser(playerName) && 'currentuser'}>
|
||||
<td>{playerName}</td>
|
||||
|
@ -4,13 +4,13 @@ import Wobler from '../../../components/Wobler';
|
||||
|
||||
|
||||
|
||||
export default function Create({ isCurrentUser }) {
|
||||
export default function Create({ isCurrentUser, onClick }) {
|
||||
const newGameCtx = useContext(GamesContext).newGame;
|
||||
|
||||
const hasPlayers = checkPlayers(newGameCtx);
|
||||
const hasCurrentUser = checkCurrentUser(newGameCtx, isCurrentUser);
|
||||
|
||||
const pushNewGame = () => {
|
||||
const prepareRequest = () => {
|
||||
if (!hasPlayers)
|
||||
return alert("Black and White players must be selected for the game");
|
||||
|
||||
@ -20,6 +20,7 @@ export default function Create({ isCurrentUser }) {
|
||||
if (newGameCtx.pushing)
|
||||
return; // current request is still being processed
|
||||
|
||||
//onClick
|
||||
console.log("send request");
|
||||
}
|
||||
|
||||
@ -27,7 +28,7 @@ export default function Create({ isCurrentUser }) {
|
||||
<button className={'Create'
|
||||
+ (hasPlayers && hasCurrentUser ? ' ready' : '')
|
||||
}
|
||||
onClick={pushNewGame}
|
||||
onClick={prepareRequest}
|
||||
>
|
||||
<Wobler text="Create" dance={newGameCtx.pushing} />
|
||||
</button>
|
||||
|
@ -7,14 +7,14 @@ import Loading from '../../../components/Loading';
|
||||
|
||||
export default function GameSelector({ yours, opponents, onClick }) {
|
||||
|
||||
const games = useContext(GamesContext);
|
||||
if (games.list === null)
|
||||
const gamesList = useContext(GamesContext).gamesList;
|
||||
if (gamesList === null)
|
||||
return <Loading />
|
||||
|
||||
const yoursList = games.list.filter(game => game.status === yours)
|
||||
const yoursList = gamesList.filter(game => game.status === yours)
|
||||
.map(game => <Selectable game={game} key={game.uuid} onClick={onClick} />)
|
||||
|
||||
const opponentsList = games.list.filter(game => game.status === opponents)
|
||||
const opponentsList = gamesList.filter(game => game.status === opponents)
|
||||
.map(game => <Selectable game={game} key={game.uuid} onClick={onClick} />)
|
||||
|
||||
return (
|
||||
|
@ -11,9 +11,9 @@ export default function NewGame({ players, onSelectPlayer }) {
|
||||
/*
|
||||
* Name options
|
||||
*/
|
||||
const nameOptions = !players.leaderboard
|
||||
const nameOptions = !players.leaderboard.table
|
||||
? [<option key='loading' value='…'>…loading</option>]
|
||||
: Object.keys(players.leaderboard).map(playerName =>
|
||||
: Object.keys(players.leaderboard.table).map(playerName =>
|
||||
<option key={playerName} value={playerName}>
|
||||
{players.isCurrentUser(playerName) ? 'You' : playerName}
|
||||
</option>)
|
||||
|
44
webapp/src/reducer/config.js
Normal file
44
webapp/src/reducer/config.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { useReducer } from 'react';
|
||||
import { namedLocalStorage } from '../util/PersistentStorage';
|
||||
import { nextState } from '../util/StateHelper';
|
||||
|
||||
const Persistent = (() => {
|
||||
const [getOnline, setOnline] = namedLocalStorage('config.online', true);
|
||||
|
||||
return {
|
||||
getOnline,
|
||||
setOnline
|
||||
}
|
||||
})(); // <<--- Execute
|
||||
|
||||
const initialState = {
|
||||
online: Persistent.getOnline() === 'true',
|
||||
|
||||
intervalMode
|
||||
};
|
||||
|
||||
function dispatch(state, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case 'toggleOnline': return {
|
||||
...state,
|
||||
online: Persistent.setOnline(!state.online)
|
||||
};
|
||||
|
||||
case 'next':
|
||||
return nextState(state, action);
|
||||
|
||||
default:
|
||||
throw Error('ConfigReducer: unknown action.type', action.type);
|
||||
}
|
||||
}
|
||||
|
||||
export default function useConfigReducer() {
|
||||
return useReducer(dispatch, initialState);
|
||||
}
|
||||
|
||||
function intervalMode(interval_sec) {
|
||||
return (this.online === true)
|
||||
? { interval_sec } // fetch from API every interval_sec
|
||||
: { interval_stop: true } // user has fliped OfflineToggel
|
||||
}
|
@ -1,16 +1,20 @@
|
||||
import { useReducer } from 'react';
|
||||
import { nextState } from '../util/StateHelper';
|
||||
|
||||
export const gamesInitialState = {
|
||||
list: null,
|
||||
const initialState = {
|
||||
gamesList: null,
|
||||
|
||||
newGame: {
|
||||
whitePlayer: '',
|
||||
blackPlayer: ''
|
||||
}
|
||||
},
|
||||
|
||||
// Network
|
||||
isPollingGamesList: false,
|
||||
isPushingNewGame: false,
|
||||
};
|
||||
|
||||
export function gamesReducer(state, action) {
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case 'next':
|
||||
@ -22,5 +26,5 @@ export function gamesReducer(state, action) {
|
||||
}
|
||||
|
||||
export default function useGamesReducer() {
|
||||
return useReducer(gamesReducer, gamesInitialState);
|
||||
return useReducer(reducer, initialState);
|
||||
}
|
24
webapp/src/reducer/leaderboard.js
Normal file
24
webapp/src/reducer/leaderboard.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { useReducer } from 'react';
|
||||
import { nextState } from '../util/StateHelper';
|
||||
|
||||
const initialState = {
|
||||
table: null,
|
||||
|
||||
// Network
|
||||
isPollingTable: false
|
||||
};
|
||||
|
||||
function reducer(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(reducer, initialState);
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import { useReducer } from 'react';
|
||||
import { namedLocalStorage } from '../util/PersistentStorage';
|
||||
import { nextState } from '../util/StateHelper';
|
||||
|
||||
const Persistent = (() => {
|
||||
const [getEnabled, setEnabled] = namedLocalStorage('polling.enabled', true);
|
||||
|
||||
return {
|
||||
getEnabled,
|
||||
setEnabled
|
||||
}
|
||||
})(); // <<--- Execute
|
||||
|
||||
export const pollingInitialState = {
|
||||
enabled: Persistent.getEnabled() === 'true',
|
||||
|
||||
games: false,
|
||||
leaderboard: false
|
||||
};
|
||||
|
||||
export function pollingReducer(curntState, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case 'toggleOnOff': return {
|
||||
...curntState,
|
||||
enabled: Persistent.setEnabled(!curntState.enabled)
|
||||
};
|
||||
|
||||
case 'next':
|
||||
return nextState(curntState, action);
|
||||
|
||||
default:
|
||||
throw Error('Unknown action.type:' + action.type);
|
||||
}
|
||||
}
|
||||
|
||||
export default function usePollingReducer() {
|
||||
return useReducer(pollingReducer, pollingInitialState);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { useReducer } from 'react';
|
||||
import { localeCompare } from '../util/Locale';
|
||||
|
||||
export const userInitialState = {
|
||||
const initialState = {
|
||||
username: '',
|
||||
|
||||
isCurrentUser: function (otherUsername) {
|
||||
@ -9,13 +9,13 @@ export const userInitialState = {
|
||||
}
|
||||
};
|
||||
|
||||
export function userReducer(state, action) {
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case 'parse':
|
||||
return {
|
||||
...state,
|
||||
username: action.json.username
|
||||
username: action.userJson.username
|
||||
};
|
||||
|
||||
default:
|
||||
@ -24,5 +24,5 @@ export function userReducer(state, action) {
|
||||
}
|
||||
|
||||
export default function useUserReducer() {
|
||||
return useReducer(userReducer, userInitialState);
|
||||
return useReducer(reducer, initialState);
|
||||
}
|
Loading…
Reference in New Issue
Block a user