#24-directory-structure #26
@ -9,40 +9,36 @@ import About from "./components/About"
 | 
			
		||||
import Games from './container/Games';
 | 
			
		||||
import Leaderboard from './container/Leaderboard';
 | 
			
		||||
 | 
			
		||||
import useUserReducer from './reducer/user';
 | 
			
		||||
import usePollingReducer from './reducer/polling';
 | 
			
		||||
import useLeaderboardReducer from './reducer/leaderboard';
 | 
			
		||||
import useGamesReducer from './reducer/games';
 | 
			
		||||
//import useGamesReducer from './reducer/games';
 | 
			
		||||
 | 
			
		||||
import useUserApi from './api/user';
 | 
			
		||||
import useLeaderboardApi from './api/leaderboard';
 | 
			
		||||
import useGamesApi from './api/games';
 | 
			
		||||
//import useGamesApi from './api/games';
 | 
			
		||||
 | 
			
		||||
export default function App() {
 | 
			
		||||
  const userReducer = useUserReducer();
 | 
			
		||||
  const pollingFlux = usePollingReducer();
 | 
			
		||||
  const leaderboardReducer = useLeaderboardReducer();
 | 
			
		||||
  const gamesReducer = useGamesReducer();
 | 
			
		||||
  const pollingReducer = usePollingReducer();
 | 
			
		||||
  //const gamesReducer = useGamesReducer();
 | 
			
		||||
 | 
			
		||||
  const user = useUserApi(userReducer).get();
 | 
			
		||||
  const leaderboard = useLeaderboardApi(leaderboardReducer).poll(pollingFlux);
 | 
			
		||||
  /*const gamesApi = */ useGamesApi(gamesReducer).list(pollingFlux);
 | 
			
		||||
  //const games = useGamesApi(gamesReducer).list(pollingReducer);
 | 
			
		||||
  const leaderboard = useLeaderboardApi().poll(pollingReducer);
 | 
			
		||||
  const user = useUserApi().get();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <BrowserRouter>
 | 
			
		||||
      <Header pollingFlux={pollingFlux} />
 | 
			
		||||
      <Header pollingReducer={pollingReducer} />
 | 
			
		||||
      <Routes>
 | 
			
		||||
        <Route path="/" element={<About />} />
 | 
			
		||||
        <Route path="/about" element={<About />} />
 | 
			
		||||
        <Route path="/games/*" element={<Games />} />
 | 
			
		||||
        <Route path="/leaderboard" element={<Leaderboard leaderboard={leaderboard} user={user} />} />
 | 
			
		||||
        <Route path='/' element={<About />} />
 | 
			
		||||
        <Route path='/about' element={<About />} />
 | 
			
		||||
        <Route path='/games/*' element={<Games />} />
 | 
			
		||||
        <Route path='/leaderboard' element={<Leaderboard leaderboard={leaderboard} user={user} />} />
 | 
			
		||||
      </Routes>
 | 
			
		||||
    </BrowserRouter>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Header({ pollingFlux }) {
 | 
			
		||||
  const [polling, dispatchPolling] = pollingFlux;
 | 
			
		||||
function Header({ pollingReducer }) {
 | 
			
		||||
  const [polling, dispatchPolling] = pollingReducer;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='Header'>
 | 
			
		||||
@ -52,20 +48,20 @@ function Header({ pollingFlux }) {
 | 
			
		||||
 | 
			
		||||
      <OnlineToggle
 | 
			
		||||
        isOnline={polling.enabled}
 | 
			
		||||
        onClick={() => dispatchPolling({ type: "toggleOnOff" })}
 | 
			
		||||
        onClick={() => dispatchPolling({ type: 'toggleOnOff' })}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <nav>
 | 
			
		||||
        <NavLink to="/about">
 | 
			
		||||
        <NavLink to='/about'>
 | 
			
		||||
          About
 | 
			
		||||
        </NavLink>
 | 
			
		||||
 | 
			
		||||
        <NavLink to="/games">
 | 
			
		||||
        <NavLink to='/games'>
 | 
			
		||||
          <Wobler text="Games" dance={polling.games} />
 | 
			
		||||
        </NavLink>
 | 
			
		||||
 | 
			
		||||
        <NavLink to="/leaderboard">
 | 
			
		||||
          <Wobler text="Leaderboard" dance={polling.leaderboard} />
 | 
			
		||||
        <NavLink to='/leaderboard'>
 | 
			
		||||
          <Wobler text='Leaderboard' dance={polling.leaderboard} />
 | 
			
		||||
        </NavLink>
 | 
			
		||||
      </nav>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -1,25 +1,19 @@
 | 
			
		||||
import usePolling from "../util/Polling"
 | 
			
		||||
 | 
			
		||||
const uri = '/api/games';
 | 
			
		||||
 | 
			
		||||
export default function useGamesApi(gamesReducer) {
 | 
			
		||||
    const [games, dispatchGames] = gamesReducer;
 | 
			
		||||
export default function useGamesApi(gamesState) {
 | 
			
		||||
    const [games, setGames] = gamesState;
 | 
			
		||||
 | 
			
		||||
    const useList = (pollingReducer) => {
 | 
			
		||||
        const [polling, dispatchPolling] = pollingReducer;
 | 
			
		||||
 | 
			
		||||
        const mode = (polling.enabled === true)
 | 
			
		||||
            ? { interval_sec: 30 }      // update games list half a minue
 | 
			
		||||
            ? { interval_sec: 30 }      // update games list every half a minue
 | 
			
		||||
            : { interval_stop: true }   // user has fliped OfflineToggel
 | 
			
		||||
 | 
			
		||||
        const [list, isFetching] = usePolling(uri, mode);
 | 
			
		||||
        const isPolling = usePolling('/api/games', setGames, mode);
 | 
			
		||||
 | 
			
		||||
        if (polling.games !== isFetching) {
 | 
			
		||||
            dispatchPolling({ type: 'next', games: isFetching });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (games.list !== list) {
 | 
			
		||||
            dispatchGames({ type: 'next', list });
 | 
			
		||||
        if (isPolling !== polling.games) {
 | 
			
		||||
            dispatchPolling({ type: 'next', games: isPolling });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return games;
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,8 @@
 | 
			
		||||
import usePolling from "../util/Polling"
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import usePolling from "../util/Polling";
 | 
			
		||||
 | 
			
		||||
const uri = '/api/leaderboard';
 | 
			
		||||
 | 
			
		||||
export default function useLeaderboardApi(leaderboardReducer) {
 | 
			
		||||
    const [leaderboard, dispatchLeaderboaed] = leaderboardReducer;
 | 
			
		||||
export default function useLeaderboardApi() {
 | 
			
		||||
    const [leaderboard, setLeaderboard] = useState(null);
 | 
			
		||||
 | 
			
		||||
    const usePoll = (pollingReducer) => {
 | 
			
		||||
        const [polling, dispatchPolling] = pollingReducer;
 | 
			
		||||
@ -12,14 +11,10 @@ export default function useLeaderboardApi(leaderboardReducer) {
 | 
			
		||||
            ? { interval_sec: 300 }     // update leaderbord stats every 5 min
 | 
			
		||||
            : { interval_stop: true }   // user has fliped OfflineToggel
 | 
			
		||||
 | 
			
		||||
        const [table, isFetching] = usePolling(uri, mode);
 | 
			
		||||
        const isPolling = usePolling('/api/leaderboard', setLeaderboard, mode);
 | 
			
		||||
 | 
			
		||||
        if (polling.leaderboard !== isFetching) {
 | 
			
		||||
            dispatchPolling({ type: 'next', leaderboard: isFetching });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (leaderboard.table !== table) {
 | 
			
		||||
            dispatchLeaderboaed({ type: 'next', table });
 | 
			
		||||
        if (isPolling !== polling.leaderboard) {
 | 
			
		||||
            dispatchPolling({ type: 'next', leaderboard: isPolling });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return leaderboard;
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,15 @@
 | 
			
		||||
import usePolling from "../util/Polling"
 | 
			
		||||
import usePolling from "../util/Polling";
 | 
			
		||||
import useUserReducer from "../reducer/user";
 | 
			
		||||
 | 
			
		||||
const uri = '/api/user';
 | 
			
		||||
 | 
			
		||||
export default function useUserApi([user, dispatchUser]) {
 | 
			
		||||
export default function useUserApi() {
 | 
			
		||||
    const [user, dispatchUser] = useUserReducer();
 | 
			
		||||
 | 
			
		||||
    const useGet = () => {
 | 
			
		||||
        const [nextUser] = usePolling(uri);
 | 
			
		||||
 | 
			
		||||
        if (typeof nextUser?.username === 'string' && nextUser.username !== user.username) {
 | 
			
		||||
            dispatchUser({ type: "next", username: nextUser.username });
 | 
			
		||||
        const onResponce = (json) => {
 | 
			
		||||
            dispatchUser({ type: "parse", json });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        usePolling('/api/user', onResponce); // <<-- fetch once
 | 
			
		||||
        return user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,15 +4,14 @@ import Loading from '../components/Loading';
 | 
			
		||||
 | 
			
		||||
export default function Leaderboard({ leaderboard, user }) {
 | 
			
		||||
 | 
			
		||||
  const table = leaderboard?.table;
 | 
			
		||||
  if (!table)
 | 
			
		||||
  if (leaderboard == null)
 | 
			
		||||
    return <Loading />
 | 
			
		||||
 | 
			
		||||
  const isCurrentUser = (playerName) =>
 | 
			
		||||
    user?.isCurrentUser(playerName) === true ? true : null;
 | 
			
		||||
 | 
			
		||||
  const tableRows = Object.keys(table).map(playerName => {
 | 
			
		||||
    var rank = table[playerName];
 | 
			
		||||
  const tableRows = Object.keys(leaderboard).map(playerName => {
 | 
			
		||||
    var rank = leaderboard[playerName];
 | 
			
		||||
 | 
			
		||||
    return <tr key={playerName} className={isCurrentUser(playerName) && 'currentuser'}>
 | 
			
		||||
      <td>{playerName}</td>
 | 
			
		||||
 | 
			
		||||
@ -1,84 +0,0 @@
 | 
			
		||||
export const reducer = (state, action) => {
 | 
			
		||||
 | 
			
		||||
  switch (action.update) {
 | 
			
		||||
 | 
			
		||||
    case "game-selector":
 | 
			
		||||
      return GameSelector_update(state, action)
 | 
			
		||||
 | 
			
		||||
    case "newGame":
 | 
			
		||||
      return updateNewGame(state, action)
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      console.warn("Unknown action.component", action.component)
 | 
			
		||||
      return state
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const initialState = {
 | 
			
		||||
  gameSelector: {
 | 
			
		||||
    selectedGameProposal: null,
 | 
			
		||||
    selectedActiveGame: null,
 | 
			
		||||
    selectedArchiveGame: null,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  newGame: {
 | 
			
		||||
    whitePlayer: '',
 | 
			
		||||
    blackPlayer: '',
 | 
			
		||||
    message: '',
 | 
			
		||||
    fetching: false,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function GameSelector_update(state, action) {
 | 
			
		||||
  if (Object.hasOwn(action, 'selectedGameProposal')) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...state,
 | 
			
		||||
      gameSelector: {
 | 
			
		||||
        ...state.gameSelector,
 | 
			
		||||
        selectedGameProposal: action.selectedGameProposal
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (Object.hasOwn(action, 'selectedActiveGame')) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...state,
 | 
			
		||||
      gameSelector: {
 | 
			
		||||
        ...state.gameSelector,
 | 
			
		||||
        selectedActiveGame: action.selectedActiveGame
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (Object.hasOwn(action, 'selectedArchiveGame')) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...state,
 | 
			
		||||
      gameSelector: {
 | 
			
		||||
        ...state.gameSelector,
 | 
			
		||||
        selectedArchiveGame: action.selectedArchiveGame
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.warn(action.component, "- bad property")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateNewGame(state, action) {
 | 
			
		||||
  const newGame = {...state.newGame}
 | 
			
		||||
 | 
			
		||||
  Object.keys(action)
 | 
			
		||||
  .slice(1) // skip 'update' property
 | 
			
		||||
  .forEach(actionKey => {
 | 
			
		||||
    if (Object.hasOwn(newGame, actionKey)) {
 | 
			
		||||
      newGame[actionKey] = action[actionKey]
 | 
			
		||||
    } else {
 | 
			
		||||
      console.warn("NewGame update: bad action property\n", actionKey + ":", action[actionKey])
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    ...state,
 | 
			
		||||
    newGame
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,46 +0,0 @@
 | 
			
		||||
import { useState, useCallback, useEffect,  } from "react"
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
  TODO: Poll(uri, flavour)
 | 
			
		||||
  - uri: string
 | 
			
		||||
  - execution_flvour:
 | 
			
		||||
    - once (i.e. now)
 | 
			
		||||
    - interval (sec)
 | 
			
		||||
    - stop
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
export default function Poll(url, interval_sec, offlineMode) {
 | 
			
		||||
    const [dataCache, setDataCache] = 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) => setDataCache(freshData))
 | 
			
		||||
            .catch((err) => console.log(err.message))
 | 
			
		||||
    }, [url])
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (dataCache == null) {
 | 
			
		||||
            fecthData()  // <<-- run immediatly on startup
 | 
			
		||||
        }
 | 
			
		||||
        else if (offlineMode === true) {
 | 
			
		||||
            clearTimeout(timeoutID) // cancel already scheduled fetch
 | 
			
		||||
            setTimeoutID(null)      // & stop interval fetching
 | 
			
		||||
        }
 | 
			
		||||
        else if (timeoutID === null && typeof interval_sec === 'number') {
 | 
			
		||||
            const timeoutID = setTimeout(fecthData, interval_sec * 1000)
 | 
			
		||||
            setTimeoutID(timeoutID)
 | 
			
		||||
            console.log("Fetch '" +url +"' scheduled in " +interval_sec +" sec")
 | 
			
		||||
        }
 | 
			
		||||
    }, [url, dataCache, fecthData, timeoutID, offlineMode, interval_sec]);
 | 
			
		||||
 | 
			
		||||
    return [ dataCache, fetching ]
 | 
			
		||||
}
 | 
			
		||||
@ -1,40 +0,0 @@
 | 
			
		||||
import React from "react"
 | 
			
		||||
import { reducer, initialState } from "./reducer"
 | 
			
		||||
 | 
			
		||||
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 [games,       gamesFetching       ] = Poll('/api/gamestate'  , 30, data.offlineMode)
 | 
			
		||||
  const [leaderboard, leaderboardFetching ] = Poll('/api/leaderboard', 60, data.offlineMode)
 | 
			
		||||
  const [user] = Poll('/api/user') // once
 | 
			
		||||
 | 
			
		||||
  data.games         = games
 | 
			
		||||
  data.gamesFetching = gamesFetching
 | 
			
		||||
 | 
			
		||||
  data.leaderboard         = leaderboard
 | 
			
		||||
  data.leaderboardFetching = leaderboardFetching
 | 
			
		||||
 | 
			
		||||
  data.isCurrentUser = (otherUsername) => {
 | 
			
		||||
    return user?.username && ciEquals(user.username, otherUsername) ? true : null
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return (
 | 
			
		||||
    <AppData.Provider value={[data, dispatchData]}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </AppData.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ciEquals(a, b) {
 | 
			
		||||
  return typeof a === 'string' && typeof b === 'string'
 | 
			
		||||
      ? a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0
 | 
			
		||||
      : a === b;
 | 
			
		||||
}
 | 
			
		||||
@ -1,25 +0,0 @@
 | 
			
		||||
export const reducer = (state, action) => {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
 | 
			
		||||
    case "toggleOfflineMode":
 | 
			
		||||
      return { ...state,
 | 
			
		||||
        offlineMode: !state.offlineMode // on/off
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      console.warn("Unknown action.type", action)
 | 
			
		||||
      return state
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
  
 | 
			
		||||
export const initialState = { 
 | 
			
		||||
  games: null,
 | 
			
		||||
  gamesFetching: false,
 | 
			
		||||
 | 
			
		||||
  leaderboard: null,
 | 
			
		||||
  leaderboardFetching: false,
 | 
			
		||||
  
 | 
			
		||||
  isCurrentUser: () => null,
 | 
			
		||||
 | 
			
		||||
  offlineMode: false
 | 
			
		||||
}
 | 
			
		||||
@ -1,17 +1,17 @@
 | 
			
		||||
import React from "react"
 | 
			
		||||
import { reducer, initialState } from "./reducer"
 | 
			
		||||
 | 
			
		||||
export const AppContext = React.createContext({
 | 
			
		||||
export const Games = React.createContext({
 | 
			
		||||
  state: initialState,
 | 
			
		||||
  dispatch: () => null
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const AppContextProvider = ({ children }) => {
 | 
			
		||||
export const GamesProvider = ({ children }) => {
 | 
			
		||||
  const [state, dispatch] = React.useReducer(reducer, initialState)
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AppContext.Provider value={[ state, dispatch ]}>
 | 
			
		||||
    <Games.Provider value={[ state, dispatch ]}>
 | 
			
		||||
    	{ children }
 | 
			
		||||
    </AppContext.Provider>
 | 
			
		||||
    </Games.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -3,18 +3,11 @@ import ReactDOM from 'react-dom/client';
 | 
			
		||||
import './index.css';
 | 
			
		||||
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>
 | 
			
		||||
    <AppContextProvider>
 | 
			
		||||
      <AppDataProvider>
 | 
			
		||||
        <App />
 | 
			
		||||
      </AppDataProvider>
 | 
			
		||||
    </AppContextProvider>
 | 
			
		||||
    <App />
 | 
			
		||||
  </React.StrictMode>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,21 +0,0 @@
 | 
			
		||||
import { useReducer } from 'react';
 | 
			
		||||
import { nextState } from '../util/StateHelper';
 | 
			
		||||
 | 
			
		||||
export const leaderboardInitialState = {
 | 
			
		||||
  table: null,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function leaderboardReducer(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(leaderboardReducer, leaderboardInitialState);
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { useReducer } from 'react';
 | 
			
		||||
import { localeCompare } from '../util/Locale';
 | 
			
		||||
import { nextState } from '../util/StateHelper';
 | 
			
		||||
//import { nextState } from '../util/StateHelper';
 | 
			
		||||
 | 
			
		||||
export const userInitialState = {
 | 
			
		||||
  username: '',
 | 
			
		||||
@ -13,8 +13,13 @@ export const userInitialState = {
 | 
			
		||||
export function userReducer(state, action) {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
 | 
			
		||||
    case 'next': 
 | 
			
		||||
      return nextState(state, action);
 | 
			
		||||
    case 'parse':
 | 
			
		||||
      const apiData = parse(action.json);
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        ...apiData
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      throw Error('UserReducer: unknown action.type', action.type);
 | 
			
		||||
@ -23,4 +28,11 @@ export function userReducer(state, action) {
 | 
			
		||||
 | 
			
		||||
export default function useUserReducer() {
 | 
			
		||||
  return useReducer(userReducer, userInitialState);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parse(json) {
 | 
			
		||||
  console.log("userreducer.parse", json);
 | 
			
		||||
  return {
 | 
			
		||||
    username: json.username
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -3,51 +3,53 @@ import { useState, useCallback, useEffect, } from "react"
 | 
			
		||||
/*
 | 
			
		||||
  - uri: string
 | 
			
		||||
  - mode:
 | 
			
		||||
    - null  - default, return cashed value while polling fresh one from server)
 | 
			
		||||
    - null  - default, fetch data ONCE
 | 
			
		||||
    - 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);
 | 
			
		||||
export default function usePolling(uri, onResponce, mode = null) {
 | 
			
		||||
    const [initialPoll, setInitialPoll] = useState(true);
 | 
			
		||||
    const [isPolling, setPolling] = useState(false);
 | 
			
		||||
    const [intervalTimer, setIntervalTimer] = useState(null);
 | 
			
		||||
 | 
			
		||||
    const fetchData = useCallback(() => {
 | 
			
		||||
        setDelayID(null);
 | 
			
		||||
        setFetching(true);
 | 
			
		||||
    const pollData = useCallback(() => {
 | 
			
		||||
        setPolling(true);
 | 
			
		||||
        setInitialPoll(false);
 | 
			
		||||
 | 
			
		||||
        fetch(url)
 | 
			
		||||
            .then((response) => response.json())
 | 
			
		||||
            .then((freshData) => {
 | 
			
		||||
                setCache(freshData);
 | 
			
		||||
                setFetching(false);
 | 
			
		||||
        fetch(uri)
 | 
			
		||||
            .then((responce) => {
 | 
			
		||||
                setPolling(false);
 | 
			
		||||
 | 
			
		||||
                if (typeof mode?.interval_sec === 'number') {
 | 
			
		||||
                    console.log("Schedule", uri, "fetch in", mode.interval_sec, "sec");
 | 
			
		||||
                    const intervalTimer = setTimeout(pollData, mode.interval_sec * 1000);
 | 
			
		||||
                    setIntervalTimer(intervalTimer);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return responce.json();
 | 
			
		||||
            })
 | 
			
		||||
            .then((json) => {
 | 
			
		||||
                onResponce(json);
 | 
			
		||||
            })
 | 
			
		||||
            .catch((err) => {
 | 
			
		||||
                console.warn(err.message);
 | 
			
		||||
                setFetching(false);
 | 
			
		||||
            })
 | 
			
		||||
    }, [url])
 | 
			
		||||
    }, [uri, mode, onResponce]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (cache === null && isFetching === false) {
 | 
			
		||||
            fetchData();
 | 
			
		||||
        if ((initialPoll || (typeof mode?.interval_sec === 'number' && intervalTimer === null)) && !isPolling) {
 | 
			
		||||
            pollData();
 | 
			
		||||
        }
 | 
			
		||||
    }, [initialPoll, mode, intervalTimer, isPolling, pollData]);
 | 
			
		||||
 | 
			
		||||
        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);       
 | 
			
		||||
        }
 | 
			
		||||
    if (mode?.interval_stop && intervalTimer) {
 | 
			
		||||
        console.log("Cancel scheduled fetch for", uri);
 | 
			
		||||
        clearTimeout(intervalTimer);
 | 
			
		||||
        setIntervalTimer(null);
 | 
			
		||||
        setInitialPoll(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    }, [url, mode, isFetching, cache, fetchData, delayID]);
 | 
			
		||||
 | 
			
		||||
    return [
 | 
			
		||||
        cache, // API responce
 | 
			
		||||
        isFetching  // true / false
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
    return isPolling;
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user