Compare commits

..

No commits in common. "472f5de9283bdce47b9e80c2691665e95dc8ca0e" and "c999302cda24a2884e565eb54f2b97fc51e6c8b9" have entirely different histories.

14 changed files with 77 additions and 242 deletions

View File

@ -1,3 +1,7 @@
.App { .App {
text-align: center; text-align: center;
} }
.Container {
margin-top: 25px;
}

View File

@ -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>

View File

@ -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
}
}

View File

@ -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} />}

View File

@ -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;
} }

View 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>
)
}

View File

@ -1,25 +1,26 @@
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>
@ -38,7 +39,7 @@ export default function Leaderboard({ leaderboard }) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{tableRows} { tableRows }
</tbody> </tbody>
</table> </table>
</div> </div>

View 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>
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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

View File

@ -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]
}

View File

@ -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
]
}