Compare commits
No commits in common. "472f5de9283bdce47b9e80c2691665e95dc8ca0e" and "c999302cda24a2884e565eb54f2b97fc51e6c8b9" have entirely different histories.
472f5de928
...
c999302cda
@ -1,3 +1,7 @@
|
|||||||
.App {
|
.App {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Container {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
@ -1,25 +1,17 @@
|
|||||||
import './App.css';
|
import './App.css';
|
||||||
import React, { useReducer } from 'react'
|
import React from 'react'
|
||||||
import { BrowserRouter, Routes, Route } from "react-router-dom"
|
import { BrowserRouter, Routes, Route } from "react-router-dom"
|
||||||
|
|
||||||
import Header from "./container/Header"
|
import Header from "./components/Header"
|
||||||
import Leaderboard from "./components/Leaderboard"
|
import Leaderboard from "./components/Leaderboard"
|
||||||
import Game from "./components/Game"
|
import Game from "./components/Game"
|
||||||
import About from "./components/About"
|
import About from "./components/About"
|
||||||
|
|
||||||
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 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 />
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* https://stackoverflow.com/questions/40541994/multiple-path-names-for-a-same-component-in-react-router */}
|
{/* https://stackoverflow.com/questions/40541994/multiple-path-names-for-a-same-component-in-react-router */}
|
||||||
<Route path="/game" element={<Game />} />
|
<Route path="/game" element={<Game />} />
|
||||||
@ -27,7 +19,8 @@ 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 leaderboard={leaderboard} />} />
|
|
||||||
|
<Route path="/leaderboard" element={<Leaderboard />} />
|
||||||
<Route path="/about" element={<About />} />
|
<Route path="/about" element={<About />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -32,8 +32,6 @@ export default function GameSelector() {
|
|||||||
if (!data.games)
|
if (!data.games)
|
||||||
return <div>Loading..</div>
|
return <div>Loading..</div>
|
||||||
|
|
||||||
console.log("Games", data.games)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='game-selector'>
|
<div className='game-selector'>
|
||||||
{isProposalPath && <Proposal games={data.games} onClick={onClick_proposal} />}
|
{isProposalPath && <Proposal games={data.games} onClick={onClick_proposal} />}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
.OnlineToggle {
|
.OnlineTgl {
|
||||||
transform: scale(.5);
|
transform: scale(.5);
|
||||||
margin-left: -19px;
|
margin-left: -19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Header {
|
.app-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Header nav {
|
.app-header nav {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -16,7 +16,7 @@
|
|||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Header a {
|
.app-header a {
|
||||||
color: lightgray;
|
color: lightgray;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: .25s ease;
|
transition: .25s ease;
|
||||||
@ -27,7 +27,7 @@
|
|||||||
padding: 0.25rem 1rem;
|
padding: 0.25rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Header .active {
|
.app-header .active {
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: cadetblue;
|
background-color: cadetblue;
|
||||||
@ -35,17 +35,17 @@
|
|||||||
padding: 0.25rem 1rem;
|
padding: 0.25rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Header a:hover:not(.active) {
|
.app-header a:hover:not(.active) {
|
||||||
color: cadetblue;
|
color: cadetblue;
|
||||||
|
|
||||||
box-shadow: 0 1.5px 0 0 currentcolor;
|
box-shadow: 0 1.5px 0 0 currentcolor;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-darkreader-scheme="dark"] .Header a {
|
[data-darkreader-scheme="dark"] .app-header a {
|
||||||
color: darkslategrey;
|
color: darkslategrey;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-darkreader-scheme="dark"] .Header .active {
|
[data-darkreader-scheme="dark"] .app-header .active {
|
||||||
color: white;
|
color: white;
|
||||||
box-shadow: 0 1.5px 0 0 currentcolor;
|
box-shadow: 0 1.5px 0 0 currentcolor;
|
||||||
}
|
}
|
30
webapp/src/components/Header.jsx
Normal file
30
webapp/src/components/Header.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import './Header.css';
|
||||||
|
import React from "react"
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import OnlineToggle from './OnlineTgl';
|
||||||
|
import { AppData } from "../context/data"
|
||||||
|
import Wobler from './Wobler';
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const [data] = React.useContext(AppData)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='app-header'>
|
||||||
|
<h1 >
|
||||||
|
CordaCheckers
|
||||||
|
</h1>
|
||||||
|
<OnlineToggle />
|
||||||
|
<nav>
|
||||||
|
<NavLink to="/about">
|
||||||
|
About
|
||||||
|
</NavLink>
|
||||||
|
<NavLink to="/game">
|
||||||
|
<Wobler text="Game" dance={data.gamesFetching} />
|
||||||
|
</NavLink>
|
||||||
|
<NavLink to="/leaderboard">
|
||||||
|
<Wobler text="Leaderboard" dance={data.leaderboardFetching} />
|
||||||
|
</NavLink>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,30 +1,31 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
import { AppData } from "../../context/data"
|
||||||
|
|
||||||
export default function Leaderboard({ leaderboard }) {
|
export default function Leaderboard() {
|
||||||
|
const [data] = React.useContext(AppData)
|
||||||
|
|
||||||
if (leaderboard == null)
|
if (data.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(leaderboard).map(playerName => {
|
const tableRows = Object.keys(data.leaderboard).map(playerName => {
|
||||||
var rank = leaderboard[playerName];
|
var rank = data.leaderboard[playerName];
|
||||||
|
|
||||||
// TODO tr: className={data.isCurrentUser(playerName) && 'username'}
|
return <tr key={playerName} 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">
|
||||||
@ -38,8 +39,8 @@ export default function Leaderboard({ leaderboard }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{tableRows}
|
{ tableRows }
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
};
|
};
|
||||||
|
12
webapp/src/components/OnlineTgl/index.jsx
Normal file
12
webapp/src/components/OnlineTgl/index.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import "./index.css"
|
||||||
|
import React from "react"
|
||||||
|
import { AppData } from "../../context/data"
|
||||||
|
|
||||||
|
export default function OnlineTgl() {
|
||||||
|
const [/*appData*/, dispatchData] = React.useContext(AppData)
|
||||||
|
|
||||||
|
return <div className="OnlineTgl">
|
||||||
|
<input className="tgl tgl-flip" id="cb5" type="checkbox" defaultChecked onClick={() => dispatchData({type: "toggleOfflineMode"})}/>
|
||||||
|
<label className="tgl-btn" data-tg-off="offline" data-tg-on="online" htmlFor="cb5"/>
|
||||||
|
</div>
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
import "./OnlineToggle.css"
|
|
||||||
import React from "react"
|
|
||||||
|
|
||||||
export default function OnlineToggle({ isOnline, onClick }) {
|
|
||||||
return (
|
|
||||||
<div className="OnlineToggle">
|
|
||||||
<input className="tgl tgl-flip" id="cb5" type="checkbox" checked={isOnline} onChange={onClick} />
|
|
||||||
<label className="tgl-btn" data-tg-off="offline" data-tg-on="online" htmlFor="cb5" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
import './Header.css';
|
|
||||||
import React from "react"
|
|
||||||
import { NavLink } from "react-router-dom";
|
|
||||||
import OnlineToggle from '../components/OnlineToggle';
|
|
||||||
import Wobler from '../components/Wobler';
|
|
||||||
|
|
||||||
export default function Header({ polling, dispatchPolling }) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='Header'>
|
|
||||||
<h1 >
|
|
||||||
CordaCheckers
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<OnlineToggle
|
|
||||||
isOnline={polling.enabled}
|
|
||||||
onClick={() => dispatchPolling({ type: "toggleOnOff" })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<NavLink to="/about">
|
|
||||||
About
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<NavLink to="/game">
|
|
||||||
<Wobler text="Game" dance={polling.games} />
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<NavLink to="/leaderboard">
|
|
||||||
<Wobler text="Leaderboard" dance={polling.leaderboard} />
|
|
||||||
</NavLink>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
import { useLocalStorage } from '../util/PersistentStorage'
|
|
||||||
|
|
||||||
const Persistent = (() => {
|
|
||||||
const [getEnabled, setEnabled] = useLocalStorage('polling.enabled', true);
|
|
||||||
|
|
||||||
return {
|
|
||||||
getEnabled,
|
|
||||||
setEnabled
|
|
||||||
}
|
|
||||||
})(); // <<--- execute
|
|
||||||
|
|
||||||
export const pollingGetInitialState = () => {
|
|
||||||
return {
|
|
||||||
enabled: Persistent.getEnabled() === 'true',
|
|
||||||
|
|
||||||
games: false,
|
|
||||||
leaderboard: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function pollingReducer(state, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
|
|
||||||
case 'toggleOnOff': return {
|
|
||||||
...state,
|
|
||||||
enabled: Persistent.setEnabled(!state.enabled)
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'setGames': return {
|
|
||||||
...state,
|
|
||||||
games: action.value
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'setLeaderboard': return {
|
|
||||||
...state,
|
|
||||||
leaderboard: action.value
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw Error('Unknown action.type:' + action.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Polling = {
|
|
||||||
initialState: pollingGetInitialState(), // <<--- execute
|
|
||||||
reducer: pollingReducer
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Polling
|
|
@ -1,31 +0,0 @@
|
|||||||
export function useLocalStorage(name, initialValue) {
|
|
||||||
|
|
||||||
const get = () => localStorage.getItem(name);
|
|
||||||
const del = () => localStorage.removeItem(name);
|
|
||||||
const set = (value) => {
|
|
||||||
localStorage.setItem(name, value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (get() === null) {
|
|
||||||
set(initialValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [get, set, del]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSessionStorage(name, initialValue) {
|
|
||||||
|
|
||||||
const get = () => sessionStorage.getItem(name);
|
|
||||||
const del = () => sessionStorage.removeItem(name);
|
|
||||||
const set = (value) => {
|
|
||||||
sessionStorage.setItem(name, value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (get() === null) {
|
|
||||||
set(initialValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [get, set, del]
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
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
|
|
||||||
]
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user